Files
----/前端源码/uni-app/components/screens/PersonalWealthAnalysis.vue

4886 lines
137 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="fortune-wrap">
<view class="bg">
<view class="bg-base" />
<view class="bg-texture" />
<view class="bg-glow bg-glow-a" />
<view class="bg-glow bg-glow-b" />
<view v-for="star in stars" :key="star.id" class="bg-star" :style="{
top: star.top,
left: star.left,
width: star.size + 'px',
height: star.size + 'px',
animationDuration: star.duration + 's',
animationDelay: star.delay + 's'
}" />
</view>
<view class="header">
<view class="header-left" @click="emit('back')">
<text class="header-back"></text>
</view>
<view class="header-mid">
<text class="header-title">财运解析详情</text>
<text class="header-subtitle">Master's Wealth Report</text>
</view>
<view class="header-right">
<view class="header-icon" @click="handleDownloadPdf">
<text class="header-icon-text">↓</text>
</view>
</view>
</view>
<scroll-view scroll-x class="tabs" show-scrollbar="false">
<view class="tabs-inner">
<view v-for="t in tabList" :key="t.id" class="tab" :class="{ active: activeTab === t.id }"
@click="activeTab = t.id">
<view class="tab-icon-wrap">
<text class="tab-icon">{{ t.icon }}</text>
<view v-if="t.locked" class="tab-lock-dot"><text class="tab-lock-dot-text">锁</text></view>
</view>
<text class="tab-label">{{ t.label }}</text>
<view v-if="activeTab === t.id" class="tab-indicator" />
</view>
</view>
</scroll-view>
<scroll-view scroll-y class="content">
<view ref="pdfRootRef" class="content-inner">
<view v-if="activeTab === 'destiny'" class="stack">
<view class="card">
<view class="card-watermark"><text class="wm">人</text></view>
<view class="section-title">
<view class="bar" />
<view>
<text class="st">基础信息解析</text>
<text class="sst">Basic Information</text>
</view>
</view>
<view class="grid-2 gap-2 mb-3">
<view class="row-wide">
<text class="muted">命造</text>
<text class="gold serif bold">{{ basicInfo.mingzao || '-' }}</text>
</view>
<view class="info-row"><text class="muted">真太阳时</text><text class="serif">{{ basicInfo.zhenTaiyang || '-' }}</text></view>
<view class="info-row"><text class="muted">农历生辰</text><text class="serif">{{ basicInfo.nongli || '-' }}</text></view>
</view>
<view class="box-dark grid-2 gap-2">
<view>
<text class="tiny muted">胎元</text>
<text class="serif">-</text>
</view>
<view>
<text class="tiny muted">命宫</text>
<text class="serif">-</text>
</view>
<view>
<text class="tiny muted">空亡</text>
<text class="serif">-</text>
</view>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">前世今生因果</text>
<text class="sst">Karma & Soul</text>
</view>
</view>
<view class="karma">
<view class="karma-item">
<view class="karma-icon"><text class="karma-icon-text">史</text></view>
<view class="col flex-1">
<text class="bold">前世印记</text>
<text class="micro muted lh">
{{ karmaInfo.qianshi || '-' }}
</text>
</view>
</view>
<view class="karma-item">
<view class="karma-icon karma-icon-gold"><text class="karma-icon-text">灯</text></view>
<view class="col flex-1">
<text class="bold">今生课题</text>
<text class="micro muted lh">
{{ karmaInfo.jinsheng || '-' }}
</text>
</view>
</view>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">八字排盘</text>
<text class="sst">Four Pillars Chart</text>
</view>
</view>
<view class="bazi-grid">
<view v-for="p in baziPillars" :key="p.title" class="bazi-pillar" :class="{ active: !!p.active }">
<text class="tiny muted mt-2">{{ p.title }}</text>
<text class="tiny gold serif bold">{{ p.god }}</text>
<view class="gan-circle"><text class="gan-text">{{ p.gan }}</text></view>
<view class="zhi-square"><text class="gan-text">{{ p.zhi }}</text></view>
<view class="hidden-list">
<text v-for="(h, i) in p.hidden" :key="i" class="hidden-item">{{ h }}</text>
</view>
<view class="stage"><text class="tiny muted">{{ p.stage }}</text></view>
</view>
</view>
</view>
<view class="card card-grad">
<view class="card-watermark"><text class="wm">冠</text></view>
<view class="section-title">
<view class="bar" />
<view>
<text class="st">命格层次</text>
<text class="sst">Destiny Grade</text>
</view>
</view>
<view class="row gap-3">
<view class="grade-badge">
<text class="tiny muted">等级</text>
<text class="grade-text serif">上等</text>
</view>
<view class="col flex-1">
<text class="bold">{{ minggeInfo || '-' }}</text>
<text class="tiny muted lh">{{ dashiPizhu || '-' }}</text>
</view>
</view>
</view>
<view class="card">
<view class="card-watermark"><text class="wm">卷</text></view>
<view class="section-title">
<view class="bar" />
<view>
<text class="st">古籍断语</text>
<text class="sst">Classical Text</text>
</view>
</view>
<view class="poem">
<text class="poem-line">{{ gujiInfo || '-' }}</text>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">奇门遁甲排盘</text>
<text class="sst">Qi Men Dun Jia</text>
</view>
</view>
<view class="qimen-box">
<view v-if="qimenCells.length" class="qimen-grid">
<view v-for="(cell, ci) in qimenCells" :key="ci" class="qimen-cell">
<view v-if="cell.center" class="qimen-center">
<text class="qimen-center-title serif">中宫</text>
<text class="qimen-center-sub">{{ Array.isArray(cell.stems) ? cell.stems.join('/') : '' }}</text>
</view>
<view v-else class="qimen-cell-inner">
<text class="qimen-loc">{{ cell.location }}</text>
<view class="qimen-top">
<text class="qimen-spirit serif">{{ cell.spirit }}</text>
<view class="qimen-stems">
<text v-for="(s, si) in (cell.stems || [])" :key="si" class="qimen-stem serif"
:class="{ gold: si === 0 }">
{{ s }}
</text>
</view>
</view>
<view class="qimen-bottom">
<text class="qimen-star serif">{{ cell.star }}</text>
<text class="qimen-door serif"
:class="{ 'qimen-door-active': cell.door && cell.door.indexOf('') !== -1 }">
{{ cell.door }}
</text>
</view>
<text class="qimen-gong">{{ cell.gong }}</text>
</view>
</view>
</view>
<view v-else class="box-dark">
<text class="micro muted lh">{{ qimenInfo.paipan || '-' }}</text>
</view>
</view>
<view class="qimen-analysis">
<view class="qimen-analysis-title">
<text class="qimen-analysis-title-text">奇门格局判词</text>
</view>
<view class="qimen-analysis-body">
<text class="qimen-p">{{ qimenInfo.geju || '-' }}</text>
</view>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">前世今生因果</text>
<text class="sst">Karma & Soul</text>
</view>
</view>
<view class="karma">
<view class="karma-item">
<view class="karma-icon"><text class="karma-icon-text">史</text></view>
<view class="col flex-1">
<text class="bold">前世印记</text>
<text class="micro muted lh">
{{ karmaInfo.qianshi || '-' }}
</text>
</view>
</view>
<view class="karma-item">
<view class="karma-icon karma-icon-gold"><text class="karma-icon-text">灯</text></view>
<view class="col flex-1">
<text class="bold">今生课题</text>
<text class="micro muted lh">
{{ karmaInfo.jinsheng || '-' }}
</text>
</view>
</view>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">五行能量与格局</text>
<text class="sst">Five Elements & Pattern</text>
</view>
</view>
<view class="scorebar" v-for="(s, i) in elementScores" :key="i">
<view class="scorebar-head">
<text class="serif">{{ s.label }}</text>
<text class="mono muted">{{ s.score }}</text>
</view>
<view class="scorebar-track">
<view class="scorebar-fill" :style="{ width: s.width, backgroundColor: s.color }" />
</view>
</view>
<view class="divider" />
<view class="note">
<text class="gold bold">大师批注:</text>
<text class="tiny muted">{{ dashiPizhu || '-' }}</text>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">五行开运指南</text>
<text class="sst">Actionable Advice</text>
</view>
</view>
<view class="table">
<view class="tr th">
<text class="tc muted">项目</text>
<text class="tc gold">喜神 (宜)</text>
<text class="tc red">忌神 (忌)</text>
</view>
<view v-for="(r, i) in elementAdvice" :key="i" class="tr">
<text class="tc muted">{{ r.k }}</text>
<text class="tc">{{ r.good }}</text>
<text class="tc">{{ r.bad }}</text>
</view>
</view>
<text class="foot">* 建议日常穿搭、选房、选号优先参考“喜神”建议。</text>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">十神性格盲点</text>
<text class="sst">Personality & Blind Spots</text>
</view>
</view>
<view class="grid-2 gap-2">
<view class="box-left gold-left">
<text class="bold">主性格分析</text>
<text class="tiny muted lh">{{ shishenMangdian || '-' }}</text>
</view>
<view class="box-left red-left">
<text class="bold">潜在盲点</text>
<text class="tiny muted lh">{{ shishenMangdian || '-' }}</text>
</view>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">事业财运定数</text>
<text class="sst">Career Path</text>
</view>
</view>
<view class="grid-2 gap-2">
<view class="box">
<text class="tiny muted">最佳行业 (喜用神: 土/金)</text>
<view class="chips">
<text v-for="(c, i) in industryChips" :key="i" class="chip">{{ c }}</text>
</view>
</view>
<view class="box">
<text class="tiny muted">财富层次</text>
<text class="bold">{{ wealthAnalysisData?.wealth_level || '-' }}</text>
<text class="micro">{{ shiyeCaiyun || '-' }}</text>
</view>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">婚姻情感剖析</text>
<text class="sst">Love & Marriage</text>
</view>
</view>
<view class="kv">
<view class="kv-row"><text class="muted">婚姻主题</text><text>{{ hunyinQinggan || '-' }}</text></view>
<view class="kv-row"><text class="muted">情感建议</text><text>{{ hunyinQinggan || '-' }}</text></view>
<view class="box-dark">
<text class="gold bold">大师批注:</text>
<text class="tiny muted">{{ hunyinQinggan || '-' }}</text>
</view>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">五行健康易患</text>
<text class="sst">Health Risks</text>
</view>
</view>
<view class="box-dark kv-row"><text class="muted">健康提示</text><text>{{ wuxingJiankang || '-' }}</text></view>
</view>
<view class="card">
<view class="row radar">
<view class="radar-left">
<svg viewBox="0 0 100 100" class="radar-svg">
<polygon points="50,10 90,40 75,90 25,90 10,40" fill="none" stroke="#333" stroke-width="0.5" />
<polygon points="50,25 75,45 65,80 35,80 25,45" fill="none" stroke="#333" stroke-width="0.5" />
<polygon points="50,5 88,38 73,92 27,92 12,38" fill="rgba(212,175,55,0.15)" stroke="#d4af37"
stroke-width="1.5" />
<circle cx="50" cy="5" r="1.5" fill="#d4af37" />
<circle cx="88" cy="38" r="1.5" fill="#d4af37" />
<circle cx="73" cy="92" r="1.5" fill="#d4af37" />
<circle cx="27" cy="92" r="1.5" fill="#d4af37" />
<circle cx="12" cy="38" r="1.5" fill="#d4af37" />
</svg>
</view>
<view class="radar-right">
<view v-for="(b, i) in wealthBars" :key="i" class="bar-item">
<view class="bar-head">
<text class="micro muted">{{ b.label }}</text>
<text class="micro" :class="b.cls">{{ b.score }}分</text>
</view>
<view class="bar-track">
<view class="bar-fill" :style="{ width: b.width, backgroundColor: b.color }" />
</view>
</view>
</view>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">神煞贵人</text>
<text class="sst">Stars & Spirits</text>
</view>
</view>
<view class="grid-2 gap-2">
<view v-for="(s, i) in shenSha" :key="i" class="star-item">
<text class="bold">{{ s.name }}</text>
<text class="tag" :class="s.level === '' ? 'tag-good' : 'tag-mid'">{{ s.pos }}</text>
</view>
</view>
</view>
</view>
<view v-else-if="activeTab === 'year'" class="stack">
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">大运走势</text>
<text class="sst">Luck Cycles</text>
</view>
</view>
<view class="timeline">
<view v-for="(d, i) in dayun" :key="i" class="timeline-item" :class="{ current: d.current }">
<view class="dot" :class="{ on: d.current }" />
<view class="timeline-body">
<view class="timeline-head">
<text class="tl-name serif" :class="{ gold: d.current }">{{ d.name }}大运</text>
<text class="pill">{{ d.range }}</text>
</view>
<text class="tiny muted lh">{{ d.desc }}</text>
<text v-if="d.current" class="current-tag">当前行运</text>
</view>
</view>
</view>
</view>
<view class="card card-grad border-gold">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">太岁将军批语</text>
<text class="sst">Tai Sui General</text>
</view>
</view>
<view class="box-dark">
<text class="tiny muted lh">{{ taisuiText }}</text>
</view>
</view>
<view class="card card-grad border-gold">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">{{ currentYearLabel }} 流年详解</text>
<text class="sst">{{ currentYearLabel }} Annual Analysis</text>
</view>
</view>
<view class="box-dark">
<text class="tiny muted lh">{{ annualText }}</text>
</view>
<view class="annual-chart">
<view class="row annual-chart-head">
<text class="gold bold">{{ currentYearLabel }} 财运起伏</text>
</view>
<view class="annual-bars">
<view v-for="(h, i) in annualBars" :key="i" class="annual-bar">
<view class="annual-bar-fill" :style="{ height: h + '%' }" />
</view>
</view>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">流年神煞盘</text>
<text class="sst">Annual Stars</text>
</view>
</view>
<view class="grid-4 gap-1">
<view v-for="(s, i) in yearlyStars" :key="i" class="star-chip" :class="{ bad: s.bad }">
<text class="micro">{{ s.name }}</text>
</view>
</view>
<view class="note mt-2">
<text class="micro muted lh">{{ liunianStarsText }}</text>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">分人群转运指引</text>
<text class="sst">Advice by Role</text>
</view>
</view>
<view class="role-list">
<view v-for="(r, i) in roleAdvice" :key="i" class="role-item"
:class="{ last: i === roleAdvice.length - 1 }">
<view class="role-icon" :style="{ background: r.bg, color: r.color }"><text class="micro">{{ r.icon
}}</text></view>
<view class="col flex-1">
<text class="bold">{{ r.title }}</text>
<text class="micro muted lh">{{ r.desc }}</text>
</view>
</view>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">流年吉凶方位</text>
<text class="sst">Lucky Directions</text>
</view>
</view>
<view class="grid-2 gap-2">
<view class="box">
<text class="gold bold">大利方位</text>
<text class="micro">{{ liunianLuckyDirs }}</text>
</view>
<view class="box">
<text class="red bold">不利方位</text>
<text class="micro">{{ liunianBadDirs }}</text>
</view>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">财富来源预测</text>
<text class="sst">Income Sources</text>
</view>
</view>
<view class="box-dark">
<text class="tiny muted lh">{{ incomeSourcesText }}</text>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">投资领域吉凶指引</text>
<text class="sst">Investment Guide</text>
</view>
</view>
<view v-if="investmentEntries.length" class="grid-2 gap-2">
<view v-for="(it, idx) in investmentEntries" :key="idx" class="box-i good">
<text class="bold" style="color:#4ade80">{{ it.label }}</text>
<text class="micro muted lh">{{ it.value }}</text>
</view>
</view>
<text v-else class="micro muted">暂无投资领域指引</text>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">{{ currentYearLabel }} 九宫飞星</text>
<text class="sst">Fengshui Flying Stars</text>
</view>
</view>
<view class="flying">
<view class="flying-border" />
<view v-for="(n, i) in flyingStars" :key="i" class="flying-cell">
<text class="serif flying-num" :class="{ gold: n.good }">{{ n.n }}</text>
<text v-if="n.good" class="flying-good">吉</text>
</view>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">流年化解锦囊</text>
<text class="sst">Misfortune Breaker</text>
</view>
</view>
<view class="box-left red-left">
<text class="micro muted lh">{{ liunianHuajieText }}</text>
</view>
</view>
</view>
<view v-else-if="activeTab === 'month'" class="lock-root" >
<view v-if="shouldShowMonthRecharge" class="lock-overlay">
<view class="lock-circle"><text class="lock-circle-text">锁</text></view>
<text class="lock-h">{{ currentYearLabel }}年 12个月运势详批</text>
<view class="lock-p">
<text class="lock-p-text">解锁后获取大师级深度解析</text>
<text class="lock-p-sub">助您趋吉避凶,财运亨通</text>
</view>
<view class="lock-cta" @click="handleUnlockMonth"><text class="lock-cta-text">立即解锁 ¥{{ unlockPriceText }}</text></view>
</view>
<view class="stack" :class="{ blurred: shouldShowMonthRecharge }">
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">流月运势详批</text>
<text class="sst">Monthly Breakdown</text>
</view>
</view>
<text class="gold bold">全年财运趋势总览</text>
<view v-if="monthScoreSummary" class="month-summary">
<view class="month-summary-item">
<text class="month-summary-label">全年均值</text>
<text class="month-summary-value">{{ monthScoreSummary.average }}</text>
</view>
<view class="month-summary-item">
<text class="month-summary-label">最佳月份</text>
<text class="month-summary-value">{{ monthScoreSummary.bestMonth }} · {{ monthScoreSummary.bestScore }}</text>
</view>
<view class="month-summary-item">
<text class="month-summary-label">需谨慎</text>
<text class="month-summary-value">{{ monthScoreSummary.worstMonth }} · {{ monthScoreSummary.worstScore }}</text>
</view>
</view>
<view class="trend">
<view class="trend-bars">
<view v-for="(v, i) in monthlyTrendDisplay" :key="i" class="trend-bar">
<view class="trend-bar-fill" :style="{ height: v + '%' }" />
</view>
</view>
<view class="trend-months">
<text v-for="(m, i) in months" :key="i" class="micro muted">{{ m }}</text>
</view>
</view>
</view>
<view class="card">
<text class="gold bold">月度关键摘要(点击查看详情)</text>
<view v-if="monthDetailRows.length" class="m-table">
<view class="m-tr m-th">
<text class="m-td gold">月份</text>
<text class="m-td gold">干支</text>
<text class="m-td gold flex-1">核心运势判词</text>
<text class="m-td gold center">吉凶</text>
</view>
<view v-for="(r, i) in monthDetailRows" :key="i" class="m-tr clickable" @click="openMonthRowDetail(r)">
<text class="m-td serif bold">{{ r.m }}</text>
<text class="m-td serif gold">{{ r.g }}</text>
<text class="m-td flex-1 muted">{{ r.d }}</text>
<view class="m-td center">
<text class="badge" :class="badgeClass(r.l)">{{ r.l }}</text>
</view>
</view>
</view>
<text v-else class="micro muted">暂无月度摘要数据</text>
</view>
<view class="card">
<text class="gold bold">月度详批全量字段</text>
<view v-if="monthDataGroups.length" class="atlas-summary">
<view class="atlas-stat">
<text class="atlas-stat-value">{{ monthDataGroups.length }}</text>
<text class="atlas-stat-label">主题分组</text>
</view>
<view class="atlas-stat">
<text class="atlas-stat-value">{{ monthFieldCount }}</text>
<text class="atlas-stat-label">字段总量</text>
</view>
<view class="atlas-stat">
<text class="atlas-stat-value">{{ monthDetailRows.length }}</text>
<text class="atlas-stat-label">月度条目</text>
</view>
</view>
<view v-if="monthDataGroups.length" class="atlas-groups">
<view v-for="(group, gidx) in monthDataGroups" :key="`month-group-${gidx}`" class="atlas-group">
<view class="atlas-group-head">
<text class="atlas-group-title">{{ group.title }}</text>
<text class="atlas-group-count">{{ group.rows.length }}项</text>
</view>
<view class="atlas-rows">
<view v-for="(row, ridx) in group.rows" :key="`month-row-${gidx}-${ridx}`" class="atlas-row">
<text class="atlas-key">{{ row.label || '字段' }}</text>
<text class="atlas-value">{{ row.value || '-' }}</text>
</view>
</view>
</view>
</view>
<text v-else class="micro muted">暂无月度详批数据</text>
</view>
</view>
</view>
<view v-else-if="activeTab === 'day'" class="lock-root" >
<view v-if="shouldShowDayRecharge" class="lock-overlay">
<view class="lock-circle"><text class="lock-circle-text">锁</text></view>
<text class="lock-h">365天每日吉凶指南</text>
<view class="lock-p">
<text class="lock-p-text">解锁后获取大师级深度解析</text>
<text class="lock-p-sub">助您趋吉避凶,财运亨通</text>
</view>
<view class="lock-cta" @click="handleUnlockDay"><text class="lock-cta-text">立即解锁 ¥{{ unlockPriceText }}</text></view>
</view>
<view class="stack" :class="{ blurred: shouldShowDayRecharge }">
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">每日运程</text>
<text class="sst">Daily Guide</text>
</view>
</view>
<view class="daily-hero">
<text class="daily-hero-title">{{ dailyCalendarTitle || '今日黄历速览' }}</text>
<text class="daily-hero-sub">{{ dailyFortuneTitle || '每日运程重点提示' }}</text>
</view>
<view class="daily-meta-grid">
<view class="daily-meta-card">
<text class="daily-meta-label">财神方位</text>
<text class="daily-meta-value">{{ dailyCaishen }}</text>
</view>
<view class="daily-meta-card">
<text class="daily-meta-label">喜神方位</text>
<text class="daily-meta-value">{{ dailyXishen }}</text>
</view>
</view>
<view class="daily-yiji-grid">
<view class="daily-yiji-card daily-yiji-good">
<text class="daily-yiji-label">宜</text>
<text class="daily-yiji-value">{{ dailyYi }}</text>
</view>
<view class="daily-yiji-card daily-yiji-bad">
<text class="daily-yiji-label">忌</text>
<text class="daily-yiji-value">{{ dailyJi }}</text>
</view>
</view>
<view class="action">
<text class="gold bold">今日开运建议</text>
<text class="micro muted lh">{{ dailyKaiyun }}</text>
</view>
</view>
<view class="card">
<text class="gold bold">时辰吉凶节奏</text>
<view class="grid-4 gap-1 mt-1">
<view v-for="(t, i) in hourList" :key="i" class="hour" :class="hourClass(t)">
<text class="micro">{{ t }}</text>
</view>
</view>
</view>
<view class="card">
<text class="gold bold">每日运程全量字段</text>
<view v-if="dayDataGroups.length" class="atlas-summary">
<view class="atlas-stat">
<text class="atlas-stat-value">{{ dayDataGroups.length }}</text>
<text class="atlas-stat-label">主题分组</text>
</view>
<view class="atlas-stat">
<text class="atlas-stat-value">{{ dayFieldCount }}</text>
<text class="atlas-stat-label">字段总量</text>
</view>
<view class="atlas-stat">
<text class="atlas-stat-value">{{ hourList.length }}</text>
<text class="atlas-stat-label">时辰条目</text>
</view>
</view>
<view v-if="dayDataGroups.length" class="atlas-groups">
<view v-for="(group, gidx) in dayDataGroups" :key="`day-group-${gidx}`" class="atlas-group">
<view class="atlas-group-head">
<text class="atlas-group-title">{{ group.title }}</text>
<text class="atlas-group-count">{{ group.rows.length }}项</text>
</view>
<view class="atlas-rows">
<view v-for="(row, ridx) in group.rows" :key="`day-row-${gidx}-${ridx}`" class="atlas-row">
<text class="atlas-value">{{ row.value || '-' }}</text>
</view>
</view>
</view>
</view>
<text v-else class="micro muted">暂无每日运程数据</text>
</view>
</view>
</view>
<view v-else-if="activeTab === 'fengshui'" class="stack">
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">贵人画像</text>
<text class="sst">Nobleman Profile</text>
</view>
</view>
<view class="box-dark">
<text class="tiny muted lh">{{ nobleProfileText }}</text>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">常见外局煞气与化解</text>
<text class="sst">External Sha Qi</text>
</view>
</view>
<view class="sha">
<view v-for="(s, i) in shaQi" :key="i" class="sha-row" :class="{ last: i === shaQi.length - 1 }">
<view class="row gap-1">
<text class="red bold">{{ s.name }}</text>
<text class="micro muted">({{ s.desc }})</text>
</view>
<view class="row gap-1" style="align-items:center;">
<text class="micro">→</text>
<text class="micro">{{ s.fix }}</text>
</view>
</view>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">寻龙点穴:家中财位</text>
<text class="sst">Wealth Corners</text>
</view>
</view>
<view class="wealth-corner">
<text class="gold bold">明财位 (入门斜对角)</text>
<view class="action mt-1">
<text class="micro muted lh">{{ mingCaiweiText }}</text>
</view>
<view class="divider" />
<text class="gold bold">暗财位 (八字/房相)</text>
<view class="action mt-1">
<text class="micro muted lh">{{ anCaiweiText }}</text>
</view>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">职场步步高升局</text>
<text class="sst">Career Boost</text>
</view>
</view>
<view class="box-dark">
<text class="micro muted lh">{{ careerBoostText }}</text>
</view>
</view>
<!-- <view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">全屋风水布局</text>
<text class="sst">Room by Room</text>
</view>
</view>
<view class="room">
<view v-for="(r, i) in rooms" :key="i" class="room-row" :class="{ last: i === rooms.length - 1 }">
<view class="room-icon"><text class="micro">{{ r.icon }}</text></view>
<view class="col flex-1">
<view class="row between">
<text class="bold">{{ r.title }}</text>
<text class="room-tag" :class="r.tagCls">{{ r.tag }}</text>
</view>
<text class="micro muted lh">{{ r.desc }}</text>
</view>
</view>
</view>
</view> -->
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">催旺桃花/感情保鲜</text>
<text class="sst">Love & Relationships</text>
</view>
</view>
<view class="box-dark">
<text class="micro muted lh">{{ taohuaText }}</text>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">家居植物</text>
<text class="sst">Fengshui Plants</text>
</view>
</view>
<view class="grid-2 gap-2">
<view v-for="(p, i) in plants" :key="i" class="box-dark">
<text class="bold">{{ p.plant }}</text>
<text class="micro muted">{{ p.place }}</text>
<text v-if="p.effect" class="micro muted">作用:{{ p.effect }}</text>
</view>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">萌宠风水</text>
<text class="sst">Pet Feng Shui</text>
</view>
</view>
<view class="box-dark">
<text class="micro muted lh">{{ petText }}</text>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">数字能量风水</text>
<text class="sst">Digital Energy</text>
</view>
</view>
<view class="grid-2 gap-2">
<view class="box-dark">
<text class="micro muted">手机尾号</text>
<text class="bold">{{ digitalPhoneText }}</text>
</view>
<view class="box-dark">
<text class="micro muted">车牌</text>
<text class="bold">{{ digitalPlateText }}</text>
</view>
</view>
<view class="box mt-2">
<text class="bold">楼层</text>
<text class="micro muted lh">{{ digitalFloorText }}</text>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">爱车平安风水</text>
<text class="sst">Car Energy</text>
</view>
</view>
<view class="box-dark">
<text class="micro muted lh">{{ carText }}</text>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">吸金钱包</text>
<text class="sst">Wallet Fengshui</text>
</view>
</view>
<view class="box-dark">
<text class="micro muted lh">颜色:{{ walletColorText }}</text>
<text class="micro muted lh">质地:{{ walletMaterialText }}</text>
<text class="micro muted lh">使用:{{ walletUsageText }}</text>
</view>
</view>
<view class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="st">幸运色与佩戴</text>
<text class="sst">Color & Accessories</text>
</view>
</view>
<view class="grid-2 gap-2">
<view class="box-dark">
<text class="tiny muted">幸运色</text>
<text class="bold">{{ luckyColorsText }}</text>
</view>
<view class="box-dark">
<text class="tiny muted">开运饰品</text>
<view v-if="accessories.length" class="col gap-1">
<text v-for="(a, i) in accessories" :key="i" class="micro muted lh">- {{ a.name }}{{ a.effect }}</text>
</view>
<text v-else class="micro muted">-</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<view v-if="showDetailModal" class="detail-modal-overlay" @click.stop="closeDetailModal">
<view class="detail-modal" @click.stop="">
<view class="detail-modal-head">
<text class="detail-modal-title">{{ detailModalTitle }}</text>
<view class="detail-modal-close" @click.stop="closeDetailModal"><text class="detail-modal-close-text">×</text>
</view>
</view>
<scroll-view scroll-y class="detail-modal-body">
<view class="detail-modal-content">
<text v-for="(t, i) in detailModalLines" :key="i" class="detail-modal-text">{{ t }}</text>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { getWealthAnalysisByReportId, type WealthAnalysisResponse } from '@/api/cai-yun';
import { checkAllWealthUnlockStatus } from '@/utils/wealth-payment';
import { paymentApi, userApi } from '@/api';
import { HIDE_RECHARGE_FEATURE } from '@/utils/feature-flags';
import pdfBgUrl from "../../utils/pdf/background.png";
const APPID = 'wx1ca1ac7ad12123ac';
const PENDING_KEY = 'wx_pending_payment';
const REPORT_ID_KEY = 'wealth_analysis_report_id';
const REPORT_CACHE_PREFIX = 'wealth_analysis_cache_';
const REPORT_CACHE_LAST_KEY = 'wealth_analysis_cache_last';
// 获取URL参数兼容hash路由
function getUrlCode(): string | null {
let search = window.location.search;
if (search === '' && window.location.hash.indexOf('?') > -1) {
search = '?' + (window.location.hash.split('?')[1] || '');
}
if (!search) return null;
const reg = new RegExp('(^|&)code=([^&]*)(&|$)');
const r = search.substr(1).match(reg);
return r ? decodeURIComponent(r[2]) : null;
}
// 清除URL中的code参数
function cleanCodeFromUrl() {
try {
const url = new URL(window.location.href);
let changed = false;
if (url.searchParams.has('code') || url.searchParams.has('state')) {
url.searchParams.delete('code');
url.searchParams.delete('state');
changed = true;
}
if (url.hash.includes('?')) {
const [hashPath, hashQuery] = url.hash.split('?');
const hashParams = new URLSearchParams(hashQuery || '');
if (hashParams.has('code') || hashParams.has('state')) {
hashParams.delete('code');
hashParams.delete('state');
const nextHashQuery = hashParams.toString();
url.hash = nextHashQuery ? `${hashPath}?${nextHashQuery}` : hashPath;
changed = true;
}
}
if (changed) {
window.history.replaceState(null, '', url.toString());
}
} catch {}
}
// 判断是否在微信浏览器
function isWechat(): boolean {
return /MicroMessenger/i.test(navigator.userAgent);
}
// 保存/读取/清除待支付状态
function savePending(state: any) {
try { localStorage.setItem(PENDING_KEY, JSON.stringify({ ...state, ts: Date.now() })); } catch {}
}
function getPending(): any {
try {
const raw = localStorage.getItem(PENDING_KEY);
if (!raw) return null;
const s = JSON.parse(raw);
if (Date.now() - s.ts > 5 * 60 * 1000) { localStorage.removeItem(PENDING_KEY); return null; }
return s;
} catch { return null; }
}
function clearPending() {
try { localStorage.removeItem(PENDING_KEY); } catch {}
}
const getReportCacheKey = (reportId: number) => `${REPORT_CACHE_PREFIX}${reportId}`;
declare const uni: any;
type TabId = 'destiny' | 'year' | 'month' | 'day' | 'fengshui';
// 接收从详解报告页面传递过来的数据
const props = defineProps<{
data?: any;
}>();
const emit = defineEmits<{
back: [];
}>();
// 响应式数据
const activeTab = ref<TabId>('destiny');
const isMonthUnlocked = ref(false);
const isDayUnlocked = ref(false);
const loading = ref(true);
const wealthAnalysisData = ref<WealthAnalysisResponse | null>(null);
const unlockLoading = ref(false);
const downloadingPdf = ref(false);
const accountRemainQuota = ref<number | null>(null);
const safeJsonParse = (input: any): any => {
if (input === null || input === undefined) return null;
if (typeof input === 'object') return input;
if (typeof input !== 'string') return null;
const text = input.trim();
if (!text) return null;
try {
return JSON.parse(text);
} catch {
return null;
}
};
const isPlainObject = (input: any): input is Record<string, any> => {
return Object.prototype.toString.call(input) === '[object Object]';
};
const tryParseJsonString = (input: any): any => {
if (typeof input !== 'string') return input;
const text = input.trim();
if (!text) return input;
const maybeJson =
(text.startsWith('{') && text.endsWith('}')) ||
(text.startsWith('[') && text.endsWith(']'));
if (!maybeJson) return input;
try {
return JSON.parse(text);
} catch {
return input;
}
};
const deepParseJsonStrings = (input: any, depth = 0): any => {
if (depth > 8) return input;
const parsed = tryParseJsonString(input);
if (Array.isArray(parsed)) {
return parsed.map((item) => deepParseJsonStrings(item, depth + 1));
}
if (isPlainObject(parsed)) {
const next: Record<string, any> = {};
Object.keys(parsed).forEach((key) => {
next[key] = deepParseJsonStrings(parsed[key], depth + 1);
});
return next;
}
return parsed;
};
type RawFieldRow = {
key: string;
value: string;
};
type AtlasRow = {
label: string;
value: string;
};
type AtlasGroup = {
title: string;
rows: AtlasRow[];
};
type MonthDetailRow = {
m: string;
g: string;
d: string;
l: string;
score: number;
raw: any;
};
type MonthScoreSummary = {
average: number;
bestMonth: string;
bestScore: number;
worstMonth: string;
worstScore: number;
};
const clampScore = (score: number): number => {
if (!Number.isFinite(score)) return 55;
return Math.max(0, Math.min(100, Math.round(score)));
};
const scoreFromLuckText = (value: any): number => {
const raw = String(value || '');
const lower = raw.toLowerCase();
if (/[凶衰险败]/.test(raw) || /bad|danger|xiong|negative|low/.test(lower)) return 28;
if (/[吉旺顺福喜]/.test(raw) || /good|auspicious|ji|positive|high/.test(lower)) return 82;
if (/[平中]/.test(raw) || /normal|mid|neutral/.test(lower)) return 55;
return 55;
};
const normalizeMonthLabel = (value: any, fallback = ''): string => {
const text = String(value ?? fallback ?? '').trim();
if (!text) return '';
if (/月/.test(text)) return text;
if (/^\d{1,2}$/.test(text)) return `${text}月`;
return text;
};
const formatDisplayValue = (value: any): string => {
if (Array.isArray(value)) {
return value
.map((item) => formatRawValue(item))
.filter((item) => item !== '')
.join('') || '-';
}
return formatRawValue(value);
};
const formatRawValue = (value: any): string => {
if (value === null) return 'null';
if (value === undefined) return 'undefined';
if (typeof value === 'string') return value;
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return String(value);
try {
return JSON.stringify(value);
} catch {
return String(value);
}
};
const flattenRawFields = (input: any, parentKey = '', rows: RawFieldRow[] = []): RawFieldRow[] => {
const rootKey = parentKey || '(root)';
if (input === null || input === undefined) {
rows.push({ key: rootKey, value: formatRawValue(input) });
return rows;
}
if (typeof input !== 'object') {
rows.push({ key: rootKey, value: formatRawValue(input) });
return rows;
}
if (Array.isArray(input)) {
if (!input.length) {
rows.push({ key: rootKey, value: '[]' });
return rows;
}
input.forEach((item, index) => {
const nextKey = parentKey ? `${parentKey}[${index}]` : `[${index}]`;
flattenRawFields(item, nextKey, rows);
});
return rows;
}
const entries = Object.entries(input);
if (!entries.length) {
rows.push({ key: rootKey, value: '{}' });
return rows;
}
entries.forEach(([key, value]) => {
const nextKey = parentKey ? `${parentKey}.${key}` : key;
if (value !== null && typeof value === 'object') {
flattenRawFields(value, nextKey, rows);
return;
}
rows.push({ key: nextKey, value: formatRawValue(value) });
});
return rows;
};
const KEY_LABEL_MAP: Record<string, string> = {
root: '基础信息',
quannian_gaishu: '全年概述',
zongyun_pinggu: '总运评估',
caiyun_zhuti: '财运主题',
guanjian_yueling: '关键月令',
zhuyao_jixiong: '主要吉凶',
yueling_xiangxi: '月令详析',
ganzhi: '干支',
hexin_yunshi: '核心运势',
jixiong_dengji: '吉凶等级',
caiyun_zhishu: '财运指数',
shiye_zhishu: '事业指数',
jiankang_zhishu: '健康指数',
renmai_zhishu: '人脉指数',
jinri_gaishu: '今日概览',
huangli: '黄历信息',
shichen_jixiong: '时辰吉凶',
yi: '',
ji: '',
caishen: '财神方位',
xishen: '喜神方位',
kaiyun: '开运建议',
tips: '建议',
zhengYue: '正月',
erYue: '二月',
sanYue: '三月',
siYue: '四月',
wuYue: '五月',
liuYue: '六月',
qiYue: '七月',
baYue: '八月',
jiuYue: '九月',
shiYue: '十月',
shiyiYue: '十一月',
shierYue: '十二月',
months: '月份列表',
trend: '趋势',
monthly_trend: '月度趋势',
month_detail: '月度详批',
month_details: '月度详批',
monthly_details: '月度详批',
details: '详批内容',
items: '条目',
rows: '行数据',
list: '列表',
today: '今日',
current: '当前',
daily: '每日',
data: '数据',
result: '结果',
title: '标题',
calendar_title: '日历标题',
fortune_title: '运程标题',
month_title: '月份标题',
weekdays: '星期',
hours: '时辰',
hour_list: '时辰列表',
caishen_fangwei: '财神方位',
xishen_fangwei: '喜神方位',
kaiyun_tip: '开运提示',
month_name: '月份',
monthLabel: '月份',
score: '评分',
level: '等级',
desc: '详解',
text: '文本',
};
const pathSegments = (path: string): string[] => {
return path.match(/[^.[\]]+|\[\d+\]/g) || [];
};
const prettySegment = (segment: string): string => {
if (!segment) return '';
const normalized = segment.replace(/^\[(\d+)\]$/, '第$1项').replace(/\[(\d+)\]/g, ' 第$1项');
if (KEY_LABEL_MAP[normalized]) return KEY_LABEL_MAP[normalized];
const snake = normalized.replace(/([a-z])([A-Z])/g, '$1_$2');
return snake
.split('_')
.filter(Boolean)
.map((part) => KEY_LABEL_MAP[part] || part)
.join(' ');
};
const buildAtlasGroups = (rows: RawFieldRow[]): AtlasGroup[] => {
const groups = new Map<string, AtlasGroup>();
rows.forEach((row) => {
const segs = pathSegments(row.key);
const groupKey = segs[0] || 'root';
if (!groups.has(groupKey)) {
groups.set(groupKey, {
title: prettySegment(groupKey),
rows: [],
});
}
const rest = segs.length > 1 ? segs.slice(1) : [];
const label = rest.length ? rest.map(prettySegment).join(' / ') : prettySegment(groupKey);
groups.get(groupKey)!.rows.push({ label, value: row.value });
});
return Array.from(groups.values()).filter((g) => g.rows.length > 0);
};
const yuedoXiangpiData = computed(() => {
const raw = (wealthAnalysisData.value as any)?.yuedo_xiangpi;
const parsed = safeJsonParse(raw) || raw || null;
return deepParseJsonStrings(parsed);
});
const meiriYunchengData = computed(() => {
const raw = (wealthAnalysisData.value as any)?.meiri_yuncheng;
const parsed = safeJsonParse(raw) || raw || null;
return deepParseJsonStrings(parsed);
});
const monthRawRows = computed<RawFieldRow[]>(() => {
return flattenRawFields(yuedoXiangpiData.value);
});
const dayRawRows = computed<RawFieldRow[]>(() => {
return flattenRawFields(meiriYunchengData.value);
});
const monthDataGroups = computed<AtlasGroup[]>(() => {
return buildAtlasGroups(monthRawRows.value);
});
const dayDataGroups = computed<AtlasGroup[]>(() => {
return buildAtlasGroups(dayRawRows.value);
});
const monthDetailRows = computed<MonthDetailRow[]>(() => {
const src = yuedoXiangpiData.value;
const detailRoot =
src?.yueling_xiangxi ||
src?.yuelingXiangxi ||
src?.monthly_details ||
src?.month_detail ||
src?.details;
const toRow = (input: any, idx: number, fallbackMonth = ''): MonthDetailRow => {
const item = isPlainObject(input) ? input : { value: input };
const monthText = normalizeMonthLabel(
item?.m ??
item?.month ??
item?.yue ??
item?.yuefen ??
item?.month_name ??
item?.monthLabel ??
fallbackMonth,
`${idx + 1}月`,
) || `${idx + 1}月`;
const descFallback = item?.value ?? input;
return {
m: monthText,
g: String(item?.g ?? item?.ganzhi ?? item?.gan_zhi ?? item?.gz ?? item?.ganZhi ?? '-'),
d: String(item?.d ?? item?.desc ?? item?.core ?? item?.panci ?? item?.text ?? item?.hexin_yunshi ?? formatDisplayValue(descFallback) ?? '-'),
l: String(item?.l ?? item?.level ?? item?.jixiong ?? item?.luck ?? item?.tag ?? item?.jixiong_dengji ?? '-'),
score: Number(item?.caiyun_zhishu ?? item?.score ?? item?.index ?? Number.NaN),
raw: input,
};
};
if (Array.isArray(detailRoot) && detailRoot.length) {
return detailRoot.map((item, idx) => toRow(item, idx));
}
if (isPlainObject(detailRoot)) {
return Object.entries(detailRoot).map(([key, item], idx) => {
const normalized = isPlainObject(item) ? item : { value: item };
return toRow(normalized, idx, prettySegment(key));
});
}
if (Array.isArray(src)) {
return src.map((item: any, idx: number) => toRow(item, idx));
}
const list = src?.items || src?.rows || src?.list;
if (Array.isArray(list) && list.length) {
return list.map((item: any, idx: number) => toRow(item, idx));
}
return [];
});
const months = computed(() => {
if (monthDetailRows.value.length) {
return monthDetailRows.value.map((row) => normalizeMonthLabel(row.m, row.m));
}
const fromApi = yuedoXiangpiData.value?.months;
if (Array.isArray(fromApi) && fromApi.length) return fromApi.map((m: any) => normalizeMonthLabel(m, String(m)));
return ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'];
});
const monthlyRows = computed(() => {
return monthDetailRows.value.map((row) => ({
m: row.m,
g: row.g,
d: row.d,
l: row.l,
}));
});
const monthlyTrend = computed(() => {
const src = yuedoXiangpiData.value;
const trend = src?.trend || src?.monthly_trend || src?.scores;
if (Array.isArray(trend) && trend.length) {
return trend.map((v: any) => clampScore(Number(v)));
}
return Array.from({ length: Math.max(monthlyRows.value.length || 0, 12) }).map(() => 55);
});
const monthlyTrendDisplay = computed(() => {
if (monthDetailRows.value.length) {
return monthDetailRows.value.map((row, index) => {
if (Number.isFinite(row.score)) {
return clampScore(Number(row.score));
}
if (Number.isFinite(monthlyTrend.value[index])) {
return clampScore(Number(monthlyTrend.value[index]));
}
return scoreFromLuckText(row.l);
});
}
return monthlyTrend.value;
});
const monthScoreSummary = computed<MonthScoreSummary | null>(() => {
const scores = monthlyTrendDisplay.value;
if (!scores.length) return null;
let bestIndex = 0;
let worstIndex = 0;
let total = 0;
scores.forEach((score, index) => {
const n = clampScore(score);
total += n;
if (n > clampScore(scores[bestIndex])) bestIndex = index;
if (n < clampScore(scores[worstIndex])) worstIndex = index;
});
return {
average: Math.round(total / scores.length),
bestMonth: String(months.value[bestIndex] || `${bestIndex + 1}月`),
bestScore: clampScore(scores[bestIndex]),
worstMonth: String(months.value[worstIndex] || `${worstIndex + 1}月`),
worstScore: clampScore(scores[worstIndex]),
};
});
const monthFieldCount = computed(() => monthRawRows.value.length);
const dayFieldCount = computed(() => dayRawRows.value.length);
const dailyView = computed(() => {
const src = meiriYunchengData.value;
if (Array.isArray(src)) return src[0] || {};
if (!isPlainObject(src)) return src || {};
const pick = src?.today || src?.current || src?.daily || src?.today_data || src?.data || src?.result || src;
if (isPlainObject(pick)) return pick;
return src;
});
const dailyCalendarTitle = computed(() => {
return String(dailyView.value?.calendar_title || dailyView.value?.calendarTitle || dailyView.value?.month_title || dailyView.value?.monthTitle || '');
});
const dailyFortuneTitle = computed(() => {
return String(dailyView.value?.title || dailyView.value?.fortune_title || dailyView.value?.fortuneTitle || '');
});
const dailyCaishen = computed(() => {
return String(dailyView.value?.caishen_fangwei || dailyView.value?.caishen_fangwei || dailyView.value?.caishenFangwei || '-');
});
const dailyXishen = computed(() => {
return String(dailyView.value?.xishen || dailyView.value?.xishen_fangwei || dailyView.value?.xishenFangwei || '-');
});
const dailyYi = computed(() => {
const v = dailyView.value?.yi || dailyView.value?.suitable || dailyView.value?.宜;
return formatDisplayValue(v || '-');
});
const dailyJi = computed(() => {
const v = dailyView.value?.ji || dailyView.value?.avoid || dailyView.value?.忌;
return formatDisplayValue(v || '-');
});
const hourList = computed(() => {
const v = dailyView.value?.hours || dailyView.value?.hour_list || dailyView.value?.hourList || dailyView.value?.shichen;
if (Array.isArray(v) && v.length) {
return v.map((x: any) => {
if (typeof x === 'string' || typeof x === 'number') return String(x);
if (isPlainObject(x)) {
const time = String(x.time ?? x.hour ?? x.name ?? x.label ?? x.shichen ?? '');
const status = String(x.level ?? x.status ?? x.jixiong ?? x.fortune ?? x.result ?? '');
if (time && status) return `${time}·${status}`;
return time || status || formatDisplayValue(x);
}
return formatDisplayValue(x);
});
}
return ['·', '·', '·', '·', '·', '·', '·', '·'];
});
const dailyKaiyun = computed(() => {
return formatDisplayValue(dailyView.value?.kaiyun || dailyView.value?.tips || dailyView.value?.kaiyun_tip || dailyView.value?.kaiyunTip || '-');
});
const saveReportCache = (reportId: number, payload: WealthAnalysisResponse | null | undefined) => {
if (!reportId || !payload) return;
try {
uni.setStorageSync(getReportCacheKey(reportId), payload);
uni.setStorageSync(REPORT_CACHE_LAST_KEY, { id: reportId, payload, ts: Date.now() });
} catch (error) {
console.error('缓存财运数据失败:', error);
}
};
const readReportCache = (reportId?: number): WealthAnalysisResponse | null => {
try {
if (reportId) {
const byId = uni.getStorageSync(getReportCacheKey(reportId));
if (byId) return deepParseJsonStrings(byId) as WealthAnalysisResponse;
}
const last = uni.getStorageSync(REPORT_CACHE_LAST_KEY);
if (last?.payload && (!reportId || Number(last.id || 0) === reportId)) {
return deepParseJsonStrings(last.payload) as WealthAnalysisResponse;
}
} catch (error) {
console.error('读取财运缓存失败:', error);
}
return null;
};
const getReportId = (): number => {
const idFromProps = Number(props.data?.id || 0);
if (idFromProps) return idFromProps;
const pendingId = Number(getPending()?.bizId || 0);
if (pendingId) return pendingId;
try {
return Number(uni.getStorageSync(REPORT_ID_KEY) || 0);
} catch {
return 0;
}
};
const restoreUnlockStatus = () => {
const reportId = getReportId();
if (!reportId) return;
const status = checkAllWealthUnlockStatus(reportId);
if (status.monthly) isMonthUnlocked.value = true;
if (status.daily) isDayUnlocked.value = true;
};
// 支付流程获取code → createOrder → WeixinJSBridge支付
const doDirectPay = async (desc: string, amount: number, bizId: number, unlockType: string, tab: string): Promise<boolean> => {
if (!isWechat()) {
uni.showToast({ title: '请在微信中打开', icon: 'none' });
return false;
}
const code = getUrlCode();
if (!code) {
// A. 没有code保存状态后跳微信授权
savePending({ tab, bizId, unlockType, amount });
const local = window.location.href;
const redirectUri = encodeURIComponent(local);
window.location.href = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${APPID}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect`;
return false;
}
// B. 有code立即清除防重复使用
cleanCodeFromUrl();
clearPending();
console.log('[支付] code:', code);
try {
uni.showLoading({ title: '创建订单中...' });
const orderRes = await paymentApi.createOrder({
description: desc,
total_amount: amount,
business_type: 'wealth_analysis',
business_id: bizId,
pay_type: 'jsapi',
code,
});
uni.hideLoading();
if (!orderRes?.appId || !orderRes?.paySign) {
uni.showToast({ title: '获取支付参数失败', icon: 'none' });
console.error('[支付] orderRes缺少支付参数:', JSON.stringify(orderRes));
return false;
}
// WeixinJSBridge 调起JSAPI支付参数直接在orderRes上
const pp = orderRes;
return await new Promise<boolean>((resolve) => {
const invoke = () => {
(window as any).WeixinJSBridge.invoke('getBrandWCPayRequest', {
appId: pp.appId,
timeStamp: pp.timeStamp,
nonceStr: pp.nonceStr,
package: pp.package,
signType: pp.signType || 'RSA',
paySign: pp.paySign,
}, (res: any) => {
if (res.err_msg === 'get_brand_wcpay_request:ok') {
uni.showToast({ title: '支付成功', icon: 'success' });
resolve(true);
} else {
uni.showToast({ title: res.err_msg === 'get_brand_wcpay_request:cancel' ? '已取消' : '支付失败', icon: 'none' });
resolve(false);
}
});
};
if (typeof (window as any).WeixinJSBridge === 'undefined') {
document.addEventListener('WeixinJSBridgeReady', invoke, false);
} else {
invoke();
}
});
} catch (error: any) {
uni.hideLoading();
uni.showToast({ title: error.msg || '创建订单失败', icon: 'none' });
return false;
}
};
const handleUnlockMonth = async () => {
if (unlockLoading.value) return;
if (!isQuotaZero.value) { uni.showToast({ title: '当前额度大于0无需充值', icon: 'none' }); return; }
const id = getReportId();
if (!id) { uni.showToast({ title: '缺少报告ID', icon: 'none' }); return; }
unlockLoading.value = true;
try {
if (await doDirectPay('财运解析-12个月运势详批', unlockPrice.value, id, 'monthly', 'month')) {
isMonthUnlocked.value = true;
uni.setStorageSync(`wealth_unlock_monthly_${id}`, true);
}
} finally { unlockLoading.value = false; }
};
const handleUnlockDay = async () => {
if (unlockLoading.value) return;
if (!isQuotaZero.value) { uni.showToast({ title: '当前额度大于0无需充值', icon: 'none' }); return; }
const id = getReportId();
if (!id) { uni.showToast({ title: '缺少报告ID', icon: 'none' }); return; }
unlockLoading.value = true;
try {
if (await doDirectPay('财运解析-365天每日吉凶指南', unlockPrice.value, id, 'daily', 'day')) {
isDayUnlocked.value = true;
uni.setStorageSync(`wealth_unlock_daily_${id}`, true);
}
} finally { unlockLoading.value = false; }
};
const unlockPrice = computed(() => {
const raw = Number((wealthAnalysisData.value as any)?.unlock_price);
return Number.isFinite(raw) && raw > 0 ? raw : 19.9;
});
const unlockPriceText = computed(() => unlockPrice.value.toFixed(1));
const rechargeFeatureVisible = computed(() => HIDE_RECHARGE_FEATURE !== true);
const isQuotaZero = computed(() => Number(accountRemainQuota.value) === 0);
const shouldShowMonthRecharge = computed(() => rechargeFeatureVisible.value && isQuotaZero.value && !isMonthUnlocked.value);
const shouldShowDayRecharge = computed(() => rechargeFeatureVisible.value && isQuotaZero.value && !isDayUnlocked.value);
const loadMyQuota = async () => {
try {
const quotaRes: any = await userApi.getMyMembershipQuota({ clearAuthOnError: false, showError: false });
const freeRename = Number(quotaRes?.free_rename_quota);
const remainRaw = quotaRes?.remaining_quota ?? quotaRes?.remain_quota ?? quotaRes?.left_quota;
const remainFallback = Number(remainRaw);
const remain = Number.isFinite(freeRename) ? freeRename : remainFallback;
accountRemainQuota.value = Number.isFinite(remain) ? remain : 0;
} catch {
accountRemainQuota.value = null;
}
};
// Tab列表
const tabList = computed(() => {
return [
{ id: 'destiny' as const, label: '命盘精批', icon: '', locked: false },
{ id: 'year' as const, label: '流年总运', icon: '', locked: false },
{ id: 'month' as const, label: '月度详批', icon: '', locked: !isMonthUnlocked.value },
{ id: 'day' as const, label: '每日运程', icon: '', locked: !isDayUnlocked.value },
{ id: 'fengshui' as const, label: '风水锦囊', icon: '', locked: false }
];
});
// 从接口数据中提取八字排盘信息
const baziPillars = computed(() => {
if (!wealthAnalysisData.value?.mingpan_jingpi?.bazi_paipan) {
return [];
}
const bazi = wealthAnalysisData.value.mingpan_jingpi.bazi_paipan;
return [
{ title: '年柱', gan: bazi.nian.gan, zhi: bazi.nian.zhi, god: '正官', hidden: [], stage: '' },
{ title: '月柱', gan: bazi.yue.gan, zhi: bazi.yue.zhi, god: '正印', hidden: [], stage: '帝旺' },
{ title: '日柱', gan: bazi.ri.gan, zhi: bazi.ri.zhi, god: '日主', hidden: [], stage: '', active: true },
{ title: '时柱', gan: bazi.shi.gan, zhi: bazi.shi.zhi, god: '七杀', hidden: [], stage: '' }
];
});
// 从接口数据中提取五行能量分布
const elementScores = computed(() => {
if (!wealthAnalysisData.value?.mingpan_jingpi?.wuxing_nengliang) {
return [];
}
const wuxing = wealthAnalysisData.value.mingpan_jingpi.wuxing_nengliang;
const elements = [
{ label: '', score: wuxing.jin, color: '#e5e7eb' },
{ label: '', score: wuxing.mu, color: '#22c55e' },
{ label: '', score: wuxing.shui, color: '#3b82f6' },
{ label: '', score: wuxing.huo, color: '#ef4444' },
{ label: '', score: wuxing.tu, color: '#ca8a04' }
];
return elements.map(e => ({
label: `${e.label} (${e.score > 3 ? '' : e.score > 1 ? '' : ''})`,
score: e.score.toFixed(1),
width: `${Math.min(100, e.score * 20)}%`,
color: e.color
}));
});
// 基础信息
const basicInfo = computed(() => {
if (!wealthAnalysisData.value?.mingpan_jingpi) {
return {
mingzao: '',
zhenTaiyang: '',
nongli: ''
};
}
const mingpan = wealthAnalysisData.value.mingpan_jingpi;
return {
mingzao: mingpan.mingzao || '',
zhenTaiyang: mingpan.zhen_taiyang_shi || '',
nongli: mingpan.nongli_shengchen || ''
};
});
// 前世今生因果
const karmaInfo = computed(() => {
if (!wealthAnalysisData.value?.mingpan_jingpi) {
return {
qianshi: '',
jinsheng: ''
};
}
const mingpan = wealthAnalysisData.value.mingpan_jingpi;
return {
qianshi: mingpan.qianshi_yinji || '',
jinsheng: mingpan.jinsheng_keti || ''
};
});
// 命格层次
const minggeInfo = computed(() => {
return wealthAnalysisData.value?.mingpan_jingpi?.mingge_cengci || '';
});
// 古籍断语
const gujiInfo = computed(() => {
return wealthAnalysisData.value?.mingpan_jingpi?.guji_duanyu || '';
});
// 奇门遁甲
const qimenInfo = computed(() => {
if (!wealthAnalysisData.value?.mingpan_jingpi) {
return {
paipan: '',
geju: ''
};
}
const mingpan = wealthAnalysisData.value.mingpan_jingpi;
return {
paipan: mingpan.qimen_paipan || '',
geju: mingpan.qimen_geju || ''
};
});
// 大师批注
const dashiPizhu = computed(() => {
return wealthAnalysisData.value?.mingpan_jingpi?.dashi_pizhu || '';
});
// 五行开运指南
const wuxingKaiyun = computed(() => {
return wealthAnalysisData.value?.mingpan_jingpi?.wuxing_kaiyun || '';
});
// 十神性格盲点
const shishenMangdian = computed(() => {
return wealthAnalysisData.value?.mingpan_jingpi?.shishen_mangdian || '';
});
// 事业财运定数
const shiyeCaiyun = computed(() => {
return wealthAnalysisData.value?.mingpan_jingpi?.shiye_caiyun_dingshu || '';
});
// 婚姻情感剖析
const hunyinQinggan = computed(() => {
return wealthAnalysisData.value?.mingpan_jingpi?.hunyin_qinggan || '';
});
// 五行健康易患
const wuxingJiankang = computed(() => {
return wealthAnalysisData.value?.mingpan_jingpi?.wuxing_jiankang || '';
});
// 神煞贵人
const shenshaGuiren = computed(() => {
return wealthAnalysisData.value?.mingpan_jingpi?.shensha_guiren || '';
});
// 流年相关数据
const liunianData = computed(() => {
if (!wealthAnalysisData.value?.liunian_zongyun) {
return {
dayunZoushi: '',
taisuiJiangjun: '',
dangnianLiunian: '',
liunianShensha: '',
fenquntiZhuanyun: { qingnian: '', zhongnian: '', laonian: '' },
jixiongFangwei: { ji: [], xiong: [] },
caifuLayuan: '',
touziLingyu: { fangdichan: '', gupiao: '', jijin: '', huangjin: '' },
jiugongFeixing: '',
liunianHuajie: ''
};
}
return wealthAnalysisData.value.liunian_zongyun;
});
// 风水锦囊数据
const fengshuiData = computed(() => {
if (!wealthAnalysisData.value?.fengshui_jinnang) {
return {
guirenHuaxiang: '',
waijuShaji: { lu_chong: '', jian_dao_sha: '', fan_gong_sha: '' },
jiajuCaiwei: { ming_caiwei: '', an_caiwei: '' },
zhichangGaosheng: '',
cuiwangTaohua: '',
jiajuZhiwu: [],
mengchongFengshui: '',
shuziNengliang: { shouji_hao: '', che_pai: '', lou_ceng: '' },
aichePingan: '',
xijinQianbao: { yanse: '', zhidi: '', shiyong: '' },
xingyunSePeidai: { xingyun_se: [], peidai_shipin: [] }
};
}
return wealthAnalysisData.value.fengshui_jinnang;
});
// 加载财运解析数据
const loadWealthAnalysis = async () => {
let reportId = Number(props.data?.id || 0);
// 上一页已预取接口数据:直接使用,同时落地缓存,避免支付回跳后丢失
if (props.data?.wealthData) {
const payload = deepParseJsonStrings(props.data.wealthData) as WealthAnalysisResponse;
if (reportId) {
try {
uni.setStorageSync(REPORT_ID_KEY, reportId);
} catch (error) {
console.error('存储报告ID到本地失败:', error);
}
saveReportCache(reportId, payload);
}
wealthAnalysisData.value = payload;
loading.value = false;
return;
}
// 先尝试从待支付上下文恢复ID再回退到本地ID
if (!reportId) {
reportId = Number(getPending()?.bizId || 0);
}
if (!reportId) {
try {
const storedId = uni.getStorageSync(REPORT_ID_KEY);
if (storedId) {
reportId = Number(storedId);
}
} catch (error) {
console.error('获取本地存储的报告ID失败:', error);
}
}
if (reportId) {
try {
uni.setStorageSync(REPORT_ID_KEY, reportId);
} catch (error) {
console.error('存储报告ID到本地失败:', error);
}
}
// 先用缓存兜底渲染,避免支付取消/回跳后页面空白
const cached = readReportCache(reportId || undefined);
if (cached) {
wealthAnalysisData.value = cached;
}
if (!reportId) {
if (wealthAnalysisData.value) {
loading.value = false;
return;
}
uni.showToast({ title: '缺少报告ID', icon: 'none' });
loading.value = false;
return;
}
try {
loading.value = true;
const response = await getWealthAnalysisByReportId(reportId);
const parsed = deepParseJsonStrings(response) as WealthAnalysisResponse;
wealthAnalysisData.value = parsed;
saveReportCache(reportId, parsed);
} catch (error) {
if (!wealthAnalysisData.value) {
uni.showToast({ title: '加载失败请重试', icon: 'none' });
}
} finally {
loading.value = false;
}
};
// 模态框相关
const showDetailModal = ref(false);
const detailModalTitle = ref('');
const detailModalLines = ref<string[]>([]);
const openDetailModal = (title: string, lines: string[]) => {
detailModalTitle.value = title;
detailModalLines.value = lines;
showDetailModal.value = true;
};
const closeDetailModal = () => {
showDetailModal.value = false;
detailModalTitle.value = '';
detailModalLines.value = [];
};
const openMonthRowDetail = (r: { m: string; g: string; d: string; l: string; raw?: any }) => {
const lines = [
`吉凶等级:${r.l}`,
r.d,
...flattenRawFields(r.raw || r)
.filter((item) => item.key !== '(root)')
.map((item) => `${item.key}: ${item.value}`)
];
openDetailModal(`${r.m} · ${r.g}`, lines);
};
const stars = ref(
Array.from({ length: 36 }).map((_, i) => ({
id: i,
top: `${Math.random() * 100}%`,
left: `${Math.random() * 100}%`,
size: Math.random() * 2 + 1,
duration: Math.random() * 3 + 2,
delay: Math.random() * 2
}))
);
const currentYearLabel = computed(() => String(new Date().getFullYear()));
const splitTokens = (input: any): string[] => {
return String(input || '')
.split(/[\n,、;;。|/]+/g)
.map((s) => s.trim())
.filter(Boolean);
};
// 奇门遁甲九宫格(优先读接口 qimen_paipan
const qimenCells = computed(() => {
const raw = safeJsonParse(qimenInfo.value?.paipan) || qimenInfo.value?.paipan;
const parsed = deepParseJsonStrings(raw);
if (Array.isArray(parsed) && parsed.length >= 9) return parsed.slice(0, 9);
if (Array.isArray(parsed?.cells) && parsed.cells.length >= 9) return parsed.cells.slice(0, 9);
return [];
});
// 五行开运指南(从接口文本拆分)
const elementAdvice = computed(() => {
const lines = splitTokens(wuxingKaiyun.value);
return lines.map((line, idx) => {
const [left, right] = line.split(/[:]/);
return { k: left || `建议${idx + 1}`, good: right || line, bad: '-' };
});
});
// 事业行业标签
const industryChips = computed(() => {
const chips = splitTokens(shiyeCaiyun.value);
return chips.slice(0, 12);
});
// 财运雷达柱状(按月度趋势/投资字段生成)
const wealthBars = computed(() => {
const sourceScores = monthlyTrendDisplay.value.slice(0, 5);
const labels = ['赚钱能力', '投资眼光', '守财能力', '抗风险力', '偏财运势'];
if (sourceScores.length) {
return sourceScores.map((score, i) => ({
label: labels[i] || `维度${i + 1}`,
score,
cls: score >= 80 ? 'gold' : '',
width: `${clampScore(score)}%`,
color: ['#d4af37', '#3b82f6', '#22c55e', '#f59e0b', '#c084fc'][i % 5],
}));
}
const fields = (liunianData.value as any)?.touzi_lingyu_zhiyin || {};
return Object.entries(fields).slice(0, 5).map(([k, v], i) => {
const score = scoreFromLuckText(v);
return {
label: prettySegment(k),
score,
cls: score >= 80 ? 'gold' : '',
width: `${clampScore(score)}%`,
color: ['#d4af37', '#3b82f6', '#22c55e', '#f59e0b', '#c084fc'][i % 5],
};
});
});
// 神煞贵人
const shenSha = computed(() => {
const stars = splitTokens(shenshaGuiren.value);
return stars.map((name) => ({ name, level: /凶|煞|灾|病/.test(name) ? '' : '', pos: '-' }));
});
// 大运走势
const dayun = computed(() => {
const src: any = liunianData.value as any;
const text = src?.dayun_zoushi || src?.dayunZoushi || '';
return splitTokens(text).map((line, idx) => ({
name: line.slice(0, 6),
range: `阶段${idx + 1}`,
desc: line,
current: idx === 0,
}));
});
// 流年财运柱状
const annualBars = computed(() => {
return monthlyTrendDisplay.value.length ? monthlyTrendDisplay.value : monthlyTrend.value;
});
// 流年神煞
const yearlyStars = computed(() => {
const src: any = liunianData.value as any;
return splitTokens(src?.liunian_shensha || src?.liunianShensha || '').map((name) => ({
name,
bad: /凶|煞|灾|病|丧|刑/.test(name),
}));
});
// 分人群转运
const roleAdvice = computed(() => {
const src: any = liunianData.value as any;
const source = src?.fenqunti_zhuanyun || src?.fenquntiZhuanyun || {};
const entries = Object.entries(source).filter(([, v]) => String(v || '').trim());
const icons = ['💼', '🎓', '🏠', '📈'];
const bgs = [
'linear-gradient(135deg,#d4af37,#b8860b)',
'linear-gradient(135deg,#3b82f6,#1e40af)',
'linear-gradient(135deg,#22c55e,#15803d)',
'linear-gradient(135deg,#c084fc,#7c3aed)',
];
return entries.map(([k, v], i) => ({
icon: icons[i % icons.length],
bg: bgs[i % bgs.length],
color: '#fff',
title: prettySegment(k),
desc: String(v || ''),
}));
});
// 九宫飞星
const flyingStars = computed(() => {
const src: any = liunianData.value as any;
const nums = String(src?.dangnian_jiugong_feixing || src?.jiugongFeixing || '').match(/\d/g) || [];
return nums.slice(0, 9).map((n) => ({ n: Number(n), good: ['1', '4', '6', '8', '9'].includes(n) }));
});
// 外局煞气
const shaQi = computed(() => {
const feng: any = fengshuiData.value as any;
const src = feng?.waiju_shaji_huajie || feng?.waijuShaji || {};
return Object.entries(src).map(([k, v]) => ({
name: prettySegment(k),
desc: '-',
fix: String(v || '-'),
}));
});
// 家居植物
const plants = computed(() => {
const feng: any = fengshuiData.value as any;
const src = Array.isArray(feng?.jiaju_zhiwu) ? feng.jiaju_zhiwu : Array.isArray(feng?.jiajuZhiwu) ? feng.jiajuZhiwu : [];
return src.map((p: any) => ({
place: String(p?.position || p?.place || '-'),
plant: String(p?.name || p?.plant || '-'),
effect: String(p?.effect || p?.desc || '').trim(),
}));
});
const taisuiText = computed(() => {
const src: any = liunianData.value as any;
return String(src?.taisui_jiangjun || src?.taisuiJiangjun || '-') || '-';
});
const annualText = computed(() => {
const src: any = liunianData.value as any;
return String(src?.dangnian_liunian || src?.dangnianLiunian || '-') || '-';
});
const liunianStarsText = computed(() => {
const src: any = liunianData.value as any;
return String(src?.liunian_shensha || src?.liunianShensha || '-') || '-';
});
const liunianHuajieText = computed(() => {
const src: any = liunianData.value as any;
return String(src?.liunian_huajie || src?.liunianHuajie || '-') || '-';
});
const liunianLuckyDirs = computed(() => {
const src: any = liunianData.value as any;
const list = src?.liunian_jixiong_fangwei?.ji || src?.jixiongFangwei?.ji || [];
return Array.isArray(list) && list.length ? list.join('') : '-';
});
const liunianBadDirs = computed(() => {
const src: any = liunianData.value as any;
const list = src?.liunian_jixiong_fangwei?.xiong || src?.jixiongFangwei?.xiong || [];
return Array.isArray(list) && list.length ? list.join('') : '-';
});
const incomeSourcesText = computed(() => {
const src: any = liunianData.value as any;
return String(src?.caifu_laiyuan || src?.caifuLayuan || '-') || '-';
});
const investmentEntries = computed(() => {
const src: any = liunianData.value as any;
const obj = src?.touzi_lingyu_zhiyin || src?.touziLingyu || {};
if (!obj || typeof obj !== 'object') return [];
return Object.entries(obj)
.filter(([, v]) => String(v || '').trim())
.map(([k, v]) => ({ label: prettySegment(k), value: String(v || '') }));
});
const nobleProfileText = computed(() => String((fengshuiData.value as any)?.guiren_huaxiang || (fengshuiData.value as any)?.guirenHuaxiang || '-') || '-');
const mingCaiweiText = computed(() => String((fengshuiData.value as any)?.jiaju_caiwei?.ming_caiwei || (fengshuiData.value as any)?.jiajuCaiwei?.ming_caiwei || (fengshuiData.value as any)?.jiajuCaiwei?.mingCaiwei || '-') || '-');
const anCaiweiText = computed(() => String((fengshuiData.value as any)?.jiaju_caiwei?.an_caiwei || (fengshuiData.value as any)?.jiajuCaiwei?.an_caiwei || (fengshuiData.value as any)?.jiajuCaiwei?.anCaiwei || '-') || '-');
const careerBoostText = computed(() => String((fengshuiData.value as any)?.zhichang_gaosheng || (fengshuiData.value as any)?.zhichangGaosheng || '-') || '-');
const taohuaText = computed(() => String((fengshuiData.value as any)?.cuiwang_taohua || (fengshuiData.value as any)?.cuiwangTaohua || '-') || '-');
const petText = computed(() => String((fengshuiData.value as any)?.mengchong_fengshui || (fengshuiData.value as any)?.mengchongFengshui || '-') || '-');
const digitalPhoneText = computed(() => String((fengshuiData.value as any)?.shuzi_nengliang?.shouji_hao || (fengshuiData.value as any)?.shuziNengliang?.shouji_hao || (fengshuiData.value as any)?.shuziNengliang?.shoujiHao || '-') || '-');
const digitalPlateText = computed(() => String((fengshuiData.value as any)?.shuzi_nengliang?.che_pai || (fengshuiData.value as any)?.shuziNengliang?.che_pai || (fengshuiData.value as any)?.shuziNengliang?.chePai || '-') || '-');
const digitalFloorText = computed(() => String((fengshuiData.value as any)?.shuzi_nengliang?.lou_ceng || (fengshuiData.value as any)?.shuziNengliang?.lou_ceng || (fengshuiData.value as any)?.shuziNengliang?.louCeng || '-') || '-');
const carText = computed(() => String((fengshuiData.value as any)?.aiche_pingan || (fengshuiData.value as any)?.aichePingan || '-') || '-');
const walletColorText = computed(() => String((fengshuiData.value as any)?.xijin_qianbao?.yanse || (fengshuiData.value as any)?.xijinQianbao?.yanse || (fengshuiData.value as any)?.xijinQianbao?.color || '-') || '-');
const walletMaterialText = computed(() => String((fengshuiData.value as any)?.xijin_qianbao?.zhidi || (fengshuiData.value as any)?.xijinQianbao?.zhidi || (fengshuiData.value as any)?.xijinQianbao?.material || '-') || '-');
const walletUsageText = computed(() => String((fengshuiData.value as any)?.xijin_qianbao?.shiyong || (fengshuiData.value as any)?.xijinQianbao?.shiyong || (fengshuiData.value as any)?.xijinQianbao?.usage || '-') || '-');
const luckyColorsText = computed(() => {
const v = (fengshuiData.value as any)?.xingyun_se_peidai?.xingyun_se || (fengshuiData.value as any)?.xingyunSePeidai?.xingyun_se || (fengshuiData.value as any)?.xingyunSePeidai?.xingyunSe;
return Array.isArray(v) && v.length ? v.join('') : '-';
});
const accessories = computed(() => {
const v = (fengshuiData.value as any)?.xingyun_se_peidai?.peidai_shipin || (fengshuiData.value as any)?.xingyunSePeidai?.peidai_shipin || (fengshuiData.value as any)?.xingyunSePeidai?.peidaiShipin;
return Array.isArray(v) ? v.map((x: any) => ({ name: String(x?.name || '-'), effect: String(x?.effect || '-') })) : [];
});
const badgeClass = (l: string) => {
const raw = String(l || '');
const text = raw.toLowerCase();
if (/[凶衰险败]/.test(raw) || /bad|danger|xiong|negative|low/.test(text)) return 'badge-gray';
if (/[吉旺顺福喜]/.test(raw) || /good|auspicious|positive|high/.test(text)) return 'badge-red';
if (/ji/.test(text) && !/xiong/.test(text)) return 'badge-red';
return 'badge-blue';
};
const hourClass = (t: string) => {
const raw = String(t || '');
const text = raw.toLowerCase();
if (/[凶衰险败]/.test(raw) || /bad|danger|xiong|negative|low/.test(text)) return 'hour-bad';
if (/[吉旺顺福喜]/.test(raw) || /good|auspicious|positive|high/.test(text)) return 'hour-good';
if (/ji/.test(text) && !/xiong/.test(text)) return 'hour-good';
return 'hour-mid';
};
const handleDownloadPdf = async () => {
if (downloadingPdf.value) return;
if (typeof window === 'undefined' || typeof document === 'undefined') {
uni.showToast({ title: '当前环境不支持下载PDF', icon: 'none' });
return;
}
downloadingPdf.value = true;
uni.showLoading({ title: '正在生成PDF...', mask: true });
try {
const [{ jsPDF }, html2canvasMod] = await Promise.all([import('jspdf'), import('html2canvas')]);
const html2canvas = (html2canvasMod as any).default || (html2canvasMod as any);
const escapeHtml = (s: string) =>
s
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
const arr = (v: any) => (Array.isArray(v) ? v : []);
const toStr = (v: any) => String(v ?? "").trim();
const flattenDetailNodes = (nodes: any[]): string[] => {
const out: string[] = [];
for (const n of nodes) {
if (!n || typeof n !== "object") continue;
if (n.type === "text" && toStr(n.text)) out.push(toStr(n.text));
if (n.type === "list") {
for (const item of arr(n.items)) {
const s = toStr(item);
if (s) out.push(`· ${s}`);
}
}
if (n.type === "kv") {
for (const item of arr(n.items)) {
const lab = toStr(item?.label);
const val = toStr(item?.value);
if (lab || val) out.push(`${lab}${val}`);
}
}
}
return out;
};
// 过滤不应导出的字段id/时间戳/内部字段等)
const excludeKeys = new Set([
"id",
"report_id",
"reportId",
"user_id",
"userId",
"created_at",
"updated_at",
"createdAt",
"updatedAt",
"ts",
"timestamp",
"is_unlocked",
"unlock_price",
"locked",
"lock",
"price",
]);
const noisyKeys = new Set(["details", "nodes", "type", "meta", "status", "code", "raw"]);
const keyLabelMap: Record<string, string> = {
mingzao: "命造",
zhen_taiyang_shi: "真太阳时",
nongli_shengchen: "农历生辰",
qianshi_yinji: "前世印记",
jinsheng_keti: "今生课题",
mingge_cengci: "命格层次",
guji_duanyu: "古籍短语",
bazi_paipan: "八字排盘",
nian: "年柱",
yue: "月柱",
ri: "日柱",
shi: "时柱",
gan: "天干",
zhi: "地支",
wuxing_nengliang: "五行能量",
mu: "木",
huo: "火",
tu: "土",
jin: "金",
shui: "水",
qimen_paipan: "奇门排盘",
qimen_geju: "奇门格局",
dashi_pizhu: "大势批注",
liunian_zongyun: "流年总运",
yuedu_xiangpi: "月度详批",
meiri_yuncheng: "每日运程",
fengshui_jinnang: "风水锦囊",
wealth_score: "财运评分",
wealth_level: "财运等级",
wealth_trend: "财运趋势",
is_unlocked: "解锁状态",
unlock_price: "解锁价格",
name: "姓名",
details: "详解",
};
const prettyKey = (k: string): string => {
if (keyLabelMap[k]) return keyLabelMap[k];
// 未映射的拼音键不展示键名,避免出现大量 mingzao:xxx 样式
if (/^[a-z0-9_]+$/i.test(k)) return "";
return k;
};
const flattenAnyToLines = (val: any, depth = 0, prefix = ""): string[] => {
if (depth > 6 || val === null || val === undefined) return [];
if (typeof val === "string" || typeof val === "number" || typeof val === "boolean") {
const s = String(val).trim();
return s ? [prefix ? `${prefix}${s}` : s] : [];
}
if (Array.isArray(val)) {
const out = val.flatMap((it) => flattenAnyToLines(it, depth + 1, ""));
return prefix ? out.map((x) => `${prefix}${x}`) : out;
}
if (typeof val === "object") {
const out: string[] = [];
Object.keys(val).forEach((k) => {
if (excludeKeys.has(k) || noisyKeys.has(k)) return;
const child = (val as any)[k];
const label = prettyKey(k);
// 先把 details.nodes 提出来(更像正文)
const nodeLines = child && typeof child === "object" ? flattenDetailNodes(arr(child?.details?.nodes)) : [];
nodeLines.forEach((ln) => out.push(label ? `${label}${ln}` : ln));
const lines = flattenAnyToLines(child, depth + 1, "");
lines.forEach((ln) => out.push(label ? `${label}${ln}` : ln));
});
return out;
}
return [];
};
const uniqueLines = (lines: string[]) => {
const seen = new Set<string>();
const out: string[] = [];
for (const l of lines) {
const s = String(l || "").trim();
if (!s || seen.has(s)) continue;
seen.add(s);
out.push(s);
}
return out;
};
const mergeLines = (...groups: any[]) => uniqueLines(groups.flatMap((g) => (Array.isArray(g) ? g : [g])).flatMap((x) => {
if (x == null) return [];
if (Array.isArray(x)) return x;
const s = String(x ?? "").trim();
return s ? [s] : [];
}));
const reportId = getReportId();
const root = wealthAnalysisData.value as any;
const moduleList: Array<{ title: string; lines: string[] }> = [];
const push = (title: string, lines: string[]) => {
const l = uniqueLines(lines.map((x) => String(x ?? "").trim()).filter(Boolean));
if (!l.length) return;
moduleList.push({ title, lines: l });
};
// 尽量按接口模块分章,全部展示(不含 id 类字段)
push("命盘精批", mergeLines(
flattenAnyToLines(root?.mingpan_jingpi),
flattenDetailNodes(arr(root?.mingpan_jingpi?.details?.nodes)),
));
push("流年总运", mergeLines(
flattenAnyToLines(root?.liunian_zongyun),
flattenDetailNodes(arr(root?.liunian_zongyun?.details?.nodes)),
));
push("月度详批", isMonthUnlocked.value
? mergeLines(flattenAnyToLines(root?.yuedu_xiangpi), flattenDetailNodes(arr(root?.yuedu_xiangpi?.details?.nodes)))
: ["(未解锁)"]);
push("每日运程", isDayUnlocked.value
? mergeLines(flattenAnyToLines(root?.meiri_yuncheng), flattenDetailNodes(arr(root?.meiri_yuncheng?.details?.nodes)))
: ["(未解锁)"]);
push("风水锦囊", mergeLines(
flattenAnyToLines(root?.fengshui_jinnang),
flattenDetailNodes(arr(root?.fengshui_jinnang?.details?.nodes)),
));
// 兜底:把根级其它字段补进 PDF过滤 id 等)
if (root && typeof root === "object") {
Object.keys(root).forEach((k) => {
if (excludeKeys.has(k)) return;
if (["mingpan_jingpi", "liunian_zongyun", "yuedu_xiangpi", "meiri_yuncheng", "fengshui_jinnang"].includes(k)) return;
const v = (root as any)[k];
const lines = mergeLines(
v && typeof v === "object" ? flattenDetailNodes(arr(v?.details?.nodes)) : [],
flattenAnyToLines(v),
);
if (!lines.length) return;
push(prettyKey(k) || "其他信息", lines);
});
}
if (!moduleList.length) {
uni.showToast({ title: "暂无可导出的报告内容", icon: "none" });
return;
}
const PAGE_W = 794;
const PAGE_H = 1123;
// 舒展版:降低单页承载量,提升留白与可读性
const MAX_LINES = 34;
const MIN_PAGE_COST = 24;
const wrappedLineCost = (line: string) => {
const text = String(line || "").trim();
if (!text) return 1;
return Math.max(1, Math.ceil(text.length / 30));
};
const splitByLines = (lines: string[], max: number) => {
const res: string[][] = [];
let bucket: string[] = [];
let cost = 0;
lines.forEach((line) => {
const lc = wrappedLineCost(line);
if (bucket.length && cost + lc > max) {
res.push(bucket);
bucket = [];
cost = 0;
}
bucket.push(line);
cost += lc;
});
if (bucket.length) res.push(bucket);
return res.length ? res : [[]];
};
const styleId = "pdf-gen-wealth-style";
let style = document.getElementById(styleId) as HTMLStyleElement | null;
if (!style) {
style = document.createElement("style");
style.id = styleId;
document.head.appendChild(style);
}
// 复用 NamingDetail 的 PDF 视觉体系
style.textContent = `
.pdf-gen-page{position:absolute;width:${PAGE_W}px;height:${PAGE_H}px;background-size:cover;background-position:center;font-family:"Songti SC","SimSun","STSong","Noto Serif SC",serif;color:#f2e6d8;}
.pdf-gen-panel{position:absolute;left:52px;right:52px;top:124px;bottom:100px;background:rgba(10,10,15,.60);border:1px solid rgba(212,175,55,.36);border-radius:16px;padding:30px 30px;box-sizing:border-box;overflow:hidden;}
.pdf-cover-kicker{text-align:center;font-size:12px;letter-spacing:.28em;color:rgba(212,175,55,.72);margin-bottom:12px;}
.pdf-cover-title{text-align:center;font-size:25px;letter-spacing:.16em;font-weight:700;color:rgba(212,175,55,.95);margin-bottom:26px;}
.pdf-name{text-align:center;font-size:44px;font-weight:800;margin-bottom:14px;color:rgba(242,230,216,.98);text-shadow:0 0 18px rgba(212,175,55,.18);}
.pdf-cover-divider{width:210px;height:1px;margin:22px auto 18px;background:linear-gradient(90deg,rgba(212,175,55,0),rgba(212,175,55,.7),rgba(212,175,55,0));}
.pdf-cover-seal{width:66px;height:66px;margin:26px auto 0;border-radius:50%;border:1px solid rgba(212,175,55,.34);box-shadow:inset 0 0 0 6px rgba(212,175,55,.05);display:flex;align-items:center;justify-content:center;color:rgba(212,175,55,.92);font-size:20px;}
.pdf-dir-title{font-size:20px;font-weight:800;letter-spacing:.14em;color:rgba(212,175,55,.95);margin-bottom:12px;}
.pdf-dir-kicker{font-size:11px;letter-spacing:.24em;color:rgba(212,175,55,.68);margin-bottom:8px;}
.pdf-dir-list{margin-top:6px;border-top:1px solid rgba(212,175,55,.14);}
.pdf-dir-item{display:flex;align-items:center;border-bottom:1px solid rgba(255,255,255,.08);min-height:42px;padding:0 4px;box-sizing:border-box;gap:8px;}
.pdf-dir-idx{width:68px;flex:0 0 68px;font-size:15px;color:rgba(212,175,55,.96);font-weight:800;}
.pdf-dir-name{flex:1;font-size:16px;color:rgba(232,232,232,.96);font-weight:700;}
.pdf-dir-dots{flex:1;border-bottom:1px dashed rgba(212,175,55,.34);height:0;transform:translateY(2px);}
.pdf-dir-page{width:62px;flex:0 0 62px;text-align:right;font-size:13px;color:rgba(226,214,188,.92);font-weight:700;border:1px solid rgba(212,175,55,.22);border-radius:999px;padding:2px 8px;box-sizing:border-box;background:rgba(212,175,55,.06);}
.pdf-chapter-bar{position:absolute;left:52px;right:52px;top:84px;height:48px;border-radius:12px;background:rgba(212,175,55,.08);border:1px solid rgba(212,175,55,.24);display:flex;align-items:center;padding:0 22px;box-sizing:border-box;}
.pdf-chapter-no{font-size:16px;font-weight:900;color:rgba(212,175,55,.98);margin-right:14px;}
.pdf-chapter-title{font-size:16px;font-weight:800;color:rgba(242,230,216,.98);}
.pdf-flow-section{margin-bottom:14px;border-radius:10px;border:1px solid rgba(212,175,55,.16);background:rgba(11,16,38,.55);overflow:hidden;}
.pdf-flow-head{height:40px;display:flex;align-items:center;gap:10px;padding:0 15px;border-bottom:1px solid rgba(212,175,55,.16);background:linear-gradient(90deg,rgba(212,175,55,.12),rgba(212,175,55,.03));}
.pdf-flow-bullet{color:rgba(212,175,55,.96);font-size:15px;}
.pdf-flow-title{color:rgba(245,236,218,.98);font-size:15px;font-weight:800;letter-spacing:.04em;}
.pdf-flow-body{padding:12px 15px 14px;}
.pdf-flow-line{font-size:14px;line-height:1.9;color:rgba(228,230,238,.95);margin-bottom:4px;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;}
.pdf-page-footer{position:absolute;left:48px;right:48px;bottom:42px;height:24px;display:flex;align-items:center;justify-content:space-between;font-size:12px;color:rgba(210,210,210,.72);letter-spacing:.04em;}
.pdf-footer-line{position:absolute;left:48px;right:48px;bottom:70px;height:1px;background:linear-gradient(90deg,rgba(212,175,55,0),rgba(212,175,55,.45),rgba(212,175,55,0));}
`;
const wrapper = document.createElement("div");
wrapper.style.position = "fixed";
wrapper.style.left = "-100000px";
wrapper.style.top = "0";
wrapper.style.zIndex = "-1";
wrapper.style.width = "0";
wrapper.style.height = "0";
document.body.appendChild(wrapper);
const pages: HTMLElement[] = [];
const createPageEl = (idx: number) => {
const el = document.createElement("div");
el.className = "pdf-gen-page";
el.style.left = `${idx * 2}px`;
el.style.top = `${idx * 2}px`;
(el.style as any).backgroundImage = `url(${pdfBgUrl})`;
wrapper.appendChild(el);
pages.push(el);
return el;
};
const appendFooter = (el: HTMLElement, pageNo: number, label = "财运解析报告") => {
const footer = document.createElement("div");
footer.className = "pdf-page-footer";
footer.innerHTML = `<div>${escapeHtml(label)}</div><div>第 ${pageNo} 页</div>`;
const line = document.createElement("div");
line.className = "pdf-footer-line";
el.appendChild(line);
el.appendChild(footer);
};
// 封面
const coverEl = createPageEl(0);
coverEl.innerHTML = `
<div class="pdf-gen-panel">
<div class="pdf-cover-kicker">玄 · 财 · 机 · 密</div>
<div class="pdf-cover-title">财运解析报告</div>
<div class="pdf-name">${escapeHtml(reportId ? `报告 ${reportId}` : "财运详解")}</div>
<div style="text-align:center;color:rgba(226,226,226,0.92);font-size:14px;margin-bottom:18px;">命盘精批 · 流年总运 · 月日详批 · 风水锦囊</div>
<div class="pdf-cover-divider"></div>
<div class="pdf-cover-seal">财</div>
</div>
`;
appendFooter(coverEl, 1, "财运解析报告 · 封面");
// 目录
const dirEl = createPageEl(1);
type ChapterSection = { chapterNo: number; title: string; lines: string[]; continued?: boolean };
type ContentPage = { sections: ChapterSection[]; cost: number };
const contentPages: ContentPage[] = [];
const dirItems: Array<{ no: number; title: string; start: number; end: number }> = [];
let currentPageNo = 3;
moduleList.forEach((m, idx) => {
const chapterNo = idx + 1;
const chunks = splitByLines(m.lines, MAX_LINES);
const start = currentPageNo;
chunks.forEach((chunk, cIdx) => {
const section: ChapterSection = { chapterNo, title: m.title, lines: chunk, continued: cIdx > 0 };
// 节标题/容器固定开销,保证分页更接近真实视觉高度
const sectionCost = 5 + chunk.reduce((sum, ln) => sum + wrappedLineCost(ln), 0);
const prev = contentPages[contentPages.length - 1];
// 续页优先独占,保持章节连续性;短章允许拼页,提高页面利用率与协调度
const canMergeIntoPrev =
!!prev &&
!section.continued &&
prev.cost + sectionCost <= MAX_LINES &&
prev.cost < MIN_PAGE_COST;
if (canMergeIntoPrev) {
prev.sections.push(section);
prev.cost += sectionCost;
} else {
contentPages.push({ sections: [section], cost: sectionCost });
currentPageNo += 1;
}
});
const end = Math.max(start, currentPageNo - 1);
dirItems.push({ no: chapterNo, title: m.title, start, end });
});
dirEl.innerHTML = `
<div class="pdf-gen-panel">
<div class="pdf-dir-kicker">卷 一 · 纲 目</div>
<div class="pdf-dir-title">目录</div>
<div class="pdf-dir-list">
${dirItems.map((it) => {
const pageText = it.start === it.end ? `${it.start}` : `${it.start}-${it.end}`;
return `<div class="pdf-dir-item">
<div class="pdf-dir-idx">第${it.no}章</div>
<div class="pdf-dir-name">${escapeHtml(it.title)}</div>
<div class="pdf-dir-dots"></div>
<div class="pdf-dir-page">${escapeHtml(pageText)}</div>
</div>`;
}).join("")}
</div>
</div>
`;
appendFooter(dirEl, 2, "财运解析报告 · 目录");
// 正文页(每页可包含多个小节,密度更均衡)
contentPages.forEach((page, i) => {
const el = createPageEl(2 + i);
const titleText = page.sections.length > 1
? `综合页 · ${page.sections.length} 个章节`
: (() => {
const s = page.sections[0];
return `${s.chapterNo}${s.title}${s.continued ? "(续)" : ""}`;
})();
const sectionsHtml = page.sections.map((section) => {
const secTitleText = `${section.chapterNo}${section.title}${section.continued ? "(续)" : ""}`;
const linesHtml = section.lines.map((ln) => `<div class="pdf-flow-line">${escapeHtml(ln || " ")}</div>`).join("");
return `<div class="pdf-flow-section">
<div class="pdf-flow-head">
<div class="pdf-flow-bullet">✧</div>
<div class="pdf-flow-title">${escapeHtml(secTitleText)}</div>
</div>
<div class="pdf-flow-body">${linesHtml}</div>
</div>`;
}).join("");
el.innerHTML = `
<div class="pdf-chapter-bar">
<div class="pdf-chapter-no">正文</div>
<div class="pdf-chapter-title">${escapeHtml(titleText)}</div>
</div>
<div class="pdf-gen-panel" style="top:138px;">${sectionsHtml}</div>
`;
appendFooter(el, 3 + i);
});
const doc = new jsPDF({ unit: "pt", format: "a4" });
const pageW = doc.internal.pageSize.getWidth();
const pageH = doc.internal.pageSize.getHeight();
const renderScale = Math.min(1.45, Math.max(1.15, Number((window as any)?.devicePixelRatio || 1)));
const jpegQuality = 0.86;
for (let i = 0; i < pages.length; i++) {
const canvas = await html2canvas(pages[i], {
scale: renderScale,
useCORS: true,
allowTaint: true,
backgroundColor: "#0a0a0f",
logging: false,
});
const img = canvas.toDataURL("image/jpeg", jpegQuality);
if (i > 0) doc.addPage();
doc.addImage(img, "JPEG", 0, 0, pageW, pageH, undefined, "FAST");
}
wrapper.remove();
const filename = `财运解析报告${reportId ? `-${reportId}` : ""}.pdf`;
doc.save(filename);
} catch (e: any) {
console.error('生成PDF失败:', e);
uni.showToast({ title: e?.msg || e?.message || '生成失败,请稍后重试', icon: 'none' });
} finally {
uni.hideLoading();
downloadingPdf.value = false;
}
};
onMounted(async () => {
restoreUnlockStatus();
await loadMyQuota();
await loadWealthAnalysis();
const pending = getPending();
if (pending?.tab) {
activeTab.value = pending.tab as TabId;
if (pending.unlockType === 'monthly') {
handleUnlockMonth();
} else if (pending.unlockType === 'daily') {
handleUnlockDay();
}
}
});
</script>
<style scoped>
.fortune-wrap {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
background: #050508;
color: #e2e2e2;
overflow: hidden;
z-index: 60;
font-size: 26rpx;
line-height: 1.6;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
margin: 0;
padding: 0;
box-sizing: border-box;
}
.bg {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
}
.bg-base {
position: absolute;
inset: 0;
background: #050508;
}
.bg-texture {
position: absolute;
inset: 0;
background-image: url('https://www.transparenttextures.com/patterns/stardust.png');
background-size: auto;
opacity: 0.2;
}
.bg-glow {
position: absolute;
width: 50%;
height: 50%;
border-radius: 999px;
filter: blur(100px);
}
.bg-glow-a {
top: -10%;
left: -10%;
background: #d4af37;
opacity: 0.05;
}
.bg-glow-b {
bottom: -10%;
right: -10%;
background: #8b2323;
opacity: 0.05;
}
.bg-star {
position: absolute;
border-radius: 50%;
background: #fff;
opacity: 0.2;
animation: twinkle 3s ease-in-out infinite;
}
@keyframes twinkle {
0%,
100% {
opacity: 0.1;
transform: scale(1);
}
50% {
opacity: 0.55;
transform: scale(1.2);
}
}
.header {
position: relative;
z-index: 20;
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 30rpx;
padding-top: calc(28rpx + env(safe-area-inset-top, 0px));
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
background: rgba(10, 10, 15, 0.8);
backdrop-filter: blur(10px);
}
.header-left {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
}
.header-back {
color: #a0a0a0;
font-size: 44rpx;
line-height: 44rpx;
}
.header-mid {
display: flex;
flex-direction: column;
align-items: center;
}
.header-title {
font-size: 30rpx;
font-weight: 700;
color: #d4af37;
letter-spacing: 0.2em;
}
.header-subtitle {
margin-top: 6rpx;
font-size: 18rpx;
color: #5a5a5a;
letter-spacing: 0.22em;
text-transform: uppercase;
}
.header-right {
display: flex;
align-items: center;
gap: 16rpx;
}
.header-icon {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
}
.header-icon-text {
font-size: 30rpx;
color: #d4af37;
}
.tabs {
position: relative;
z-index: 10;
background: rgba(10, 10, 15, 0.5);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.tabs-inner {
display: flex;
align-items: center;
min-width: max-content;
padding: 0 12rpx;
}
.tab {
position: relative;
min-width: 160rpx;
padding: 18rpx 18rpx 20rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
color: #666;
}
.tab.active {
color: #d4af37;
}
.tab-icon-wrap {
position: relative;
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
}
.tab-icon {
font-size: 28rpx;
line-height: 1;
display: block;
}
.tab-lock-dot {
position: absolute;
top: -6rpx;
right: -10rpx;
width: 32rpx;
height: 32rpx;
padding: 0;
border-radius: 999rpx;
border: 1px solid rgba(212, 175, 55, 0.35);
background: rgba(212, 175, 55, 0.12);
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.tab-lock-dot-text {
font-size: 18rpx;
line-height: 1;
color: #d4af37;
}
.tab-label {
font-size: 22rpx;
font-weight: 700;
letter-spacing: 0.18em;
}
.tab-indicator {
position: absolute;
bottom: 0;
width: 64rpx;
height: 4rpx;
background: #d4af37;
}
.content {
position: relative;
z-index: 10;
flex: 1;
height: 0;
}
.content-inner {
padding: 38rpx 38rpx 160rpx;
}
.stack {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.card {
position: relative;
background: #1a1a2e;
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 18rpx;
padding: 28rpx;
overflow: hidden;
}
.card-grad {
background: linear-gradient(135deg, #1a1a2e, #0a0a0f);
}
.border-gold {
border-color: rgba(212, 175, 55, 0.3);
}
.card-watermark {
position: absolute;
top: 0;
right: 0;
padding: 14rpx;
opacity: 0.08;
}
.wm {
font-size: 80rpx;
color: #d4af37;
}
.section-title {
display: flex;
align-items: center;
gap: 14rpx;
margin-bottom: 18rpx;
}
.bar {
width: 8rpx;
height: 44rpx;
border-radius: 999rpx;
background: #d4af37;
box-shadow: 0 0 16rpx rgba(212, 175, 55, 0.5);
}
.st {
display: block;
font-size: 28rpx;
font-weight: 800;
letter-spacing: 0.18em;
font-family: "Songti SC", "Noto Serif SC", SimSun, serif;
}
.sst {
display: block;
margin-top: 6rpx;
font-size: 18rpx;
color: #a0a0a0;
letter-spacing: 0.22em;
text-transform: uppercase;
}
.muted {
color: #a0a0a0;
}
.gold {
color: #d4af37;
}
.red {
color: #f87171;
}
.serif {
font-family: "Songti SC", "Noto Serif SC", SimSun, serif;
}
.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
monospace;
}
.bold {
font-weight: 800;
}
.tiny {
font-size: 22rpx;
}
.micro {
font-size: 20rpx;
}
.lh {
line-height: 1.6;
}
.row {
display: flex;
align-items: flex-start;
}
.col {
display: flex;
flex-direction: column;
}
.flex-1 {
flex: 1;
}
.gap-1 {
gap: 8rpx;
}
.gap-2 {
gap: 16rpx;
}
.gap-3 {
gap: 24rpx;
}
.mt-1 {
margin-top: 8rpx;
}
.mt-2 {
margin-top: 16rpx;
}
.mb-3 {
margin-bottom: 24rpx;
}
.divider {
height: 1px;
background: rgba(255, 255, 255, 0.05);
margin: 20rpx 0;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
}
.grid-4 {
display: grid;
grid-template-columns: repeat(4, 1fr);
}
.center {
text-align: center;
}
.row-wide {
grid-column: 1 / -1;
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255, 255, 255, 0.05);
padding: 14rpx 16rpx;
border-radius: 14rpx;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12rpx 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.info-row:last-child {
border-bottom: 0;
}
.box-dark {
background: rgba(0, 0, 0, 0.2);
border-radius: 14rpx;
padding: 18rpx;
border: 1px solid rgba(255, 255, 255, 0.06);
}
.bazi-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
border-radius: 16rpx;
overflow: hidden;
background: #0a0a0f;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.bazi-pillar {
display: flex;
flex-direction: column;
align-items: center;
border-right: 1px solid rgba(255, 255, 255, 0.08);
padding-bottom: 18rpx;
}
.bazi-pillar:last-child {
border-right: 0;
}
.bazi-pillar.active {
background: rgba(212, 175, 55, 0.06);
}
.gan-circle {
width: 80rpx;
height: 80rpx;
border-radius: 999rpx;
border: 1px solid rgba(212, 175, 55, 0.3);
background: #1a1a2e;
display: flex;
align-items: center;
justify-content: center;
margin: 14rpx 0 10rpx;
}
.zhi-square {
width: 80rpx;
height: 80rpx;
border-radius: 12rpx;
border: 1px solid rgba(255, 255, 255, 0.1);
background: #1a1a2e;
display: flex;
align-items: center;
justify-content: center;
}
.gan-text {
font-size: 40rpx;
font-weight: 800;
font-family: "Songti SC", "Noto Serif SC", SimSun, serif;
}
.hidden-list {
display: flex;
flex-direction: column;
align-items: center;
gap: 4rpx;
margin-top: 10rpx;
min-height: 72rpx;
}
.hidden-item {
font-size: 18rpx;
color: #666;
}
.stage {
margin-top: 8rpx;
padding: 4rpx 10rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.05);
}
.grade-badge {
width: 140rpx;
height: 140rpx;
border-radius: 999rpx;
background: rgba(212, 175, 55, 0.1);
border: 1px solid #d4af37;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.grade-text {
font-size: 40rpx;
font-weight: 900;
color: #d4af37;
}
.poem {
background: rgba(0, 0, 0, 0.2);
border-top: 1px solid rgba(255, 255, 255, 0.05);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
padding: 18rpx;
border-radius: 14rpx;
}
.poem-line {
display: block;
text-align: center;
font-style: italic;
font-family: "Songti SC", "Noto Serif SC", SimSun, serif;
color: #a0a0a0;
line-height: 1.8;
font-size: 18rpx;
margin: 8rpx 0;
}
.scorebar {
margin-bottom: 16rpx;
}
.scorebar-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6rpx;
}
.scorebar-track {
width: 100%;
height: 12rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 999rpx;
overflow: hidden;
}
.scorebar-fill {
height: 100%;
border-radius: 999rpx;
}
.note {
background: rgba(255, 255, 255, 0.05);
border-radius: 14rpx;
padding: 18rpx;
}
.table {
border-radius: 14rpx;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.tr {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
padding: 12rpx 10rpx;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.tr:last-child {
border-bottom: 0;
}
.th {
background: rgba(255, 255, 255, 0.04);
}
.tc {
font-size: 20rpx;
}
.foot {
display: block;
margin-top: 14rpx;
text-align: center;
color: #666;
font-style: italic;
font-size: 18rpx;
}
.box-left {
background: rgba(255, 255, 255, 0.05);
border-radius: 14rpx;
padding: 18rpx;
border-left-width: 6rpx;
border-left-style: solid;
}
.gold-left {
border-left-color: #d4af37;
}
.red-left {
border-left-color: #ef4444;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
margin-top: 10rpx;
}
.chip {
font-size: 18rpx;
padding: 6rpx 10rpx;
border: 1px solid rgba(212, 175, 55, 0.3);
border-radius: 12rpx;
color: #d4af37;
}
.mb-2 {
margin-bottom: 16rpx;
}
.kv {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.kv-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14rpx 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.kv-row:last-child {
border-bottom: 0;
}
.raw-list {
margin-top: 12rpx;
display: flex;
flex-direction: column;
gap: 10rpx;
}
.raw-row {
padding: 12rpx 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.raw-row:last-child {
border-bottom: 0;
}
.raw-key {
display: block;
font-size: 20rpx;
color: #d4af37;
word-break: break-all;
line-height: 1.5;
}
.raw-value {
display: block;
margin-top: 6rpx;
font-size: 22rpx;
color: #d1d5db;
line-height: 1.65;
white-space: pre-wrap;
word-break: break-all;
}
.atlas-groups {
margin-top: 18rpx;
display: flex;
flex-direction: column;
gap: 20rpx;
}
.atlas-summary {
margin-top: 16rpx;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12rpx;
}
.atlas-stat {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12rpx;
padding: 14rpx 10rpx;
text-align: center;
}
.atlas-stat-value {
display: block;
color: #f5d56a;
font-weight: 800;
font-size: 28rpx;
line-height: 1.2;
}
.atlas-stat-label {
display: block;
margin-top: 6rpx;
color: #9ca3af;
font-size: 18rpx;
}
.atlas-group {
border: 1px solid rgba(212, 175, 55, 0.18);
border-radius: 16rpx;
padding: 18rpx;
background: linear-gradient(145deg, rgba(18, 18, 28, 0.88), rgba(10, 10, 16, 0.88));
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.22);
}
.atlas-group-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.atlas-group-title {
font-size: 24rpx;
color: #f8dda4;
font-weight: 700;
}
.atlas-group-count {
font-size: 18rpx;
color: #9ca3af;
padding: 4rpx 12rpx;
border-radius: 999rpx;
background: rgba(212, 175, 55, 0.12);
border: 1px solid rgba(212, 175, 55, 0.24);
}
.atlas-rows {
display: flex;
flex-direction: column;
gap: 10rpx;
}
.atlas-row {
padding: 12rpx;
border-radius: 12rpx;
background: rgba(255, 255, 255, 0.02);
border-bottom: 1px dashed rgba(255, 255, 255, 0.08);
}
.atlas-row:last-child {
border-bottom: 0;
}
.atlas-key {
display: block;
font-size: 20rpx;
color: #e5c36e;
line-height: 1.45;
}
.atlas-value {
display: block;
margin-top: 6rpx;
font-size: 22rpx;
color: #d6d6d6;
line-height: 1.62;
white-space: pre-wrap;
word-break: break-all;
}
.month-summary {
margin-top: 16rpx;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12rpx;
}
.month-summary-item {
padding: 14rpx 12rpx;
border-radius: 12rpx;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.month-summary-label {
display: block;
font-size: 18rpx;
color: #9ca3af;
}
.month-summary-value {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #f5d56a;
font-weight: 700;
line-height: 1.35;
}
.daily-hero {
margin-top: 12rpx;
padding: 20rpx;
border-radius: 14rpx;
border: 1px solid rgba(212, 175, 55, 0.22);
background: linear-gradient(130deg, rgba(212, 175, 55, 0.12), rgba(255, 255, 255, 0.02));
}
.daily-hero-title {
display: block;
color: #f8dda4;
font-size: 28rpx;
font-weight: 800;
line-height: 1.35;
}
.daily-hero-sub {
display: block;
margin-top: 8rpx;
color: #c7cad1;
font-size: 20rpx;
line-height: 1.6;
}
.daily-meta-grid {
margin-top: 14rpx;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12rpx;
}
.daily-meta-card {
padding: 14rpx;
border-radius: 12rpx;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
}
.daily-meta-label {
display: block;
font-size: 18rpx;
color: #9ca3af;
}
.daily-meta-value {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #f5d56a;
font-weight: 700;
line-height: 1.35;
word-break: break-all;
}
.daily-yiji-grid {
margin-top: 14rpx;
display: grid;
grid-template-columns: 1fr;
gap: 10rpx;
}
.daily-yiji-card {
border-radius: 12rpx;
padding: 12rpx 14rpx;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.daily-yiji-good {
background: rgba(16, 185, 129, 0.08);
border-color: rgba(16, 185, 129, 0.25);
}
.daily-yiji-bad {
background: rgba(239, 68, 68, 0.08);
border-color: rgba(239, 68, 68, 0.25);
}
.daily-yiji-label {
display: block;
font-size: 18rpx;
color: #9ca3af;
}
.daily-yiji-value {
display: block;
margin-top: 8rpx;
font-size: 22rpx;
color: #d1d5db;
line-height: 1.55;
white-space: pre-wrap;
word-break: break-all;
}
.star-item {
background: rgba(255, 255, 255, 0.05);
border-radius: 14rpx;
padding: 14rpx 16rpx;
border-left: 6rpx solid #d4af37;
display: flex;
justify-content: space-between;
align-items: center;
}
.tag {
font-size: 18rpx;
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
.tag-good {
background: rgba(127, 29, 29, 0.4);
color: #fca5a5;
}
.tag-mid {
background: rgba(55, 65, 81, 1);
color: #9ca3af;
}
.timeline {
margin-top: 10rpx;
}
.timeline-item {
position: relative;
padding-left: 24rpx;
padding-bottom: 24rpx;
border-left: 1px solid rgba(255, 255, 255, 0.1);
}
.timeline-item.current {
border-left-color: rgba(212, 175, 55, 0.8);
}
.dot {
position: absolute;
left: -10rpx;
top: 6rpx;
width: 18rpx;
height: 18rpx;
border-radius: 999rpx;
background: #333;
border: 4rpx solid #050508;
}
.dot.on {
background: #d4af37;
box-shadow: 0 0 12rpx rgba(212, 175, 55, 0.6);
}
.timeline-body {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.timeline-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.tl-name {
font-size: 24rpx;
font-weight: 800;
}
.pill {
font-size: 18rpx;
color: #666;
background: rgba(255, 255, 255, 0.05);
padding: 6rpx 14rpx;
border-radius: 999rpx;
}
.current-tag {
align-self: flex-start;
font-size: 18rpx;
color: #d4af37;
background: rgba(212, 175, 55, 0.1);
padding: 6rpx 12rpx;
border-radius: 12rpx;
}
.taisui-badge {
width: 96rpx;
height: 96rpx;
background: rgba(127, 29, 29, 0.2);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
}
.taisui-text {
font-size: 40rpx;
color: #f87171;
font-weight: 900;
}
.lock-root {
position: relative;
min-height: 100%;
}
.lock-root-locked {
height: min(100vh, 100dvh);
max-height: min(100vh, 100dvh);
overflow: hidden;
}
.lock-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-height: calc(100% - 40rpx);
overflow-y: auto;
z-index: 30;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(5, 5, 8, 0.95);
border: 1px solid rgba(212, 175, 55, 0.3);
border-radius: 18rpx;
backdrop-filter: blur(10px);
padding: 48rpx 40rpx;
width: 85%;
max-width: 600rpx;
}
.lock-circle {
width: 96rpx;
height: 96rpx;
border-radius: 999rpx;
background: #d4af37;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24rpx;
box-shadow: 0 0 24rpx rgba(212, 175, 55, 0.6);
}
.lock-circle-text {
color: #000;
font-weight: 900;
}
.lock-h {
font-size: 30rpx;
font-weight: 800;
margin-bottom: 14rpx;
}
.lock-p {
text-align: center;
margin-bottom: 24rpx;
}
.lock-p-text {
display: block;
color: #a0a0a0;
}
.lock-p-sub {
display: block;
color: rgba(212, 175, 55, 0.8);
margin-top: 6rpx;
}
.lock-cta {
padding: 18rpx 44rpx;
border-radius: 999rpx;
background: linear-gradient(90deg, #d4af37, #b49120);
}
.lock-cta-text {
color: #000;
font-weight: 900;
letter-spacing: 0.12em;
}
.blurred {
filter: blur(8px);
opacity: 0.4;
pointer-events: none;
}
.clickable {
transition: opacity 0.15s ease;
}
.clickable:active {
opacity: 0.82;
}
.detail-modal-overlay {
position: fixed;
inset: 0;
z-index: 120;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
padding: 28rpx;
}
.detail-modal {
width: 100%;
max-width: 680rpx;
background: rgba(10, 10, 15, 0.98);
border: 1px solid rgba(212, 175, 55, 0.25);
border-radius: 18rpx;
overflow: hidden;
}
.detail-modal-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18rpx 18rpx;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.detail-modal-title {
flex: 1;
color: #d4af37;
font-weight: 900;
font-size: 28rpx;
padding-right: 16rpx;
}
.detail-modal-close {
width: 56rpx;
height: 56rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.06);
display: flex;
align-items: center;
justify-content: center;
}
.detail-modal-close-text {
color: #a0a0a0;
font-size: 34rpx;
line-height: 1;
}
.detail-modal-body {
max-height: 70vh;
}
.detail-modal-content {
padding: 18rpx;
}
.detail-modal-text {
display: block;
color: #a0a0a0;
font-size: 24rpx;
line-height: 1.8;
margin-bottom: 12rpx;
}
.trend {
margin-top: 14rpx;
height: 220rpx;
position: relative;
}
.trend-bars {
position: absolute;
left: 8rpx;
right: 8rpx;
top: 10rpx;
bottom: 40rpx;
display: flex;
align-items: flex-end;
gap: 8rpx;
}
.trend-bar {
flex: 1;
height: 100%;
border-radius: 10rpx 10rpx 0 0;
background: rgba(255, 255, 255, 0.04);
overflow: hidden;
display: flex;
align-items: flex-end;
}
.trend-bar-fill {
width: 100%;
background: linear-gradient(180deg, rgba(212, 175, 55, 0.7), rgba(212, 175, 55, 0.1));
}
.trend-months {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
padding: 0 8rpx;
}
.m-table {
margin-top: 14rpx;
border-radius: 14rpx;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.m-tr {
display: flex;
gap: 12rpx;
padding: 12rpx;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.m-tr:last-child {
border-bottom: 0;
}
.m-th {
background: rgba(212, 175, 55, 0.1);
}
.m-td {
width: 120rpx;
font-size: 20rpx;
}
.badge {
padding: 4rpx 10rpx;
border-radius: 12rpx;
font-size: 18rpx;
}
.badge-red {
background: rgba(127, 29, 29, 0.3);
color: #fca5a5;
}
.badge-gray {
background: rgba(31, 41, 55, 1);
color: #9ca3af;
}
.badge-blue {
background: rgba(30, 58, 138, 0.3);
color: #93c5fd;
}
.cal-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14rpx;
padding: 0 12rpx;
}
.cal-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 12rpx;
}
.cal-cell {
aspect-ratio: 1 / 1;
border-radius: 12rpx;
background: rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.cal-cell.hit {
background: rgba(212, 175, 55, 0.2);
border: 1px solid rgba(212, 175, 55, 0.5);
}
.tag-green {
background: rgba(20, 83, 45, 0.3);
color: #4ade80;
}
.tag-red {
background: rgba(127, 29, 29, 0.3);
color: #f87171;
}
.hour {
padding: 10rpx 8rpx;
border-radius: 12rpx;
text-align: center;
}
.hour-good {
background: rgba(212, 175, 55, 0.1);
color: #d4af37;
}
.hour-bad {
background: rgba(127, 29, 29, 0.1);
color: #f87171;
}
.hour-mid {
background: rgba(255, 255, 255, 0.05);
color: #666;
}
.action {
margin-top: 18rpx;
background: rgba(212, 175, 55, 0.06);
border: 1px solid rgba(212, 175, 55, 0.2);
border-radius: 14rpx;
padding: 18rpx;
}
.noble {
width: 120rpx;
height: 160rpx;
background: #0a0a0f;
border: 1px solid rgba(212, 175, 55, 0.3);
border-radius: 14rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.noble-avatar {
width: 64rpx;
height: 64rpx;
border-radius: 999rpx;
background: rgba(212, 175, 55, 0.2);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10rpx;
}
.noble-tag {
font-size: 14rpx;
color: #d4af37;
border: 1px solid rgba(212, 175, 55, 0.3);
padding: 4rpx 10rpx;
border-radius: 10rpx;
}
.dotc {
width: 44rpx;
height: 44rpx;
border-radius: 999rpx;
box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.3);
}
.box {
background: rgba(255, 255, 255, 0.05);
border-radius: 14rpx;
padding: 18rpx;
}
.noble-avatar-text {
font-size: 36rpx;
}
.karma {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.karma-item {
background: rgba(255, 255, 255, 0.05);
border-radius: 14rpx;
padding: 18rpx;
display: flex;
gap: 16rpx;
align-items: flex-start;
}
.karma-icon {
width: 48rpx;
height: 48rpx;
border-radius: 12rpx;
background: rgba(160, 160, 160, 0.12);
display: flex;
align-items: center;
justify-content: center;
}
.karma-icon-gold {
background: rgba(212, 175, 55, 0.12);
}
.karma-icon-text {
color: #a0a0a0;
font-size: 18rpx;
font-weight: 900;
}
.radar {
align-items: center;
gap: 24rpx;
}
.radar-left {
position: relative;
width: 260rpx;
height: 260rpx;
}
.radar-svg {
width: 100%;
height: 100%;
transform: rotate(180deg);
}
.radar-label {
position: absolute;
font-size: 14rpx;
color: #e2e2e2;
}
.radar-top {
top: 0;
left: 50%;
transform: translateX(-50%);
font-weight: 900;
}
.radar-right-pos {
right: 0;
top: 35%;
transform: translateX(6rpx);
}
.radar-left-label {
left: 0;
top: 35%;
transform: translateX(-6rpx);
}
.radar-br {
bottom: 0;
right: 5%;
}
.radar-bl {
bottom: 0;
left: 5%;
}
.radar-right {
flex: 1;
border-left: 1px solid rgba(255, 255, 255, 0.05);
padding-left: 18rpx;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.bar-item {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.bar-head {
display: flex;
justify-content: space-between;
}
.bar-track {
width: 100%;
height: 8rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 999rpx;
overflow: hidden;
}
.bar-fill {
height: 100%;
}
.grid-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.annual-head {
align-items: flex-start;
}
.annual-tag {
width: 96rpx;
height: 96rpx;
border-radius: 14rpx;
background: #d4af37;
color: #000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.annual-tag-text {
font-size: 40rpx;
font-weight: 900;
}
.annual-pill {
margin-top: 10rpx;
background: rgba(255, 255, 255, 0.05);
border-radius: 12rpx;
padding: 8rpx 12rpx;
text-align: center;
}
.annual-chart {
margin-top: 18rpx;
background: rgba(26, 26, 46, 0.8);
border-radius: 14rpx;
padding: 16rpx;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.annual-chart-head {
justify-content: space-between;
align-items: center;
}
.annual-bars {
height: 180rpx;
display: flex;
align-items: flex-end;
gap: 8rpx;
margin-top: 16rpx;
}
.annual-bar {
flex: 1;
background: rgba(255, 255, 255, 0.05);
border-radius: 10rpx 10rpx 0 0;
overflow: hidden;
display: flex;
align-items: flex-end;
}
.annual-bar-fill {
width: 100%;
background: rgba(212, 175, 55, 0.5);
}
.annual-bars-foot {
justify-content: space-between;
margin-top: 8rpx;
}
.star-chip {
padding: 16rpx 10rpx;
border-radius: 14rpx;
text-align: center;
border: 1px solid rgba(212, 175, 55, 0.3);
background: rgba(212, 175, 55, 0.1);
color: #d4af37;
}
.star-chip.bad {
border-color: rgba(239, 68, 68, 0.3);
background: rgba(127, 29, 29, 0.1);
color: #f87171;
}
.role-list {
display: flex;
flex-direction: column;
}
.role-item {
display: flex;
gap: 16rpx;
padding: 18rpx 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.role-item.last {
border-bottom: 0;
}
.role-icon {
width: 44rpx;
height: 44rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
}
.compass {
position: relative;
width: 420rpx;
height: 420rpx;
border-radius: 999rpx;
background: #0a0a0f;
border: 2px solid rgba(255, 255, 255, 0.05);
margin: 0 auto 20rpx;
}
.compass-center {
position: absolute;
top: 50%;
left: 50%;
width: 12rpx;
height: 12rpx;
border-radius: 999rpx;
background: #d4af37;
transform: translate(-50%, -50%);
}
.compass-t {
position: absolute;
top: 10rpx;
left: 50%;
transform: translateX(-50%);
font-size: 14rpx;
color: #f87171;
font-weight: 900;
}
.compass-b {
position: absolute;
bottom: 10rpx;
left: 50%;
transform: translateX(-50%);
font-size: 14rpx;
color: #666;
}
.compass-l {
position: absolute;
left: 10rpx;
top: 50%;
transform: translateY(-50%);
font-size: 14rpx;
color: #666;
}
.compass-r {
position: absolute;
right: 10rpx;
top: 50%;
transform: translateY(-50%);
font-size: 14rpx;
color: #666;
}
.compass-tr {
position: absolute;
top: 15%;
right: 15%;
font-size: 12rpx;
color: #a0a0a0;
}
.compass-tl {
position: absolute;
top: 15%;
left: 15%;
font-size: 12rpx;
}
.compass-br {
position: absolute;
bottom: 15%;
right: 15%;
font-size: 12rpx;
color: #666;
}
.compass-bl {
position: absolute;
bottom: 15%;
left: 15%;
font-size: 12rpx;
}
.pie {
width: 160rpx;
height: 160rpx;
position: relative;
}
.pie-svg {
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.pie-center {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.between {
justify-content: space-between;
width: 100%;
}
.box-i {
border-radius: 14rpx;
padding: 18rpx;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.box-i.good {
background: rgba(20, 83, 45, 0.12);
border-color: rgba(34, 197, 94, 0.2);
}
.box-i.bad {
background: rgba(127, 29, 29, 0.12);
border-color: rgba(239, 68, 68, 0.2);
}
.flying {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8rpx;
width: 480rpx;
max-width: 100%;
aspect-ratio: 1 / 1;
margin: 0 auto;
position: relative;
}
.flying-border {
position: absolute;
inset: 0;
border: 2px solid rgba(212, 175, 55, 0.2);
border-radius: 14rpx;
pointer-events: none;
}
.flying-cell {
background: rgba(255, 255, 255, 0.05);
border-radius: 14rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.flying-num {
font-size: 40rpx;
font-weight: 900;
color: #666;
}
.flying-good {
margin-top: 6rpx;
font-size: 14rpx;
color: #d4af37;
background: rgba(212, 175, 55, 0.1);
padding: 2rpx 8rpx;
border-radius: 10rpx;
}
.deep {
background: #1a1a2e;
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 14rpx;
padding: 18rpx;
margin-bottom: 16rpx;
}
.qimen-box {
margin-top: 14rpx;
padding: 16rpx;
border-radius: 16rpx;
background: rgba(26, 26, 46, 0.55);
border: 1px solid rgba(212, 175, 55, 0.18);
}
.qimen-top-tag {
display: flex;
justify-content: center;
margin-bottom: 12rpx;
}
.qimen-top-tag-text {
display: inline-block;
padding: 4rpx 12rpx;
border-radius: 999rpx;
border: 1px solid rgba(212, 175, 55, 0.6);
background: rgba(0, 0, 0, 0.45);
color: #d4af37;
font-size: 18rpx;
font-weight: 900;
}
.qimen-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0;
border-radius: 14rpx;
overflow: hidden;
border: 2rpx solid rgba(212, 175, 55, 0.55);
}
.qimen-cell {
aspect-ratio: 1 / 1;
padding: 10rpx;
background: rgba(10, 10, 15, 0.35);
border-right: 2rpx solid rgba(212, 175, 55, 0.45);
border-bottom: 2rpx solid rgba(212, 175, 55, 0.45);
}
.qimen-cell:nth-child(3n) {
border-right: 0;
}
.qimen-cell:nth-last-child(-n + 3) {
border-bottom: 0;
}
.qimen-center {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6rpx;
}
.qimen-center-title {
font-size: 30rpx;
font-weight: 900;
color: #d4af37;
}
.qimen-center-sub {
font-size: 20rpx;
color: rgba(212, 175, 55, 0.7);
}
.qimen-cell-inner {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
}
.qimen-loc {
display: none;
}
.qimen-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.qimen-spirit {
font-size: 22rpx;
font-weight: 800;
color: #e5e7eb;
}
.qimen-stems {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2rpx;
}
.qimen-stem {
font-size: 22rpx;
font-weight: 900;
color: #f87171;
line-height: 1;
}
.qimen-bottom {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.qimen-star {
font-size: 22rpx;
font-weight: 800;
color: #e5e7eb;
}
.qimen-door {
font-size: 20rpx;
font-weight: 900;
color: #d4af37;
padding: 2rpx 8rpx;
border-radius: 8rpx;
border: 1px solid rgba(212, 175, 55, 0.75);
background: rgba(0, 0, 0, 0.45);
line-height: 1.2;
}
.qimen-door-active {
box-shadow: 0 0 10rpx rgba(212, 175, 55, 0.35);
}
.qimen-gong {
position: absolute;
left: 50%;
bottom: 4rpx;
transform: translateX(-50%);
font-size: 18rpx;
color: #a0a0a0;
}
.deep-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
padding-bottom: 12rpx;
}
.deep-month {
font-size: 32rpx;
font-weight: 900;
}
.deep-chip {
font-size: 14rpx;
padding: 4rpx 10rpx;
border-radius: 12rpx;
border: 1px solid rgba(212, 175, 55, 0.2);
background: rgba(212, 175, 55, 0.1);
color: #d4af37;
}
.star {
font-size: 18rpx;
color: #333;
}
.star.on {
color: #d4af37;
}
.sha {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.sha-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16rpx;
padding-bottom: 12rpx;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.sha-row.last {
border-bottom: 0;
padding-bottom: 0;
}
.wealth-corner {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.corner-diagram {
position: relative;
aspect-ratio: 16 / 9;
border-radius: 14rpx;
background: #0a0a0f;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.door {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 80rpx;
height: 10rpx;
background: #333;
}
.corner-dot {
position: absolute;
top: 12rpx;
left: 12rpx;
width: 56rpx;
height: 56rpx;
border-radius: 999rpx;
border: 1px solid #d4af37;
display: flex;
align-items: center;
justify-content: center;
background: rgba(212, 175, 55, 0.06);
}
.room {
display: flex;
flex-direction: column;
}
.room-row {
display: flex;
gap: 16rpx;
padding: 18rpx 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.room-row.last {
border-bottom: 0;
}
.room-icon {
width: 44rpx;
height: 44rpx;
border-radius: 12rpx;
background: rgba(255, 255, 255, 0.05);
display: flex;
align-items: center;
justify-content: center;
}
.room-tag {
font-size: 14rpx;
padding: 4rpx 10rpx;
border-radius: 10rpx;
}
.room-good {
background: rgba(20, 83, 45, 0.2);
color: #4ade80;
}
.room-bad {
background: rgba(127, 29, 29, 0.2);
color: #f87171;
}
.room-gold {
background: rgba(212, 175, 55, 0.12);
color: #d4af37;
}
.love-icon {
width: 80rpx;
height: 80rpx;
border-radius: 999rpx;
background: rgba(236, 72, 153, 0.12);
border: 1px solid rgba(236, 72, 153, 0.2);
display: flex;
align-items: center;
justify-content: center;
color: #f472b6;
}
.pet-box {
background: rgba(255, 255, 255, 0.05);
border-radius: 14rpx;
padding: 18rpx;
text-align: center;
border-top: 6rpx solid #d4af37;
}
.wallet {
width: 96rpx;
height: 80rpx;
background: rgba(0, 0, 0, 0.4);
border-radius: 14rpx;
border: 1px solid #d4af37;
display: flex;
align-items: center;
justify-content: center;
}
</style>
<style>
/* 全局样式:重置 page 默认样式,确保页面占满全屏 */
page {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
box-sizing: border-box;
overflow: hidden;
}
</style>