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

2950 lines
82 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="naming-detail">
<!-- Starry Background -->
<view class="starry-bg">
<view v-for="star in stars" :key="star.id" class="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="glow-top"></view>
<view class="glow-bottom"></view>
</view>
<!-- Header -->
<view class="detail-header">
<view class="detail-header-back" @click="handleBack">
<text class="back-icon"></text>
<text class="back-text">返回</text>
</view>
<text class="detail-header-title">详解报告</text>
<view class="detail-header-download" @click="handleDownloadPdf">
<text class="detail-header-download-icon"></text>
<text class="detail-header-download-text">下载PDF</text>
</view>
</view>
<!-- Content -->
<scroll-view scroll-y class="detail-content">
<!-- 1. 总分卡片 -->
<view class="score-card clickable" @click="openScoreDetail">
<view class="score-corner score-corner-tl"></view>
<view class="score-corner score-corner-tr"></view>
<view class="score-corner score-corner-bl"></view>
<view class="score-corner score-corner-br"></view>
<view class="score-label">Total Score</view>
<view class="score-value-wrap">
<text class="score-value">{{ report.header.score }}</text>
<view class="score-ring"></view>
</view>
<view class="score-stars">
<text v-for="i in 5" :key="i" class="score-star" :class="{ active: i <= report.header.stars }"></text>
</view>
<view class="score-divider"></view>
<text class="score-name">{{ report.header.name }}</text>
<text class="score-pinyin">{{ report.header.pinyin }}</text>
</view>
<!-- 2. 五行生克与八卦放第一位 -->
<view v-if="hasDetailWuxingBagua" class="section">
<view class="section-title">
<text class="section-icon"></text>
<text class="section-text">五行生克与八卦</text>
</view>
<view class="wuge-card clickable" @click="openWuxingBaguaDetail">
<text v-if="detailWuxingBaguaPreview[0]" class="info-card-title">{{ detailWuxingBaguaPreview[0] }}</text>
<text v-if="detailWuxingBaguaPreview[1]" class="info-card-text">{{ detailWuxingBaguaPreview[1] }}</text>
<text v-if="detailWuxingBaguaPreview[2]" class="info-card-text">{{ detailWuxingBaguaPreview[2] }}</text>
</view>
</view>
<!-- 2a. 五行单字详解用于五行八卦补全每个字 -->
<view v-if="hasCharDetail" class="section">
<view class="section-title">
<text class="section-icon"></text>
<text class="section-text">五行单字详解</text>
</view>
<view class="info-card clickable" @click="openCharDetail">
<text v-for="(it, idx) in charDetailPreview" :key="idx" class="info-card-text">{{ charDetailLine(it) }}</text>
<text v-if="charDetailItems.length > 2" class="info-card-text muted">查看更多</text>
</view>
</view>
<!-- 2b. 八字与姓名五行喜用 · 生克 · 补益 -->
<view v-if="hasDetailBazi" class="section">
<view class="section-title">
<text class="section-icon"></text>
<text class="section-text">八字与姓名五行</text>
</view>
<view class="zodiac-card clickable" @click="openBaziNameFitDetail">
<view class="zodiac-header">
<view class="zodiac-info">
<text v-if="detailBaziLines[0]" class="info-card-text">{{ detailBaziLines[0] }}</text>
<text v-if="detailBaziLines[1]" class="info-card-text">{{ detailBaziLines[1] }}</text>
<text v-if="detailBaziLines[2]" class="info-card-text">{{ detailBaziLines[2] }}</text>
</view>
<text
v-if="detailBaziScoreText"
class="zodiac-score"
>{{ detailBaziScoreText }}</text>
</view>
</view>
</view>
<!-- 2c. 字义与生肖解析 -->
<view
v-if="report.meaningZodiac.meaning || report.meaningZodiac.judge || report.meaningZodiac.zodiac.value || report.meaningZodiac.zodiac.desc"
class="section">
<view class="section-title">
<text class="section-icon">📖</text>
<text class="section-text">字义与生肖解析</text>
</view>
<view class="meaning-card clickable" @click="openMeaningDetail">
<view class="meaning-quote"></view>
<text class="meaning-label">名字寓意</text>
<text class="meaning-text">{{ report.meaningZodiac.meaning }}</text>
<view class="meaning-judge">
<text class="meaning-judge-label">判断</text>
<text class="meaning-judge-text">{{ report.meaningZodiac.judge }}</text>
</view>
</view>
<view class="zodiac-card clickable" @click="openMeaningDetail">
<view class="zodiac-header">
<view class="zodiac-icon-wrap">
<text class="zodiac-icon">{{ report.meaningZodiac.zodiac.icon }}</text>
</view>
<view class="zodiac-info">
<text class="zodiac-label">{{ report.meaningZodiac.zodiac.label }}</text>
<text class="zodiac-value">{{ report.meaningZodiac.zodiac.value }}</text>
</view>
<text class="zodiac-score">{{ report.meaningZodiac.zodiac.scoreText }}</text>
</view>
<text class="zodiac-desc">
{{ report.meaningZodiac.zodiac.desc }}
</text>
</view>
</view>
<!-- 2d. 属相与名字合宜 -->
<view v-if="hasDetailZodiacSign" class="section">
<view class="section-title">
<text class="section-icon">🐲</text>
<text class="section-text">属相与名字</text>
</view>
<view class="zodiac-card clickable" @click="openZodiacSignDetail">
<view class="zodiac-header">
<view class="zodiac-icon-wrap">
<text class="zodiac-icon">{{ detailZodiacIcon }}</text>
</view>
<view class="zodiac-info">
<text class="zodiac-label">生肖</text>
<text class="zodiac-value">{{ detailZodiacTitle }}</text>
<text v-if="detailZodiacHarmony" class="zodiac-desc">{{ detailZodiacHarmony }}</text>
</view>
</view>
</view>
</view>
<!-- 3. 家族与起名原理 -->
<view v-if="report.family.cards.length" class="section">
<view class="section-title">
<text class="section-icon">👥</text>
<text class="section-text">家族与起名原理</text>
</view>
<view v-for="(card, idx) in report.family.cards" :key="idx" class="info-card clickable"
@click="openFamilyDetail">
<text class="info-card-title">{{ card.title }}</text>
<text class="info-card-text">{{ card.text }}</text>
</view>
</view>
<!-- 4. 笔画数理分析 -->
<view v-if="report.stroke.wuge.items.length || report.stroke.wuxing.items.length" class="section">
<view class="section-title">
<text class="section-icon">#</text>
<text class="section-text">笔画数理分析</text>
</view>
<view class="wuge-card clickable" @click="openWugeDetail">
<view class="wuge-header">
<text class="wuge-label">三才五格配置</text>
<text class="wuge-badge">{{ report.stroke.wuge.badge }}</text>
</view>
<view v-for="(it, idx) in report.stroke.wuge.items" :key="idx" class="wuge-item">
<view class="wuge-item-header">
<text class="wuge-item-name" :class="it.primary ? 'wuge-item-name-primary' : ''">{{ it.name }}</text>
<text class="wuge-item-result" :class="it.primary ? 'wuge-item-result-primary' : ''">{{ it.result
}}</text>
</view>
<view class="wuge-bar">
<view class="wuge-bar-fill" :style="{ width: it.percent + '%', background: it.color }"></view>
</view>
</view>
</view>
<!-- 五行能量分布 -->
<view v-if="report.stroke.wuxing.items.length" class="wuxing-card clickable" @click="openWuxingDetail">
<text class="wuxing-title">{{ report.stroke.wuxing.title }}</text>
<view class="wuxing-list">
<view v-for="item in wuxingData" :key="item.label" class="wuxing-item">
<text class="wuxing-label">{{ item.label }}</text>
<view class="wuxing-bar">
<view class="wuxing-bar-fill" :style="{ width: item.value + '%', background: item.color }"></view>
</view>
<text class="wuxing-value">{{ item.value }}%</text>
</view>
</view>
</view>
</view>
<!-- 5. 三才配置深度解析 -->
<view v-if="report.sancai.items.length || report.sancai.configText || report.sancai.badge" class="section">
<view class="section-title">
<text class="section-icon"></text>
<text class="section-text">三才配置深度解析</text>
</view>
<view class="sancai-card clickable" @click="openSancaiDetail">
<view class="sancai-header">
<view class="sancai-config">
<text class="sancai-config-text">{{ report.sancai.configText }}</text>
<text class="sancai-config-label">{{ report.sancai.configLabel }}</text>
</view>
<text class="sancai-badge">{{ report.sancai.badge }}</text>
</view>
<view v-for="(it, idx) in report.sancai.items" :key="idx" class="sancai-item">
<text :class="it.primary ? 'sancai-item-label' : 'sancai-item-label-normal'">{{ it.label }}</text>
<text class="sancai-item-text">{{ it.text }}</text>
</view>
</view>
</view>
<!-- 6. 潜在性格剖析 -->
<view v-if="report.personality.strengths.items.length || report.personality.challenges.items.length"
class="section">
<view class="section-title">
<text class="section-icon">👤</text>
<text class="section-text">潜在性格剖析</text>
</view>
<view class="personality-grid">
<view class="personality-card personality-card-strength clickable" @click="openPersonalityDetail('strength')">
<text class="personality-title personality-title-gold">{{ report.personality.strengths.title }}</text>
<view class="personality-list">
<view v-for="(t, idx) in report.personality.strengths.items" :key="idx" class="personality-item">
<view class="personality-dot personality-dot-gold"></view>
<text class="personality-text">{{ t }}</text>
</view>
</view>
</view>
<view class="personality-card personality-card-challenge clickable"
@click="openPersonalityDetail('challenge')">
<text class="personality-title personality-title-red">{{ report.personality.challenges.title }}</text>
<view class="personality-list">
<view v-for="(t, idx) in report.personality.challenges.items" :key="idx" class="personality-item">
<view class="personality-dot personality-dot-red"></view>
<text class="personality-text">{{ t }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 7. 周易卦象 -->
<view v-if="report.gua.name || report.gua.desc || report.gua.lines.length" class="section">
<view class="section-title">
<text class="section-icon"></text>
<text class="section-text">周易卦象</text>
</view>
<view class="gua-card clickable" @click="openGuaDetail">
<view class="gua-bg-text">{{ report.gua.bgText }}</view>
<view class="gua-content">
<view class="gua-symbol">
<view v-for="(ln, idx) in report.gua.lines" :key="idx">
<view v-if="ln === 'solid'" class="gua-line gua-line-solid"></view>
<view v-else class="gua-line gua-line-broken">
<view class="gua-line-part"></view>
<view class="gua-line-part"></view>
</view>
<view v-if="idx === 2" class="gua-spacer"></view>
</view>
</view>
<view class="gua-info">
<view class="gua-header">
<text class="gua-name">{{ report.gua.name }}</text>
<text class="gua-badge">{{ report.gua.badge }}</text>
</view>
<text class="gua-desc">{{ report.gua.desc }}</text>
</view>
</view>
</view>
</view>
<!-- 8. 开运锦囊 -->
<view v-if="report.lucky.cards.length || report.lucky.health.value || report.lucky.health.note" class="section">
<view class="section-title">
<text class="section-icon"></text>
<text class="section-text">开运锦囊</text>
</view>
<view class="lucky-grid">
<view v-for="(c, idx) in report.lucky.cards" :key="idx" class="lucky-card clickable"
@click="openLuckyDetail(c.key)">
<view class="lucky-icon" :class="c.iconClass">{{ c.icon }}</view>
<view class="lucky-info">
<text class="lucky-label">{{ c.label }}</text>
<text class="lucky-value">{{ c.value }}</text>
</view>
</view>
</view>
<view class="health-card clickable" @click="openHealthDetail">
<view class="health-icon"></view>
<view class="health-info">
<view class="health-header">
<text class="health-label">健康易感提示</text>
<text class="health-note">{{ report.lucky.health.note }}</text>
</view>
<text class="health-value">{{ report.lucky.health.value }}</text>
</view>
</view>
</view>
<!-- 9. 六维格局 -->
<SixDimensionRadar v-if="report.sixDimension.labels.length && report.sixDimension.values.length"
:labels="report.sixDimension.labels" :values="report.sixDimension.values"
:remark="report.sixDimension.remark" />
<!-- 10. 人生运程模拟 -->
<view v-if="report.fortune.labels.length || report.fortune.note" class="section">
<view class="section-title">
<text class="section-icon">📈</text>
<text class="section-text">人生运程模拟</text>
</view>
<view class="fortune-card clickable" @click="openFortuneDetail">
<view class="fortune-chart">
<view class="fortune-line">
<view class="fortune-point fortune-point-1"></view>
<view class="fortune-point fortune-point-2"></view>
<view class="fortune-point fortune-point-3"></view>
<view class="fortune-point fortune-point-4"></view>
</view>
<view class="fortune-labels">
<view v-for="(lb, idx) in report.fortune.labels" :key="idx" class="fortune-label-item"
:class="lb.peak ? 'fortune-label-peak' : ''">
<text class="fortune-label-text">{{ lb.text }}</text>
</view>
</view>
</view>
<text class="fortune-note">{{ report.fortune.note }}</text>
</view>
</view>
<!-- 11. 情感与社交运势 -->
<view v-if="report.emotion.title || report.emotion.desc || report.emotion.tags.length" class="section">
<view class="section-title">
<text class="section-icon">💕</text>
<text class="section-text">情感与社交运势</text>
</view>
<view class="emotion-card clickable" @click="openEmotionDetail">
<view class="emotion-header">
<view class="emotion-icon-wrap">
<text class="emotion-icon">💗</text>
</view>
<view class="emotion-info">
<text class="emotion-title">{{ report.emotion.title }}</text>
<text class="emotion-desc">{{ report.emotion.desc }}</text>
</view>
</view>
<view class="emotion-divider"></view>
<view class="emotion-tags-section">
<text class="emotion-tags-title">{{ report.emotion.tagsTitle }}</text>
<view class="emotion-tags">
<text v-for="(t, idx) in report.emotion.tags" :key="idx" class="emotion-tag"
:class="idx === 0 ? 'emotion-tag-primary' : ''">{{ t }}</text>
</view>
</view>
</view>
</view>
<!-- 12. 日常生活开运指南 -->
<view v-if="report.daily.cards.length || report.daily.avatar.items.length" class="section">
<view class="section-title">
<text class="section-icon"></text>
<text class="section-text">日常生活开运指南</text>
</view>
<view class="daily-grid">
<view v-for="(c, idx) in report.daily.cards" :key="idx" class="daily-card clickable" @click="openDailyDetail">
<text class="daily-label">{{ c.title }}</text>
<template v-if="c.type === 'time'">
<view v-for="(t, i) in c.times" :key="i" class="daily-time-item">
<text class="daily-time-icon">{{ t.icon }}</text>
<text class="daily-time-text">{{ t.text }}</text>
</view>
</template>
<template v-else>
<view class="daily-items">
<view v-for="(it, i) in c.items" :key="i" :class="it.class">{{ it.text }}</view>
</view>
<text class="daily-tip">{{ c.tip }}</text>
</template>
</view>
</view>
<!-- 开运头像风格推荐 -->
<view class="avatar-card">
<text class="avatar-label">{{ report.daily.avatar.label }}</text>
<view class="avatar-list">
<view v-for="(it, idx) in report.daily.avatar.items" :key="idx" :class="it.class">
<text class="avatar-text">{{ it.text }}</text>
</view>
</view>
</view>
</view>
<!-- 13. 诗词出处 -->
<view v-if="report.poetry.text" class="section">
<view class="section-title">
<text class="section-icon">📜</text>
<text class="section-text">诗词出处</text>
</view>
<view class="poetry-card">
<view class="poetry-quote"></view>
<text class="poetry-text">"{{ report.poetry.text }}"</text>
</view>
</view>
<!-- Action Buttons -->
<view class="action-buttons">
<!-- <view class="action-btn action-btn-share">
<text class="action-btn-text">生成海报</text>
</view> -->
<view class="action-btn action-btn-primary" @click="handleWealthAnalysis">
<text class="action-btn-text">财运解析</text>
</view>
<!-- <view class="action-btn action-btn-download">
<text class="action-btn-text">下载报告</text>
</view> -->
</view>
<!-- Footer -->
<view class="detail-footer">
<text class="footer-text">壹梵 · 致力于东方美学与易经智慧的传承</text>
</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 } from "vue";
import SixDimensionRadar from "../SixDimensionRadar.vue";
import { getWealthAnalysisByReportId } from "@/api/cai-yun";
import pdfBgUrl from "../../utils/pdf/background.png";
const props = defineProps<{
data: any | null;
}>();
const emit = defineEmits<{
back: [];
wealthAnalysis: [any];
}>();
type DetailBlock = { title: string; lines: string[] };
type PersonalNamingReport = {
header: {
score: number;
stars: number;
name: string;
pinyin: string;
};
meaningZodiac: {
meaning: string;
judge: string;
zodiac: {
icon: string;
label: string;
value: string;
scoreText: string;
desc: string;
};
};
family: {
cards: Array<{ title: string; text: string }>;
};
stroke: {
wuge: {
badge: string;
items: Array<{
name: string;
result: string;
primary?: boolean;
percent: number;
color: string;
}>;
};
wuxing: {
title: string;
items: Array<{ label: string; value: number; color: string }>;
};
};
sancai: {
configText: string;
configLabel: string;
badge: string;
items: Array<{ label: string; text: string; primary?: boolean }>;
};
personality: {
strengths: { title: string; items: string[] };
challenges: { title: string; items: string[] };
};
gua: {
bgText: string;
name: string;
badge: string;
desc: string;
lines: Array<'solid' | 'broken'>;
};
lucky: {
cards: Array<{ key: 'color' | 'number' | 'industry' | 'constellation'; icon: string; iconClass: string; label: string; value: string }>;
health: { note: string; value: string };
};
sixDimension: { labels: string[]; values: number[]; remark: string };
fortune: { labels: Array<{ text: string; peak?: boolean }>; note: string };
emotion: { title: string; desc: string; tagsTitle: string; tags: string[] };
daily: {
cards: Array<
| { type: 'time'; title: string; times: Array<{ icon: string; text: string }> }
| { type: 'wear'; title: string; items: Array<{ text: string; class: string }>; tip: string }
>;
avatar: { label: string; items: Array<{ text: string; class: string }> };
};
poetry: { text: string };
/** 与测名详解同构:五行生克、八字与名、属相与名 */
detailModules?: {
bazi_name_fit?: any;
wuxing_bagua?: any;
zodiac_sign?: any;
/** 单字五行/阴阳/卦象(用于“五行八卦”里展示每个字) */
char_detail?: any;
};
details: {
score: DetailBlock;
meaning: DetailBlock;
family: DetailBlock;
wuge: DetailBlock;
wuxing: DetailBlock;
sancai: DetailBlock;
personality_strength: DetailBlock;
personality_challenge: DetailBlock;
gua: DetailBlock;
lucky: Record<'color' | 'number' | 'industry' | 'constellation', DetailBlock>;
health: DetailBlock;
fortune: DetailBlock;
emotion: DetailBlock;
daily: DetailBlock;
};
};
const normalizePersonalReport = (raw: any): PersonalNamingReport => {
if (raw && raw.header && raw.meaningZodiac && raw.stroke && raw.details) {
const legacy = raw as any;
return {
...legacy,
detailModules: {
bazi_name_fit: legacy?.bazi_name_fit,
wuxing_bagua: legacy?.wuxing_bagua,
zodiac_sign: legacy?.zodiac_sign,
char_detail: legacy?.char_detail,
},
family: {
cards: Array.isArray(legacy?.family?.cards) ? legacy.family.cards : [],
},
stroke: {
...legacy?.stroke,
wuge: {
...(legacy?.stroke?.wuge || {}),
items: Array.isArray(legacy?.stroke?.wuge?.items) ? legacy.stroke.wuge.items : [],
},
wuxing: {
...(legacy?.stroke?.wuxing || {}),
items: Array.isArray(legacy?.stroke?.wuxing?.items) ? legacy.stroke.wuxing.items : [],
},
},
sancai: {
...(legacy?.sancai || {}),
configText: String(legacy?.sancai?.configText || ''),
configLabel: String(legacy?.sancai?.configLabel || '(天 - 人 - 地)'),
badge: String(legacy?.sancai?.badge || ''),
items: Array.isArray(legacy?.sancai?.items) ? legacy.sancai.items.filter(Boolean) : [],
},
personality: {
strengths: {
...(legacy?.personality?.strengths || {}),
items: Array.isArray(legacy?.personality?.strengths?.items) ? legacy.personality.strengths.items : [],
},
challenges: {
...(legacy?.personality?.challenges || {}),
items: Array.isArray(legacy?.personality?.challenges?.items) ? legacy.personality.challenges.items : [],
},
},
gua: {
...(legacy?.gua || {}),
lines: Array.isArray(legacy?.gua?.lines) ? legacy.gua.lines : [],
},
lucky: {
...(legacy?.lucky || {}),
cards: Array.isArray(legacy?.lucky?.cards) ? legacy.lucky.cards : [],
health: legacy?.lucky?.health || { note: '', value: '' },
},
sixDimension: {
...(legacy?.sixDimension || {}),
labels: Array.isArray(legacy?.sixDimension?.labels) ? legacy.sixDimension.labels : [],
values: Array.isArray(legacy?.sixDimension?.values) ? legacy.sixDimension.values : [],
},
fortune: {
...(legacy?.fortune || {}),
labels: Array.isArray(legacy?.fortune?.labels) ? legacy.fortune.labels : [],
},
emotion: {
...(legacy?.emotion || {}),
tags: Array.isArray(legacy?.emotion?.tags) ? legacy.emotion.tags : [],
},
daily: {
cards: Array.isArray(legacy?.daily?.cards) ? legacy.daily.cards : [],
avatar: {
label: String(legacy?.daily?.avatar?.label || ''),
items: Array.isArray(legacy?.daily?.avatar?.items) ? legacy.daily.avatar.items : [],
},
},
} as PersonalNamingReport;
}
const name = String(raw?.name || '');
const pinyin = String(raw?.pinyin || '');
const score = Number(raw?.total_score ?? raw?.score ?? 0);
const stars = Number(raw?.star_rating ?? 0);
const meaning = String(raw?.name_meaning ?? raw?.meaning ?? '');
const poetrySource = String(raw?.poetry_source ?? raw?.source ?? '');
const toLines = (v: any): string[] => {
if (typeof v !== 'string') return [];
const s = v.trim();
if (!s) return [];
return s.split(/\r?\n/).map((x) => x.trim()).filter(Boolean);
};
const zodiacAnalysis = String(raw?.zodiac_analysis ?? '');
const wuxingAnalysis = String(raw?.wuxing_analysis ?? '');
const sancaiAnalysis = String(raw?.sancai_analysis ?? '');
const strokeAnalysis = String(raw?.stroke_analysis ?? '');
const sixDim = raw?.six_dimension;
const sixLabels = sixDim && typeof sixDim === 'object'
? Object.keys(sixDim)
: [];
const sixValues = sixDim && typeof sixDim === 'object'
? Object.values(sixDim).map((n: any) => Number(n) || 0)
: [];
const derivedStars = stars || (score ? Math.min(5, Math.max(1, Math.round(score / 20))) : 0);
return {
header: {
score: Number.isFinite(score) ? score : 0,
stars: Number.isFinite(derivedStars) ? derivedStars : 0,
name,
pinyin,
},
meaningZodiac: {
meaning,
judge: zodiacAnalysis,
zodiac: {
icon: '',
label: '生肖适配度',
value: '',
scoreText: '',
desc: zodiacAnalysis,
},
},
family: {
cards: [],
},
stroke: {
wuge: {
badge: '',
items: [],
},
wuxing: {
title: '五行能量分布',
items: [],
},
},
sancai: {
configText: '',
configLabel: '(天 - 人 - 地)',
badge: '',
items: sancaiAnalysis ? [{ label: '', text: sancaiAnalysis, primary: true }] : [],
},
personality: {
strengths: { title: '', items: [] },
challenges: { title: '', items: [] },
},
gua: {
bgText: '',
name: '',
badge: '',
desc: '',
lines: [],
},
lucky: {
cards: [],
health: { note: '', value: '' },
},
sixDimension: {
labels: sixLabels,
values: sixValues,
remark: '',
},
fortune: {
labels: [],
note: '',
},
emotion: {
title: '',
desc: '',
tagsTitle: '',
tags: [],
},
daily: {
cards: [],
avatar: { label: '', items: [] },
},
poetry: { text: poetrySource },
detailModules: {
bazi_name_fit: raw?.bazi_name_fit,
wuxing_bagua: raw?.wuxing_bagua,
zodiac_sign: raw?.zodiac_sign,
char_detail: raw?.char_detail,
},
details: {
score: {
title: '总分详解',
lines: [
name ? `姓名:${name}` : '',
pinyin ? `拼音:${pinyin}` : '',
score ? `综合评分:${score}` : '',
].filter(Boolean),
},
meaning: {
title: '字义与生肖详解',
lines: [
meaning ? `名字寓意:${meaning}` : '',
...(raw?.zodiac_sign?.name_harmony
? [`属相与名字:${String(raw.zodiac_sign.name_harmony).trim()}`]
: []),
...toLines(zodiacAnalysis),
].filter(Boolean),
},
family: {
title: '家族与起名原理详解',
lines: [],
},
wuge: {
title: '三才五格配置详解',
lines: toLines(strokeAnalysis),
},
wuxing: {
title: '五行能量分布详解',
lines: [
...toLines(wuxingAnalysis),
...(raw?.wuxing_bagua?.mutual_sketch ? [String(raw.wuxing_bagua.mutual_sketch).trim()] : []),
...(raw?.wuxing_bagua?.summary ? [String(raw.wuxing_bagua.summary).trim()] : []),
].filter(Boolean),
},
sancai: {
title: '三才配置深度解析',
lines: toLines(sancaiAnalysis),
},
personality_strength: {
title: '优势性格详解',
lines: [],
},
personality_challenge: {
title: '潜在盲点详解',
lines: [],
},
gua: {
title: '周易卦象详解',
lines: [],
},
lucky: {
color: { title: '幸运色详解', lines: [] },
number: { title: '幸运数详解', lines: [] },
industry: { title: '建议行业详解', lines: [] },
constellation: { title: '星座详解', lines: [] },
},
health: {
title: '健康开运详解',
lines: [],
},
fortune: {
title: '人生运程模拟详解',
lines: [],
},
emotion: {
title: '情感与社交运势详解',
lines: [],
},
daily: {
title: '日常生活开运指南详解',
lines: [],
},
},
};
};
const rawData = computed(() => props.data);
const report = computed<PersonalNamingReport>(() => normalizePersonalReport(rawData.value));
const arr = (v: any) => (Array.isArray(v) ? v : []);
const toStr = (v: any) => String(v ?? "").trim();
const charDetailItems = computed(() => {
const x = report.value.detailModules?.char_detail;
const items = Array.isArray(x?.items) ? x.items : [];
return items;
});
const hasCharDetail = computed(() => charDetailItems.value.length > 0);
const charDetailPreview = computed(() => charDetailItems.value.slice(0, 2));
function charDetailLine(it: any): string {
const char = toStr(it?.char);
const element = toStr(it?.element);
const yinYang = toStr(it?.yin_yang_element) || toStr(it?.yin_yang);
const trigram = [toStr(it?.bagua_trigram_symbol), toStr(it?.bagua_trigram)].filter(Boolean).join(' ');
const parts = [char, element, yinYang, trigram ? `卦:${trigram}` : ''].filter(Boolean);
return parts.join(' · ');
}
function 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;
}
function linesFromCharDetail(x: any): string[] {
if (!x || typeof x !== "object") return [];
const lines: string[] = [];
const items = Array.isArray((x as any).items) ? (x as any).items : [];
for (const it of items) {
const head = charDetailLine(it);
if (head) lines.push(`${head}`);
const note = toStr(it?.note);
if (note) lines.push(note);
const nodeLines = flattenDetailNodes(arr(it?.details?.nodes));
nodeLines.forEach((l) => lines.push(l));
const hexName = toStr(it?.hexagram_name);
const hexCodeRaw = it?.hexagram_code;
const hexCode = Number(hexCodeRaw);
if (hexName || Number.isFinite(hexCode)) {
lines.push(
[
'卦象:',
hexName || '',
Number.isFinite(hexCode) ? `${hexCode}` : '',
].join('')
);
}
lines.push('');
}
while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
const summaryNodes = flattenDetailNodes(arr((x as any)?.details?.nodes));
if (summaryNodes.length) {
if (lines.length) lines.push('');
summaryNodes.forEach((l) => lines.push(l));
}
return lines.filter((l) => l !== undefined && l !== null).map((l) => String(l));
}
function linesFromBazi(b: any): string[] {
if (!b || typeof b !== "object") return [];
const lines: string[] = [];
const xy = toStr(b.xiyongshen);
const profile = toStr(b.name_wuxing_profile);
const comp = toStr(b.complement_summary);
if (xy) lines.push(`喜用神:${xy}`);
if (profile) lines.push(`姓名五行:${profile}`);
if (comp) lines.push(comp);
const rawScore = b.fit_score;
const n = Number(rawScore);
if (rawScore !== undefined && rawScore !== null && rawScore !== "" && Number.isFinite(n)) {
lines.push(`与八字契合度:${n}`);
}
return lines.filter(Boolean);
}
function linesFromWuxingBagua(x: any): string[] {
if (!x || typeof x !== "object") return [];
const lines: string[] = [];
const w = toStr(x.wuxing_sketch);
const bg = toStr(x.bagua_profile);
const mut = toStr(x.mutual_sketch);
const sum = toStr(x.summary);
if (w) lines.push(`五行:${w}`);
if (bg) lines.push(`八卦:${bg}`);
if (mut) lines.push(`生克互助:${mut}`);
if (sum) lines.push(sum);
return lines.filter(Boolean);
}
function linesFromZodiac(z: any): string[] {
if (!z || typeof z !== "object") return [];
const lines: string[] = [];
const animal = toStr(z.animal);
const br = toStr(z.earthly_branch);
if (animal || br) lines.push(`属相:${animal}${br ? `${br}` : ""}`);
const trait = toStr(z.trait_summary);
if (trait) lines.push(`生肖特性:${trait}`);
const harm = toStr(z.name_harmony);
if (harm) lines.push(`与名字:${harm}`);
return lines.filter(Boolean);
}
const hasDetailBazi = computed(() => linesFromBazi(report.value.detailModules?.bazi_name_fit).length > 0);
const hasDetailWuxingBagua = computed(() => linesFromWuxingBagua(report.value.detailModules?.wuxing_bagua).length > 0);
const hasDetailZodiacSign = computed(() => linesFromZodiac(report.value.detailModules?.zodiac_sign).length > 0);
const detailBaziLines = computed(() => {
const b = report.value.detailModules?.bazi_name_fit;
const full = linesFromBazi(b);
return full.slice(0, 3);
});
const detailBaziScoreText = computed(() => {
const b = report.value.detailModules?.bazi_name_fit;
if (!b) return "";
const n = Number(b.fit_score);
if (!Number.isFinite(n)) return "";
return `${n}`;
});
const detailWuxingBaguaPreview = computed(() => linesFromWuxingBagua(report.value.detailModules?.wuxing_bagua).slice(0, 3));
const detailZodiacIcon = computed(() => {
const z = report.value.detailModules?.zodiac_sign;
const icon = toStr(z?.animal_icon);
if (icon) return icon;
const a = toStr(z?.animal);
return a || "肖";
});
const detailZodiacTitle = computed(() => {
const z = report.value.detailModules?.zodiac_sign;
const animal = toStr(z?.animal);
const br = toStr(z?.earthly_branch);
if (animal && br) return `${animal} · ${br}`;
return animal || br || "属相";
});
const detailZodiacHarmony = computed(() => toStr(report.value.detailModules?.zodiac_sign?.name_harmony));
// 星星数据
const stars = ref(
Array.from({ length: 30 }).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 wuxingData = computed(() => report.value.stroke.wuxing.items);
const handleBack = () => {
emit("back");
};
const handleDownloadPdf = async () => {
if (typeof window === "undefined" || typeof document === "undefined") return;
uni.showLoading({ title: "正在生成PDF..." });
try {
const raw = rawData.value || {};
const name = String(report.value.header?.name || raw?.name || "").trim();
const score = Number(report.value.header?.score ?? raw?.total_score ?? raw?.score ?? 0) || 0;
const stars = Number(report.value.header?.stars ?? raw?.star_rating ?? 0) || 0;
const escapeHtml = (s: string) =>
s
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
const normalizeLines = (lines: any): string[] => {
if (Array.isArray(lines)) return lines.map((x) => String(x ?? "").trim()).filter(Boolean);
const single = String(lines ?? "").trim();
return single ? [single] : [];
};
const uniqueLines = (lines: string[]) => {
const seen = new Set<string>();
const out: string[] = [];
lines.forEach((line) => {
const k = line.trim();
if (!k || seen.has(k)) return;
seen.add(k);
out.push(k);
});
return out;
};
const noisyKeys = new Set(["details", "nodes", "type", "meta", "status", "code", "raw"]);
const flattenAnyToLines = (val: any, depth = 0): string[] => {
if (depth > 4 || val === null || val === undefined) return [];
if (typeof val === "string" || typeof val === "number" || typeof val === "boolean") {
const s = String(val).trim();
return s ? [s] : [];
}
if (Array.isArray(val)) {
return val.flatMap((it) => flattenAnyToLines(it, depth + 1));
}
if (typeof val === "object") {
const out: string[] = [];
Object.keys(val).forEach((k) => {
if (noisyKeys.has(k)) return;
const child = flattenAnyToLines((val as any)[k], depth + 1);
if (!child.length) return;
child.forEach((line) => out.push(`${k}${line}`));
});
return out;
}
return [];
};
const mergeLines = (...groups: any[]) => uniqueLines(groups.flatMap((g) => normalizeLines(g)));
const charDetailLines = mergeLines(
linesFromCharDetail((raw as any)?.char_detail),
linesFromCharDetail(report.value.detailModules?.char_detail),
// 兼容老结构:没有 items 时仍尽量保留
charDetailItems.value.map(charDetailLine),
flattenDetailNodes(arr((raw as any)?.char_detail?.details?.nodes)),
flattenAnyToLines((raw as any)?.char_detail),
flattenAnyToLines(report.value.detailModules?.char_detail),
);
const rawLineMap: Record<string, string[]> = {
"分数": mergeLines(flattenAnyToLines((raw as any)?.header)),
"五行单字详解": charDetailLines,
"五行生克与八卦": mergeLines(flattenAnyToLines((raw as any)?.wuxing_bagua), flattenAnyToLines(report.value.detailModules?.wuxing_bagua)),
"八字与姓名五行": mergeLines(flattenAnyToLines((raw as any)?.bazi_name_fit), flattenAnyToLines(report.value.detailModules?.bazi_name_fit)),
"字义与生肖": mergeLines(flattenAnyToLines((raw as any)?.meaning_zodiac), flattenAnyToLines((raw as any)?.meaning), flattenAnyToLines((raw as any)?.zodiac_sign)),
"属相与名字": mergeLines(flattenAnyToLines((raw as any)?.zodiac_sign), flattenAnyToLines(report.value.detailModules?.zodiac_sign)),
"三才五格": mergeLines(flattenAnyToLines((raw as any)?.sancai), flattenAnyToLines((raw as any)?.strokes_wuge_sancai)),
"笔画数理": mergeLines(flattenAnyToLines((raw as any)?.stroke), flattenAnyToLines((raw as any)?.wuge)),
"五行能量分布": mergeLines(flattenAnyToLines((raw as any)?.wuxing)),
"周易卦象": mergeLines(flattenAnyToLines((raw as any)?.gua), flattenAnyToLines((raw as any)?.liuyao)),
"幸运建议": mergeLines(flattenAnyToLines((raw as any)?.lucky_numbers), flattenAnyToLines((raw as any)?.lucky_colors), flattenAnyToLines((raw as any)?.lucky_tips)),
"健康开运": mergeLines(flattenAnyToLines((raw as any)?.health)),
"人生运程": mergeLines(flattenAnyToLines((raw as any)?.fortune), flattenAnyToLines((raw as any)?.lifespan)),
"情感与社交": mergeLines(flattenAnyToLines((raw as any)?.emotion)),
"日常开运指南": mergeLines(flattenAnyToLines((raw as any)?.daily)),
"诗词出处": mergeLines(flattenAnyToLines((raw as any)?.poetry_source), flattenAnyToLines((raw as any)?.poetry)),
};
const moduleList: Array<{ title: string; lines: string[] }> = [];
const push = (title: string, lines: string[]) => {
const l = normalizeLines(lines);
if (!l.length) return;
moduleList.push({ title, lines: l });
};
// 主要模块(起名/改名详解)
push("五行生克与八卦", (() => {
const x = report.value.detailModules?.wuxing_bagua;
const nodeLines = flattenDetailNodes(arr(x?.details?.nodes));
return mergeLines(rawLineMap["五行生克与八卦"], nodeLines, linesFromWuxingBagua(x), flattenAnyToLines(x));
})());
push("五行单字详解", mergeLines(rawLineMap["五行单字详解"]));
push("八字与姓名五行", (() => {
const b = report.value.detailModules?.bazi_name_fit;
const nodeLines = flattenDetailNodes(arr(b?.details?.nodes));
return mergeLines(rawLineMap["八字与姓名五行"], nodeLines, linesFromBazi(b), flattenAnyToLines(b));
})());
push("字义与生肖", mergeLines(rawLineMap["字义与生肖"], report.value.details?.meaning?.lines || []));
push("属相与名字", (() => {
const z = report.value.detailModules?.zodiac_sign;
const nodeLines = flattenDetailNodes(arr(z?.details?.nodes));
return mergeLines(rawLineMap["属相与名字"], nodeLines, linesFromZodiac(z), flattenAnyToLines(z));
})());
push("三才五格", mergeLines(rawLineMap["三才五格"], report.value.details?.sancai?.lines || []));
push("笔画数理", mergeLines(rawLineMap["笔画数理"], report.value.details?.wuge?.lines || []));
push("五行能量分布", mergeLines(rawLineMap["五行能量分布"], report.value.details?.wuxing?.lines || []));
push("周易卦象", mergeLines(rawLineMap["周易卦象"], report.value.details?.gua?.lines || []));
push("幸运建议", mergeLines(rawLineMap["幸运建议"], [
...normalizeLines(report.value.details?.lucky?.color?.lines),
...normalizeLines(report.value.details?.lucky?.number?.lines),
...normalizeLines(report.value.details?.lucky?.industry?.lines),
...normalizeLines(report.value.details?.lucky?.constellation?.lines),
]));
push("健康开运", mergeLines(rawLineMap["健康开运"], report.value.details?.health?.lines || []));
push("人生运程", mergeLines(rawLineMap["人生运程"], report.value.details?.fortune?.lines || []));
push("情感与社交", mergeLines(rawLineMap["情感与社交"], report.value.details?.emotion?.lines || []));
push("日常开运指南", mergeLines(rawLineMap["日常开运指南"], report.value.details?.daily?.lines || []));
push("诗词出处", mergeLines(rawLineMap["诗词出处"], normalizeLines([report.value.poetry?.text])));
// 通用兜底:把接口新增但未纳入固定章节的字段补进 PDF避免后端新增字段丢失
const consumedRawKeys = new Set([
"header",
"char_detail",
"wuxing_bagua",
"bazi_name_fit",
"meaning_zodiac",
"meaning",
"zodiac_sign",
"sancai",
"strokes_wuge_sancai",
"stroke",
"wuge",
"wuxing",
"gua",
"liuyao",
"lucky_numbers",
"lucky_colors",
"lucky_tips",
"health",
"fortune",
"lifespan",
"emotion",
"daily",
"poetry_source",
"poetry",
"details",
"detailModules",
"id",
"report_id",
"is_unlocked",
"unlock_price",
"locked",
"lock",
"price",
]);
const rawKeyTitleMap: Record<string, string> = {
lucky_numbers: "幸运数字",
lucky_colors: "幸运颜色",
career_plan: "事业规划",
liuyao: "六爻分析",
};
Object.keys(raw || {}).forEach((k) => {
if (consumedRawKeys.has(k)) return;
const v = (raw as any)[k];
const lines = mergeLines(
v && typeof v === "object" ? flattenDetailNodes(arr((v as any)?.details?.nodes)) : [],
flattenAnyToLines(v),
);
if (!lines.length) return;
const title = rawKeyTitleMap[k] || (/^[a-z0-9_]+$/i.test(k) ? "" : k);
push(title || "其他信息", lines);
});
// 兜底:若后端在根级还带了 detailModules 的 nodes尽量补齐
const extraKeys = ["bazi_name_fit", "wuxing_bagua", "zodiac_sign"];
extraKeys.forEach((k) => {
const v = (raw as any)?.[k];
const lines = mergeLines(flattenDetailNodes(arr(v?.details?.nodes)), flattenAnyToLines(v));
if (!lines.length) return;
const titleMap: any = {
bazi_name_fit: "八字与姓名五行(详)",
wuxing_bagua: "五行生克与八卦(详)",
zodiac_sign: "属相与名字(详)",
};
push(titleMap[k] || "补充", lines);
});
if (!moduleList.length) {
uni.showToast({ title: "暂无可导出的报告内容", icon: "none" });
return;
}
const [{ jsPDF }, html2canvasMod] = await Promise.all([import("jspdf"), import("html2canvas")]);
const html2canvas = (html2canvasMod as any).default || (html2canvasMod as any);
const PAGE_W = 794;
const PAGE_H = 1123;
const MAX_LINES = 46;
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-mystic-style";
let style = document.getElementById(styleId) as HTMLStyleElement | null;
if (!style) {
style = document.createElement("style");
style.id = styleId;
document.head.appendChild(style);
}
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:48px;right:48px;top:116px;bottom:92px;background:rgba(10,10,15,.62);border:1px solid rgba(212,175,55,.38);border-radius:16px;padding:30px 28px;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:22px;}
.pdf-name{text-align:center;font-size:50px;font-weight:800;margin-bottom:14px;color:rgba(242,230,216,.98);text-shadow:0 0 18px rgba(212,175,55,.18);}
.pdf-score-row{display:flex;align-items:center;justify-content:center;gap:16px;margin-top:8px;}
.pdf-score{font-size:38px;font-weight:900;color:rgba(212,175,55,.98);}
.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-stars{display:flex;gap:8px;font-size:16px;color:rgba(212,175,55,.95);letter-spacing:.08em;}
.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:48px;right:48px;top:78px;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:10px;border-radius:10px;border:1px solid rgba(212,175,55,.16);background:rgba(11,16,38,.55);overflow:hidden;}
.pdf-flow-head{height:38px;display:flex;align-items:center;gap:10px;padding:0 14px;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:16px;font-weight:800;letter-spacing:.05em;}
.pdf-flow-body{padding:10px 14px 12px;}
.pdf-flow-line{font-size:15px;line-height:1.72;color:rgba(228,230,238,.95);margin-bottom:2px;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(name || "未命名")}</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-score-row">
<div class="pdf-score">${escapeHtml(String(score || ""))}</div>
<div class="pdf-stars">${"★".repeat(Math.max(0, Math.min(5, stars)))}${"☆".repeat(Math.max(0, 5 - Math.max(0, Math.min(5, stars))))}</div>
</div>
<div class="pdf-cover-seal">名</div>
</div>
`;
appendFooter(coverEl, 1, "起名详解报告 · 封面");
// 目录
const dirEl = createPageEl(1);
// 分章分页:每章至少独占一页;超长章节自动续页
type ChapterPage = { chapterNo: number; title: string; lines: string[]; continued?: boolean };
const contentPages: ChapterPage[] = [];
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) => {
contentPages.push({
chapterNo,
title: m.title,
lines: chunk,
continued: cIdx > 0,
});
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((section, i) => {
const el = createPageEl(2 + i);
const titleText = `${section.chapterNo}${section.title}${section.continued ? "(续)" : ""}`;
const linesHtml = section.lines.map((ln) => `<div class="pdf-flow-line">${escapeHtml(ln || " ")}</div>`).join("");
const sectionsHtml = `<div class="pdf-flow-section">
<div class="pdf-flow-head">
<div class="pdf-flow-bullet">✧</div>
<div class="pdf-flow-title">${escapeHtml(titleText)}</div>
</div>
<div class="pdf-flow-body">${linesHtml}</div>
</div>`;
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();
// 提速关键:降低渲染倍率并使用 JPEG通常可显著减少导出耗时
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 safeName = name ? name.replace(/[\\/:*?\"<>|]/g, "_") : "起名详解";
doc.save(`${safeName}-起名详解.pdf`);
uni.showToast({ title: "PDF下载成功", icon: "success" });
} catch (e) {
console.error("生成PDF失败:", e);
uni.showToast({ title: "PDF下载失败", icon: "none" });
} finally {
uni.hideLoading();
}
};
const handleWealthAnalysis = async () => {
const solutionId = Number(props.data?.id || 0);
console.log('props.data===>>>', props.data);
if (!solutionId) {
uni.showToast({ title: '方案ID不存在', icon: 'none' });
return;
}
try {
uni.showLoading({ title: '加载中...' });
const reportId = props.data?.report_id;
if (!reportId) {
uni.hideLoading();
uni.showToast({ title: '报告ID不存在', icon: 'none' });
return;
}
// 使用report_id获取财运解析数据
const wealthData = await getWealthAnalysisByReportId(reportId);
uni.hideLoading();
uni.setStorageSync('wealth_analysis_report_id', reportId);
emit('wealthAnalysis', { id: reportId, wealthData });
} catch (error: any) {
uni.hideLoading();
uni.showToast({ title: error?.msg || error?.message || '加载失败,请重试', icon: 'none' });
}
};
// 详情弹窗
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 openScoreDetail = () => {
const d = report.value.details.score;
openDetailModal(d.title, d.lines);
};
const openMeaningDetail = () => {
const d = report.value.details.meaning;
openDetailModal(d.title, d.lines);
};
const openBaziNameFitDetail = () => {
const b = report.value.detailModules?.bazi_name_fit;
const nodes = flattenDetailNodes(arr(b?.details?.nodes));
const lines = nodes.length ? nodes : linesFromBazi(b);
openDetailModal("八字与姓名五行详解", lines);
};
const openWuxingBaguaDetail = () => {
const x = report.value.detailModules?.wuxing_bagua;
const nodes = flattenDetailNodes(arr(x?.details?.nodes));
const charLines = charDetailItems.value.map(charDetailLine).filter(Boolean);
const baguaLines = nodes.length ? nodes : linesFromWuxingBagua(x);
const lines = [
...(charLines.length ? ['【五行单字详解 · 阴阳 · 卦象】', ...charLines, ''] : []),
...baguaLines,
];
openDetailModal("五行生克与八卦详解", lines);
};
const openCharDetail = () => {
const x = report.value.detailModules?.char_detail;
const lines = linesFromCharDetail(x);
openDetailModal("五行单字详解", lines.length ? lines : ['暂无数据']);
};
const openZodiacSignDetail = () => {
const z = report.value.detailModules?.zodiac_sign;
const nodes = flattenDetailNodes(arr(z?.details?.nodes));
const lines = nodes.length ? nodes : linesFromZodiac(z);
openDetailModal("属相与名字详解", lines);
};
const openFamilyDetail = () => {
const d = report.value.details.family;
openDetailModal(d.title, d.lines);
};
const openWugeDetail = () => {
const d = report.value.details.wuge;
openDetailModal(d.title, d.lines);
};
const openWuxingDetail = () => {
const d = report.value.details.wuxing;
openDetailModal(d.title, d.lines);
};
const openSancaiDetail = () => {
const d = report.value.details.sancai;
openDetailModal(d.title, d.lines);
};
const openPersonalityDetail = (type: 'strength' | 'challenge') => {
const d = type === 'strength'
? report.value.details.personality_strength
: report.value.details.personality_challenge;
openDetailModal(d.title, d.lines);
};
const openGuaDetail = () => {
const d = report.value.details.gua;
openDetailModal(d.title, d.lines);
};
const openLuckyDetail = (type: string) => {
const key = type as 'color' | 'number' | 'industry' | 'constellation';
const d = report.value.details.lucky[key];
if (d) openDetailModal(d.title, d.lines);
};
const openHealthDetail = () => {
const d = report.value.details.health;
openDetailModal(d.title, d.lines);
};
const openFortuneDetail = () => {
const d = report.value.details.fortune;
openDetailModal(d.title, d.lines);
};
const openEmotionDetail = () => {
const d = report.value.details.emotion;
openDetailModal(d.title, d.lines);
};
const openDailyDetail = () => {
const d = report.value.details.daily;
openDetailModal(d.title, d.lines);
};
</script>
<style scoped>
.naming-detail {
min-height: 100vh;
background: #0a0a0f;
position: relative;
color: #e2e2e2;
}
/* Starry Background */
.starry-bg {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
pointer-events: none;
z-index: 0;
background: linear-gradient(to bottom, #050508, #10101a, #1a1a2e);
}
.star {
position: absolute;
border-radius: 50%;
background: #fff;
opacity: 0.2;
animation: twinkle ease-in-out infinite;
}
@keyframes twinkle {
0%,
100% {
opacity: 0.1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(1.2);
}
}
.glow-top {
position: absolute;
top: -10%;
left: -10%;
width: 50%;
height: 50%;
background: #2a3d5d;
opacity: 0.2;
filter: blur(100px);
border-radius: 50%;
}
.glow-bottom {
position: absolute;
bottom: -10%;
right: -10%;
width: 50%;
height: 50%;
background: #9c2a2a;
opacity: 0.1;
filter: blur(100px);
border-radius: 50%;
}
/* Header */
.detail-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
padding: 60rpx 32rpx 24rpx;
background: rgba(10, 10, 15, 0.8);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.detail-header-back {
display: flex;
align-items: center;
color: #a0a0a0;
}
.back-icon {
font-size: 24px;
margin-right: 8rpx;
}
.back-text {
font-size: 14px;
letter-spacing: 0.1em;
}
.detail-header-title {
font-size: 18px;
font-weight: 700;
color: #d4af37;
letter-spacing: 0.3em;
}
.detail-header-download {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8rpx;
padding: 10rpx 16rpx;
border-radius: 999rpx;
border: 1px solid rgba(212, 175, 55, 0.35);
background: rgba(212, 175, 55, 0.10);
color: rgba(255, 242, 210, 0.92);
backdrop-filter: blur(8px);
}
.detail-header-download-icon {
font-size: 18px;
line-height: 1;
}
.detail-header-download-text {
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
}
/* Content */
.detail-content {
position: relative;
z-index: 10;
padding: 180rpx 32rpx 60rpx;
height: 100vh;
box-sizing: border-box;
}
/* Score Card */
.score-card {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8rpx;
padding: 48rpx;
text-align: center;
position: relative;
overflow: hidden;
margin-bottom: 48rpx;
}
.score-corner {
position: absolute;
width: 12rpx;
height: 12rpx;
border-color: #d4af37;
border-style: solid;
}
.score-corner-tl {
top: 0;
left: 0;
border-width: 4rpx 0 0 4rpx;
}
.score-corner-tr {
top: 0;
right: 0;
border-width: 4rpx 4rpx 0 0;
}
.score-corner-bl {
bottom: 0;
left: 0;
border-width: 0 0 4rpx 4rpx;
}
.score-corner-br {
bottom: 0;
right: 0;
border-width: 0 4rpx 4rpx 0;
}
.score-label {
font-size: 10px;
color: #d4af37;
letter-spacing: 0.5em;
text-transform: uppercase;
opacity: 0.8;
margin-bottom: 16rpx;
}
.score-value-wrap {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 280rpx;
height: 280rpx;
margin-bottom: 16rpx;
}
.score-value {
font-size: 72px;
font-weight: 700;
background: linear-gradient(to bottom, #d4af37, #8a6e1e);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.score-ring {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 1px dashed rgba(212, 175, 55, 0.2);
border-radius: 50%;
animation: rotate 10s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.score-stars {
display: flex;
justify-content: center;
gap: 8rpx;
margin-bottom: 24rpx;
}
.score-star {
font-size: 14px;
color: #5a5a5a;
}
.score-star.active {
color: #d4af37;
}
.score-divider {
height: 1px;
background: linear-gradient(to right, transparent, rgba(212, 175, 55, 0.3), transparent);
margin: 16rpx 0;
}
.score-name {
display: block;
font-size: 28px;
font-weight: 700;
color: #e2e2e2;
letter-spacing: 0.2em;
margin-top: 16rpx;
margin-bottom: 8rpx;
}
.score-pinyin {
display: block;
font-size: 14px;
color: #a0a0a0;
font-style: italic;
}
/* Section */
.section {
margin-bottom: 48rpx;
}
.section-title {
display: flex;
align-items: center;
margin-bottom: 24rpx;
padding-left: 8rpx;
}
.section-icon {
font-size: 16px;
color: #d4af37;
margin-right: 12rpx;
}
.section-text {
font-size: 16px;
font-weight: 700;
color: #e2e2e2;
letter-spacing: 0.1em;
}
/* Meaning Card */
.meaning-card {
background: #1a1a2e;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12rpx;
padding: 32rpx;
position: relative;
overflow: hidden;
margin-bottom: 20rpx;
}
.meaning-quote {
position: absolute;
top: 0;
right: 16rpx;
font-size: 48px;
color: #d4af37;
opacity: 0.1;
}
.meaning-label {
display: block;
font-size: 14px;
font-weight: 700;
color: #d4af37;
margin-bottom: 16rpx;
}
.meaning-text {
display: block;
font-size: 14px;
color: #a0a0a0;
line-height: 1.8;
margin-bottom: 16rpx;
}
.meaning-judge {
display: flex;
flex-wrap: wrap;
}
.meaning-judge-label {
font-size: 14px;
color: #d4af37;
}
.meaning-judge-text {
font-size: 14px;
color: #a0a0a0;
}
/* Zodiac Card */
.zodiac-card {
background: linear-gradient(to right, #16213e, #1a1a2e);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12rpx;
padding: 32rpx;
}
.zodiac-header {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.zodiac-icon-wrap {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: rgba(212, 175, 55, 0.2);
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.zodiac-icon {
font-size: 24px;
}
.zodiac-info {
flex: 1;
}
.zodiac-label {
display: block;
font-size: 12px;
color: #a0a0a0;
margin-bottom: 4rpx;
}
.zodiac-value {
display: block;
font-size: 16px;
font-weight: 700;
color: #e2e2e2;
}
.zodiac-score {
font-size: 24px;
font-weight: 700;
color: #d4af37;
}
.zodiac-desc {
font-size: 14px;
color: #a0a0a0;
line-height: 1.6;
}
/* Info Card */
.info-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12rpx;
padding: 32rpx;
margin-bottom: 16rpx;
}
.info-card-title {
display: block;
font-size: 14px;
font-weight: 700;
color: #d4af37;
margin-bottom: 16rpx;
}
.info-card-text {
display: block;
font-size: 14px;
color: #a0a0a0;
line-height: 1.8;
}
/* Wuge Card */
.wuge-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12rpx;
padding: 32rpx;
margin-bottom: 20rpx;
}
.wuge-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
}
.wuge-label {
font-size: 14px;
color: #e2e2e2;
}
.wuge-badge {
font-size: 12px;
color: #d4af37;
border: 1px solid #d4af37;
padding: 4rpx 16rpx;
border-radius: 4rpx;
}
.wuge-item {
margin-bottom: 20rpx;
}
.wuge-item-header {
display: flex;
justify-content: space-between;
margin-bottom: 8rpx;
}
.wuge-item-name {
font-size: 12px;
color: #a0a0a0;
}
.wuge-item-name-primary {
color: #e2e2e2;
font-weight: 700;
}
.wuge-item-result {
font-size: 12px;
color: #a0a0a0;
}
.wuge-item-result-primary {
color: #d4af37;
}
.wuge-bar {
height: 8rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 4rpx;
overflow: hidden;
}
.wuge-bar-fill {
height: 100%;
border-radius: 4rpx;
transition: width 1s ease-out;
}
/* Wuxing Card */
.wuxing-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12rpx;
padding: 32rpx;
}
.wuxing-title {
display: block;
font-size: 14px;
font-weight: 700;
color: #d4af37;
margin-bottom: 20rpx;
padding-left: 16rpx;
border-left: 4rpx solid #d4af37;
}
.wuxing-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.wuxing-item {
display: flex;
align-items: center;
gap: 16rpx;
}
.wuxing-label {
width: 48rpx;
font-size: 14px;
color: #a0a0a0;
}
.wuxing-bar {
flex: 1;
height: 8rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 4rpx;
overflow: hidden;
}
.wuxing-bar-fill {
height: 100%;
border-radius: 4rpx;
transition: width 1.2s ease-out;
}
.wuxing-value {
width: 60rpx;
text-align: right;
font-size: 14px;
color: #e2e2e2;
font-family: monospace;
}
/* Sancai Card */
.sancai-card {
background: #1a1a2e;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12rpx;
padding: 32rpx;
}
.sancai-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
}
.sancai-config {
display: flex;
align-items: baseline;
gap: 12rpx;
}
.sancai-config-text {
font-size: 14px;
font-weight: 700;
color: #d4af37;
}
.sancai-config-label {
font-size: 12px;
color: #a0a0a0;
}
.sancai-badge {
font-size: 12px;
color: #e2e2e2;
background: rgba(212, 175, 55, 0.2);
border: 1px solid #d4af37;
padding: 4rpx 16rpx;
border-radius: 999rpx;
}
.sancai-item {
margin-bottom: 16rpx;
}
.sancai-item-label {
font-size: 14px;
font-weight: 700;
color: #d4af37;
}
.sancai-item-label-normal {
font-size: 14px;
font-weight: 700;
color: #e2e2e2;
}
.sancai-item-text {
font-size: 14px;
color: #a0a0a0;
line-height: 1.6;
}
/* Personality Grid */
.personality-grid {
display: flex;
gap: 16rpx;
}
.personality-card {
flex: 1;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12rpx;
padding: 24rpx;
}
.personality-title {
display: block;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 16rpx;
}
.personality-title-gold {
color: #d4af37;
}
.personality-title-red {
color: #9c2a2a;
}
.personality-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.personality-item {
display: flex;
align-items: center;
}
.personality-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
margin-right: 12rpx;
}
.personality-dot-gold {
background: #d4af37;
}
.personality-dot-red {
background: #9c2a2a;
}
.personality-text {
font-size: 12px;
color: #a0a0a0;
}
/* Gua Card */
.gua-card {
background: linear-gradient(to right, #1a1a2e, #16213e);
border: 1px solid rgba(212, 175, 55, 0.3);
border-radius: 12rpx;
padding: 32rpx;
position: relative;
overflow: hidden;
}
.gua-bg-text {
position: absolute;
right: -20rpx;
top: -20rpx;
font-size: 120px;
color: rgba(255, 255, 255, 0.05);
pointer-events: none;
}
.gua-content {
display: flex;
align-items: center;
gap: 40rpx;
position: relative;
z-index: 1;
}
.gua-symbol {
display: flex;
flex-direction: column;
gap: 8rpx;
width: 120rpx;
}
.gua-line {
height: 8rpx;
}
.gua-line-solid {
background: #d4af37;
box-shadow: 0 0 8px #d4af37;
}
.gua-line-broken {
display: flex;
justify-content: space-between;
}
.gua-line-part {
width: 45%;
height: 8rpx;
background: rgba(212, 175, 55, 0.5);
}
.gua-spacer {
height: 8rpx;
}
.gua-info {
flex: 1;
}
.gua-header {
display: flex;
align-items: baseline;
gap: 16rpx;
margin-bottom: 16rpx;
}
.gua-name {
font-size: 24px;
font-weight: 700;
color: #e2e2e2;
letter-spacing: 0.1em;
}
.gua-badge {
font-size: 12px;
color: #9c2a2a;
border: 1px solid #9c2a2a;
padding: 4rpx 12rpx;
border-radius: 4rpx;
}
.gua-desc {
font-size: 14px;
color: #a0a0a0;
line-height: 1.6;
}
/* Lucky Grid */
.lucky-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 16rpx;
}
.lucky-card {
width: calc(50% - 8rpx);
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12rpx;
padding: 24rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-sizing: border-box;
}
.lucky-icon {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.lucky-icon-blue {
color: #60a5fa;
}
.lucky-icon-pink {
color: #f472b6;
}
.lucky-icon-green {
color: #4ade80;
}
.lucky-icon-yellow {
color: #facc15;
}
.lucky-info {
flex: 1;
}
.lucky-label {
display: block;
font-size: 12px;
color: #a0a0a0;
margin-bottom: 4rpx;
}
.lucky-value {
display: block;
font-size: 14px;
font-weight: 700;
color: #e2e2e2;
}
/* Health Card */
.health-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12rpx;
padding: 24rpx;
display: flex;
align-items: center;
gap: 20rpx;
}
.health-icon {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: #f87171;
}
.health-info {
flex: 1;
}
.health-header {
display: flex;
justify-content: space-between;
margin-bottom: 4rpx;
}
.health-label {
font-size: 12px;
color: #a0a0a0;
}
.health-note {
font-size: 12px;
color: #5a5a5a;
}
.health-value {
font-size: 14px;
font-weight: 700;
color: #e2e2e2;
}
/* Poetry Card */
.poetry-card {
background: rgba(226, 226, 226, 0.05);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12rpx;
padding: 40rpx;
position: relative;
overflow: hidden;
}
.poetry-quote {
position: absolute;
top: -16rpx;
left: -16rpx;
font-size: 80px;
color: rgba(255, 255, 255, 0.05);
transform: rotate(180deg);
}
.poetry-text {
display: block;
font-size: 16px;
color: #e2e2e2;
line-height: 2;
font-style: italic;
text-align: center;
position: relative;
z-index: 1;
}
/* Action Buttons */
.action-buttons {
display: flex;
gap: 16rpx;
margin: 48rpx 0;
}
.action-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12rpx;
padding: 24rpx;
border-radius: 12rpx;
}
.action-btn-share,
.action-btn-download {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #a0a0a0;
}
.action-btn-primary {
background: rgba(212, 175, 55, 0.1);
border: 1px solid rgba(212, 175, 55, 0.5);
color: #d4af37;
box-shadow: 0 0 15px rgba(212, 175, 55, 0.15);
}
.action-btn-icon {
font-size: 20px;
}
.action-btn-text {
font-size: 12px;
letter-spacing: 0.1em;
}
/* Fortune Card - 人生运程模拟 */
.fortune-card {
background: #1a1a2e;
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12rpx;
padding: 32rpx;
position: relative;
overflow: hidden;
}
.fortune-chart {
height: 200rpx;
position: relative;
margin-bottom: 24rpx;
}
.fortune-line {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 40rpx;
background: linear-gradient(90deg,
transparent 5%,
rgba(212, 175, 55, 0.1) 15%,
rgba(212, 175, 55, 0.3) 30%,
rgba(212, 175, 55, 0.5) 35%,
rgba(212, 175, 55, 0.3) 50%,
rgba(212, 175, 55, 0.2) 70%,
transparent 95%);
clip-path: polygon(0% 80%, 25% 50%, 35% 15%, 50% 25%, 70% 35%, 100% 55%, 100% 100%, 0% 100%);
}
.fortune-point {
position: absolute;
width: 16rpx;
height: 16rpx;
background: #d4af37;
border-radius: 50%;
}
.fortune-point-1 {
left: 10%;
top: 70%;
}
.fortune-point-2 {
left: 35%;
top: 15%;
}
.fortune-point-3 {
left: 60%;
top: 30%;
}
.fortune-point-4 {
left: 90%;
top: 50%;
}
.fortune-labels {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
}
.fortune-label-item {
text-align: center;
}
.fortune-label-text {
font-size: 12px;
color: #a0a0a0;
}
.fortune-label-peak .fortune-label-text {
color: #d4af37;
font-weight: 700;
}
.fortune-note {
display: block;
font-size: 12px;
color: #5a5a5a;
text-align: center;
padding-top: 16rpx;
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
/* Emotion Card - 情感与社交运势 */
.emotion-card {
background: #1a1a2e;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12rpx;
padding: 32rpx;
}
.emotion-header {
display: flex;
gap: 24rpx;
margin-bottom: 24rpx;
}
.emotion-icon-wrap {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background: rgba(236, 72, 153, 0.1);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.emotion-icon {
font-size: 32px;
}
.emotion-info {
flex: 1;
}
.emotion-title {
display: block;
font-size: 14px;
font-weight: 700;
color: #e2e2e2;
margin-bottom: 12rpx;
}
.emotion-desc {
display: block;
font-size: 12px;
color: #a0a0a0;
line-height: 1.6;
}
.emotion-divider {
height: 1px;
background: rgba(255, 255, 255, 0.05);
margin: 24rpx 0;
}
.emotion-tags-title {
display: block;
font-size: 14px;
font-weight: 700;
color: #e2e2e2;
margin-bottom: 16rpx;
}
.emotion-tags {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.emotion-tag {
padding: 12rpx 20rpx;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8rpx;
font-size: 12px;
color: #a0a0a0;
}
.emotion-tag-primary {
color: #d4af37;
border-color: rgba(212, 175, 55, 0.3);
}
/* Daily Grid - 日常生活开运指南 */
.daily-grid {
display: flex;
gap: 16rpx;
margin-bottom: 16rpx;
}
.daily-card {
flex: 1;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12rpx;
padding: 24rpx;
}
.daily-label {
display: block;
font-size: 12px;
color: #a0a0a0;
margin-bottom: 16rpx;
}
.daily-time-item {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 8rpx;
}
.daily-time-icon {
font-size: 14px;
}
.daily-time-text {
font-size: 14px;
font-weight: 700;
color: #e2e2e2;
}
.daily-items {
display: flex;
gap: 16rpx;
margin-bottom: 12rpx;
}
.daily-item {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
}
.daily-item-red {
background: rgba(127, 29, 29, 0.5);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #fecaca;
}
.daily-item-yellow {
background: rgba(113, 63, 18, 0.5);
border: 1px solid rgba(234, 179, 8, 0.3);
color: #fef08a;
}
.daily-tip {
display: block;
font-size: 10px;
color: #5a5a5a;
}
/* Avatar Card - 开运头像风格推荐 */
.avatar-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12rpx;
padding: 24rpx;
}
.avatar-label {
display: block;
font-size: 12px;
color: #a0a0a0;
margin-bottom: 20rpx;
}
.avatar-list {
display: flex;
gap: 20rpx;
overflow-x: auto;
}
.avatar-item {
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.avatar-item-sunrise {
background: linear-gradient(135deg, #fb923c, #ef4444);
}
.avatar-item-calligraphy {
background: #2c2c2c;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.avatar-item-festive {
background: #fecaca;
}
.avatar-text {
font-size: 10px;
color: #fff;
}
.avatar-item-calligraphy .avatar-text {
color: #a0a0a0;
}
.avatar-item-festive .avatar-text {
color: #991b1b;
}
/* Footer */
.detail-footer {
text-align: center;
padding: 40rpx 0 80rpx;
}
.footer-text {
font-size: 10px;
color: #5a5a5a;
}
.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;
}
</style>