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

2117 lines
49 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="fortune-wrap">
<view class="bg">
<view class="bg-base" />
<view class="bg-texture" />
<view class="bg-glow bg-glow-a" />
<view class="bg-glow bg-glow-b" />
<view v-for="star in stars" :key="star.id" class="bg-star" :style="{
top: star.top,
left: star.left,
width: star.size + 'px',
height: star.size + 'px',
animationDuration: star.duration + 's',
animationDelay: star.delay + 's'
}" />
</view>
<view class="header">
<view class="header-left" @click="emit('back')">
<text class="header-back"></text>
</view>
<view class="header-mid">
<text class="header-title">{{ props.data?.header?.title || '商业运势批复' }}</text>
<text class="header-subtitle">{{ props.data?.header?.subtitle || 'Business Fortune 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>
<view class="content-area">
<scroll-view scroll-y class="content">
<view class="content-inner">
<!-- 通用section/node渲染器 -->
<view v-if="!shouldShowLockOverlay" class="stack">
<!-- 有数据时显示内容 -->
<view v-if="activeSections.length > 0">
<view v-for="(section, sectionIndex) in activeSections" :key="sectionIndex" class="card">
<view v-if="section.title" class="section-title">
<view class="bar" />
<view>
<text class="section-title-text">{{ section.title }}</text>
<text v-if="section.subtitle" class="section-subtitle">{{ section.subtitle }}</text>
</view>
</view>
<view v-for="(node, nodeIndex) in section.nodes" :key="nodeIndex" class="node-container">
<!-- KV节点 -->
<view v-if="node.type === 'kv'" class="kv">
<view v-for="(item, itemIndex) in node.items" :key="itemIndex" class="kv-row">
<text class="muted">{{ item.label }}</text>
<text class="serif">{{ item.value }}</text>
</view>
</view>
<!-- 文本节点 -->
<view v-else-if="node.type === 'text'" class="box-dark">
<text class="tiny muted lh">{{ node.text }}</text>
</view>
<!-- 列表节点 -->
<view v-else-if="node.type === 'list'" class="box-dark">
<view v-for="(item, itemIndex) in node.items" :key="itemIndex" class="list-item">
<text class="tiny muted"> {{ item }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 无数据时显示占位内容 -->
<view v-else class="card">
<view class="section-title">
<view class="bar" />
<view>
<text class="section-title-text">数据加载中</text>
<text class="section-subtitle">Loading...</text>
</view>
</view>
<view class="box-dark">
<text class="tiny muted">正在获取您的商业运势数据请稍候...</text>
</view>
</view>
</view>
<view class="bottom-spacer" />
</view>
</scroll-view>
<!-- 通用锁定浮层 -->
<view v-if="shouldShowLockOverlay && activeLockInfo" class="lock-overlay">
<view class="lock-card">
<view class="lock-circle"><text class="lock-circle-text"></text></view>
<text class="lock-h">{{ activeLockInfo.title }}</text>
<view class="lock-p">
<text class="lock-p-text">解锁后获取精准指导</text>
<text class="lock-p-sub">助企业把握商机规避风险</text>
</view>
<view class="lock-cta" @click="unlockActiveTab"><text class="lock-cta-text">立即解锁 ¥{{ activeLockInfo.price }}</text></view>
</view>
</view>
</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="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="(line, i) in detailModalLines" :key="i" class="detail-modal-text">{{ line }}</text>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
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 CBF_PENDING_KEY = 'wx_cbf_pending';
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 r = search.substr(1).match(/(^|&)code=([^&]*)(&|$)/);
return r ? decodeURIComponent(r[2]) : null;
}
function cleanCodeFromUrl() {
try { const url = new URL(window.location.href); if (url.searchParams.has('code')) { url.searchParams.delete('code'); url.searchParams.delete('state'); window.history.replaceState(null, '', url.toString()); } } catch {}
}
function isWechat(): boolean { return /MicroMessenger/i.test(navigator.userAgent); }
declare const uni: any;
const props = defineProps<{
data?: any;
}>();
const emit = defineEmits<{
back: [];
}>();
const activeTab = ref('destiny');
const isMonthUnlocked = ref(false);
const isDayUnlocked = ref(false);
const accountRemainQuota = ref<number | null>(null);
// 背景星星
const stars = Array.from({ length: 30 }, (_, 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
}));
// Tab图标映射
const tabIconMap: Record<string, string> = {
destiny: '商',
year: '运',
month: '月',
day: '日',
fengshui: '风'
};
// 从props.data解析的tabs数据
const dataTabs = computed(() => {
const tabs = props.data?.tabs;
if (!Array.isArray(tabs)) return [];
return tabs;
});
// Tab列表含icon和lock状态
const tabList = computed(() => {
return dataTabs.value.map((t: any) => ({
id: t.id,
label: t.label,
icon: tabIconMap[t.id] || t.id.charAt(0),
locked: t.id === 'month' ? (t.locked && !isMonthUnlocked.value) : t.id === 'day' ? (t.locked && !isDayUnlocked.value) : false
}));
});
// 当前激活tab的数据
const activeTabData = computed(() => {
return dataTabs.value.find((t: any) => t.id === activeTab.value) || null;
});
// 当前激活tab的sections
const activeSections = computed(() => {
return activeTabData.value?.sections || [];
});
// 当前tab是否处于锁定状态
const isActiveTabLocked = computed(() => {
if (activeTab.value === 'month' && !isMonthUnlocked.value && activeTabData.value?.locked) return true;
if (activeTab.value === 'day' && !isDayUnlocked.value && activeTabData.value?.locked) return true;
return false;
});
const isQuotaZero = computed(() => Number(accountRemainQuota.value) === 0);
const rechargeFeatureVisible = computed(() => HIDE_RECHARGE_FEATURE !== true);
const isRechargeTargetTab = computed(() => activeTab.value === 'month' || activeTab.value === 'day');
const shouldShowLockOverlay = computed(() =>
rechargeFeatureVisible.value && isRechargeTargetTab.value && isQuotaZero.value && isActiveTabLocked.value
);
// 当前tab的锁定信息
const activeLockInfo = computed(() => {
const lock = activeTabData.value?.lock || {};
const fallbackPrice = Number((props.data as any)?.unlock_price);
return {
title: lock?.title || (activeTab.value === 'month' ? '企业商运-月度详批' : '企业商运-每日运程'),
price: String(lock?.price ?? (Number.isFinite(fallbackPrice) && fallbackPrice > 0 ? fallbackPrice : 19.9)),
};
});
// 支付解锁当前tab
const unlockActiveTab = async () => {
if (!isQuotaZero.value) {
uni.showToast({ title: '当前额度大于0无需充值', icon: 'none' });
return;
}
const tabData = activeTabData.value;
const lockInfo = activeLockInfo.value;
if (!tabData || !lockInfo) {
uni.showToast({ title: '解锁信息不完整', icon: 'none' });
return;
}
const reportId = props.data?.id;
if (!reportId) {
uni.showToast({ title: '报告ID不存在', icon: 'none' });
return;
}
if (!isWechat()) {
uni.showToast({ title: '请在微信中打开', icon: 'none' });
return;
}
const price = parseFloat(lockInfo.price);
const unlockType = activeTab.value === 'month' ? 'monthly' : 'daily';
const desc = activeTab.value === 'month' ? '企业商运-月度详批' : '企业商运-每日运程';
const code = getUrlCode();
if (!code) {
try { localStorage.setItem(CBF_PENDING_KEY, JSON.stringify({ tab: activeTab.value, reportId, unlockType, ts: Date.now() })); } catch {}
const redirectUri = encodeURIComponent(window.location.href);
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;
}
cleanCodeFromUrl();
try { localStorage.removeItem(CBF_PENDING_KEY); } catch {}
try {
uni.showLoading({ title: '创建订单中...' });
const orderRes = await paymentApi.createOrder({
description: desc,
total_amount: price,
business_type: 'wealth_analysis',
business_id: reportId,
pay_type: 'jsapi',
code,
});
uni.hideLoading();
if (!orderRes?.appId || !orderRes?.paySign) {
uni.showToast({ title: '获取支付参数失败', icon: 'none' });
return;
}
const pp = orderRes;
await new Promise<void>((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' });
if (activeTab.value === 'month') isMonthUnlocked.value = true;
else if (activeTab.value === 'day') isDayUnlocked.value = true;
uni.setStorageSync(`wealth_unlock_${unlockType}_${reportId}`, true);
} else {
uni.showToast({ title: res.err_msg === 'get_brand_wcpay_request:cancel' ? '已取消' : '支付失败', icon: 'none' });
}
resolve();
});
};
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' });
}
};
// 刷新解锁后的内容
const refreshUnlockedContent = async () => {
try {
// 这里可以重新调用API获取解锁后的详细数据
// 暂时只显示解锁成功的提示
setTimeout(() => {
uni.showToast({
title: '解锁成功!请查看详细内容',
icon: 'success',
duration: 2000
});
}, 500);
} catch (error) {
console.error('刷新解锁内容失败:', error);
}
};
// 检查本地存储的解锁状态
const checkUnlockStatus = () => {
const reportId = props.data?.id;
if (reportId) {
const unlockStatus = checkAllWealthUnlockStatus(reportId);
if (unlockStatus.monthly) isMonthUnlocked.value = true;
if (unlockStatus.daily) isDayUnlocked.value = true;
}
};
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;
}
};
// 组件挂载时检查解锁状态
onMounted(async () => {
checkUnlockStatus();
await loadMyQuota();
});
// 详情弹窗
const showDetailModal = ref(false);
const detailModalTitle = ref('');
const detailModalLines = ref<string[]>([]);
const closeDetailModal = () => {
showDetailModal.value = false;
detailModalTitle.value = '';
detailModalLines.value = [];
};
const handleDownloadPdf = async () => {
if (typeof window === "undefined" || typeof document === "undefined") return;
uni.showLoading({ title: "正在生成PDF..." });
const escapeHtml = (s: string) =>
s
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
const toStr = (v: any) => String(v ?? "").trim();
const arr = (v: any) => (Array.isArray(v) ? v : []);
const uniqueLines = (lines: string[]) => {
const seen = new Set<string>();
const out: string[] = [];
lines.forEach((line) => {
const v = toStr(line);
if (!v || seen.has(v)) return;
seen.add(v);
out.push(v);
});
return out;
};
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") {
arr(n.items).forEach((it: any) => {
const s = toStr(it);
if (s) out.push(`· ${s}`);
});
}
if (n.type === "kv") {
arr(n.items).forEach((it: any) => {
const k = toStr(it?.label);
const v = toStr(it?.value);
if (k || v) out.push(`${k}${v}`);
});
}
}
return out;
};
const excludeKeys = new Set([
"id", "report_id", "reportId", "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> = {
title: "标题",
subtitle: "副标题",
summary: "概述",
advice: "建议",
score: "评分",
trend: "趋势",
name: "名称",
company_name: "企业名称",
wealth_score: "财运评分",
wealth_level: "财运等级",
wealth_trend: "财运趋势",
yuedo_xiangpi: "月度详批",
yuedu_xiangpi: "月度详批",
is_unlocked: "解锁状态",
unlock_price: "解锁价格",
price: "价格",
section: "章节",
sections: "内容",
lock: "解锁",
label: "标签",
value: "内容",
nodes: "内容",
text: "说明",
items: "条目",
};
const prettyKey = (k: string): string => {
if (keyLabelMap[k]) return keyLabelMap[k];
if (/^[a-z0-9_]+$/i.test(k)) return "";
return k;
};
const flattenAnyToLines = (val: any, depth = 0): string[] => {
if (depth > 6 || val === null || val === undefined) return [];
if (typeof val === "string" || typeof val === "number" || typeof val === "boolean") {
const s = toStr(val);
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 (excludeKeys.has(k) || noisyKeys.has(k)) return;
const label = prettyKey(k);
const child = flattenAnyToLines((val as any)[k], depth + 1);
child.forEach((line) => out.push(label ? `${label}${line}` : line));
});
return out;
}
return [];
};
try {
const raw = props.data || {};
const tabs = Array.isArray(raw?.tabs) ? raw.tabs : [];
const moduleList: Array<{ title: string; lines: string[] }> = [];
const push = (title: string, lines: string[]) => {
const l = uniqueLines(lines);
if (!l.length) return;
moduleList.push({ title, lines: l });
};
tabs.forEach((tab: any) => {
const title = toStr(tab?.label || tab?.id || "章节");
const sections = Array.isArray(tab?.sections) ? tab.sections : [];
const lines: string[] = [];
sections.forEach((sec: any) => {
const secTitle = toStr(sec?.title);
const secSub = toStr(sec?.subtitle);
if (secTitle) lines.push(`${secTitle}${secSub ? ` · ${secSub}` : ""}`);
const nodes = Array.isArray(sec?.nodes) ? sec.nodes : [];
nodes.forEach((node: any) => {
if (node?.type === "kv") {
arr(node?.items).forEach((it: any) => {
const k = toStr(it?.label);
const v = toStr(it?.value);
if (k || v) lines.push(`${k}${v}`);
});
} else if (node?.type === "text") {
const t = toStr(node?.text);
if (t) lines.push(t);
} else if (node?.type === "list") {
arr(node?.items).forEach((it: any) => {
const s = toStr(it);
if (s) lines.push(`· ${s}`);
});
} else {
lines.push(...flattenAnyToLines(node));
}
});
});
if (!lines.length) lines.push(...flattenAnyToLines(tab));
push(title, lines);
});
// 兜底补充:接口根级其他字段
Object.keys(raw || {}).forEach((k) => {
if (excludeKeys.has(k) || k === "tabs") return;
const v = (raw as any)[k];
const lines = uniqueLines([
...(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 [{ 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 = 42;
const wrappedLineCost = (line: string) => {
const t = toStr(line);
if (!t) return 1;
return Math.max(1, Math.ceil(t.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-company-fortune-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:46px;font-weight:800;margin-bottom:14px;color:rgba(242,230,216,.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);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-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);
const companyName = toStr(raw?.header?.company_name || raw?.header?.name || "企业");
coverEl.innerHTML = `
<div class="pdf-gen-panel">
<div class="pdf-cover-kicker">玄 · 商 · 运 · 势</div>
<div class="pdf-cover-title">商业运势批复</div>
<div class="pdf-name">${escapeHtml(companyName)}</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 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("");
el.innerHTML = `
<div class="pdf-gen-panel">
<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>
</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();
doc.save(`${companyName || "商业运势"}-商业运势批复.pdf`);
uni.showToast({ title: "PDF下载成功", icon: "success" });
} catch (e) {
console.error("生成PDF失败:", e);
uni.showToast({ title: "PDF下载失败", icon: "none" });
} finally {
uni.hideLoading();
}
};
</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: 140rpx;
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;
}
.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-3 {
display: grid;
grid-template-columns: repeat(3, 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);
}
.box {
background: rgba(255, 255, 255, 0.05);
border-radius: 14rpx;
padding: 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;
}
.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;
}
.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;
}
.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;
}
.chip-bad {
border-color: rgba(239, 68, 68, 0.3);
color: #f87171;
}
.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;
}
.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;
}
.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;
}
.tag-green {
background: rgba(20, 83, 45, 0.3);
color: #4ade80;
}
.tag-red {
background: rgba(127, 29, 29, 0.3);
color: #f87171;
}
.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%;
}
.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;
}
.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;
}
.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;
}
.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;
}
.compass-s {
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;
}
.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);
}
.lock-root {
position: relative;
min-height: 800rpx;
}
.content-area {
position: relative;
flex: 1;
height: 0;
display: flex;
flex-direction: column;
}
.lock-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 30;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(5, 5, 8, 0.6);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.lock-card {
display: flex;
flex-direction: column;
align-items: center;
background: rgba(10, 10, 18, 0.75);
border: 1px solid rgba(212, 175, 55, 0.3);
border-radius: 24rpx;
padding: 56rpx 48rpx;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8rpx 48rpx rgba(0, 0, 0, 0.5);
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;
}
.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);
}
.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-avatar-text {
font-size: 36rpx;
}
.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);
}
.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;
}
.bottom-spacer {
height: 120rpx;
}
</style>
<style>
page {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
box-sizing: border-box;
overflow: hidden;
}
</style>