2117 lines
49 KiB
Vue
2117 lines
49 KiB
Vue
<template>
|
||
<view class="fortune-wrap">
|
||
<view class="bg">
|
||
<view class="bg-base" />
|
||
<view class="bg-texture" />
|
||
<view class="bg-glow bg-glow-a" />
|
||
<view class="bg-glow bg-glow-b" />
|
||
<view v-for="star in stars" :key="star.id" class="bg-star" :style="{
|
||
top: star.top,
|
||
left: star.left,
|
||
width: star.size + 'px',
|
||
height: star.size + 'px',
|
||
animationDuration: star.duration + 's',
|
||
animationDelay: star.delay + 's'
|
||
}" />
|
||
</view>
|
||
|
||
<view class="header">
|
||
<view class="header-left" @click="emit('back')">
|
||
<text class="header-back">‹</text>
|
||
</view>
|
||
<view class="header-mid">
|
||
<text class="header-title">{{ 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, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
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>
|