1370 lines
33 KiB
Vue
1370 lines
33 KiB
Vue
<template>
|
||
<view class="aus-result">
|
||
<view class="aus-result-texture"></view>
|
||
|
||
<!-- 固定Header -->
|
||
<view ref="fixedHeaderRef" class="aus-result-fixed-header">
|
||
<view class="status-bar-placeholder" :style="{ height: statusBarHeight }"></view>
|
||
<view class="aus-result-header">
|
||
<view class="aus-result-back" @click="$emit('back')">‹</view>
|
||
<text class="aus-result-title">择吉结果</text>
|
||
<view class="aus-result-header-spacer"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 头部占位 -->
|
||
<view class="aus-result-header-placeholder" :style="{ height: headerPlaceholderHeight }"></view>
|
||
|
||
<scroll-view scroll-y class="aus-result-scroll">
|
||
<view class="aus-result-container">
|
||
<!-- Advice -->
|
||
<view class="aus-advice-card">
|
||
<view class="aus-advice-glow"></view>
|
||
<view class="aus-advice-header">
|
||
<view class="aus-advice-icon">吉</view>
|
||
<view>
|
||
<text class="aus-advice-title">择吉分析报告</text>
|
||
<text class="aus-advice-sub">福主: {{ data.name }} · {{ data.birth_date }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="aus-advice-body">
|
||
<view class="aus-advice-bullet">✦</view>
|
||
<text class="aus-advice-text">
|
||
{{ advice }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Dates timeline -->
|
||
<view class="aus-timeline">
|
||
<view class="aus-timeline-line"></view>
|
||
<view class="aus-timeline-list">
|
||
<view v-for="(item, idx) in resolvedDates" :key="item.date + '_' + idx" class="aus-timeline-item"
|
||
@click="handleDate(item, idx)">
|
||
<view class="aus-timeline-dot" :class="isLocked(idx) ? 'is-locked' : 'is-open'"></view>
|
||
|
||
<view class="aus-date-card" :class="isLocked(idx) ? 'is-locked' : 'is-open'">
|
||
<view v-if="isLocked(idx)" class="aus-date-lock">
|
||
<view class="aus-date-lock-icon">🔒</view>
|
||
<view class="aus-date-lock-text">解锁后查看完整吉时与冲煞</view>
|
||
</view>
|
||
<view class="aus-date-inner">
|
||
<view class="aus-date-head">
|
||
<view>
|
||
<text class="aus-date-day">{{ item.date }}</text>
|
||
<text class="aus-date-lunar">{{ item.lunar }}</text>
|
||
</view>
|
||
<view class="aus-date-score">
|
||
<text class="aus-date-score-num">{{ item.score }}</text>
|
||
<text class="aus-date-score-label">吉运指数</text>
|
||
</view>
|
||
</view>
|
||
<view class="aus-date-desc">
|
||
<text>{{ item.desc }}</text>
|
||
</view>
|
||
<view class="aus-date-meta">
|
||
<view class="aus-date-meta-item">
|
||
<text>吉时</text>
|
||
<text>{{ displayHours(item, idx) }}</text>
|
||
</view>
|
||
<view class="aus-date-meta-item">
|
||
<text>冲</text>
|
||
<text>{{ displayClash(item, idx) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="!resolvedDates.length" class="aus-empty">
|
||
<text class="aus-empty-title">{{ repairingDates ? '正在恢复择吉结果...' : '暂无可展示的择吉日期' }}</text>
|
||
<text class="aus-empty-sub">
|
||
{{ repairingDates ? '系统正在自动补全数据,请稍候' : (repairError || '请返回重试,或稍后再查看报告结果') }}
|
||
</text>
|
||
<button v-if="!repairingDates" class="aus-empty-action" @click="attemptRecalculate(true)">
|
||
重新获取择吉结果
|
||
</button>
|
||
</view>
|
||
|
||
<view class="aus-bottom-gap"></view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- Pay sheet -->
|
||
<view v-if="false && showPay && unlocked !== 'premium'" class="aus-paymask" @tap="showPay = false" @touchmove.stop.prevent>
|
||
<view class="aus-paycard" @tap.stop>
|
||
<view class="aus-pay-title-row">
|
||
<text class="aus-pay-title">解锁完整择吉报告</text>
|
||
</view>
|
||
|
||
<view class="aus-pay-close" @tap="showPay = false">×</view>
|
||
|
||
<view class="aus-pay-cards">
|
||
<button class="aus-pay-card aus-pay-card-basic" @click="unlock('basic')">
|
||
<view class="aus-pay-card-title">近期吉日</view>
|
||
<view class="aus-pay-card-sub">未来30天精选</view>
|
||
<view class="aus-pay-price">
|
||
<text class="aus-pay-price-sign">¥</text>
|
||
<text class="aus-pay-price-num">9.9</text>
|
||
</view>
|
||
</button>
|
||
|
||
<button class="aus-pay-card aus-pay-card-premium" @click="unlock('premium')">
|
||
<view class="aus-pay-card-top">
|
||
<view class="aus-pay-card-title">全年尊享</view>
|
||
<view class="aus-pay-chip">推荐</view>
|
||
</view>
|
||
<view class="aus-pay-card-sub">1年吉日 + 时辰</view>
|
||
<view class="aus-pay-price">
|
||
<text class="aus-pay-price-sign">¥</text>
|
||
<text class="aus-pay-price-num">38</text>
|
||
<text class="aus-pay-price-origin">99.9</text>
|
||
</view>
|
||
</button>
|
||
</view>
|
||
|
||
<button class="aus-pay-share" open-type="share" @click="prepareShareUnlock">
|
||
<text class="aus-pay-share-text">分享到朋友圈免费解锁</text>
|
||
</button>
|
||
<text class="aus-pay-footnote">*每月限 1 次免费解锁机会</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Poster -->
|
||
<view v-if="showPoster && activeDate" class="aus-poster-mask" @click="showPoster = false">
|
||
<view class="aus-poster" @click.stop>
|
||
<div class="aus-poster-close" @click="showPoster = false">×</div>
|
||
<view class="aus-poster-body">
|
||
<view class="aus-poster-bar"></view>
|
||
<view class="aus-poster-content">
|
||
<view class="aus-poster-icon">吉</view>
|
||
<text class="aus-poster-title">{{ label(data.zeji_type, data.zeji_purpose) }}大吉</text>
|
||
<text class="aus-poster-sub">壹梵玄学 · 精准八字择吉</text>
|
||
<view class="aus-poster-card">
|
||
<view class="aus-poster-card-line"></view>
|
||
<text class="aus-poster-date">{{ activeDate.date }}</text>
|
||
<text class="aus-poster-lunar">{{ activeDate.lunar }}</text>
|
||
<view class="aus-poster-meta">
|
||
<view class="aus-poster-meta-block">
|
||
<text class="aus-poster-meta-label">吉时</text>
|
||
<text class="aus-poster-meta-text">{{ activeDate.hours }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="aus-poster-divider"></view>
|
||
<text class="aus-poster-desc">“{{ activeDate.desc }}”</text>
|
||
</view>
|
||
</view>
|
||
<!-- <view class="aus-poster-footer">
|
||
<view class="aus-poster-owner">
|
||
<text class="aus-poster-owner-label">福主</text>
|
||
<text class="aus-poster-owner-name">{{ data.name }}</text>
|
||
</view>
|
||
<view class="aus-poster-qr">
|
||
<view class="aus-poster-qr-box"></view>
|
||
<text class="aus-poster-qr-tip">扫码测算</text>
|
||
</view>
|
||
</view> -->
|
||
</view>
|
||
<!-- <view class="aus-poster-actions">
|
||
<button class="aus-poster-btn aus-poster-btn-dark">保存图片</button>
|
||
<button class="aus-poster-btn aus-poster-btn-gold" @click="showPoster=false">关闭</button>
|
||
</view> -->
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||
import { baziZejiApi, type BaziZejiCalculateRequest, type BaziZejiCalculateResponse } from "../../api";
|
||
|
||
const props = defineProps<{ data: BaziZejiCalculateResponse | Record<string, any> | null }>();
|
||
defineEmits<{
|
||
back: [];
|
||
}>();
|
||
|
||
type UnlockLevel = "none" | "basic" | "premium";
|
||
type AnyRecord = Record<string, any>;
|
||
|
||
const unlocked = ref<UnlockLevel>("premium");
|
||
const showPay = ref(false);
|
||
const showPoster = ref(false);
|
||
const activeDate = ref<AnyRecord | null>(null);
|
||
const fixedHeaderRef = ref<any>(null);
|
||
const headerPlaceholderHeight = ref("120px");
|
||
const repairingDates = ref(false);
|
||
const repairedOnce = ref(false);
|
||
const repairError = ref("");
|
||
const hydratedData = ref<AnyRecord | null>(null);
|
||
|
||
declare const uni: any;
|
||
|
||
const statusBarHeight = ref("0px");
|
||
try {
|
||
const systemInfo = uni?.getSystemInfoSync?.();
|
||
statusBarHeight.value = `${Number(systemInfo?.statusBarHeight || 20)}px`;
|
||
} catch {
|
||
statusBarHeight.value = "44px";
|
||
}
|
||
|
||
const updateHeaderPlaceholderHeight = () => {
|
||
if (typeof window === "undefined") return;
|
||
const el =
|
||
(fixedHeaderRef.value?.$el as HTMLElement | undefined) ||
|
||
(fixedHeaderRef.value as HTMLElement | null) ||
|
||
(document.querySelector(".aus-result-fixed-header") as HTMLElement | null);
|
||
if (!el) return;
|
||
const measured = Math.ceil(el.getBoundingClientRect().height);
|
||
if (measured > 0) {
|
||
headerPlaceholderHeight.value = `${measured}px`;
|
||
return;
|
||
}
|
||
headerPlaceholderHeight.value = `calc(${statusBarHeight.value} + 120rpx)`;
|
||
};
|
||
|
||
const DEFAULT_ADVICE = "顺应天时,趋吉避凶。所选吉日均为黄道瑞日,结合福主八字喜用神,可助事半功倍,平安顺遂。";
|
||
|
||
const tryParseJson = (value: any): any => {
|
||
if (value == null) return null;
|
||
if (typeof value === "object") return value;
|
||
if (typeof value !== "string") return null;
|
||
const raw = value.trim();
|
||
if (!raw) return null;
|
||
try {
|
||
return JSON.parse(raw);
|
||
} catch {
|
||
return null;
|
||
}
|
||
};
|
||
|
||
const toText = (value: any): string => {
|
||
if (value == null) return "";
|
||
if (typeof value === "string") return value.trim();
|
||
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||
return "";
|
||
};
|
||
|
||
const toNumber = (value: any, fallback = 0): number => {
|
||
const num = Number(value);
|
||
return Number.isFinite(num) ? num : fallback;
|
||
};
|
||
|
||
const firstNonEmpty = (...values: any[]): any => {
|
||
for (const value of values) {
|
||
if (Array.isArray(value) && value.length) return value;
|
||
const text = toText(value);
|
||
if (text) return value;
|
||
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
||
}
|
||
return null;
|
||
};
|
||
|
||
const toStringArray = (value: any): string[] => {
|
||
if (Array.isArray(value)) {
|
||
return value.map((item) => toText(item)).filter(Boolean);
|
||
}
|
||
if (typeof value === "string") {
|
||
return value
|
||
.split(/[,\uFF0C\u3001|/]/)
|
||
.map((item) => item.trim())
|
||
.filter(Boolean);
|
||
}
|
||
return [];
|
||
};
|
||
|
||
const normalizeHours = (value: any): string => {
|
||
if (Array.isArray(value)) {
|
||
return value.map((item) => toText(item)).filter(Boolean).join(", ");
|
||
}
|
||
if (value && typeof value === "object") {
|
||
return Object.values(value)
|
||
.map((item) => toText(item))
|
||
.filter(Boolean)
|
||
.join(", ");
|
||
}
|
||
return toText(value);
|
||
};
|
||
|
||
const looksLikeDateItem = (value: any): boolean => {
|
||
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
||
return Boolean(
|
||
firstNonEmpty(
|
||
value.date,
|
||
value.day,
|
||
value.selected_date,
|
||
value.good_date,
|
||
value.target_date,
|
||
value.lunar,
|
||
value.desc,
|
||
value.hours,
|
||
value.clash,
|
||
value.score
|
||
)
|
||
);
|
||
};
|
||
|
||
const DATE_LIST_KEYS = [
|
||
"dates",
|
||
"date_list",
|
||
"auspicious_dates",
|
||
"good_dates",
|
||
"result_dates",
|
||
"days",
|
||
"list",
|
||
"items",
|
||
];
|
||
|
||
const extractDateList = (source: any, depth = 0, visited = new Set<any>()): any[] => {
|
||
if (source == null || depth > 8) return [];
|
||
|
||
const parsed = tryParseJson(source);
|
||
if (parsed && parsed !== source) {
|
||
return extractDateList(parsed, depth + 1, visited);
|
||
}
|
||
|
||
if (Array.isArray(source)) {
|
||
if (source.some((item) => looksLikeDateItem(tryParseJson(item) ?? item))) {
|
||
return source;
|
||
}
|
||
for (const item of source) {
|
||
const nested = extractDateList(item, depth + 1, visited);
|
||
if (nested.length) return nested;
|
||
}
|
||
return [];
|
||
}
|
||
|
||
if (typeof source !== "object") return [];
|
||
if (visited.has(source)) return [];
|
||
visited.add(source);
|
||
|
||
for (const key of DATE_LIST_KEYS) {
|
||
const nested = extractDateList((source as AnyRecord)[key], depth + 1, visited);
|
||
if (nested.length) return nested;
|
||
}
|
||
|
||
if (looksLikeDateItem(source)) {
|
||
return [source];
|
||
}
|
||
|
||
for (const value of Object.values(source as AnyRecord)) {
|
||
const nested = extractDateList(value, depth + 1, visited);
|
||
if (nested.length) return nested;
|
||
}
|
||
|
||
return [];
|
||
};
|
||
|
||
const normalizeDateItem = (item: any): AnyRecord | null => {
|
||
const value = tryParseJson(item) ?? item;
|
||
|
||
if (typeof value === "string") {
|
||
const date = value.trim();
|
||
return date ? { date, lunar: "", desc: "", score: 0, hours: "", clash: "", suitable: [], avoid: [] } : null;
|
||
}
|
||
|
||
if (!value || typeof value !== "object") return null;
|
||
|
||
const date = toText(
|
||
firstNonEmpty(
|
||
value.date,
|
||
value.day,
|
||
value.selected_date,
|
||
value.good_date,
|
||
value.target_date,
|
||
value.gregorian_date,
|
||
value.solar_date
|
||
)
|
||
);
|
||
const lunar = toText(firstNonEmpty(value.lunar, value.lunar_date, value.nongli));
|
||
const desc = toText(firstNonEmpty(value.desc, value.description, value.reason, value.summary, value.comment));
|
||
const hours = normalizeHours(
|
||
firstNonEmpty(value.hours, value.good_hours, value.lucky_hours, value.best_hours, value.time_ranges, value.time)
|
||
);
|
||
const clash = toText(firstNonEmpty(value.clash, value.chong, value.conflict, value.avoid_zodiac));
|
||
const score = toNumber(firstNonEmpty(value.score, value.rate, value.rating), 0);
|
||
const suitable = toStringArray(firstNonEmpty(value.suitable, value.yi, value.recommend, value.fit));
|
||
const avoid = toStringArray(firstNonEmpty(value.avoid, value.ji, value.taboo, value.not_fit));
|
||
|
||
if (!date && !desc && !hours && !clash) return null;
|
||
|
||
return { date, lunar, desc, score, hours, clash, suitable, avoid };
|
||
};
|
||
|
||
const uniqueBy = (items: AnyRecord[]): AnyRecord[] => {
|
||
const seen = new Set<string>();
|
||
const result: AnyRecord[] = [];
|
||
for (const item of items) {
|
||
const key = `${item.date}|${item.lunar}|${item.desc}|${item.hours}|${item.clash}`;
|
||
if (seen.has(key)) continue;
|
||
seen.add(key);
|
||
result.push(item);
|
||
}
|
||
return result;
|
||
};
|
||
|
||
const sourceData = computed<AnyRecord | null>(() => {
|
||
return (hydratedData.value || props.data || null) as AnyRecord | null;
|
||
});
|
||
|
||
const resolvedDates = computed<AnyRecord[]>(() => {
|
||
const rawList = extractDateList(sourceData.value);
|
||
const normalized = uniqueBy(
|
||
rawList.map((item) => normalizeDateItem(item)).filter(Boolean) as AnyRecord[]
|
||
);
|
||
if (normalized.length) return normalized;
|
||
|
||
const fallback = normalizeDateItem(sourceData.value);
|
||
return fallback ? [fallback] : [];
|
||
});
|
||
|
||
const safeData = computed<AnyRecord>(() => {
|
||
const base =
|
||
sourceData.value && typeof sourceData.value === "object" && !Array.isArray(sourceData.value)
|
||
? (sourceData.value as AnyRecord)
|
||
: {};
|
||
const nested = [
|
||
base.data,
|
||
base.result,
|
||
base.report,
|
||
base.payload,
|
||
tryParseJson(base.result_json),
|
||
tryParseJson(base.result_data),
|
||
tryParseJson(base.report_json),
|
||
tryParseJson(base.detail_json),
|
||
tryParseJson(base.extra_data),
|
||
];
|
||
const merged: AnyRecord = { ...base };
|
||
for (const candidate of nested) {
|
||
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
|
||
Object.assign(merged, candidate);
|
||
}
|
||
}
|
||
return merged;
|
||
});
|
||
|
||
const advice = computed(() => {
|
||
return (
|
||
toText(
|
||
firstNonEmpty(
|
||
safeData.value.advice,
|
||
safeData.value.summary,
|
||
safeData.value.recommendation,
|
||
safeData.value.description
|
||
)
|
||
) || DEFAULT_ADVICE
|
||
);
|
||
});
|
||
|
||
const normalizeGender = (input: any): "male" | "female" | null => {
|
||
const value = toText(input).toLowerCase();
|
||
if (!value) return null;
|
||
if (value === "male" || value === "m" || value === "1" || value === "男") return "male";
|
||
if (value === "female" || value === "f" || value === "0" || value === "女") return "female";
|
||
return null;
|
||
};
|
||
|
||
const buildRepairRequest = (): BaziZejiCalculateRequest | null => {
|
||
const data = safeData.value;
|
||
const name = toText(firstNonEmpty(data.name, data.user_name, data.username));
|
||
const gender = normalizeGender(firstNonEmpty(data.gender, data.sex, data.user_gender));
|
||
const birthDate = toText(firstNonEmpty(data.birth_date, data.birthDate, data.birth_date_display));
|
||
const birthDateApi = toText(firstNonEmpty(data.birth_date_api, data.birth_datetime, data.birth_date_api_value, data.birth_date));
|
||
const birthPlace = toText(firstNonEmpty(data.birth_place, data.birthPlace, data.city, data.location));
|
||
const zejiType = toText(firstNonEmpty(data.zeji_type, data.event_type, data.type));
|
||
const zejiPurpose = toText(firstNonEmpty(data.zeji_purpose, data.event_name, data.purpose, data.target));
|
||
const dateRangeStart = toText(firstNonEmpty(data.date_range_start, data.start_date, data.range_start));
|
||
const dateRangeEnd = toText(firstNonEmpty(data.date_range_end, data.end_date, data.range_end));
|
||
|
||
if (!name || !gender || !birthDate || !birthDateApi || !birthPlace || !zejiType || !zejiPurpose || !dateRangeStart || !dateRangeEnd) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
name,
|
||
gender,
|
||
birth_date: birthDate,
|
||
birth_date_api: birthDateApi,
|
||
birth_place: birthPlace,
|
||
zeji_type: zejiType as any,
|
||
zeji_purpose: zejiPurpose,
|
||
date_range_start: dateRangeStart,
|
||
date_range_end: dateRangeEnd,
|
||
};
|
||
};
|
||
|
||
const attemptRecalculate = async (force = false) => {
|
||
if (repairingDates.value) return;
|
||
if (resolvedDates.value.length && !force) return;
|
||
if (repairedOnce.value && !force) return;
|
||
|
||
const request = buildRepairRequest();
|
||
if (!request) {
|
||
repairError.value = "缺少关键参数,无法自动恢复,请返回择吉表单重新提交。";
|
||
return;
|
||
}
|
||
|
||
repairedOnce.value = true;
|
||
repairingDates.value = true;
|
||
repairError.value = "";
|
||
try {
|
||
const result = await baziZejiApi.calculateBaziZeji(request);
|
||
hydratedData.value = result as AnyRecord;
|
||
} catch (error: any) {
|
||
repairError.value = toText(error?.message || error?.msg) || "自动恢复失败,请稍后重试";
|
||
} finally {
|
||
repairingDates.value = false;
|
||
nextTick(updateHeaderPlaceholderHeight);
|
||
}
|
||
};
|
||
|
||
const label = (type: string, purpose?: string) => {
|
||
const map: Record<string, string> = {
|
||
wedding: "婚嫁择吉",
|
||
business: "开业择吉",
|
||
move: "搬家择吉",
|
||
travel: "出行择吉",
|
||
investment: "投资择吉",
|
||
surgery: "手术择吉",
|
||
contract: "签约择吉",
|
||
other: purpose || "择吉",
|
||
};
|
||
return map[type] || purpose || "择吉";
|
||
};
|
||
|
||
const isLocked = (_idx: number) => false;
|
||
|
||
const displayHours = (item: AnyRecord, idx: number): string => {
|
||
if (isLocked(idx)) return "解锁查看";
|
||
const hours = normalizeHours(item?.hours);
|
||
if (!hours) return "暂无";
|
||
const segments = hours
|
||
.split(/[,\uFF0C\u3001|/]/)
|
||
.map((value) => value.trim())
|
||
.filter(Boolean);
|
||
return segments.length > 1 ? `${segments[0]}...` : segments[0];
|
||
};
|
||
|
||
const displayClash = (item: AnyRecord, idx: number): string => {
|
||
if (isLocked(idx)) return "解锁查看";
|
||
return toText(item?.clash) || "暂无";
|
||
};
|
||
|
||
const prepareShareUnlock = () => {
|
||
showPay.value = false;
|
||
};
|
||
|
||
const handleDate = (item: AnyRecord, _idx: number) => {
|
||
activeDate.value = item;
|
||
showPoster.value = true;
|
||
};
|
||
|
||
const unlock = async (_level: "basic" | "premium") => {
|
||
unlocked.value = "premium";
|
||
showPay.value = false;
|
||
};
|
||
|
||
onMounted(() => {
|
||
nextTick(() => {
|
||
updateHeaderPlaceholderHeight();
|
||
setTimeout(updateHeaderPlaceholderHeight, 0);
|
||
});
|
||
|
||
if (typeof window !== "undefined") {
|
||
window.addEventListener("resize", updateHeaderPlaceholderHeight);
|
||
}
|
||
|
||
// 不在此自动调用 calculate 接口:会重复创建择吉任务(例如从「我的方案」进入详情、
|
||
// 或返回上一页时若误触发 watch/挂载)。需要补算时由用户点击「重新获取择吉结果」。
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
if (typeof window !== "undefined") {
|
||
window.removeEventListener("resize", updateHeaderPlaceholderHeight);
|
||
}
|
||
});
|
||
|
||
watch(
|
||
() => props.data,
|
||
() => {
|
||
hydratedData.value = null;
|
||
repairedOnce.value = false;
|
||
repairError.value = "";
|
||
nextTick(() => {
|
||
updateHeaderPlaceholderHeight();
|
||
});
|
||
},
|
||
{ deep: true }
|
||
);
|
||
</script>
|
||
|
||
<style scoped>
|
||
.aus-result {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #f0efe9;
|
||
position: relative;
|
||
overflow: hidden;
|
||
font-family: SimSun, "Songti SC", "Songti TC", "Noto Serif SC", STSong, serif;
|
||
}
|
||
|
||
.aus-result-texture {
|
||
position: absolute;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
opacity: 0.4;
|
||
mix-blend-mode: multiply;
|
||
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||
z-index: 0;
|
||
}
|
||
|
||
/* 固定头部容器 */
|
||
.aus-result-fixed-header {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 100;
|
||
background: #f0efe9;
|
||
}
|
||
|
||
/* 状态栏占位 */
|
||
.status-bar-placeholder {
|
||
width: 100%;
|
||
}
|
||
|
||
/* 头部占位 */
|
||
.aus-result-header-placeholder {
|
||
height: calc(var(--status-bar-height, 0) + 100rpx);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.aus-result-header {
|
||
position: relative;
|
||
z-index: 10;
|
||
padding: 24rpx 32rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: rgba(253, 251, 247, 0.9);
|
||
border-bottom: 1px solid #e5e5e5;
|
||
backdrop-filter: blur(8px);
|
||
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.aus-result-back {
|
||
padding: 16rpx;
|
||
margin-left: -8rpx;
|
||
color: #5a5a5a;
|
||
background: transparent;
|
||
border: none;
|
||
}
|
||
|
||
.aus-result-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #2c2c2c;
|
||
letter-spacing: 0.3em;
|
||
}
|
||
|
||
.aus-result-header-spacer {
|
||
width: 32rpx;
|
||
}
|
||
|
||
.aus-result-scroll {
|
||
flex: 1;
|
||
position: relative;
|
||
z-index: 5;
|
||
padding: 24rpx 24rpx 40rpx;
|
||
height: 0;
|
||
}
|
||
|
||
.aus-result-container {
|
||
width: 96%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 32rpx;
|
||
}
|
||
|
||
.aus-advice-card {
|
||
position: relative;
|
||
background: linear-gradient(135deg, #2c2c2c, #1a1a1a);
|
||
color: #f2e6d8;
|
||
border-radius: 20rpx;
|
||
padding: 32rpx;
|
||
overflow: hidden;
|
||
box-shadow: 0 16rpx 24rpx -12rpx rgba(0, 0, 0, 0.35);
|
||
}
|
||
|
||
.aus-advice-glow {
|
||
position: absolute;
|
||
top: -24rpx;
|
||
right: -24rpx;
|
||
width: 160rpx;
|
||
height: 160rpx;
|
||
background: #d4af37;
|
||
opacity: 0.18;
|
||
filter: blur(80rpx);
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.aus-advice-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.aus-advice-icon {
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
border-radius: 50%;
|
||
background: #d4af37;
|
||
color: #1a1a1a;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: bold;
|
||
font-size: 22px;
|
||
border: 2rpx solid rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.aus-advice-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #f2e6d8;
|
||
display: block;
|
||
}
|
||
|
||
.aus-advice-sub {
|
||
font-size: 10px;
|
||
color: #ccc;
|
||
letter-spacing: 0.1em;
|
||
}
|
||
|
||
.aus-advice-body {
|
||
background: rgba(255, 255, 255, 0.06);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 12rpx;
|
||
padding: 20rpx;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 10rpx;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
|
||
.aus-advice-bullet {
|
||
color: #d4af37;
|
||
margin-top: 2rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.aus-advice-text {
|
||
font-size: 14px;
|
||
line-height: 1.7;
|
||
color: #e5e5e5;
|
||
}
|
||
|
||
.aus-timeline {
|
||
position: relative;
|
||
}
|
||
|
||
.aus-timeline-line {
|
||
position: absolute;
|
||
left: 48rpx;
|
||
top: 16rpx;
|
||
bottom: 16rpx;
|
||
width: 2rpx;
|
||
background: #dcd3c9;
|
||
}
|
||
|
||
.aus-timeline-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24rpx;
|
||
}
|
||
|
||
.aus-timeline-item {
|
||
position: relative;
|
||
padding-left: 88rpx;
|
||
}
|
||
|
||
.aus-timeline-dot {
|
||
position: absolute;
|
||
left: 42rpx;
|
||
top: 32rpx;
|
||
width: 12rpx;
|
||
height: 12rpx;
|
||
border-radius: 50%;
|
||
border: 4rpx solid #f0efe9;
|
||
z-index: 2;
|
||
}
|
||
|
||
.aus-timeline-dot.is-open {
|
||
background: #8b2323;
|
||
}
|
||
|
||
.aus-timeline-dot.is-locked {
|
||
background: #ccc;
|
||
}
|
||
|
||
.aus-date-card {
|
||
position: relative;
|
||
background: #fffdf9;
|
||
border: 1px solid #d4af37;
|
||
border-radius: 16rpx;
|
||
overflow: hidden;
|
||
box-shadow: 0 10rpx 16rpx -8rpx rgba(0, 0, 0, 0.16);
|
||
}
|
||
|
||
.aus-date-card.is-locked {
|
||
border-color: #e5e5e5;
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.aus-date-card.is-open {
|
||
border-color: rgba(212, 175, 55, 0.3);
|
||
}
|
||
|
||
.aus-date-lock {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: rgba(255, 255, 255, 0.4);
|
||
backdrop-filter: blur(2px);
|
||
z-index: 10;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.aus-date-lock-icon {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
background: #2c2c2c;
|
||
color: #d4af37;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 8rpx 14rpx rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.aus-date-lock-text {
|
||
font-size: 12px;
|
||
font-weight: bold;
|
||
color: #2c2c2c;
|
||
background: rgba(255, 255, 255, 0.8);
|
||
padding: 6rpx 12rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.aus-date-inner {
|
||
padding: 24rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.aus-date-head {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.aus-date-day {
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
color: #2c2c2c;
|
||
display: block;
|
||
}
|
||
|
||
.aus-date-lunar {
|
||
font-size: 12px;
|
||
color: #8b2323;
|
||
}
|
||
|
||
.aus-date-score {
|
||
text-align: right;
|
||
}
|
||
|
||
.aus-date-score-num {
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
color: #d4af37;
|
||
display: block;
|
||
}
|
||
|
||
.aus-date-score-label {
|
||
font-size: 10px;
|
||
color: #999;
|
||
}
|
||
|
||
.aus-date-desc {
|
||
background: #f9f7f2;
|
||
border-radius: 12rpx;
|
||
padding: 14rpx;
|
||
font-size: 12px;
|
||
color: #5a5a5a;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.aus-date-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-top: 1px solid #f0f0f0;
|
||
padding-top: 12rpx;
|
||
font-size: 10px;
|
||
color: #999;
|
||
}
|
||
|
||
.aus-date-meta-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6rpx;
|
||
}
|
||
|
||
.aus-empty {
|
||
background: #fffdf9;
|
||
border: 1px dashed #d4af37;
|
||
border-radius: 16rpx;
|
||
padding: 28rpx 24rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
margin-left: 88rpx;
|
||
}
|
||
|
||
.aus-empty-title {
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
color: #2c2c2c;
|
||
}
|
||
|
||
.aus-empty-sub {
|
||
font-size: 12px;
|
||
color: #6a6a6a;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.aus-empty-action {
|
||
margin-top: 8rpx;
|
||
align-self: flex-start;
|
||
background: #2c2c2c;
|
||
color: #fdfbf7;
|
||
border: none;
|
||
border-radius: 999rpx;
|
||
padding: 0 20rpx;
|
||
height: 56rpx;
|
||
line-height: 56rpx;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.aus-bottom-gap {
|
||
height: 120rpx;
|
||
}
|
||
|
||
.aus-paymask {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 10000;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-end;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
backdrop-filter: blur(6px);
|
||
}
|
||
|
||
.aus-pay-close {
|
||
position: absolute;
|
||
right: 28rpx;
|
||
top: 24rpx;
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
border-radius: 999rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(0, 0, 0, 0.06);
|
||
color: #5a5a5a;
|
||
font-size: 22px;
|
||
}
|
||
|
||
.aus-paycard {
|
||
background: #fdfbf7;
|
||
border-top-left-radius: 24rpx;
|
||
border-top-right-radius: 24rpx;
|
||
position: relative;
|
||
padding: 40rpx 32rpx;
|
||
padding-bottom: calc(150rpx + env(safe-area-inset-bottom, 0px));
|
||
}
|
||
|
||
.aus-pay-title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.aus-pay-title {
|
||
font-weight: bold;
|
||
color: #2c2c2c;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.aus-pay-badge {
|
||
font-size: 10px;
|
||
color: #8b2323;
|
||
background: rgba(139, 35, 35, 0.1);
|
||
padding: 6rpx 12rpx;
|
||
border-radius: 999rpx;
|
||
border: 1rpx solid rgba(139, 35, 35, 0.2);
|
||
}
|
||
|
||
.aus-pay-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 30rpx;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.aus-pay-card {
|
||
border-radius: 16rpx;
|
||
padding: 14rpx;
|
||
border: 1rpx solid #e5e5e5;
|
||
text-align: left;
|
||
box-shadow: 0 10rpx 20rpx -12rpx rgba(0, 0, 0, 0.16);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2rpx;
|
||
width: 100%;
|
||
}
|
||
|
||
.aus-pay-card-basic {
|
||
background: #fff;
|
||
color: #2c2c2c;
|
||
}
|
||
|
||
.aus-pay-card-premium {
|
||
background: #2c2c2c;
|
||
color: #fdfbf7;
|
||
border-color: #2c2c2c;
|
||
}
|
||
|
||
.aus-pay-card-title {
|
||
font-weight: bold;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.aus-pay-card-sub {
|
||
font-size: 10px;
|
||
color: #5a5a5a;
|
||
}
|
||
|
||
.aus-pay-card-premium .aus-pay-card-sub {
|
||
color: #dcd3c9;
|
||
}
|
||
|
||
.aus-pay-price {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 2rpx;
|
||
margin-top: 0;
|
||
}
|
||
|
||
.aus-pay-price-sign {
|
||
font-size: 11px;
|
||
color: inherit;
|
||
}
|
||
|
||
.aus-pay-price-num {
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
color: inherit;
|
||
}
|
||
|
||
.aus-pay-price-origin {
|
||
font-size: 10px;
|
||
color: #999;
|
||
text-decoration: line-through;
|
||
}
|
||
|
||
.aus-pay-card-premium .aus-pay-price-origin {
|
||
color: rgba(253, 251, 247, 0.6);
|
||
}
|
||
|
||
.aus-pay-card-top {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.aus-pay-chip {
|
||
font-size: 10px;
|
||
color: #2c2c2c;
|
||
background: #f7d774;
|
||
padding: 4rpx 10rpx;
|
||
border-radius: 999rpx;
|
||
}
|
||
|
||
.aus-pay-share {
|
||
width: 100%;
|
||
background: #16c05d;
|
||
color: #fdfbf7;
|
||
border: none;
|
||
border-radius: 12rpx;
|
||
padding: 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12rpx;
|
||
font-weight: bold;
|
||
box-shadow: 0 12rpx 24rpx -12rpx rgba(22, 192, 93, 0.5);
|
||
}
|
||
|
||
.aus-pay-share-icon {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.aus-pay-share-text {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.aus-pay-footnote {
|
||
display: block;
|
||
text-align: center;
|
||
font-size: 10px;
|
||
color: #999;
|
||
margin-top: 12rpx;
|
||
}
|
||
|
||
.aus-poster-mask {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 70;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(0, 0, 0, 0.8);
|
||
backdrop-filter: blur(6px);
|
||
padding: 16rpx;
|
||
}
|
||
|
||
.aus-poster {
|
||
background: #fdfbf7;
|
||
width: 100%;
|
||
max-width: 640rpx;
|
||
border-radius: 16rpx;
|
||
overflow: hidden;
|
||
box-shadow: 0 18rpx 32rpx rgba(0, 0, 0, 0.35);
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.aus-poster-close {
|
||
position: absolute;
|
||
top: 24rpx;
|
||
right: 24rpx;
|
||
z-index: 20;
|
||
color: #5a5a5a;
|
||
background: rgba(255, 255, 255, 0.6);
|
||
border-radius: 50%;
|
||
padding: 10rpx;
|
||
border: none;
|
||
width: 15px;
|
||
height: 15px;
|
||
}
|
||
|
||
.aus-poster-body {
|
||
width: 100%;
|
||
background: url("https://www.transparenttextures.com/patterns/rice-paper.png");
|
||
padding-bottom: 24rpx;
|
||
}
|
||
|
||
.aus-poster-bar {
|
||
height: 8rpx;
|
||
background: #8b2323;
|
||
width: 100%;
|
||
}
|
||
|
||
.aus-poster-content {
|
||
padding: 48rpx 32rpx 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.aus-poster-icon {
|
||
width: 128rpx;
|
||
height: 128rpx;
|
||
margin: 0 auto 24rpx;
|
||
background: #8b2323;
|
||
color: #fdfbf7;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 6rpx solid #d4af37;
|
||
box-shadow: 0 10rpx 18rpx rgba(0, 0, 0, 0.2);
|
||
font-size: 28px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.aus-poster-title {
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
color: #2c2c2c;
|
||
letter-spacing: 0.2em;
|
||
display: block;
|
||
margin-bottom: 6rpx;
|
||
}
|
||
|
||
.aus-poster-sub {
|
||
font-size: 12px;
|
||
color: #5a5a5a;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.aus-poster-card {
|
||
background: #fffdf9;
|
||
border: 1px solid #e5e5e5;
|
||
border-radius: 12rpx;
|
||
padding: 24rpx;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.aus-poster-card-line {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
height: 6rpx;
|
||
width: 100%;
|
||
background: linear-gradient(90deg, transparent, #d4af37, transparent);
|
||
}
|
||
|
||
.aus-poster-date {
|
||
font-size: 32px;
|
||
font-weight: bold;
|
||
color: #8b2323;
|
||
display: block;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.aus-poster-lunar {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #2c2c2c;
|
||
display: block;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.aus-poster-meta {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
font-size: 12px;
|
||
color: #5a5a5a;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.aus-poster-meta-block {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 6rpx;
|
||
}
|
||
|
||
.aus-poster-meta-label {
|
||
color: #8b2323;
|
||
}
|
||
|
||
.aus-poster-meta-text {
|
||
color: #5a5a5a;
|
||
}
|
||
|
||
.aus-poster-divider {
|
||
height: 2rpx;
|
||
width: 100%;
|
||
background: #e5e5e5;
|
||
margin: 12rpx 0;
|
||
}
|
||
|
||
.aus-poster-desc {
|
||
font-size: 14px;
|
||
color: #2c2c2c;
|
||
font-style: italic;
|
||
}
|
||
|
||
.aus-poster-footer {
|
||
margin-top: 24rpx;
|
||
padding: 0 32rpx 24rpx;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.aus-poster-owner-label {
|
||
font-size: 10px;
|
||
color: #999;
|
||
display: block;
|
||
}
|
||
|
||
.aus-poster-owner-name {
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
color: #2c2c2c;
|
||
}
|
||
|
||
.aus-poster-qr {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 6rpx;
|
||
}
|
||
|
||
.aus-poster-qr-box {
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
background: #2c2c2c;
|
||
}
|
||
|
||
.aus-poster-qr-tip {
|
||
font-size: 10px;
|
||
color: #999;
|
||
}
|
||
|
||
.aus-poster-actions {
|
||
background: #fff;
|
||
border-top: 1px solid #e5e5e5;
|
||
padding: 24rpx 16rpx;
|
||
padding-bottom: calc(32rpx + env(safe-area-inset-bottom, 0px));
|
||
display: flex;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.aus-poster-btn {
|
||
flex: 1;
|
||
padding: 16rpx;
|
||
border-radius: 12rpx;
|
||
font-weight: bold;
|
||
border: none;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.aus-poster-btn-dark {
|
||
background: #2c2c2c;
|
||
color: #fdfbf7;
|
||
}
|
||
|
||
.aus-poster-btn-gold {
|
||
background: #d4af37;
|
||
color: #2c2c2c;
|
||
}
|
||
</style>
|