1373 lines
51 KiB
Vue
1373 lines
51 KiB
Vue
<template>
|
||
<view class="page">
|
||
<view class="shell">
|
||
<view scroll-y class="content" :class="{ 'no-padding': !needsPadding, 'with-tabbar': !hideTabBar }">
|
||
<HomeScreen v-if="currentTab === 'home'" :active="currentTab === 'home'"
|
||
:on-navigate-to-calendar="() => setTab('calendar')" :on-navigate-to-naming="() => setTab('naming')"
|
||
:on-navigate-to-test="() => setTab('test')" :on-navigate-to-affinity="() => setTab('affinity')"
|
||
:on-show-solution-detail="handleShowSolutionDetail" />
|
||
|
||
<CalendarScreen v-else-if="currentTab === 'calendar'" @back="setTab('home')"
|
||
@auspicious="setTab('auspicious_form')" />
|
||
<AuspiciousForm v-else-if="currentTab === 'auspicious_form'" @back="setTab('calendar')"
|
||
@submit="handleAuspiciousSubmit" />
|
||
<AuspiciousLoading v-else-if="currentTab === 'auspicious_loading'" />
|
||
<AuspiciousResult v-else-if="currentTab === 'auspicious_result'" :data="auspiciousData"
|
||
@back="handleAuspiciousBack" />
|
||
|
||
<Affinity v-else-if="currentTab === 'affinity'" @back="setTab('home')" @showResult="handleAffinityResult" />
|
||
<AffinityResult v-else-if="currentTab === 'affinity_result'" :data="affinityData" @back="handleAffinityResultBack" />
|
||
|
||
<TestNameScreen v-else-if="currentTab === 'test'" ref="testNameScreenRef" @test="handleTest" />
|
||
|
||
<AnalysisScreen v-else-if="currentTab === 'analysis'" @back="setTab('test')" />
|
||
<CompanyAnalysisScreen v-else-if="currentTab === 'company_analysis'" :params="companyAnalysisParams"
|
||
@back="setTab('test')" />
|
||
<NamingScreen v-else-if="currentTab === 'naming'" @showDetail="handleShowNamingDetail" />
|
||
<NamingDetail v-else-if="currentTab === 'naming_detail'" :data="namingDetailData" @back="setTab(getReturnTab('naming_detail','naming'))"
|
||
@wealthAnalysis="handlePersonalWealthAnalysis" />
|
||
<CompanyNamingDetailDesktop v-else-if="currentTab === 'company_naming_detail' && isDesktop" :data="namingDetailData"
|
||
:showBusinessFortune="companyNamingShowBusinessFortune"
|
||
@back="setTab(getReturnTab('company_naming_detail','naming'))" @business-fortune="handleCompanyBusinessFortune" />
|
||
<CompanyNamingDetail v-else-if="currentTab === 'company_naming_detail'" :data="namingDetailData"
|
||
:showBusinessFortune="companyNamingShowBusinessFortune"
|
||
@back="setTab(getReturnTab('company_naming_detail','naming'))" @businessFortune="handleCompanyBusinessFortune" />
|
||
<CompanyBusinessFortune v-else-if="currentTab === 'company_business_fortune'" :data="companyBusinessFortuneData"
|
||
@back="setTab(getReturnTab('company_business_fortune','company_naming_detail'))" />
|
||
<PersonalWealthAnalysis v-else-if="currentTab === 'personal_wealth_analysis'" :data="personalWealthAnalysisData"
|
||
@back="handleBackFromPersonalWealthAnalysis" />
|
||
<RenamingScreen v-else-if="currentTab === 'renaming'" @showDetail="handleShowRenamingDetail" />
|
||
<RenamingDetail v-else-if="currentTab === 'renaming_detail'" :data="renamingDetailData" :mode="renamingMode"
|
||
@back="setTab(getReturnTab('renaming_detail','renaming'))" />
|
||
<TestNameDetail v-else-if="currentTab === 'test_name_detail'" :data="testNameDetailData"
|
||
@back="setTab(getReturnTab('test_name_detail','myNamingPlans'))" />
|
||
<ProfileScreen v-else-if="currentTab === 'profile'" @navigate="setTab" />
|
||
<ProfileFavorites v-else-if="currentTab === 'favorites'" @back="setTab('profile')"
|
||
@showDetail="handleMyPlanDetail" />
|
||
<ProfileUserInfo v-else-if="currentTab === 'profile_user_info'" @back="setTab('profile')" />
|
||
<ProfileReports v-else-if="currentTab === 'reports'" @back="setTab('profile')" @navigate="setTab" />
|
||
<MyNamingPlansScreen v-else-if="currentTab === 'myNamingPlans'" :focus-list-tab="myPlansFocusListTab"
|
||
@back="setTab('profile')" @navigate="setTab" @focusListTabConsumed="myPlansFocusListTab = null"
|
||
@showDetail="handleMyPlanDetail" @showAffinityResult="handleAffinityResult"
|
||
@showAuspiciousResult="handleAuspiciousResult"
|
||
@showNamingSolutionsList="handleShowNamingSolutionsList" />
|
||
<NamingSolutionsList v-else-if="currentTab === 'naming_solutions_list'" :report-id="namingSolutionsListData?.reportId || 0"
|
||
:solutions="namingSolutionsListData?.solutions || []" :category="namingSolutionsListData?.category"
|
||
:service-type="namingSolutionsListData?.serviceType"
|
||
@back="setTab('myNamingPlans')" @showDetail="handleMyPlanDetail" />
|
||
<ProfileOrdersScreen v-else-if="currentTab === 'orders'" @back="setTab('profile')"
|
||
@showOrderDetail="handleShowOrderDetail" />
|
||
<ProfileOrderDetailScreen v-else-if="currentTab === 'order_detail'" :data="currentOrderDetail"
|
||
@back="setTab(getReturnTab('order_detail','orders'))" @openBusiness="handleOpenOrderBusiness" />
|
||
<ProfileSettings v-else-if="currentTab === 'settings'" @back="setTab('profile')" @navigate="setTab" />
|
||
<ProfileFAQ v-else-if="currentTab === 'faq'" @back="setTab('settings')" />
|
||
<ProfilePrivacy v-else-if="currentTab === 'privacy'" @back="setTab('settings')" />
|
||
<ProfileFeedbackScreen v-else-if="currentTab === 'feedback'" @back="setTab('settings')" />
|
||
<TestResultScreen v-else-if="currentTab === 'test_result'" :mode="analysisMode"
|
||
:data="analysisMode === 'company' ? companyScoringResult : personalScoringResult" @back="setTab(getReturnTab('test_result','test'))" />
|
||
</view>
|
||
|
||
<CustomTabBar v-if="!hideTabBar" :current-tab="currentTab" @change="handleTabChange" />
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted, watch, onUnmounted } from "vue";
|
||
import CustomTabBar from "../../components/CustomTabBar.vue";
|
||
import HomeScreen from "../../components/screens/Home.vue";
|
||
import TestNameScreen from "../../components/screens/TestName.vue";
|
||
import CalendarScreen from "../../components/screens/Calendar.vue";
|
||
import AuspiciousForm from "../../components/screens/AuspiciousForm.vue";
|
||
import AuspiciousLoading from "../../components/screens/AuspiciousLoading.vue";
|
||
import AuspiciousResult from "../../components/screens/AuspiciousResult.vue";
|
||
import Affinity from "../../components/screens/Affinity.vue";
|
||
import AffinityResult from "../../components/screens/AffinityResult.vue";
|
||
import ProfileScreen from "../../components/screens/Profile.vue";
|
||
import AnalysisScreen from "../../components/screens/Analysis.vue";
|
||
import CompanyAnalysisScreen from "../../components/screens/CompanyAnalysis.vue";
|
||
import NamingScreen from "../../components/screens/Naming.vue";
|
||
import NamingDetail from "../../components/screens/NamingDetail.vue";
|
||
import CompanyNamingDetail from "../../components/screens/CompanyNamingDetail.vue";
|
||
import CompanyNamingDetailDesktop from "../../components/screens/CompanyNamingDetailDesktop.vue";
|
||
import CompanyBusinessFortune from "../../components/screens/CompanyBusinessFortune.vue";
|
||
import PersonalWealthAnalysis from "../../components/screens/PersonalWealthAnalysis.vue";
|
||
import RenamingScreen from "../../components/screens/Renaming.vue";
|
||
import RenamingDetail from "../../components/screens/RenamingDetail.vue";
|
||
import TestNameDetail from "../../components/screens/TestNameDetail.vue";
|
||
import ProfileFavorites from "../../components/screens/ProfileFavorites.vue";
|
||
import ProfileReports from "../../components/screens/ProfileReports.vue";
|
||
import MyNamingPlansScreen from "../../components/screens/MyNamingPlansScreen.vue";
|
||
import NamingSolutionsList from "../../components/screens/NamingSolutionsList.vue";
|
||
import ProfileOrdersScreen from "../../components/screens/ProfileOrdersScreen.vue";
|
||
import ProfileOrderDetailScreen from "../../components/screens/ProfileOrderDetailScreen.vue";
|
||
import ProfileSettings from "../../components/screens/ProfileSettings.vue";
|
||
import ProfileFAQ from "../../components/screens/ProfileFAQ.vue";
|
||
import ProfilePrivacy from "../../components/screens/ProfilePrivacy.vue";
|
||
import ProfileFeedbackScreen from "../../components/screens/ProfileFeedbackScreen.vue";
|
||
import ProfileUserInfo from "../../components/screens/ProfileUserInfo.vue";
|
||
import TestResultScreen from "../../components/screens/TestResult.vue";
|
||
import type { GeneratedName } from "../../components/NamingResult.vue";
|
||
import type { QueryOrderResponse } from "../../api/types";
|
||
import { userApi } from "../../api";
|
||
import { namingApi } from "../../api/naming";
|
||
import { showLoading, hideLoading, showToast } from "../../utils/uni-compat";
|
||
import { getIsDesktopLayout } from "../../utils/device-layout";
|
||
import {
|
||
classifyLiveTestDetail,
|
||
isCompanyTestDetailShape,
|
||
pollTestSolutionDetail,
|
||
pollUntilSolutionIdFromScoring,
|
||
} from "../../utils/poll-test-solution-detail";
|
||
|
||
const currentTab = ref("home");
|
||
const isDesktop = ref(false);
|
||
let desktopResizeHandler: (() => void) | null = null;
|
||
const analysisMode = ref<"personal" | "company">("personal");
|
||
const companyAnalysisParams = ref<any>(null);
|
||
const auspiciousData = ref<any>(null);
|
||
const namingSolutionsListData = ref<{ reportId: number; solutions: any[]; category?: string; serviceType?: string } | null>(null);
|
||
const namingDetailData = ref<any>(null);
|
||
const namingMode = ref<'personal' | 'company'>('personal');
|
||
const companyNamingShowBusinessFortune = ref(true);
|
||
const companyBusinessFortuneData = ref<any>(null);
|
||
const personalWealthAnalysisData = ref<any>(null);
|
||
const renamingDetailData = ref<any>(null);
|
||
const renamingMode = ref<'personal' | 'company'>('personal');
|
||
const testNameDetailData = ref<any>(null);
|
||
const affinityData = ref<any>(null);
|
||
/** 从「我的方案」子列表返回详情页时,用于恢复选中的 tab(名字 / 缘分合盘 / 八字择吉) */
|
||
const myPlansFocusListTab = ref<'naming' | 'affinity' | 'zeji' | null>(null);
|
||
/** 缘分合盘结果是否从「我的方案」进入(返回时应回我的方案而非测算页) */
|
||
const affinityResultFromMyPlans = ref(false);
|
||
const auspiciousBackTarget = ref<'auspicious_form' | 'myNamingPlans'>('auspicious_form');
|
||
const currentOrderDetail = ref<QueryOrderResponse | null>(null);
|
||
const testNameScreenRef = ref<{ closeLoading: () => void } | null>(null);
|
||
let testPollAbort: AbortController | null = null;
|
||
const safeAreaBottom = ref(0);
|
||
const screenWidth = ref(375);
|
||
const windowHeight = ref(0);
|
||
const tabBarHeight = ref(0);
|
||
const WEALTH_PENDING_KEY = 'wx_pending_payment';
|
||
const WEALTH_REPORT_ID_KEY = 'wealth_analysis_report_id';
|
||
const WEALTH_RETURN_CONTEXT_KEY = 'wealth_return_naming_context';
|
||
const WEALTH_RETURN_CONTEXT_TTL = 24 * 60 * 60 * 1000;
|
||
|
||
// 测名结果(真实接口返回数据)
|
||
const personalScoringResult = ref<any | null>(null);
|
||
const companyScoringResult = ref<any | null>(null);
|
||
|
||
// 配置哪些页面不需要 padding
|
||
const noPaddingPages = ['analysis', 'company_analysis', 'naming_detail', 'company_naming_detail', 'company_business_fortune', 'personal_wealth_analysis', 'renaming_detail', 'test_result', 'test_name_detail', 'order_detail', 'profile_user_info'];
|
||
|
||
// 配置哪些页面隐藏 TabBar
|
||
const hideTabBarPages = ['analysis', 'company_analysis', 'naming_detail', 'company_naming_detail', 'company_business_fortune', 'personal_wealth_analysis', 'renaming_detail', 'test_result', 'test_name_detail', 'favorites', 'reports', 'myNamingPlans', 'orders', 'order_detail', 'settings', 'faq', 'privacy', 'feedback', 'calendar', 'auspicious_form', 'auspicious_loading', 'auspicious_result', 'affinity', 'affinity_result', 'profile_user_info'];
|
||
|
||
// 详解类页面:点击返回希望回到“进入该页面之前的上一页”
|
||
const detailTabSet = new Set([
|
||
'naming_detail',
|
||
'company_naming_detail',
|
||
'personal_wealth_analysis',
|
||
'company_business_fortune',
|
||
'renaming_detail',
|
||
'test_name_detail',
|
||
'order_detail',
|
||
'test_result',
|
||
]);
|
||
const tabReturnTargets = ref<Record<string, string>>({});
|
||
|
||
// rpx 转 px
|
||
const rpxToPx = (rpx: number) => {
|
||
return (rpx / 750) * screenWidth.value;
|
||
};
|
||
|
||
// 获取系统信息计算安全区域
|
||
onMounted(() => {
|
||
// H5 环境下使用 window 对象获取屏幕信息
|
||
isDesktop.value = getIsDesktopLayout();
|
||
desktopResizeHandler = () => {
|
||
isDesktop.value = getIsDesktopLayout();
|
||
};
|
||
window.addEventListener("resize", desktopResizeHandler, { passive: true });
|
||
|
||
screenWidth.value = window.innerWidth || 375;
|
||
windowHeight.value = window.innerHeight || 667;
|
||
// H5 环境下安全区域通常为 0
|
||
safeAreaBottom.value = 0;
|
||
// 计算 tabbar 高度(120rpx 转 px + 安全区域)
|
||
tabBarHeight.value = rpxToPx(120) + safeAreaBottom.value;
|
||
|
||
// OAuth回跳后恢复页面状态
|
||
try {
|
||
const raw = localStorage.getItem(WEALTH_PENDING_KEY);
|
||
if (raw) {
|
||
const pending = JSON.parse(raw);
|
||
if (pending?.tab && Date.now() - pending.ts < 5 * 60 * 1000) {
|
||
restoreNamingDetailFromContext();
|
||
// OAuth 回跳恢复:此时“上一页”应是详解报告
|
||
tabReturnTargets.value['personal_wealth_analysis'] = 'naming_detail';
|
||
setTab('personal_wealth_analysis');
|
||
}
|
||
}
|
||
} catch {}
|
||
|
||
// 推广合伙人:授权回跳后自动创建订单并调起微信支付(与财运月度详批一致)
|
||
void (async () => {
|
||
try {
|
||
const { tryResumePartnerPaymentAfterOAuth } = await import("../../utils/wechat-h5-jsapi-pay");
|
||
const r = await tryResumePartnerPaymentAfterOAuth();
|
||
if (r.ok) {
|
||
currentTab.value = "profile";
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
})();
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
if (desktopResizeHandler && typeof window !== "undefined") {
|
||
window.removeEventListener("resize", desktopResizeHandler);
|
||
}
|
||
});
|
||
|
||
// 判断当前页面是否需要 padding
|
||
const needsPadding = computed(() => {
|
||
return !noPaddingPages.includes(currentTab.value);
|
||
});
|
||
|
||
// 判断是否隐藏 TabBar
|
||
const hideTabBar = computed(() => {
|
||
return hideTabBarPages.includes(currentTab.value);
|
||
});
|
||
|
||
const setTab = (tab: string) => {
|
||
const from = currentTab.value;
|
||
// 进入详解类页面时记录上一页,用于返回
|
||
if (detailTabSet.has(tab)) {
|
||
const hasExisting = typeof tabReturnTargets.value[tab] === 'string' && tabReturnTargets.value[tab].length > 0;
|
||
const fromIsDetail = detailTabSet.has(from);
|
||
// 规则:
|
||
// - 从“非详解页”进入“详解页”:总是记录(最新的上一页)
|
||
// - 从“详解页”切到“详解页”:不覆盖已有记录,避免形成 A<->B 返回死循环
|
||
// (例如:详解 -> 财运 -> 返回详解,不应把详解的上一页改成财运)
|
||
if (!fromIsDetail || !hasExisting) {
|
||
tabReturnTargets.value[tab] = from;
|
||
}
|
||
}
|
||
currentTab.value = tab;
|
||
};
|
||
|
||
const getReturnTab = (detailTab: string, fallback: string) => {
|
||
return tabReturnTargets.value[detailTab] || fallback;
|
||
};
|
||
|
||
const tryParseJson = (value: any): any | null => {
|
||
if (value == null) return null;
|
||
if (typeof value === 'object') return value;
|
||
if (typeof value !== 'string') return null;
|
||
const raw = value.trim();
|
||
if (!raw) return null;
|
||
try {
|
||
return JSON.parse(raw);
|
||
} catch {
|
||
return null;
|
||
}
|
||
};
|
||
|
||
const normalizeAuspiciousData = (raw: any): any => {
|
||
if (!raw || typeof raw !== 'object') return raw;
|
||
|
||
let normalized: any = { ...raw };
|
||
const candidates = [
|
||
raw,
|
||
raw.data,
|
||
raw.result,
|
||
raw.detail,
|
||
raw.report,
|
||
raw.report_data,
|
||
raw.payload,
|
||
tryParseJson(raw.result_json),
|
||
tryParseJson(raw.result_data),
|
||
tryParseJson(raw.report_json),
|
||
tryParseJson(raw.detail_json),
|
||
tryParseJson(raw.extra_data),
|
||
];
|
||
|
||
for (const candidate of candidates) {
|
||
if (candidate && typeof candidate === 'object') {
|
||
normalized = { ...normalized, ...candidate };
|
||
}
|
||
}
|
||
|
||
let dates: any[] = [];
|
||
if (Array.isArray(normalized.dates)) {
|
||
dates = normalized.dates;
|
||
} else {
|
||
const parsedDates = tryParseJson(normalized.dates);
|
||
if (Array.isArray(parsedDates)) {
|
||
dates = parsedDates;
|
||
}
|
||
}
|
||
|
||
if (!dates.length) {
|
||
const altDateLists = [
|
||
normalized.date_list,
|
||
normalized.auspicious_dates,
|
||
normalized.good_dates,
|
||
normalized.result_dates,
|
||
];
|
||
for (const item of altDateLists) {
|
||
if (Array.isArray(item)) {
|
||
dates = item;
|
||
break;
|
||
}
|
||
const parsed = tryParseJson(item);
|
||
if (Array.isArray(parsed)) {
|
||
dates = parsed;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!dates.length) {
|
||
const selectedDate = normalized.selected_date || normalized.date || normalized.good_date;
|
||
if (selectedDate) {
|
||
dates = [{
|
||
date: String(selectedDate),
|
||
lunar: String(normalized.selected_lunar || normalized.lunar || ''),
|
||
desc: String(normalized.selected_desc || normalized.desc || normalized.advice || ''),
|
||
score: Number(normalized.score || 90),
|
||
hours: String(normalized.selected_hours || normalized.hours || ''),
|
||
clash: String(normalized.selected_clash || normalized.clash || ''),
|
||
suitable: Array.isArray(normalized.suitable) ? normalized.suitable : [],
|
||
avoid: Array.isArray(normalized.avoid) ? normalized.avoid : [],
|
||
}];
|
||
}
|
||
}
|
||
|
||
normalized.dates = (Array.isArray(dates) ? dates : [])
|
||
.map((item: any) => ({
|
||
date: String(item?.date || ''),
|
||
lunar: String(item?.lunar || ''),
|
||
desc: String(item?.desc || ''),
|
||
score: Number(item?.score || 0),
|
||
hours: String(item?.hours || ''),
|
||
clash: String(item?.clash || ''),
|
||
suitable: Array.isArray(item?.suitable) ? item.suitable : [],
|
||
avoid: Array.isArray(item?.avoid) ? item.avoid : [],
|
||
}))
|
||
.filter((item: any) => item.date || item.desc);
|
||
|
||
return normalized;
|
||
};
|
||
|
||
const readWealthReturnContext = (): any | null => {
|
||
try {
|
||
const raw = uni.getStorageSync(WEALTH_RETURN_CONTEXT_KEY);
|
||
if (!raw) return null;
|
||
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
||
const ts = Number(parsed?.ts || 0);
|
||
if (!ts || Date.now() - ts > WEALTH_RETURN_CONTEXT_TTL) {
|
||
uni.removeStorageSync(WEALTH_RETURN_CONTEXT_KEY);
|
||
return null;
|
||
}
|
||
return parsed;
|
||
} catch {
|
||
return null;
|
||
}
|
||
};
|
||
|
||
const saveWealthReturnContext = (extra: { reportId?: number } = {}) => {
|
||
const detail = namingDetailData.value;
|
||
if (!detail || typeof detail !== 'object' || Object.keys(detail).length === 0) return;
|
||
const context = {
|
||
ts: Date.now(),
|
||
detailData: detail,
|
||
solutionId: Number((detail as any)?.id || 0),
|
||
reportId: Number(extra.reportId || (detail as any)?.report_id || 0),
|
||
};
|
||
try {
|
||
uni.setStorageSync(WEALTH_RETURN_CONTEXT_KEY, JSON.stringify(context));
|
||
} catch {}
|
||
};
|
||
|
||
const restoreNamingDetailFromContext = (): boolean => {
|
||
const context = readWealthReturnContext();
|
||
if (!context?.detailData) return false;
|
||
namingDetailData.value = context.detailData;
|
||
return true;
|
||
};
|
||
|
||
const fetchNamingDetailByReportId = async (reportId: number): Promise<any | null> => {
|
||
const solutionsResult = await namingApi.getSolutionsByReportId(reportId);
|
||
const solutions = solutionsResult?.solutions || solutionsResult?.items || solutionsResult;
|
||
if (!Array.isArray(solutions) || !solutions.length) return null;
|
||
const firstSolution = solutions[0];
|
||
const solutionId = Number(firstSolution?.id || firstSolution?.solution_id || 0);
|
||
if (!solutionId) return null;
|
||
return namingApi.getSolutionDetail(solutionId);
|
||
};
|
||
|
||
const handleBackFromPersonalWealthAnalysis = async () => {
|
||
const targetTab = getReturnTab('personal_wealth_analysis', 'naming_detail');
|
||
if (namingDetailData.value && Object.keys(namingDetailData.value).length > 0) {
|
||
setTab(targetTab);
|
||
return;
|
||
}
|
||
|
||
if (restoreNamingDetailFromContext()) {
|
||
setTab(targetTab);
|
||
return;
|
||
}
|
||
|
||
let reportId = Number(personalWealthAnalysisData.value?.id || 0);
|
||
if (!reportId) {
|
||
try {
|
||
reportId = Number(uni.getStorageSync(WEALTH_REPORT_ID_KEY) || 0);
|
||
} catch {}
|
||
}
|
||
|
||
if (!reportId) {
|
||
showToast({ title: '未找到详解报告,请重新进入', icon: 'none' });
|
||
setTab('naming');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
showLoading({ title: '正在恢复详解报告...' });
|
||
const detail = await fetchNamingDetailByReportId(reportId);
|
||
hideLoading();
|
||
|
||
if (!detail) {
|
||
showToast({ title: '详解报告恢复失败,请重新进入', icon: 'none' });
|
||
setTab('naming');
|
||
return;
|
||
}
|
||
|
||
namingDetailData.value = detail;
|
||
saveWealthReturnContext({ reportId });
|
||
setTab(targetTab);
|
||
} catch (error: any) {
|
||
hideLoading();
|
||
showToast({ title: error?.msg || '详解报告恢复失败,请重试', icon: 'none' });
|
||
setTab('naming');
|
||
}
|
||
};
|
||
|
||
watch(namingDetailData, (value) => {
|
||
if (!value || typeof value !== 'object' || Object.keys(value).length === 0) return;
|
||
saveWealthReturnContext();
|
||
}, { deep: false });
|
||
|
||
// 处理显示起名详情
|
||
const handleShowNamingDetail = (data: GeneratedName, mode: 'personal' | 'company') => {
|
||
namingDetailData.value = data;
|
||
namingMode.value = mode;
|
||
if (mode === 'company') {
|
||
setTab('company_naming_detail');
|
||
} else {
|
||
setTab('naming_detail');
|
||
}
|
||
};
|
||
|
||
// 处理订单详情跳转
|
||
const handleShowOrderDetail = (order: QueryOrderResponse) => {
|
||
currentOrderDetail.value = order;
|
||
setTab('order_detail');
|
||
};
|
||
|
||
const handleOpenOrderBusiness = async (order: QueryOrderResponse) => {
|
||
const businessType = String(order?.business_type || '');
|
||
const businessId = Number(order?.business_id || 0);
|
||
|
||
if (businessType === 'partner_apply') {
|
||
showToast({
|
||
title: '推广合伙人权益已开通',
|
||
icon: 'success',
|
||
});
|
||
setTab('profile');
|
||
return;
|
||
}
|
||
|
||
if (['naming_report', 'test_report', 'fortune_report'].includes(businessType) && businessId > 0) {
|
||
try {
|
||
showLoading({ title: '正在打开业务...' });
|
||
const detail = await userApi.getSolutionDetail(businessId);
|
||
hideLoading();
|
||
|
||
if (!detail) {
|
||
showToast({ title: '未找到对应业务数据', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
namingDetailData.value = detail;
|
||
const category = String((detail as any)?.category || '').toLowerCase();
|
||
if (category === 'company') {
|
||
setTab('company_naming_detail');
|
||
} else {
|
||
setTab('naming_detail');
|
||
}
|
||
return;
|
||
} catch (error: any) {
|
||
hideLoading();
|
||
showToast({ title: error?.msg || '打开业务失败', icon: 'none' });
|
||
return;
|
||
}
|
||
}
|
||
|
||
showToast({
|
||
title: '该订单暂不支持一键跳转',
|
||
icon: 'none',
|
||
});
|
||
};
|
||
|
||
const handleShowRenamingDetail = (data: any, mode: 'personal' | 'company') => {
|
||
if (mode === 'personal') {
|
||
// 个人改名详情页复用个人起名详解报告(NamingDetail.vue)。
|
||
// 为避免改名结果里携带的异构字段影响展示,这里只透传基础字段,交由 NamingDetail 内部做统一 normalize。
|
||
namingDetailData.value = {
|
||
id: data?.id,
|
||
name: data?.name,
|
||
pinyin: data?.pinyin,
|
||
score: data?.score,
|
||
meaning: data?.meaning,
|
||
source: data?.source,
|
||
tags: Array.isArray(data?.tags) ? data.tags : [],
|
||
wuxing: data?.wuxing,
|
||
zodiac: data?.zodiac,
|
||
constellation: data?.constellation,
|
||
};
|
||
} else {
|
||
namingDetailData.value = data;
|
||
}
|
||
namingMode.value = mode;
|
||
if (mode === 'company') {
|
||
setTab('company_naming_detail');
|
||
} else {
|
||
setTab('naming_detail');
|
||
}
|
||
};
|
||
|
||
|
||
const buildCompanyBusinessFortuneFromWealthData = (wealthData: any) => {
|
||
const data = wealthData?.businessFortuneData;
|
||
if (!data) return null;
|
||
|
||
// 解析JSON字符串字段
|
||
let mingpanData = null;
|
||
let liunianData = null;
|
||
let fengshuiData = null;
|
||
let monthDetailData = null;
|
||
let dayDetailData = null;
|
||
|
||
const sectionLabelMap: Record<string, string> = {
|
||
quannian_gaishu: '全年概述',
|
||
yueling_xiangxi: '月令详析',
|
||
kaiyun_zhidao: '开运指导',
|
||
jinri_gaishu: '今日概述',
|
||
yunshi_xiangxi: '运势详析',
|
||
fangwei_zhidao: '方位指导',
|
||
shichen_jixiong: '时辰吉凶',
|
||
jinri_zhidao: '今日指导',
|
||
teb_ie_tixing: '特别提醒',
|
||
tebie_tixing: '特别提醒',
|
||
};
|
||
const keyLabelMap: Record<string, string> = {
|
||
ganzhi: '干支',
|
||
hexin_yunshi: '核心运势',
|
||
jixiong_dengji: '吉凶等级',
|
||
caiyun_zhishu: '财运指数',
|
||
shiye_zhishu: '事业指数',
|
||
jiankang_zhishu: '健康指数',
|
||
renmai_zhishu: '人脉指数',
|
||
qinggan_zhishu: '情感指数',
|
||
shendu_jiexi: '深度解析',
|
||
zhongdian_shixiang: '重点事项',
|
||
daji_rizi: '大吉日',
|
||
xiaoji_rizi: '小吉日',
|
||
jiji_rizi: '忌讳日',
|
||
riqi_ganzhi: '日期干支',
|
||
wuxing_zhuti: '五行主题',
|
||
zongyun_pinggu: '总运评估',
|
||
zhuyao_yingxiang: '主要影响',
|
||
kaiyun_yanse: '开运颜色',
|
||
jiji_yanse: '忌讳颜色',
|
||
caiyun_fenxi: '财运分析',
|
||
shiye_fenxi: '事业分析',
|
||
jiankang_fenxi: '健康分析',
|
||
qinggan_fenxi: '情感分析',
|
||
caishen_fangwei: '财神方位',
|
||
xishen_fangwei: '喜神方位',
|
||
wenchang_fangwei: '文昌方位',
|
||
jiji_fangwei: '忌讳方位',
|
||
jiji_zhidao: '忌讳指导',
|
||
zuijia_shijian: '最佳时间',
|
||
zhuyao_jiji: '主要忌讳',
|
||
kaiyun_jianyi: '开运建议',
|
||
zhuyi_shixiang: '注意事项',
|
||
chuanyi_jianyi: '穿衣建议',
|
||
zhongda_shixiang: '重大事项',
|
||
touzi_licai: '投资理财',
|
||
renmai_guanxi: '人脉关系',
|
||
jiankang_yangsheng: '健康养生',
|
||
shijian: '时间',
|
||
jixiong: '吉凶',
|
||
zhishu: '指数',
|
||
shiyong_shixiang: '适用事项',
|
||
xiangxi_fenxi: '详细分析',
|
||
yanse_xuanze: '颜色选择',
|
||
fengge_jianyi: '风格建议',
|
||
peishi_zhidao: '配饰指导',
|
||
};
|
||
const toLabel = (k: string) => keyLabelMap[k] || sectionLabelMap[k] || k;
|
||
const asText = (v: any) => (v === null || v === undefined ? '' : String(v));
|
||
const flattenLines = (input: any, prefix = ''): string[] => {
|
||
if (input === null || input === undefined) return [];
|
||
if (Array.isArray(input)) {
|
||
return input
|
||
.map((item) => asText(item))
|
||
.filter(Boolean)
|
||
.map((line) => (prefix ? `${toLabel(prefix)}:${line}` : line));
|
||
}
|
||
if (typeof input !== 'object') {
|
||
const text = asText(input).trim();
|
||
if (!text) return [];
|
||
return [prefix ? `${toLabel(prefix)}:${text}` : text];
|
||
}
|
||
|
||
const lines: string[] = [];
|
||
Object.entries(input).forEach(([k, v]) => {
|
||
if (v === null || v === undefined || v === '') return;
|
||
if (Array.isArray(v)) {
|
||
const arr = v.map((x) => asText(x)).filter(Boolean);
|
||
if (arr.length) lines.push(`${toLabel(k)}:${arr.join('、')}`);
|
||
return;
|
||
}
|
||
if (typeof v === 'object') {
|
||
const nested = flattenLines(v);
|
||
if (nested.length) {
|
||
lines.push(`${toLabel(k)}:`);
|
||
nested.forEach((line) => lines.push(`- ${line}`));
|
||
}
|
||
return;
|
||
}
|
||
lines.push(`${toLabel(k)}:${asText(v)}`);
|
||
});
|
||
return lines;
|
||
};
|
||
const buildSectionsFromDetail = (detail: any) => {
|
||
if (!detail || typeof detail !== 'object') return [];
|
||
return Object.entries(detail).map(([key, value]) => {
|
||
const lines = flattenLines(value);
|
||
return {
|
||
title: toLabel(key),
|
||
subtitle: '',
|
||
nodes: lines.length ? [{ type: 'list', items: lines }] : []
|
||
};
|
||
}).filter((s: any) => s.nodes.length > 0);
|
||
};
|
||
|
||
try {
|
||
mingpanData = typeof data.mingpan_jingpi === 'string' ? JSON.parse(data.mingpan_jingpi) : data.mingpan_jingpi;
|
||
} catch (e) {
|
||
console.error('解析mingpan_jingpi失败:', e);
|
||
}
|
||
|
||
try {
|
||
liunianData = typeof data.liunian_zongyun === 'string' ? JSON.parse(data.liunian_zongyun) : data.liunian_zongyun;
|
||
} catch (e) {
|
||
console.error('解析liunian_zongyun失败:', e);
|
||
}
|
||
|
||
try {
|
||
fengshuiData = typeof data.fengshui_jinnang === 'string' ? JSON.parse(data.fengshui_jinnang) : data.fengshui_jinnang;
|
||
} catch (e) {
|
||
console.error('解析fengshui_jinnang失败:', e);
|
||
}
|
||
try {
|
||
const monthRaw = data.yuedo_xiangpi ?? data.yuedu_xiangpi;
|
||
monthDetailData = typeof monthRaw === 'string' ? JSON.parse(monthRaw) : monthRaw;
|
||
} catch (e) {
|
||
console.error('解析yuedo_xiangpi失败:', e);
|
||
}
|
||
try {
|
||
const dayRaw = data.meiri_yuncheng;
|
||
dayDetailData = typeof dayRaw === 'string' ? JSON.parse(dayRaw) : dayRaw;
|
||
} catch (e) {
|
||
console.error('解析meiri_yuncheng失败:', e);
|
||
}
|
||
|
||
return {
|
||
header: {
|
||
title: '商业运势批复',
|
||
subtitle: 'Business Fortune Report'
|
||
},
|
||
tabs: [
|
||
{
|
||
id: 'destiny',
|
||
label: '命盘精批',
|
||
sections: [
|
||
{
|
||
title: '基础信息解析',
|
||
subtitle: 'Basic Information',
|
||
nodes: [
|
||
{
|
||
type: 'kv',
|
||
items: [
|
||
{ label: '企业名称', value: data.name || '' },
|
||
{ label: '财运评分', value: `${data.wealth_score || 0}分` },
|
||
{ label: '财运等级', value: data.wealth_level || '' },
|
||
{ label: '财运趋势', value: data.wealth_trend || '' }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
...(mingpanData?.qiye_jichu_xinxi ? [
|
||
{
|
||
title: '企业基础信息',
|
||
subtitle: 'Company Foundation',
|
||
nodes: [
|
||
...(mingpanData.qiye_jichu_xinxi.wuxing_shuxing ? [{ type: 'text', text: mingpanData.qiye_jichu_xinxi.wuxing_shuxing }] : []),
|
||
...(mingpanData.qiye_jichu_xinxi.mingge_cengci ? [{ type: 'text', text: mingpanData.qiye_jichu_xinxi.mingge_cengci }] : []),
|
||
...(mingpanData.qiye_jichu_xinxi.caiku_zhuangtai ? [{ type: 'text', text: mingpanData.qiye_jichu_xinxi.caiku_zhuangtai }] : []),
|
||
...(mingpanData.qiye_jichu_xinxi.guiren_fangwei ? [{ type: 'text', text: mingpanData.qiye_jichu_xinxi.guiren_fangwei }] : [])
|
||
]
|
||
}
|
||
] : []),
|
||
...(mingpanData?.qiye_mingge_jiexi ? [
|
||
{
|
||
title: '企业命格解析',
|
||
subtitle: 'Company Destiny',
|
||
nodes: [
|
||
...(mingpanData.qiye_mingge_jiexi.qiye_mingge ? [{ type: 'text', text: mingpanData.qiye_mingge_jiexi.qiye_mingge }] : []),
|
||
...(mingpanData.qiye_mingge_jiexi.fazhan_dingshu ? [{ type: 'text', text: mingpanData.qiye_mingge_jiexi.fazhan_dingshu }] : [])
|
||
]
|
||
}
|
||
] : []),
|
||
...(mingpanData?.wuxing_nengliang ? [
|
||
{
|
||
title: '五行能量分析',
|
||
subtitle: 'Five Elements Energy',
|
||
nodes: [
|
||
...(mingpanData.wuxing_nengliang.wuxing_pingheng ? [{ type: 'text', text: mingpanData.wuxing_nengliang.wuxing_pingheng }] : []),
|
||
...(mingpanData.wuxing_nengliang.nengliang_liuxiang ? [{ type: 'text', text: mingpanData.wuxing_nengliang.nengliang_liuxiang }] : []),
|
||
...(mingpanData.wuxing_nengliang.buqiang_jianyi ? [{ type: 'text', text: mingpanData.wuxing_nengliang.buqiang_jianyi }] : [])
|
||
]
|
||
}
|
||
] : []),
|
||
...(mingpanData?.dashi_pizhu ? [
|
||
{
|
||
title: '大师批注',
|
||
subtitle: 'Master Analysis',
|
||
nodes: [
|
||
...(mingpanData.dashi_pizhu.hexin_youshi ? [{ type: 'text', text: `核心优势:${mingpanData.dashi_pizhu.hexin_youshi}` }] : []),
|
||
...(mingpanData.dashi_pizhu.guanjian_fengxian ? [{ type: 'text', text: `关键风险:${mingpanData.dashi_pizhu.guanjian_fengxian}` }] : []),
|
||
...(mingpanData.dashi_pizhu.fazhan_jianyi ? [{ type: 'text', text: `发展建议:${mingpanData.dashi_pizhu.fazhan_jianyi}` }] : [])
|
||
]
|
||
}
|
||
] : [])
|
||
]
|
||
},
|
||
{
|
||
id: 'year',
|
||
label: '流年总运',
|
||
sections: [
|
||
...(liunianData?.qiye_dayun ? [
|
||
{
|
||
title: '企业大运',
|
||
subtitle: 'Company Fortune Cycle',
|
||
nodes: [
|
||
...(liunianData.qiye_dayun.shinian_dayun ? [{ type: 'text', text: liunianData.qiye_dayun.shinian_dayun }] : []),
|
||
...(liunianData.qiye_dayun.guanjian_jiedian ? [{ type: 'text', text: liunianData.qiye_dayun.guanjian_jiedian }] : []),
|
||
...(liunianData.qiye_dayun.fazhan_jieduan ? [{ type: 'text', text: liunianData.qiye_dayun.fazhan_jieduan }] : [])
|
||
]
|
||
}
|
||
] : []),
|
||
...(liunianData?.dangnian_yunshi ? [
|
||
{
|
||
title: '当年运势',
|
||
subtitle: 'Current Year Fortune',
|
||
nodes: [
|
||
...(liunianData.dangnian_yunshi.zhengti_yunshi ? [{ type: 'text', text: liunianData.dangnian_yunshi.zhengti_yunshi }] : []),
|
||
...(liunianData.dangnian_yunshi.caiyun_zhishu ? [{ type: 'text', text: `财运指数:${liunianData.dangnian_yunshi.caiyun_zhishu}` }] : []),
|
||
...(liunianData.dangnian_yunshi.yunshi_tedian ? [{ type: 'text', text: liunianData.dangnian_yunshi.yunshi_tedian }] : [])
|
||
]
|
||
}
|
||
] : []),
|
||
...(liunianData?.jidu_yunshi ? [
|
||
{
|
||
title: '季度运势',
|
||
subtitle: 'Quarterly Fortune',
|
||
nodes: [
|
||
...(liunianData.jidu_yunshi.chun_yunshi ? [{ type: 'text', text: `春季:${liunianData.jidu_yunshi.chun_yunshi}` }] : []),
|
||
...(liunianData.jidu_yunshi.xia_yunshi ? [{ type: 'text', text: `夏季:${liunianData.jidu_yunshi.xia_yunshi}` }] : []),
|
||
...(liunianData.jidu_yunshi.qiu_yunshi ? [{ type: 'text', text: `秋季:${liunianData.jidu_yunshi.qiu_yunshi}` }] : []),
|
||
...(liunianData.jidu_yunshi.dong_yunshi ? [{ type: 'text', text: `冬季:${liunianData.jidu_yunshi.dong_yunshi}` }] : [])
|
||
]
|
||
}
|
||
] : []),
|
||
...(liunianData?.tourongzi_yunshi ? [
|
||
{
|
||
title: '投融资运势',
|
||
subtitle: 'Investment Fortune',
|
||
nodes: [
|
||
...(liunianData.tourongzi_yunshi.rongzi_shiji ? [{ type: 'text', text: liunianData.tourongzi_yunshi.rongzi_shiji }] : []),
|
||
...(liunianData.tourongzi_yunshi.touzi_fangxiang ? [{ type: 'text', text: liunianData.tourongzi_yunshi.touzi_fangxiang }] : [])
|
||
]
|
||
}
|
||
] : [])
|
||
]
|
||
},
|
||
{
|
||
id: 'month',
|
||
label: '月度详批',
|
||
locked: !data.is_unlocked,
|
||
lock: { title: '12个月运势详批', price: String(data.unlock_price || 19.9) },
|
||
sections: buildSectionsFromDetail(monthDetailData)
|
||
},
|
||
{
|
||
id: 'day',
|
||
label: '每日运程',
|
||
locked: !data.is_unlocked,
|
||
lock: { title: '365天每日吉凶指南', price: String(data.unlock_price || 19.9) },
|
||
sections: buildSectionsFromDetail(dayDetailData)
|
||
},
|
||
{
|
||
id: 'fengshui',
|
||
label: '风水锦囊',
|
||
sections: [
|
||
...(fengshuiData?.bangongshi_buju ? [
|
||
{
|
||
title: '办公室布局',
|
||
subtitle: 'Office Layout',
|
||
nodes: [
|
||
...(fengshuiData.bangongshi_buju.zhengti_buju ? [{ type: 'text', text: fengshuiData.bangongshi_buju.zhengti_buju }] : []),
|
||
...(fengshuiData.bangongshi_buju.lingdao_bangongshi ? [{ type: 'text', text: `领导办公室:${fengshuiData.bangongshi_buju.lingdao_bangongshi}` }] : []),
|
||
...(fengshuiData.bangongshi_buju.caiwushi_yaodan ? [{ type: 'text', text: `财务室要点:${fengshuiData.bangongshi_buju.caiwushi_yaodan}` }] : [])
|
||
]
|
||
}
|
||
] : []),
|
||
...(fengshuiData?.zhaocai_zhenffa ? [
|
||
{
|
||
title: '招财阵法',
|
||
subtitle: 'Wealth Attraction',
|
||
nodes: [
|
||
...(fengshuiData.zhaocai_zhenffa.jucai_zhenfa ? [{ type: 'text', text: fengshuiData.zhaocai_zhenffa.jucai_zhenfa }] : []),
|
||
...(fengshuiData.zhaocai_zhenffa.cuicai_buju ? [{ type: 'text', text: fengshuiData.zhaocai_zhenffa.cuicai_buju }] : []),
|
||
...(fengshuiData.zhaocai_zhenffa.huasha_fangfa ? [{ type: 'text', text: fengshuiData.zhaocai_zhenffa.huasha_fangfa }] : [])
|
||
]
|
||
}
|
||
] : []),
|
||
...(fengshuiData?.jixiangwu_tuijian ? [
|
||
{
|
||
title: '吉祥物推荐',
|
||
subtitle: 'Lucky Items',
|
||
nodes: [
|
||
...(fengshuiData.jixiangwu_tuijian.zhuyao_jixiangwu ? [{ type: 'text', text: fengshuiData.jixiangwu_tuijian.zhuyao_jixiangwu }] : []),
|
||
...(fengshuiData.jixiangwu_tuijian.baifang_weizhi ? [{ type: 'text', text: fengshuiData.jixiangwu_tuijian.baifang_weizhi }] : [])
|
||
]
|
||
}
|
||
] : []),
|
||
...(fengshuiData?.yanse_logo ? [
|
||
{
|
||
title: '色彩与Logo建议',
|
||
subtitle: 'Color & Logo',
|
||
nodes: [
|
||
...(fengshuiData.yanse_logo.zhu_sediao ? [{ type: 'text', text: `主色调:${fengshuiData.yanse_logo.zhu_sediao}` }] : []),
|
||
...(fengshuiData.yanse_logo.fuzhu_se ? [{ type: 'text', text: `辅助色:${fengshuiData.yanse_logo.fuzhu_se}` }] : []),
|
||
...(fengshuiData.yanse_logo.logo_youhua ? [{ type: 'text', text: `Logo建议:${fengshuiData.yanse_logo.logo_youhua}` }] : [])
|
||
]
|
||
}
|
||
] : [])
|
||
]
|
||
}
|
||
]
|
||
};
|
||
};
|
||
|
||
const handleCompanyBusinessFortune = (payload: any) => {
|
||
const transformedData = buildCompanyBusinessFortuneFromWealthData(payload);
|
||
companyBusinessFortuneData.value = transformedData;
|
||
setTab('company_business_fortune');
|
||
};
|
||
|
||
const buildPersonalWealthAnalysisData = (personalDetail: any) => {
|
||
const getSection = (type: string) => {
|
||
const sections = personalDetail?.sections;
|
||
if (!Array.isArray(sections)) return null;
|
||
return sections.find((s: any) => s.section_type === type) || null;
|
||
};
|
||
|
||
const getContent = (section: any) => {
|
||
const details = section?.details;
|
||
if (!Array.isArray(details) || details.length === 0) return null;
|
||
return details[0]?.content || null;
|
||
};
|
||
|
||
const zongheSection = getSection('zonghe');
|
||
const shuliSection = getSection('shuli');
|
||
const wuxingSection = getSection('wuxing');
|
||
const guaxiangSection = getSection('guaxiang');
|
||
const yunshiSection = getSection('yunshi');
|
||
const fengshuiSection = getSection('fengshui');
|
||
|
||
const zongheContent = getContent(zongheSection);
|
||
const shuliContent = getContent(shuliSection);
|
||
const wuxingContent = getContent(wuxingSection);
|
||
const guaxiangContent = getContent(guaxiangSection);
|
||
const yunshiContent = getContent(yunshiSection);
|
||
const fengshuiContent = getContent(fengshuiSection);
|
||
|
||
const baseKv = [
|
||
{ label: '姓名', value: String(personalDetail?.name || '') },
|
||
{ label: '拼音', value: String(personalDetail?.pinyin || '') },
|
||
{ label: '总分', value: String(personalDetail?.score ?? '') },
|
||
{ label: '星级', value: String(personalDetail?.star_rating ?? '') }
|
||
].filter((x) => x.value);
|
||
|
||
const wuxingKv = wuxingContent?.elements
|
||
? wuxingContent.elements.map((e: any) => ({
|
||
label: String(e.element || ''),
|
||
value: String(e.score || '')
|
||
}))
|
||
: [];
|
||
|
||
const yunshiList = Array.isArray(yunshiContent?.years)
|
||
? yunshiContent.years.map((y: any) => `${y?.y || ''}年 ${y?.l ? '· ' + y.l : ''} ${y?.s ? '(' + y.s + '分)' : ''} ${y?.d || ''}`.trim())
|
||
: [];
|
||
|
||
return {
|
||
header: {
|
||
title: '财运批复',
|
||
subtitle: 'Personal Wealth Report'
|
||
},
|
||
tabs: [
|
||
{
|
||
id: 'destiny',
|
||
label: '命盘精批',
|
||
sections: [
|
||
{
|
||
title: '基础信息解析',
|
||
subtitle: 'Basic Information',
|
||
nodes: [{ type: 'kv', items: baseKv }]
|
||
},
|
||
{
|
||
title: '大师总评',
|
||
subtitle: 'Master Comment',
|
||
nodes: [
|
||
...(zongheContent?.rating ? [{ type: 'text', text: `评级:${String(zongheContent.rating)}` }] : []),
|
||
...(zongheContent?.summary ? [{ type: 'text', text: String(zongheContent.summary) }] : [])
|
||
]
|
||
},
|
||
{
|
||
title: '姓名数理解读',
|
||
subtitle: 'Numerology',
|
||
nodes: [
|
||
...(shuliContent?.luck_name ? [{ type: 'text', text: String(shuliContent.luck_name) }] : []),
|
||
...(shuliContent?.basic ? [{ type: 'text', text: String(shuliContent.basic) }] : []),
|
||
...(shuliContent?.biz ? [{ type: 'text', text: String(shuliContent.biz) }] : [])
|
||
]
|
||
},
|
||
{
|
||
title: '五行能量分析',
|
||
subtitle: 'Five Elements',
|
||
nodes: wuxingKv.length ? [{ type: 'kv', items: wuxingKv }] : []
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: 'year',
|
||
label: '流年总运',
|
||
sections: [
|
||
{
|
||
title: '个人卦象',
|
||
subtitle: 'Yi Jing',
|
||
nodes: [
|
||
...(guaxiangContent?.gua ? [{ type: 'text', text: String(guaxiangContent.gua) }] : []),
|
||
...(guaxiangContent?.interp ? [{ type: 'text', text: String(guaxiangContent.interp) }] : []),
|
||
...(guaxiangContent?.biz ? [{ type: 'text', text: String(guaxiangContent.biz) }] : []),
|
||
...(guaxiangContent?.guide ? [{ type: 'text', text: String(guaxiangContent.guide) }] : [])
|
||
]
|
||
},
|
||
{
|
||
title: '未来运势',
|
||
subtitle: 'Fortune Trend',
|
||
nodes: yunshiList.length ? [{ type: 'list', items: yunshiList }] : []
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: 'month',
|
||
label: '月度详批',
|
||
locked: true,
|
||
lock: { title: '12个月运势详批', price: '18.8' },
|
||
sections: []
|
||
},
|
||
{
|
||
id: 'day',
|
||
label: '每日运程',
|
||
locked: true,
|
||
lock: { title: '365天每日吉凶指南', price: '28.8' },
|
||
sections: []
|
||
},
|
||
{
|
||
id: 'fengshui',
|
||
label: '风水锦囊',
|
||
sections: [
|
||
{
|
||
title: '个人风水布局',
|
||
subtitle: 'Personal Fengshui',
|
||
nodes: [
|
||
...(fengshuiContent?.color ? [{ type: 'text', text: `主色调:${String(fengshuiContent.color)}` }] : []),
|
||
...(fengshuiContent?.direction ? [{ type: 'text', text: `朝向:${String(fengshuiContent.direction)}` }] : []),
|
||
...(fengshuiContent?.office ? [{ type: 'text', text: `办公布局:${String(fengshuiContent.office)}` }] : [])
|
||
]
|
||
}
|
||
]
|
||
}
|
||
]
|
||
};
|
||
};
|
||
|
||
const handlePersonalWealthAnalysis = (payload: any) => {
|
||
saveWealthReturnContext({ reportId: Number(payload?.id || 0) });
|
||
personalWealthAnalysisData.value = payload;
|
||
setTab('personal_wealth_analysis');
|
||
};
|
||
|
||
// 处理我的方案详情
|
||
const handleMyPlanDetail = (data: any, category?: string, serviceType?: string) => {
|
||
const st = String(serviceType || '').trim().toLowerCase();
|
||
// 起名/改名方案详情 JSON 常与「测名详解」同构(含 header、各模块字段),必须按报告 service_type 分流,否则会误进测名详情页
|
||
const isNamingOrRenamingReport =
|
||
st === 'naming' ||
|
||
st === 'renaming' ||
|
||
st === 'company_naming' ||
|
||
st === 'company_renaming';
|
||
|
||
const isTestNameDetailData =
|
||
!!data &&
|
||
typeof data === 'object' &&
|
||
!!data.header &&
|
||
(
|
||
!!data.meaning_and_zodiac ||
|
||
!!data.strokes_wuge_sancai ||
|
||
!!data.six_dimension ||
|
||
!!data.master_message ||
|
||
!!data.phonetics ||
|
||
!!data.bazi_name_fit ||
|
||
!!data.name_popularity ||
|
||
!!data.liuyao ||
|
||
!!data.wuxing_bagua ||
|
||
!!data.zodiac_sign ||
|
||
!!data.career_plan ||
|
||
!!data.lucky_numbers ||
|
||
!!data.lucky_colors
|
||
);
|
||
|
||
// 关键修复:公司测名 JSON 与个人同含 header、liuyao 等,需先排除公司形态再进个人测名详解
|
||
if (
|
||
!isNamingOrRenamingReport &&
|
||
category !== 'company' &&
|
||
!isCompanyTestDetailShape(data) &&
|
||
(isTestNameDetailData || category === 'test')
|
||
) {
|
||
testNameDetailData.value = data;
|
||
setTab('test_name_detail');
|
||
return;
|
||
}
|
||
|
||
namingDetailData.value = data;
|
||
if (category === 'company') {
|
||
const isCompanyTestByServiceType = serviceType === 'test';
|
||
const isCompanyTestByShape =
|
||
!!data &&
|
||
typeof data === 'object' &&
|
||
(
|
||
// 公司测名常见字段(旧/中间结构)
|
||
(!!data.businessPattern && !!data.characterAnalysis) ||
|
||
// 公司测名新结构字段(你提供的后端结构)
|
||
(!!data.header && !!data.team && !!data.years && !!data.execution)
|
||
);
|
||
// 公司测名详情不展示“商业运势”按钮;公司起名/改名详情保持展示
|
||
companyNamingShowBusinessFortune.value = !(isCompanyTestByServiceType || isCompanyTestByShape);
|
||
setTab('company_naming_detail');
|
||
} else {
|
||
companyNamingShowBusinessFortune.value = true;
|
||
setTab('naming_detail');
|
||
}
|
||
};
|
||
|
||
const handleShowNamingSolutionsList = (payload: { reportId: number; solutions: any[]; category?: string; serviceType?: string }) => {
|
||
namingSolutionsListData.value = payload;
|
||
setTab('naming_solutions_list');
|
||
};
|
||
|
||
// 处理佳名赏析详情(从首页点击)
|
||
const handleShowSolutionDetail = async (id: number) => {
|
||
try {
|
||
showLoading({ title: '加载中...' });
|
||
const detail = await userApi.getSolutionDetail(id);
|
||
hideLoading();
|
||
|
||
if (detail) {
|
||
namingDetailData.value = detail;
|
||
// 跳转到个人起名详情页
|
||
setTab('naming_detail');
|
||
}
|
||
} catch (error: any) {
|
||
hideLoading();
|
||
showToast({
|
||
title: error.msg || '加载详情失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
};
|
||
|
||
// 处理合盘结果(从缘分合盘表单和我的方案列表)
|
||
const handleAffinityResult = (data: any, fromMyPlans?: boolean) => {
|
||
affinityData.value = data;
|
||
affinityResultFromMyPlans.value = !!fromMyPlans;
|
||
setTab('affinity_result');
|
||
};
|
||
|
||
const handleAffinityResultBack = () => {
|
||
if (affinityResultFromMyPlans.value) {
|
||
myPlansFocusListTab.value = 'affinity';
|
||
setTab('myNamingPlans');
|
||
affinityResultFromMyPlans.value = false;
|
||
} else {
|
||
setTab('affinity');
|
||
}
|
||
};
|
||
|
||
// 处理八字择吉详情(从八字择吉表单和我的方案列表)
|
||
const handleAuspiciousResult = (data: any, fromMyPlans?: boolean) => {
|
||
auspiciousData.value = normalizeAuspiciousData(data);
|
||
auspiciousBackTarget.value = fromMyPlans ? 'myNamingPlans' : 'auspicious_form';
|
||
setTab('auspicious_result');
|
||
};
|
||
|
||
const handleAuspiciousBack = () => {
|
||
if (auspiciousBackTarget.value === 'myNamingPlans') {
|
||
myPlansFocusListTab.value = 'zeji';
|
||
}
|
||
setTab(auspiciousBackTarget.value);
|
||
};
|
||
|
||
const handleTabChange = (tab: string) => {
|
||
setTab(tab);
|
||
};
|
||
|
||
const handleTest = async (mode: "personal" | "company", params: any) => {
|
||
analysisMode.value = mode;
|
||
testPollAbort?.abort();
|
||
testPollAbort = new AbortController();
|
||
const signal = testPollAbort.signal;
|
||
|
||
try {
|
||
let res: any;
|
||
if (mode === "personal") {
|
||
// 个人测名 -> 真实接口 personalScoring
|
||
res = await namingApi.personalScoring({
|
||
surname: String(params.lastName || "").trim(),
|
||
given_name: String(params.firstName || "").trim(),
|
||
gender: params.gender,
|
||
birthday: params.birthDate,
|
||
});
|
||
personalScoringResult.value = res;
|
||
} else {
|
||
// 公司测名 -> 真实接口 companyScoring
|
||
res = await namingApi.companyScoring({
|
||
company_name: String(params.companyName || "").trim(),
|
||
industry: String(params.industry || "").trim(),
|
||
address: String(params.address || "").trim(),
|
||
target_audience: String(params.target_audience || "").trim(),
|
||
members: Array.isArray(params.members)
|
||
? params.members.map((m: any) => ({
|
||
name: String(m.name || "").trim(),
|
||
birthday: m.birth_date,
|
||
}))
|
||
: [],
|
||
});
|
||
companyScoringResult.value = res;
|
||
}
|
||
|
||
// 手机端/电脑端统一:loading 中每 5 秒轮询一次,拿到详情再跳转
|
||
const solutionId = await pollUntilSolutionIdFromScoring(res, {
|
||
intervalMs: 5000,
|
||
signal,
|
||
});
|
||
const detail = await pollTestSolutionDetail(solutionId, mode, {
|
||
intervalMs: 5000,
|
||
signal,
|
||
});
|
||
const classified = classifyLiveTestDetail(detail, mode);
|
||
|
||
if (classified.kind === "company") {
|
||
namingDetailData.value = detail;
|
||
// 公司测名详情不展示“商业运势”按钮
|
||
companyNamingShowBusinessFortune.value = classified.showBusinessFortune;
|
||
setTab("company_naming_detail");
|
||
} else {
|
||
testNameDetailData.value = detail;
|
||
setTab("test_name_detail");
|
||
}
|
||
} catch (error: any) {
|
||
if (String(error?.message || "") === "aborted") {
|
||
return;
|
||
}
|
||
showToast({
|
||
title: error?.msg || "测算失败,请稍后重试",
|
||
icon: "none",
|
||
});
|
||
} finally {
|
||
testNameScreenRef.value?.closeLoading();
|
||
}
|
||
};
|
||
|
||
const handleAuspiciousSubmit = (data: any) => {
|
||
auspiciousData.value = normalizeAuspiciousData(data);
|
||
auspiciousBackTarget.value = 'auspicious_form';
|
||
setTab("auspicious_loading");
|
||
setTimeout(() => {
|
||
setTab("auspicious_result");
|
||
}, 2000);
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page {
|
||
background: #f2e6d8 url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.shell {
|
||
width: 260%;
|
||
max-width: 750rpx;
|
||
background: #fdfbf7;
|
||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
border-left: 1px solid #dcd3c9;
|
||
border-right: 1px solid #dcd3c9;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.navbar {
|
||
background: #fdfbf7;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
border-bottom: 1px solid #eaddcf;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.status-bar {
|
||
height: 88rpx;
|
||
}
|
||
|
||
.navbar-content {
|
||
height: 88rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
padding: 0 32rpx;
|
||
}
|
||
|
||
.title {
|
||
font-size: 34rpx;
|
||
font-weight: 700;
|
||
letter-spacing: 0.3em;
|
||
color: #2c2c2c;
|
||
}
|
||
|
||
.capsule {
|
||
position: absolute;
|
||
right: 32rpx;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 174rpx;
|
||
height: 64rpx;
|
||
background: rgba(255, 255, 255, 0.6);
|
||
border: 1px solid #dcd3c9;
|
||
border-radius: 32rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6rpx;
|
||
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.dot {
|
||
width: 12rpx;
|
||
height: 12rpx;
|
||
border-radius: 50%;
|
||
background: #2c2c2c;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.dot.ghost {
|
||
opacity: 0;
|
||
}
|
||
|
||
.circle {
|
||
width: 24rpx;
|
||
height: 24rpx;
|
||
border-radius: 50%;
|
||
border: 3rpx solid #2c2c2c;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.divider {
|
||
width: 2rpx;
|
||
height: 36rpx;
|
||
background: #dcd3c9;
|
||
margin: 0 12rpx;
|
||
}
|
||
|
||
.content {
|
||
background: url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||
box-sizing: border-box;
|
||
flex: 1;
|
||
height: 0;
|
||
overflow-y: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
/* 隐藏滚动条但保持滚动功能 */
|
||
.content::-webkit-scrollbar {
|
||
display: none;
|
||
width: 0;
|
||
height: 0;
|
||
}
|
||
|
||
.content.with-tabbar {
|
||
padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px));
|
||
}
|
||
|
||
.content.no-padding {
|
||
padding: 0;
|
||
}
|
||
|
||
.content.no-padding.with-tabbar {
|
||
padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px));
|
||
}
|
||
|
||
.placeholder {
|
||
height: 400rpx;
|
||
border: 1px dashed #dcd3c9;
|
||
border-radius: 24rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #8b2323;
|
||
background: rgba(255, 255, 255, 0.6);
|
||
}
|
||
</style>
|