Files
----/前端源码/uni-app/pages/index/index.vue

1373 lines
51 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>