4886 lines
137 KiB
Vue
4886 lines
137 KiB
Vue
<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, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
|
||
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>
|