upload project source code

This commit is contained in:
2026-04-30 18:49:43 +08:00
commit 9b394ba682
2277 changed files with 660945 additions and 0 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,131 @@
<template>
<view class="analysis-screen">
<!-- Starry Background -->
<view class="analysis-bg">
<view class="analysis-bg-gradient"></view>
<view
v-for="s in stars"
:key="s.id"
class="analysis-star"
:style="{top: s.top + '%', left: s.left + '%', width: s.size + 'px', height: s.size + 'px'}">
</view>
<view class="analysis-bg-blur-1"></view>
<view class="analysis-bg-blur-2"></view>
</view>
<!-- Loading / Analyzing Stage -->
<view v-if="stage !== 'result'" class="analysis-loading">
<MysticCompass />
</view>
<!-- Result Stage -->
<AnalysisResult v-else @back="$emit('back')" />
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import MysticCompass from '../MysticCompass.vue';
import AnalysisResult from '../AnalysisResult.vue';
const emit = defineEmits<{
back: [];
}>();
const stage = ref<'loading' | 'analyzing' | 'result'>('loading');
// Starry background
const stars = ref(Array.from({ length: 50 }).map((_, i) => ({
id: i,
top: Math.random() * 100,
left: Math.random() * 100,
size: Math.random() * 2 + 1
})));
onMounted(() => {
setTimeout(() => {
stage.value = 'analyzing';
}, 100);
setTimeout(() => {
stage.value = 'result';
}, 3500);
});
</script>
<style scoped>
.analysis-screen {
height: 100%;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
font-family: SimSun, "Songti SC", "Songti TC", "Noto Serif SC", STSong, serif;
color: #e2e2e2;
}
/* Background */
.analysis-bg {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 0;
}
.analysis-bg-gradient {
position: absolute;
inset: 0;
background: linear-gradient(to bottom, #050508, #10101a, #1a1a2e);
}
.analysis-star {
position: absolute;
border-radius: 50%;
background: white;
opacity: 0.2;
animation: twinkle 3s ease-in-out infinite;
}
@keyframes twinkle {
0%, 100% { opacity: 0.1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.2); }
}
.analysis-bg-blur-1 {
position: absolute;
top: -10%;
left: -10%;
width: 50%;
height: 50%;
background: #2a3d5d;
opacity: 0.2;
filter: blur(100px);
border-radius: 50%;
}
.analysis-bg-blur-2 {
position: absolute;
bottom: -10%;
right: -10%;
width: 50%;
height: 50%;
background: #9c2a2a;
opacity: 0.1;
filter: blur(100px);
border-radius: 50%;
}
/* Loading Stage */
.analysis-loading {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
z-index: 10;
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,12 @@
<template>
<view class="h-full flex flex-col items-center justify-center bg-[#050508] text-[#d4af37] font-serif relative overflow-hidden">
<view class="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-[#1a1a2e] via-[#050508] to-[#000] opacity-80"></view>
<view class="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/stardust.png')] opacity-20"></view>
<view class="relative z-10 text-center px-8">
<view class="text-5xl mb-4"></view>
<text class="block text-xl font-bold tracking-[0.4em] mb-2">命盘解析生成中</text>
<text class="block text-sm text-[#e2e2e2]/80">分析时间预计1-2分钟可点击返回按钮退出并在我的方案中查看结果</text>
</view>
</view>
</template>

View File

@@ -0,0 +1,590 @@
<template>
<view class="auspicious-form">
<view class="auspicious-texture"></view>
<!-- 固定Header -->
<view class="auspicious-fixed-header">
<view class="status-bar-placeholder"></view>
<view class="auspicious-header">
<view class="auspicious-back" @click="$emit('back')"></view>
<text class="auspicious-title">精准八字择吉</text>
<view class="auspicious-header-spacer"></view>
</view>
</view>
<!-- 头部占位 -->
<view class="auspicious-header-placeholder"></view>
<scroll-view scroll-y class="auspicious-scroll">
<view class="auspicious-container">
<view class="auspicious-intro">
<text class="auspicious-intro-title">顺天时 · 得地利 · 人和顺</text>
<text class="auspicious-intro-sub">根据您的生辰八字精准测算最佳黄道吉日</text>
</view>
<!-- 事项 -->
<view class="auspicious-section">
<label class="auspicious-label">
您要求测的事项
</label>
<view class="auspicious-grid">
<button v-for="type in eventTypes" :key="type.id" @click="form.eventType = type.id"
:class="['auspicious-event-card', form.eventType === type.id ? 'is-active' : '']">
<text class="auspicious-event-text">{{ type.label }}</text>
</button>
</view>
<view v-if="form.eventType === 'other'" class="auspicious-custom">
<input v-model="form.customEvent" type="text" placeholder="请输入您要求测的事项 (如: 签约, 出行, 动土...)"
class="auspicious-input" />
</view>
</view>
<!-- 福主信息 -->
<view class="auspicious-section">
<label class="auspicious-label">
福主信息
</label>
<view class="auspicious-card">
<view class="auspicious-field">
<label class="auspicious-field-label">您的姓名</label>
<input v-model="form.name" type="text" placeholder="请输入真实姓名" class="auspicious-field-input" />
</view>
<view class="auspicious-field">
<label class="auspicious-field-label">性别</label>
<view class="auspicious-gender">
<button :class="['auspicious-gender-btn', form.gender === 'male' ? 'is-active' : '']"
@click="form.gender = 'male'">
</button>
<button :class="['auspicious-gender-btn', form.gender === 'female' ? 'is-active' : '']"
@click="form.gender = 'female'">
</button>
</view>
</view>
<view class="auspicious-field">
<label class="auspicious-field-label">出生日期时辰</label>
<view class="auspicious-date-trigger" @click="showDatePicker = true">
<text class="auspicious-date-text" :class="{ 'is-placeholder': !form.birthDateDisplay }">
{{ form.birthDateDisplay || '请选择出生日期时辰' }}
</text>
<text class="auspicious-date-arrow"></text>
</view>
</view>
<view class="auspicious-field">
<label class="auspicious-field-label">出生地</label>
<input v-model="form.birthPlace" type="text" placeholder="请输入出生地(如:临沂市)" class="auspicious-field-input" />
</view>
</view>
</view>
<!-- 择吉目的 -->
<view class="auspicious-section">
<label class="auspicious-label">
择吉目的
</label>
<view class="auspicious-card">
<textarea v-model="form.zejiPurpose" placeholder="请描述您的择吉目的(如:选择结婚吉日,希望婚姻美满幸福)" class="auspicious-textarea"
maxlength="200" />
</view>
</view>
<!-- 期望日期范围 -->
<view class="auspicious-section">
<label class="auspicious-label">
期望日期范围
</label>
<view class="auspicious-card">
<view class="auspicious-field">
<label class="auspicious-field-label">开始日期</label>
<view class="auspicious-date-trigger" @click="showStartDatePicker = true">
<text class="auspicious-date-text" :class="{ 'is-placeholder': !form.dateRangeStartDisplay }">
{{ form.dateRangeStartDisplay || '请选择开始日期' }}
</text>
<text class="auspicious-date-arrow"></text>
</view>
</view>
<view class="auspicious-field">
<label class="auspicious-field-label">结束日期</label>
<view class="auspicious-date-trigger" @click="showEndDatePicker = true">
<text class="auspicious-date-text" :class="{ 'is-placeholder': !form.dateRangeEndDisplay }">
{{ form.dateRangeEndDisplay || '请选择结束日期' }}
</text>
<text class="auspicious-date-arrow"></text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 日期选择器 -->
<MysticDatePicker :is-open="showDatePicker" title="请择良辰" :default-value="form.birthDateDisplay"
@close="showDatePicker = false" @confirm="handleDateConfirm" />
<!-- 开始日期不可晚于今天年份为过去至今年 -->
<MysticDatePicker :is-open="showStartDatePicker" title="选择开始日期" :default-value="form.dateRangeStartDisplay"
:min-year="zejiStartMinYear" :max-year="zejiStartMaxYear" cap-at-today
footer-tip="开始日期不可选择今天之后的日期滑动选择后自动对应农历干支"
@close="showStartDatePicker = false" @confirm="handleStartDateConfirm" />
<!-- 结束日期选择器 -->
<MysticDatePicker :is-open="showEndDatePicker" title="选择结束日期" :default-value="form.dateRangeEndDisplay"
:min-year="zejiExpectRangeMinYear" :max-year="zejiExpectRangeMaxYear"
footer-tip="期望日期区间支持选择至未来多年滑动选择后自动对应农历干支"
@close="showEndDatePicker = false" @confirm="handleEndDateConfirm" />
<!-- Footer -->
<view class="auspicious-footer">
<button class="auspicious-submit" @click="submit">
立即测算
</button>
<text class="auspicious-footer-tip">
已有 28,392 人通过壹梵择得良辰吉日
</text>
</view>
</view>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from "vue";
import MysticDatePicker from "../MysticDatePicker.vue";
import { baziZejiApi, type BaziZejiCalculateRequest } from '../../api';
declare const uni: any;
const emit = defineEmits<{
submit: [data: any];
back: [];
}>();
const form = reactive({
eventType: "wedding",
customEvent: "",
name: "",
gender: "male",
birthDateDisplay: "",
birthDateApi: "",
birthPlace: "",
zejiPurpose: "",
dateRangeStartDisplay: "",
dateRangeStart: "",
dateRangeEndDisplay: "",
dateRangeEnd: "",
});
const showDatePicker = ref(false);
const showStartDatePicker = ref(false);
const showEndDatePicker = ref(false);
/** 精准八字择吉 · 结束日期:从当年起可往后选多年 */
const ZEJI_RANGE_FORWARD_YEARS = 50;
const zejiExpectRangeMinYear = computed(() => new Date().getFullYear());
const zejiExpectRangeMaxYear = computed(() => new Date().getFullYear() + ZEJI_RANGE_FORWARD_YEARS);
/** 开始日期:至多为今天,年份列与生辰类似(当年往前若干年) */
const zejiStartMinYear = computed(() => new Date().getFullYear() - 85);
const zejiStartMaxYear = computed(() => new Date().getFullYear());
const eventTypes = [
{ id: "wedding", label: "婚嫁择吉", icon: "💒" },
{ id: "business", label: "开业择吉", icon: "🧧" },
{ id: "move", label: "搬家择吉", icon: "🏠" },
{ id: "travel", label: "出行择吉", icon: "✈️" },
{ id: "investment", label: "投资择吉", icon: "💰" },
{ id: "surgery", label: "手术择吉", icon: "🏥" },
{ id: "contract", label: "签约择吉", icon: "📝" },
{ id: "other", label: "其他择吉", icon: "✍️" },
];
const handleDateConfirm = (displayVal: string, apiVal: string) => {
form.birthDateDisplay = displayVal;
form.birthDateApi = apiVal;
showDatePicker.value = false;
};
const handleStartDateConfirm = (displayVal: string, apiVal: string) => {
form.dateRangeStartDisplay = displayVal;
// 从API格式中提取日期部分 (YYYY-MM-DD)
form.dateRangeStart = apiVal.split(' ')[0];
showStartDatePicker.value = false;
};
const handleEndDateConfirm = (displayVal: string, apiVal: string) => {
form.dateRangeEndDisplay = displayVal;
// 从API格式中提取日期部分 (YYYY-MM-DD)
form.dateRangeEnd = apiVal.split(' ')[0];
showEndDatePicker.value = false;
};
const submit = async () => {
if (form.eventType === "other" && !form.customEvent.trim()) {
uni.showToast({ title: "请输入您要求测的事项", icon: "none" });
return;
}
if (!form.name || !form.birthDateDisplay) {
uni.showToast({ title: "请填写真实信息以确保准确", icon: "none" });
return;
}
if (!form.birthPlace) {
uni.showToast({ title: "请输入出生地", icon: "none" });
return;
}
if (!form.zejiPurpose) {
uni.showToast({ title: "请输入择吉目的", icon: "none" });
return;
}
if (!form.dateRangeStart || !form.dateRangeEnd) {
uni.showToast({ title: "请选择期望日期范围", icon: "none" });
return;
}
uni.showLoading({ title: '测算中...', mask: true });
try {
const requestData: BaziZejiCalculateRequest = {
name: form.name,
gender: form.gender as 'male' | 'female',
birth_date: form.birthDateDisplay,
birth_date_api: form.birthDateApi,
birth_place: form.birthPlace,
zeji_type: form.eventType as any,
zeji_purpose: form.eventType === 'other' ? form.customEvent : form.zejiPurpose,
date_range_start: form.dateRangeStart,
date_range_end: form.dateRangeEnd,
};
const result = await baziZejiApi.calculateBaziZeji(requestData);
uni.hideLoading();
emit('submit', result);
} catch (error: any) {
uni.hideLoading();
// 如果是认证失败错误不显示toast因为API函数中已经处理了跳转
if (error.message !== '认证失败,请登录后再试') {
uni.showToast({
title: error.message || '测算失败,请稍后重试',
icon: 'none',
duration: 2000,
});
}
}
};
</script>
<style scoped>
.auspicious-form {
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;
}
.auspicious-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;
}
/* 固定头部容器 */
.auspicious-fixed-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: #f0efe9;
}
/* 状态栏占位 */
.status-bar-placeholder {
height: var(--status-bar-height, 0);
width: 100%;
}
/* 头部占位 */
.auspicious-header-placeholder {
height: calc(var(--status-bar-height, 0) + 100rpx);
flex-shrink: 0;
}
.auspicious-header {
position: relative;
z-index: 10;
padding: 24rpx 32rpx;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #eaddcf;
}
.auspicious-back {
padding: 16rpx;
margin-left: -8rpx;
color: #5a5a5a;
background: transparent;
border: none;
}
.auspicious-title {
font-size: 18px;
font-weight: bold;
color: #2c2c2c;
letter-spacing: 0.3em;
}
.auspicious-header-spacer {
width: 32rpx;
}
.auspicious-scroll {
flex: 1;
position: relative;
z-index: 10;
height: 0;
}
.auspicious-container {
max-width: 700rpx;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 32rpx;
}
.auspicious-intro {
text-align: center;
margin-bottom: 48rpx;
}
.auspicious-intro-title {
font-size: 24px;
font-weight: bold;
color: #2c2c2c;
display: block;
margin-bottom: 12rpx;
letter-spacing: 0.1em;
}
.auspicious-intro-sub {
font-size: 12px;
color: #5a5a5a;
letter-spacing: 0.05em;
}
.auspicious-section {
margin-bottom: 48rpx;
}
.auspicious-label {
display: block;
font-size: 14px;
font-weight: bold;
color: #2c2c2c;
margin-bottom: 20rpx;
padding-left: 12rpx;
border-left: 4rpx solid #8b2323;
letter-spacing: 0.05em;
}
.auspicious-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
align-items: stretch;
}
.auspicious-event-card {
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 24rpx 16rpx;
border-radius: 16rpx;
border: 1px solid #e5e5e5;
background: #fffdf9;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
color: #5a5a5a;
transition: all 0.2s ease;
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.04);
}
.auspicious-event-card.is-active {
background: #8b2323;
border-color: #8b2323;
color: #fdfbf7;
box-shadow: 0 10rpx 16rpx -4rpx rgba(139, 35, 35, 0.35);
}
.auspicious-event-icon {
font-size: 22px;
}
.auspicious-event-text {
font-size: 14px;
font-weight: bold;
letter-spacing: 0.05em;
}
.auspicious-custom {
margin-top: 16rpx;
}
.auspicious-input {
width: 100%;
background: #fffdf9;
border: 1px solid #e5e5e5;
border-radius: 14rpx;
padding: 20rpx;
font-size: 14px;
color: #2c2c2c;
outline: none;
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.04);
}
.auspicious-input::placeholder {
color: #bfbfbf;
}
.auspicious-card {
background: #fffdf9;
border: 1px solid #e5e5e5;
border-radius: 16rpx;
padding: 28rpx;
box-shadow: 0 6rpx 12rpx -4rpx rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
gap: 20rpx;
}
.auspicious-field {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.auspicious-field-label {
font-size: 12px;
color: #8a8a8a;
}
.auspicious-field-input {
width: 100%;
background: transparent;
border: none;
border-bottom: 1px solid #e5e5e5;
padding: 14rpx 0;
font-size: 14px;
color: #2c2c2c;
outline: none;
}
.auspicious-field-input::placeholder {
color: #bfbfbf;
}
.auspicious-textarea {
width: 100%;
min-height: 120rpx;
background: transparent;
border: none;
border-bottom: 1px solid #e5e5e5;
padding: 14rpx 0;
font-size: 14px;
color: #2c2c2c;
outline: none;
resize: none;
font-family: inherit;
}
.auspicious-textarea::placeholder {
color: #bfbfbf;
}
.auspicious-gender {
display: flex;
gap: 16rpx;
}
.auspicious-gender-btn {
flex: 1;
border-radius: 12rpx;
border: 1px solid #e5e5e5;
background: transparent;
color: #5a5a5a;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
}
.auspicious-gender-btn.is-active {
background: #2c2c2c;
color: #d4af37;
border-color: #2c2c2c;
}
.auspicious-date-trigger {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e5e5e5;
padding: 14rpx 0;
}
.auspicious-date-text {
font-size: 14px;
color: #2c2c2c;
}
.auspicious-date-text.is-placeholder {
color: #bfbfbf;
}
.auspicious-date-arrow {
color: #8b2323;
font-size: 24rpx;
opacity: 0.6;
}
.auspicious-footer {
padding: 32rpx;
background: #fdfbf7;
border-top: 1px solid #e5e5e5;
position: relative;
z-index: 20;
}
.auspicious-submit {
width: 100%;
background: #8b2323;
color: #fdfbf7;
font-weight: bold;
padding: 18rpx 0;
border-radius: 16rpx;
box-shadow: 0 12rpx 18rpx -8rpx rgba(139, 35, 35, 0.35);
font-size: 16px;
border: none;
}
.auspicious-footer-tip {
display: block;
text-align: center;
font-size: 10px;
color: #999;
margin-top: 12rpx;
}
</style>

View File

@@ -0,0 +1,320 @@
<template>
<view class="aus-load">
<view class="aus-load-bg"></view>
<view class="aus-compass-wrapper">
<view class="aus-compass">
<view class="compass-glow"></view>
<view class="compass-svg-wrap">
<!-- 外圈虚线圆 -->
<view class="circle-outer"></view>
<view class="circle-main"></view>
<!-- 天干地支 -->
<view
v-for="(char, i) in runes"
:key="'rune-' + i"
class="rune-char"
:class="{ active: i === activeCharIndex }"
:style="getRuneStyle(i)"
>
{{ char }}
</view>
<!-- 八卦符号 - 旋转圈 -->
<view class="bagua-circle" :style="{ transform: 'rotate(' + baGuaRotation + 'deg)' }">
<view
v-for="(gua, i) in baGua"
:key="'gua-' + i"
class="bagua-char"
:style="getBaGuaStyle(i)"
>
{{ gua }}
</view>
</view>
<!-- 中心罗盘指针 -->
<view class="compass-pointer" :style="{ transform: 'rotate(' + pointerRotation + 'deg)' }">
<view class="pointer-bar"></view>
<view class="pointer-dot"></view>
</view>
</view>
</view>
<!-- 加载文本 -->
<view class="loading-text">
<view class="loading-title">择吉推演中</view>
<view class="loading-subtitle">结合八字 · 排盘择吉 · 避讳冲煞</view>
<view class="loading-tip">分析时间预计1-2分钟可点击返回按钮退出并在我的方案中查看结果</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
const runes = [
"甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸",
"子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"
];
const baGua = ["☰", "☱", "☲", "☳", "☴", "☵", "☶", "☷"];
const activeCharIndex = ref(-1);
const baGuaRotation = ref(0);
const pointerRotation = ref(0);
let runeInterval: number | null = null;
let baGuaInterval: number | null = null;
let pointerInterval: number | null = null;
const getRuneStyle = (index: number) => {
const angle = (index * 360) / runes.length;
const radius = 240;
const angleRad = (angle - 90) * (Math.PI / 180);
const x = 300 + radius * Math.cos(angleRad);
const y = 300 + radius * Math.sin(angleRad);
return {
left: x + "rpx",
top: y + "rpx",
transform: `translate(-50%, -50%) rotate(${angle}deg)`
};
};
const getBaGuaStyle = (index: number) => {
const angle = (index * 360) / 8;
const radius = 145;
const angleRad = (angle - 90) * (Math.PI / 180);
const x = 180 + radius * Math.cos(angleRad);
const y = 180 + radius * Math.sin(angleRad);
return {
left: x + "rpx",
top: y + "rpx",
transform: `translate(-50%, -50%) rotate(${angle}deg)`
};
};
onMounted(() => {
let count = 0;
runeInterval = setInterval(() => {
activeCharIndex.value = count % runes.length;
count++;
}, 100);
baGuaInterval = setInterval(() => {
baGuaRotation.value += 0.25;
}, 30);
let pointerCount = 0;
pointerInterval = setInterval(() => {
pointerCount += 1;
pointerRotation.value = pointerCount * 1.2;
}, 10);
});
onUnmounted(() => {
if (runeInterval) clearInterval(runeInterval);
if (baGuaInterval) clearInterval(baGuaInterval);
if (pointerInterval) clearInterval(pointerInterval);
});
</script>
<style scoped>
.aus-load {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #1a1a1a;
position: relative;
overflow: hidden;
font-family: SimSun, "Songti SC", "Songti TC", "Noto Serif SC", STSong, serif;
}
.aus-load-bg {
position: absolute;
inset: 0;
background: url("https://www.transparenttextures.com/patterns/rice-paper.png");
opacity: 0.08;
pointer-events: none;
}
.aus-compass-wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 96rpx;
}
.aus-compass {
position: relative;
width: 100%;
height: 60vh;
display: flex;
align-items: center;
justify-content: center;
min-width: 512rpx;
min-height: 512rpx;
}
.compass-glow {
position: absolute;
inset: 0;
border-radius: 50%;
background-color: #d4af37;
filter: blur(60rpx);
opacity: 0.2;
animation: pulse 4s ease-in-out infinite;
max-width: 80vw;
max-height: 80vw;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
@keyframes pulse {
0%, 100% {
opacity: 0.15;
transform: translate(-50%, -50%) scale(0.95);
}
50% {
opacity: 0.35;
transform: translate(-50%, -50%) scale(1.05);
}
}
.compass-svg-wrap {
position: relative;
width: 80vw;
height: 80vw;
max-width: 600rpx;
max-height: 600rpx;
}
.circle-outer {
position: absolute;
left: 10rpx;
top: 10rpx;
width: 580rpx;
height: 580rpx;
border-radius: 50%;
border: 1rpx dashed #d4af37;
opacity: 0.3;
}
.circle-main {
position: absolute;
left: 20rpx;
top: 20rpx;
width: 560rpx;
height: 560rpx;
border-radius: 50%;
border: 2rpx solid #d4af37;
opacity: 0.4;
}
.rune-char {
position: absolute;
font-size: 24rpx;
color: #888;
font-family: SimSun, serif;
transition: all 0.2s;
}
.rune-char.active {
color: #d4af37;
font-weight: bold;
text-shadow: 0 0 10rpx #d4af37;
}
.bagua-circle {
position: absolute;
left: 120rpx;
top: 120rpx;
width: 360rpx;
height: 360rpx;
border-radius: 50%;
border: 2rpx solid rgba(212, 175, 55, 0.15);
transition: transform 0.3s linear;
}
.bagua-char {
position: absolute;
font-size: 48rpx;
color: #d4af37;
opacity: 0.7;
font-family: SimSun, serif;
}
.compass-pointer {
position: absolute;
left: 300rpx;
top: 300rpx;
width: 0;
height: 0;
transition: transform 0.05s linear;
}
.pointer-bar {
position: absolute;
left: 50%;
top: -160rpx;
width: 4rpx;
height: 160rpx;
background-color: #8b2323;
transform: translateX(-50%);
}
.pointer-dot {
position: absolute;
left: 50%;
top: 50%;
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: #d4af37;
transform: translate(-50%, -50%);
}
.loading-text {
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
animation: fade 2.5s ease-in-out infinite;
}
@keyframes fade {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
.loading-title {
color: #d4af37;
letter-spacing: 0.4em;
font-size: 40rpx;
font-weight: bold;
text-shadow: 0 0 20rpx rgba(212, 175, 55, 0.5);
}
.loading-subtitle {
color: #888;
letter-spacing: 0.2em;
font-size: 24rpx;
}
.loading-tip {
margin-top: 16rpx;
padding: 0 48rpx;
color: rgba(226, 226, 226, 0.72);
letter-spacing: 0.06em;
font-size: 22rpx;
line-height: 1.6;
text-align: center;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,608 @@
<template>
<view class="calendar-screen">
<view class="calendar-texture"></view>
<!-- 固定头部 -->
<view class="calendar-fixed-header">
<!-- 状态栏占位 -->
<view class="status-bar-placeholder"></view>
<!-- Header -->
<view class="calendar-header">
<view class="calendar-back" @click="$emit('back')"></view>
<view class="calendar-title">
<text class="calendar-year">{{ year }} {{ lunarMonths[month] }}</text>
<text class="calendar-subtitle">Year of the Dragon</text>
</view>
<view class="calendar-header-spacer"></view>
</view>
</view>
<!-- 头部占位 -->
<view class="calendar-header-placeholder"></view>
<!-- Content -->
<scroll-view scroll-y class="calendar-scroll">
<!-- Controls -->
<view class="calendar-controls">
<button class="calendar-nav-button" @click="prevMonth"></button>
<text class="calendar-month">
{{ month + 1 }} <text class="calendar-month-unit"></text>
</text>
<button class="calendar-nav-button" @click="nextMonth"></button>
</view>
<!-- Weekdays -->
<view class="calendar-weekdays">
<text v-for="(d, i) in weekdays" :key="i" class="calendar-weekday"
:class="{ 'calendar-weekday-weekend': i === 0 || i === 6 }">
{{ d }}
</text>
</view>
<!-- Calendar Grid -->
<view class="calendar-grid">
<view v-for="cell in calendarCells" :key="cell.key" class="calendar-cell"
:class="cell.day ? ['calendar-cell-filled', cellSelected(cell.day) ? 'is-selected' : '', cellToday(cell.day) ? 'is-today' : ''] : 'calendar-cell-empty'"
@click="cell.day && selectDay(cell.day)">
<template v-if="cell.day">
<text class="calendar-cell-day"
:class="{ 'is-selected': cellSelected(cell.day), 'is-today': cellToday(cell.day) }">
{{ cell.day }}
</text>
<text class="calendar-cell-lunar" :class="{ 'is-selected': cellSelected(cell.day) }">
{{ lunarDay(cell.day) }}
</text>
<view v-if="cellToday(cell.day) && !cellSelected(cell.day)" class="calendar-today-dot"></view>
</template>
</view>
</view>
<!-- Detail Card -->
<view class="calendar-detail-wrapper">
<view class="calendar-detail">
<view class="calendar-detail-corner"></view>
<view class="calendar-detail-header">
<view class="calendar-detail-date">
<text class="calendar-detail-day">{{ selected.getDate() }}</text>
<view class="calendar-detail-meta">
<text class="calendar-detail-lunar">{{ lunarDay(selected.getDate()) }}</text>
<text class="calendar-detail-weekday">{{ weekdayText(selected.getDay()) }}</text>
</view>
</view>
<view class="calendar-detail-badge">今日运势</view>
</view>
<view class="calendar-detail-body">
<view class="calendar-detail-row">
<view class="calendar-detail-icon calendar-detail-icon-yi"></view>
<view class="calendar-detail-tags">
<text v-for="(item, i) in yiJi.yi" :key="i" class="calendar-detail-tag">{{ item }}</text>
</view>
</view>
<view class="calendar-detail-row">
<view class="calendar-detail-icon calendar-detail-icon-ji"></view>
<view class="calendar-detail-tags">
<text v-for="(item, i) in yiJi.ji" :key="i" class="calendar-detail-tag calendar-detail-tag-ji">{{ item
}}</text>
</view>
</view>
</view>
</view>
</view>
<!-- CTA -->
<view class="calendar-cta">
<button class="calendar-cta-card" @click="$emit('auspicious')">
<view class="calendar-cta-glow"></view>
<view class="calendar-cta-content">
<view>
<text class="calendar-cta-title">精准八字择吉</text>
<text class="calendar-cta-subtitle">结婚 · 开业 · 乔迁 · 动土</text>
</view>
<view class="calendar-cta-arrow"></view>
</view>
<view class="calendar-cta-tags">
<text class="calendar-cta-tag">个人定制</text>
<text class="calendar-cta-tag">避讳冲煞</text>
</view>
</button>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
const props = defineProps<{
onBack?: () => void;
onNavigateToAuspicious?: () => void;
}>();
const weekdays = ["日", "一", "二", "三", "四", "五", "六"];
const lunarMonths = ["正月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "冬月", "腊月"];
const lunarDays = [
"初一", "初二", "初三", "初四", "初五", "初六", "初七", "初八", "初九", "初十",
"十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十",
"廿一", "廿二", "廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九", "三十"
];
const yiJiData = [
{ yi: ["出行", "开市", "交易", "裁衣", "安床"], ji: ["动土", "安葬", "破土", "作灶", "入宅"] },
{ yi: ["嫁娶", "订盟", "纳采", "祭祀", "祈福"], ji: ["开仓", "出货", "盖屋", "造桥", "破土"] },
{ yi: ["解除", "扫舍", "整手足甲", "沐浴"], ji: ["安门", "分居", "修造", "动土"] },
{ yi: ["塑绘", "开光", "进人口", "纳畜"], ji: ["嫁娶", "安葬", "行丧", "伐木"] },
{ yi: ["祭祀", "会亲友", "纳财", "捕捉"], ji: ["嫁娶", "开市", "安床", "探病"] }
];
const current = ref(new Date());
const selected = ref(new Date());
const year = computed(() => current.value.getFullYear());
const month = computed(() => current.value.getMonth());
const daysInMonth = computed(() => new Date(year.value, month.value + 1, 0).getDate());
const firstDayOfMonth = computed(() => new Date(year.value, month.value, 1).getDay());
const calendarCells = computed(() => {
const cells: { key: string; day?: number }[] = [];
for (let i = 0; i < firstDayOfMonth.value; i++) {
cells.push({ key: `empty-${i}` });
}
for (let d = 1; d <= daysInMonth.value; d++) {
cells.push({ key: `d-${d}`, day: d });
}
return cells;
});
const lunarDay = (d: number) => lunarDays[(d - 1) % 30];
const getYiJi = (d: number) => yiJiData[d % yiJiData.length];
const yiJi = computed(() => getYiJi(selected.value.getDate()));
const weekdayText = (d: number) => ["周日", "周一", "周二", "周三", "周四", "周五", "周六"][d];
const selectDay = (d: number) => {
selected.value = new Date(year.value, month.value, d);
};
const prevMonth = () => {
current.value = new Date(year.value, month.value - 1, 1);
if (month.value === selected.value.getMonth()) {
selected.value = new Date(year.value, month.value, 1);
}
};
const nextMonth = () => {
current.value = new Date(year.value, month.value + 1, 1);
if (month.value === selected.value.getMonth()) {
selected.value = new Date(year.value, month.value + 1, 1);
}
};
const cellSelected = (d: number) => selected.value.getDate() === d && selected.value.getMonth() === month.value;
const cellToday = (d: number) => {
const today = new Date();
return today.getFullYear() === year.value && today.getMonth() === month.value && today.getDate() === d;
};
</script>
<style scoped>
.calendar-screen {
height: 100vh;
display: flex;
flex-direction: column;
background: #fdfbf7;
font-family: SimSun, "Songti SC", "Songti TC", "Noto Serif SC", STSong, serif;
position: relative;
overflow: hidden;
box-sizing: border-box;
}
/* 固定头部容器 */
.calendar-fixed-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: #fdfbf7;
}
/* 状态栏占位 */
.status-bar-placeholder {
height: var(--status-bar-height, 0);
width: 100%;
flex-shrink: 0;
}
/* 头部占位,防止内容被固定头部遮挡 */
.calendar-header-placeholder {
height: calc(var(--status-bar-height, 0) + 120rpx);
flex-shrink: 0;
}
.calendar-texture {
position: absolute;
inset: 0;
opacity: 0.1;
pointer-events: none;
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
}
.calendar-header {
position: relative;
z-index: 10;
padding: 28rpx 32rpx;
border-bottom: 1px solid #eaddcf;
background: rgba(255, 253, 249, 0.85);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.04);
}
.calendar-back {
padding: 16rpx;
margin-left: -12rpx;
color: #5a5a5a;
background: transparent;
border: none;
}
.calendar-title {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.calendar-year {
font-size: 18px;
font-weight: bold;
color: #2c2c2c;
letter-spacing: 0.3em;
}
.calendar-subtitle {
font-size: 10px;
color: #8a8a8a;
letter-spacing: 0.2em;
text-transform: uppercase;
}
.calendar-header-spacer {
width: 48rpx;
}
.calendar-scroll {
flex: 1;
position: relative;
z-index: 10;
padding-bottom: 80rpx;
height: 0;
}
.calendar-controls {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 48rpx 16rpx;
}
.calendar-nav-button {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
border: 1px solid #eaddcf;
color: #8b2323;
background: #fffdf9;
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.04);
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
line-height: 1;
padding: 0;
}
.calendar-month {
font-size: 24px;
font-weight: bold;
color: #2c2c2c;
}
.calendar-month-unit {
font-size: 12px;
color: #8a8a8a;
margin-left: 8rpx;
}
.calendar-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
text-align: center;
padding: 0 32rpx;
margin-bottom: 12rpx;
}
.calendar-weekday {
font-size: 12px;
font-weight: 600;
color: #5a5a5a;
letter-spacing: 0.1em;
}
.calendar-weekday-weekend {
color: #8b2323;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
padding: 0 32rpx;
gap: 12rpx 0;
margin-bottom: 32rpx;
}
.calendar-cell {
height: 112rpx;
}
.calendar-cell-filled {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
border-radius: 12rpx;
transition: all 0.2s ease;
}
.calendar-cell-filled.is-selected {
background: #8b2323;
box-shadow: 0 6rpx 12rpx rgba(139, 35, 35, 0.2);
}
.calendar-cell-filled.is-today:not(.is-selected) {
background: rgba(139, 35, 35, 0.05);
}
.calendar-cell-day {
font-size: 18px;
font-weight: bold;
color: #2c2c2c;
letter-spacing: 0.1em;
}
.calendar-cell-day.is-selected {
color: #fdfbf7;
}
.calendar-cell-day.is-today:not(.is-selected) {
color: #8b2323;
}
.calendar-cell-lunar {
font-size: 10px;
color: #8a8a8a;
transform: scale(0.95);
}
.calendar-cell-lunar.is-selected {
color: rgba(253, 251, 247, 0.8);
}
.calendar-today-dot {
position: absolute;
bottom: 6rpx;
width: 8rpx;
height: 8rpx;
border-radius: 50%;
background: #8b2323;
}
.calendar-detail-wrapper {
padding: 0 32rpx;
}
.calendar-detail {
background: #fffdf9;
border: 1px solid #eaddcf;
border-radius: 20rpx;
box-shadow: 0 12rpx 20rpx -8rpx rgba(0, 0, 0, 0.12);
padding: 32rpx 32rpx 28rpx;
position: relative;
overflow: hidden;
}
.calendar-detail-corner {
position: absolute;
top: 0;
right: 0;
width: 96rpx;
height: 96rpx;
background: rgba(139, 35, 35, 0.05);
border-bottom-left-radius: 160rpx;
}
.calendar-detail-header {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-bottom: 24rpx;
border-bottom: 1px solid #eaddcf;
padding-bottom: 16rpx;
}
.calendar-detail-date {
display: flex;
align-items: flex-end;
gap: 16rpx;
}
.calendar-detail-day {
font-size: 44px;
font-weight: bold;
color: #2c2c2c;
letter-spacing: 0.05em;
}
.calendar-detail-meta {
display: flex;
flex-direction: column;
gap: 6rpx;
}
.calendar-detail-lunar {
font-size: 16px;
font-weight: bold;
color: #2c2c2c;
}
.calendar-detail-weekday {
font-size: 12px;
color: #8a8a8a;
}
.calendar-detail-badge {
display: inline-block;
padding: 6rpx 12rpx;
background: #8b2323;
color: #fdfbf7;
font-size: 10px;
letter-spacing: 0.2em;
border-radius: 6rpx;
}
.calendar-detail-body {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.calendar-detail-row {
display: flex;
align-items: flex-start;
gap: 16rpx;
}
.calendar-detail-icon {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
border: 1px solid #2c2c2c;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
flex-shrink: 0;
}
.calendar-detail-icon-yi {
color: #2c2c2c;
}
.calendar-detail-icon-ji {
border-color: #8b2323;
color: #8b2323;
}
.calendar-detail-tags {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
padding-top: 6rpx;
}
.calendar-detail-tag {
font-size: 14px;
color: #5a5a5a;
padding: 6rpx 12rpx;
background: #f7f2ea;
border-radius: 8rpx;
}
.calendar-detail-tag-ji {
color: #8b2323;
background: rgba(139, 35, 35, 0.08);
}
.calendar-cta {
padding: 24rpx 32rpx 64rpx;
}
.calendar-cta-card {
width: 100%;
background: #2c2c2c;
border-radius: 20rpx;
padding: 32rpx;
position: relative;
overflow: hidden;
box-shadow: 0 12rpx 24rpx rgba(0, 0, 0, 0.18);
text-align: left;
}
.calendar-cta-glow {
position: absolute;
top: -20rpx;
right: -20rpx;
width: 180rpx;
height: 180rpx;
background: #d4af37;
opacity: 0.25;
filter: blur(40rpx);
border-radius: 50%;
}
.calendar-cta-content {
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.calendar-cta-title {
font-size: 18px;
font-weight: bold;
color: #d4af37;
letter-spacing: 0.1em;
display: block;
margin-bottom: 6rpx;
}
.calendar-cta-subtitle {
font-size: 12px;
color: rgba(242, 230, 216, 0.8);
display: block;
}
.calendar-cta-arrow {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: rgba(212, 175, 55, 0.15);
color: #d4af37;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.calendar-cta-tags {
position: relative;
z-index: 2;
display: flex;
gap: 12rpx;
margin-top: 16rpx;
}
.calendar-cta-tag {
font-size: 10px;
color: rgba(255, 255, 255, 0.8);
background: rgba(255, 255, 255, 0.08);
padding: 6rpx 12rpx;
border-radius: 8rpx;
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,823 @@
<template>
<view class="company-desktop-detail">
<view class="detail-header">
<view class="detail-header-back" @click="emit('back')">
<text class="back-icon"></text>
<text class="back-text">返回</text>
</view>
<text class="detail-header-title">公司测名详解</text>
<view class="detail-header-placeholder" />
</view>
<scroll-view scroll-y class="detail-content">
<view class="report-body">
<!-- header -->
<view
class="section"
:class="{ 'section--click': hasNodes(header.details?.nodes) }"
@click="hasNodes(header.details?.nodes) && openDetail(header.details?.title || '总分详解', header.details?.nodes)"
>
<text class="section-label">header · 总分</text>
<view class="score-row">
<text class="name">{{ toText(header.name) || '—' }}</text>
<text class="score">{{ num(header.score, 0) }}</text>
</view>
<view class="tag-row">
<text class="pill">{{ toText(header.tagLeft) }}</text>
<text class="pill">{{ toText(header.tagRight) }}</text>
</view>
<text class="body-text">{{ toText(header.quote) }}</text>
<text v-if="hasNodes(header.details?.nodes)" class="section-hint">点击本段查看{{ toText(header.details?.title) || '详解' }}</text>
</view>
<!-- characterAnalysis -->
<view
class="section"
:class="{ 'section--click': hasNodes(characterAnalysis.details?.nodes) }"
@click="
hasNodes(characterAnalysis.details?.nodes) &&
openDetail(characterAnalysis.details?.title || '字义数理详解', characterAnalysis.details?.nodes)
"
>
<text class="section-label">characterAnalysis · 字义数理</text>
<view class="char-grid">
<view v-for="(it, idx) in arr(characterAnalysis.characters)" :key="idx" class="char-box">
<text class="char-single">{{ toText(it?.char) }}</text>
<text class="char-meta">{{ toText(it?.element) }} · {{ num(it?.stroke, 0) }}</text>
<text class="body-text small">{{ toText(it?.meaning) }}</text>
</view>
</view>
<text class="body-text">{{ toText(characterAnalysis.analysis) }}</text>
<text v-if="hasNodes(characterAnalysis.details?.nodes)" class="section-hint">
点击本段查看{{ toText(characterAnalysis.details?.title) || '详细拆解' }}
</text>
</view>
<!-- businessPattern -->
<view class="section">
<text class="section-label">businessPattern · 商业格局</text>
<SixDimensionRadarDesktopEchart
:labels="arr(businessPattern.radar?.labels)"
:values="arr(businessPattern.radar?.values)"
:remark="summaryText"
/>
<view v-if="arr(businessPattern.summary).length" class="kv-inline">
<view v-for="(s, si) in arr(businessPattern.summary)" :key="si" class="summary-chip">
<text class="chip-k">{{ toText(s?.label) }}</text>
<text class="chip-v">{{ toText(s?.value) }}</text>
</view>
</view>
<view
v-if="hasNodes(businessPattern.details?.nodes)"
class="section-link"
:class="{ 'section--click': true }"
@click="openDetail(businessPattern.details?.title || '商业六维详解', businessPattern.details?.nodes)"
>
<text>{{ toText(businessPattern.details?.title) || '商业六维 · 解释与建议' }} </text>
</view>
</view>
<!-- gua -->
<view
class="section"
:class="{ 'section--click': hasNodes(gua.details?.nodes) }"
@click="hasNodes(gua.details?.nodes) && openDetail(gua.details?.title || '卦象解读', gua.details?.nodes)"
>
<text class="section-label">gua · 卦象</text>
<text v-if="toText(gua.bg)" class="gua-bg">卦字{{ toText(gua.bg) }}</text>
<text class="headline">{{ toText(gua.name) }} · {{ toText(gua.badge) }}</text>
<text class="body-text">{{ toText(gua.desc) }}</text>
<view v-if="arr(gua.tags).length" class="tag-list">
<text v-for="(t, ti) in arr(gua.tags)" :key="ti" class="pill pill--soft">{{ toText(t) }}</text>
</view>
<text class="body-text">{{ toText(gua.insight) }}</text>
<text v-if="hasNodes(gua.details?.nodes)" class="section-hint">点击本段查看卦象详解</text>
</view>
<!-- team -->
<view
class="section"
:class="{ 'section--click': hasNodes(team.details?.nodes) }"
@click="hasNodes(team.details?.nodes) && openDetail(team.details?.title || '团队契合', team.details?.nodes)"
>
<text class="section-label">team · 团队契合</text>
<view v-for="(m, mi) in arr(team.members)" :key="mi" class="member-block">
<text class="member-line">
{{ toText(m?.role) }} · {{ num(m?.score, 0) }} · {{ toText(m?.match) }}
</text>
<text v-if="toText(m?.desc)" class="body-text small">{{ toText(m?.desc) }}</text>
</view>
<text v-if="toText(team.note)" class="body-text note">{{ toText(team.note) }}</text>
<text v-if="hasNodes(team.details?.nodes)" class="section-hint">点击本段查看团队说明</text>
</view>
<!-- years -->
<view
class="section"
:class="{ 'section--click': hasNodes(years.details?.nodes) }"
@click="hasNodes(years.details?.nodes) && openDetail(years.details?.title || '流年运势', years.details?.nodes)"
>
<text class="section-label">years · 流年</text>
<view v-for="(y, yi) in arr(years.items)" :key="yi" class="year-row">
<text class="year-key">{{ toText(y?.year) }}</text>
<text class="year-luck">{{ toText(y?.luck) }}</text>
<text class="year-text">{{ toText(y?.text) }}</text>
</view>
<text v-if="hasNodes(years.details?.nodes)" class="section-hint">点击本段查看流年预警/建议</text>
</view>
<!-- wealthTrend -->
<view
class="section"
:class="{ 'section--click': hasNodes(wealthTrend.details?.nodes) }"
@click="hasNodes(wealthTrend.details?.nodes) && openDetail(wealthTrend.details?.title || '财运走势', wealthTrend.details?.nodes)"
>
<text class="section-label">wealthTrend · 财运走势</text>
<view class="bars">
<view v-for="(v, vi) in arr(wealthTrend.bars)" :key="vi" class="bar-wrap">
<view class="bar" :style="{ height: `${Math.max(8, Math.min(100, num(v, 0)))}%` }" />
</view>
</view>
<text class="body-text">{{ toText(wealthTrend.note) }}</text>
<text v-if="hasNodes(wealthTrend.details?.nodes)" class="section-hint">点击本段查看走势建议</text>
</view>
<!-- direction -->
<view
class="section"
:class="{ 'section--click': hasNodes(direction.details?.nodes) }"
@click="hasNodes(direction.details?.nodes) && openDetail(direction.details?.title || '吉凶方位', direction.details?.nodes)"
>
<text class="section-label">direction · 方位</text>
<text class="body-text">{{ toText(direction.note) }}</text>
<text v-if="direction.goodDot && (direction.goodDot.x != null || direction.goodDot.y != null)" class="body-text small">
吉位参考点相对坐标x {{ num(direction.goodDot?.x, 0) }}y {{ num(direction.goodDot?.y, 0) }}
</text>
<text v-if="hasNodes(direction.details?.nodes)" class="section-hint">点击本段查看方位说明</text>
</view>
<!-- layout -->
<view
class="section"
:class="{ 'section--click': hasNodes(layout.details?.nodes) }"
@click="hasNodes(layout.details?.nodes) && openDetail(layout.details?.title || '办公布局', layout.details?.nodes)"
>
<text class="section-label">layout · 办公布局</text>
<view v-for="(it, li) in arr(layout.items)" :key="li" class="layout-line">
<text class="layout-strong">{{ toText(it?.strong) }}</text>
<view class="layout-flow">
<text class="body-text small">{{ toText(it?.textBefore) }}</text>
<text v-for="(h, hi) in arr(it?.highlights)" :key="hi" class="highlight">{{ toText(h) }}</text>
<text class="body-text small">{{ toText(it?.textAfter) }}</text>
</view>
</view>
<text v-if="hasNodes(layout.details?.nodes)" class="section-hint">点击本段查看布局建议</text>
</view>
<!-- execution -->
<view
class="section section--wide"
:class="{ 'section--click': hasNodes(execution.details?.nodes) }"
@click="hasNodes(execution.details?.nodes) && openDetail(execution.details?.title || '执行建议', execution.details?.nodes)"
>
<text class="section-label">execution · 执行建议</text>
<text class="body-text">{{ toText(execution.text) }}</text>
<text v-if="hasNodes(execution.details?.nodes)" class="section-hint">点击本段查看执行条目</text>
</view>
<!-- liuyao -->
<view
v-if="hasLiuyao"
class="section"
:class="{ 'section--click': hasNodes(liuyao.details?.nodes) }"
@click="hasNodes(liuyao.details?.nodes) && openDetail('六爻', liuyao.details?.nodes)"
>
<text class="section-label">liuyao · 六爻</text>
<text class="headline">{{ toText(liuyao.hexagram_title) }}</text>
<text class="body-text">{{ toText(liuyao.changing_summary) }}</text>
<text class="body-text">{{ toText(liuyao.interpretation) }}</text>
<text v-for="(yl, yi) in arr(liuyao.yao_lines)" :key="yi" class="body-text small mono">· {{ toText(yl) }}</text>
<text v-if="hasNodes(liuyao.details?.nodes)" class="section-hint">点击本段查看六爻详解</text>
</view>
<!-- wuxing_bagua -->
<view
v-if="hasWuxingBagua"
class="section"
:class="{ 'section--click': hasNodes(wuxingBagua.details?.nodes) }"
@click="hasNodes(wuxingBagua.details?.nodes) && openDetail('五行八卦', wuxingBagua.details?.nodes)"
>
<text class="section-label">wuxing_bagua · 五行八卦</text>
<text class="body-text">{{ toText(wuxingBagua.wuxing_sketch) }}</text>
<text class="body-text">{{ toText(wuxingBagua.bagua_profile) }}</text>
<text class="body-text">{{ toText(wuxingBagua.mutual_sketch) }}</text>
<text class="body-text strong-end">{{ toText(wuxingBagua.summary) }}</text>
<text v-if="hasNodes(wuxingBagua.details?.nodes)" class="section-hint">点击本段查看详解</text>
</view>
<!-- zodiac_sign -->
<view
v-if="hasZodiac"
class="section"
:class="{ 'section--click': hasNodes(zodiacSign.details?.nodes) }"
@click="hasNodes(zodiacSign.details?.nodes) && openDetail('属相', zodiacSign.details?.nodes)"
>
<text class="section-label">zodiac_sign · 属相</text>
<text class="headline">
{{ toText(zodiacSign.animal_icon) }} {{ toText(zodiacSign.animal) }}{{ toText(zodiacSign.earthly_branch) }}
</text>
<text class="body-text">{{ toText(zodiacSign.trait_summary) }}</text>
<text class="body-text">{{ toText(zodiacSign.name_harmony) }}</text>
<text v-if="hasNodes(zodiacSign.details?.nodes)" class="section-hint">点击本段查看属相详解</text>
</view>
<!-- career_plan -->
<view
v-if="hasCareerPlan"
class="section"
:class="{ 'section--click': hasNodes(careerPlan.details?.nodes) }"
@click="hasNodes(careerPlan.details?.nodes) && openDetail('事业规划', careerPlan.details?.nodes)"
>
<text class="section-label">career_plan · 事业规划</text>
<text class="body-text">{{ toText(careerPlan.summary) }}</text>
<view v-for="(ms, msi) in arr(careerPlan.milestones)" :key="msi" class="milestone">
<text class="milestone-title">{{ toText(ms?.phase) }}{{ toText(ms?.period) ? ' · ' + toText(ms.period) : '' }}</text>
<text v-if="toText(ms?.focus)" class="body-text small">重点{{ toText(ms.focus) }}</text>
<text v-if="toText(ms?.advice)" class="body-text small">建议{{ toText(ms.advice) }}</text>
</view>
<text v-if="hasNodes(careerPlan.details?.nodes)" class="section-hint">点击本段查看规划详解</text>
</view>
<!-- lucky_numbers -->
<view
v-if="hasLuckyNumbers"
class="section"
:class="{ 'section--click': hasNodes(luckyNumbers.details?.nodes) }"
@click="hasNodes(luckyNumbers.details?.nodes) && openDetail('幸运数字', luckyNumbers.details?.nodes)"
>
<text class="section-label">lucky_numbers · 幸运数字</text>
<text class="headline">首推{{ toText(luckyNumbers.primary) }}</text>
<text class="body-text">{{ arr(luckyNumbers.numbers).join('、') }}</text>
<text class="body-text">{{ toText(luckyNumbers.meaning) }}</text>
<text v-if="hasNodes(luckyNumbers.details?.nodes)" class="section-hint">点击本段查看数字详解</text>
</view>
<!-- lucky_colors -->
<view
v-if="hasLuckyColors"
class="section"
:class="{ 'section--click': hasNodes(luckyColors.details?.nodes) }"
@click="hasNodes(luckyColors.details?.nodes) && openDetail('幸运色', luckyColors.details?.nodes)"
>
<text class="section-label">lucky_colors · 幸运色</text>
<text class="headline">主推{{ toText(luckyColors.primary) }}</text>
<view class="color-row">
<view v-for="(c, ci) in arr(luckyColors.colors)" :key="ci" class="color-item">
<view class="color-swatch" :style="{ backgroundColor: toText(c?.hex) || '#333' }" />
<text class="body-text small">{{ toText(c?.name) }}{{ toText(c?.note) ? ' · ' + toText(c.note) : '' }}</text>
</view>
</view>
<text class="body-text">{{ toText(luckyColors.meaning) }}</text>
<text v-if="hasNodes(luckyColors.details?.nodes)" class="section-hint">点击本段查看用色详解</text>
</view>
</view>
</scroll-view>
<view v-if="props.showBusinessFortune !== false" class="footer-action">
<button class="fortune-btn" type="button" @click="emit('businessFortune', detailData)">查看商业运势</button>
</view>
<view v-if="showModal" class="modal-mask" @click="closeModal">
<view class="detail-modal" @click.stop>
<view class="detail-modal-card">
<text class="modal-title">{{ modalTitle }}</text>
<text class="close" @click="closeModal">×</text>
</view>
<scroll-view scroll-y class="detail-modal-body">
<view v-for="(node, idx) in modalNodes" :key="idx" class="node">
<text v-if="node?.type === 'text'" class="line">{{ toText(node.text) }}</text>
<view v-else-if="node?.type === 'list'">
<text v-for="(item, j) in arr(node.items)" :key="j" class="line">- {{ toText(item) }}</text>
</view>
<view v-else-if="node?.type === 'kv'">
<text v-for="(item, j) in arr(node.items)" :key="j" class="line">{{ toText(item?.label) }}{{ toText(item?.value) }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import SixDimensionRadarDesktopEchart from "../SixDimensionRadarDesktopEchart.vue";
const props = defineProps<{
data: any;
showBusinessFortune?: boolean;
}>();
const emit = defineEmits<{
back: [];
businessFortune: [any];
}>();
const parseMaybeJson = (value: any) => {
if (value && typeof value === "object") return value;
if (typeof value !== "string") return {};
try {
return JSON.parse(value);
} catch {
return {};
}
};
const toText = (v: any) => String(v ?? "").trim();
const num = (v: any, fallback = 0) => {
const n = Number(v);
return Number.isFinite(n) ? n : fallback;
};
const arr = (v: any) => (Array.isArray(v) ? v : []);
const hasNodes = (nodes: any) => arr(nodes).length > 0;
const detailData = computed(() => parseMaybeJson(props.data));
const header = computed(() => detailData.value?.header || {});
const characterAnalysis = computed(() => detailData.value?.characterAnalysis || {});
const businessPattern = computed(() => detailData.value?.businessPattern || {});
const gua = computed(() => detailData.value?.gua || {});
const team = computed(() => detailData.value?.team || {});
const years = computed(() => detailData.value?.years || {});
const wealthTrend = computed(() => detailData.value?.wealthTrend || {});
const direction = computed(() => detailData.value?.direction || {});
const layout = computed(() => detailData.value?.layout || {});
const execution = computed(() => detailData.value?.execution || {});
const liuyao = computed(() => detailData.value?.liuyao || {});
const wuxingBagua = computed(() => detailData.value?.wuxing_bagua || {});
const zodiacSign = computed(() => detailData.value?.zodiac_sign || {});
const careerPlan = computed(() => detailData.value?.career_plan || {});
const luckyNumbers = computed(() => detailData.value?.lucky_numbers || {});
const luckyColors = computed(() => detailData.value?.lucky_colors || {});
const summaryText = computed(() =>
arr(businessPattern.value?.summary)
.map((x: any) => `${toText(x?.label)} ${toText(x?.value)}`)
.filter(Boolean)
.join(" · "),
);
const hasLiuyao = computed(
() =>
!!(
toText(liuyao.value?.hexagram_title) ||
toText(liuyao.value?.changing_summary) ||
toText(liuyao.value?.interpretation) ||
arr(liuyao.value?.yao_lines).length ||
hasNodes(liuyao.value?.details?.nodes)
),
);
const hasWuxingBagua = computed(
() =>
!!(
toText(wuxingBagua.value?.wuxing_sketch) ||
toText(wuxingBagua.value?.bagua_profile) ||
toText(wuxingBagua.value?.mutual_sketch) ||
toText(wuxingBagua.value?.summary) ||
hasNodes(wuxingBagua.value?.details?.nodes)
),
);
const hasZodiac = computed(
() =>
!!(
toText(zodiacSign.value?.animal) ||
toText(zodiacSign.value?.trait_summary) ||
toText(zodiacSign.value?.name_harmony) ||
hasNodes(zodiacSign.value?.details?.nodes)
),
);
const hasCareerPlan = computed(
() =>
!!(
toText(careerPlan.value?.summary) ||
arr(careerPlan.value?.milestones).length ||
hasNodes(careerPlan.value?.details?.nodes)
),
);
const hasLuckyNumbers = computed(
() =>
!!(toText(luckyNumbers.value?.primary) || arr(luckyNumbers.value?.numbers).length || toText(luckyNumbers.value?.meaning)),
);
const hasLuckyColors = computed(
() =>
!!(
toText(luckyColors.value?.primary) ||
arr(luckyColors.value?.colors).length ||
toText(luckyColors.value?.meaning)
),
);
const showModal = ref(false);
const modalTitle = ref("");
const modalNodes = ref<any[]>([]);
const openDetail = (title: string, nodes: any[]) => {
const list = arr(nodes);
if (!list.length) return;
modalTitle.value = toText(title) || "详情";
modalNodes.value = list;
showModal.value = true;
};
const closeModal = () => {
showModal.value = false;
};
</script>
<style scoped>
.company-desktop-detail {
min-height: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: linear-gradient(165deg, #070a12 0%, #12102a 42%, #0a1628 100%);
color: #e8e4dc;
}
.detail-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: rgba(15, 23, 42, 0.72);
border-bottom: 1px solid rgba(212, 175, 55, 0.2);
flex-shrink: 0;
}
.detail-header-back {
display: inline-flex;
align-items: center;
gap: 6px;
color: #d4af37;
}
.detail-header-title {
font-size: 16px;
font-weight: 700;
color: #f2e6d8;
}
.detail-header-placeholder {
width: 48px;
}
.detail-content {
flex: 1;
min-height: 0;
box-sizing: border-box;
}
.report-body {
max-width: 820px;
margin: 0 auto;
padding: 16px 18px 20px;
box-sizing: border-box;
}
.section {
margin-bottom: 16px;
padding: 14px 16px;
border-radius: 12px;
background: rgba(15, 23, 42, 0.52);
border: 1px solid rgba(212, 175, 55, 0.14);
border-left: 3px solid rgba(212, 175, 55, 0.45);
backdrop-filter: blur(8px);
}
.section--wide {
max-width: 100%;
}
.section--click {
cursor: pointer;
}
.section--click:active {
opacity: 0.92;
}
.section-label {
display: block;
font-size: 11px;
letter-spacing: 0.06em;
color: rgba(212, 175, 55, 0.75);
margin-bottom: 10px;
font-weight: 600;
}
/* 商业六维内嵌 ECharts 组件自带外边距,报告式布局里收紧 */
:deep(.sixdim-section) {
margin-bottom: 0 !important;
}
.score-row {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 8px;
}
.name {
font-size: 22px;
font-weight: 800;
color: #f2e6d8;
}
.score {
font-size: 32px;
font-weight: 800;
color: #d4af37;
}
.tag-row,
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
}
.pill {
font-size: 11px;
padding: 4px 10px;
border-radius: 999px;
background: rgba(212, 175, 55, 0.12);
border: 1px solid rgba(212, 175, 55, 0.28);
color: #ede6d8;
}
.pill--soft {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(148, 163, 184, 0.28);
color: rgba(232, 228, 220, 0.9);
}
.body-text {
display: block;
font-size: 13px;
line-height: 1.55;
color: rgba(232, 228, 220, 0.92);
margin-bottom: 8px;
}
.body-text.small {
font-size: 12px;
color: rgba(232, 228, 220, 0.82);
}
.body-text.note {
font-style: italic;
color: rgba(212, 175, 55, 0.65);
}
.mono {
font-family: ui-monospace, monospace;
}
.strong-end {
font-weight: 600;
color: #f0dba9;
}
.headline {
display: block;
font-size: 15px;
font-weight: 700;
color: #f4e5c4;
margin-bottom: 8px;
}
.gua-bg {
display: block;
font-size: 12px;
color: rgba(212, 175, 55, 0.8);
margin-bottom: 6px;
}
.section-hint {
display: block;
margin-top: 8px;
font-size: 11px;
color: rgba(148, 163, 184, 0.85);
}
.section-link {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid rgba(212, 175, 55, 0.12);
font-size: 12px;
color: #d4af37;
}
.char-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 10px;
margin-bottom: 12px;
}
.char-box {
padding: 10px;
border-radius: 10px;
background: rgba(2, 6, 23, 0.4);
border: 1px solid rgba(212, 175, 55, 0.12);
}
.char-single {
font-size: 22px;
font-weight: 800;
}
.char-meta {
display: block;
font-size: 11px;
color: rgba(203, 213, 225, 0.8);
margin: 4px 0 6px;
}
.kv-inline {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.summary-chip {
padding: 6px 10px;
border-radius: 8px;
background: rgba(2, 6, 23, 0.45);
border: 1px solid rgba(148, 163, 184, 0.22);
}
.chip-k {
font-size: 11px;
color: rgba(203, 213, 225, 0.85);
margin-right: 6px;
}
.chip-v {
font-size: 12px;
font-weight: 700;
color: #f0dba9;
}
.member-block {
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.member-block:last-of-type {
border-bottom: none;
}
.member-line {
font-size: 13px;
font-weight: 600;
color: #f2e6d8;
}
.year-row {
display: grid;
grid-template-columns: 56px 52px 1fr;
gap: 8px;
align-items: start;
margin-bottom: 8px;
font-size: 12px;
}
.year-key {
color: #d4af37;
font-weight: 700;
}
.year-luck {
color: #a7f3d0;
}
.layout-line {
margin-bottom: 10px;
}
.layout-strong {
display: block;
font-size: 13px;
font-weight: 700;
color: #f0dba9;
margin-bottom: 4px;
}
.highlight {
color: #fde68a;
margin: 0 2px;
font-size: 12px;
}
.layout-flow {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 2px 6px;
}
.bars {
height: 96px;
display: flex;
align-items: flex-end;
gap: 6px;
margin: 12px 0;
}
.bar-wrap {
flex: 1;
height: 100%;
border-radius: 6px;
background: rgba(255, 255, 255, 0.06);
overflow: hidden;
display: flex;
align-items: flex-end;
}
.bar {
width: 100%;
background: linear-gradient(180deg, rgba(255, 205, 96, 0.9), rgba(230, 129, 38, 0.78));
}
.milestone {
margin-top: 10px;
padding: 10px;
border-radius: 10px;
background: rgba(2, 6, 23, 0.35);
border: 1px solid rgba(212, 175, 55, 0.1);
}
.milestone-title {
display: block;
font-size: 13px;
font-weight: 700;
color: #f4e5c4;
margin-bottom: 6px;
}
.color-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin: 10px 0;
}
.color-item {
display: flex;
align-items: center;
gap: 8px;
}
.color-swatch {
width: 28px;
height: 28px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
flex-shrink: 0;
}
.footer-action {
padding: 10px 18px 14px;
flex-shrink: 0;
max-width: 820px;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
}
.fortune-btn {
width: 100%;
border-radius: 10px;
padding: 10px 12px;
background: linear-gradient(135deg, rgba(139, 35, 35, 0.95), rgba(90, 20, 20, 0.98));
color: #fdfbf7;
border: 1px solid rgba(212, 175, 55, 0.35);
}
.modal-mask {
position: fixed;
inset: 0;
z-index: 3200;
background: rgba(2, 6, 23, 0.72);
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
box-sizing: border-box;
}
.detail-modal {
width: min(640px, 100%);
max-height: min(82vh, 760px);
background: rgba(10, 12, 20, 0.96);
border: 1px solid rgba(212, 175, 55, 0.2);
border-radius: 12px;
overflow: hidden;
}
.detail-modal-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
border-bottom: 1px solid rgba(212, 175, 55, 0.16);
}
.modal-title {
color: #d4af37;
font-size: 14px;
font-weight: 700;
}
.close {
color: #d4af37;
font-size: 20px;
}
.detail-modal-body {
max-height: calc(min(82vh, 760px) - 48px);
padding: 12px;
box-sizing: border-box;
}
.node {
margin-bottom: 8px;
}
.line {
display: block;
font-size: 13px;
line-height: 1.5;
color: rgba(232, 228, 220, 0.9);
margin-bottom: 6px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,661 @@
<template>
<view class="login-screen">
<!-- 背景装饰 -->
<view class="login-bg">
<view class="login-bg-pattern"></view>
</view>
<!-- 主要内容 -->
<view class="login-content">
<!-- Logo/标题区域 -->
<view class="login-header">
<view class="login-logo">
<text class="login-logo-text">壹梵</text>
</view>
<text class="login-title">壹梵起名</text>
<text class="login-subtitle">传承千年文化 · 赋予美好寓意</text>
</view>
<!-- 登录/注册表单 -->
<view class="login-form-wrapper">
<!-- 标签切换 -->
<view class="login-tabs">
<view class="login-tab" :class="{ active: currentTab === 'login' }" @click="currentTab = 'login'">
<text class="login-tab-text">登录</text>
</view>
<view class="login-tab" :class="{ active: currentTab === 'register' }" @click="currentTab = 'register'">
<text class="login-tab-text">注册</text>
</view>
</view>
<!-- 登录表单 -->
<view v-if="currentTab === 'login'" class="login-form">
<view class="login-form-item">
<text class="login-form-label">手机号</text>
<input v-model="loginForm.mobile" type="tel" class="login-form-input" placeholder="请输入手机号" maxlength="11" />
</view>
<view class="login-form-item">
<text class="login-form-label">密码</text>
<input v-model="loginForm.password" :type="showPassword ? 'text' : 'password'" class="login-form-input"
placeholder="请输入密码" />
<view class="login-form-eye" @click="showPassword = !showPassword">
<text>{{ showPassword ? '👁️' : '👁️‍🗨️' }}</text>
</view>
</view>
<view class="login-forgot" @click="currentTab = 'forgot'">
<text class="login-forgot-text">忘记密码</text>
</view>
<button class="login-btn login-btn-primary" :disabled="!canLogin || loading" @click="handleLogin">
<text class="login-btn-text">{{ loading ? '登录中...' : '登录' }}</text>
</button>
</view>
<!-- 注册表单 -->
<view v-if="currentTab === 'register'" class="login-form">
<view class="login-form-item">
<text class="login-form-label">手机号</text>
<input v-model="registerForm.mobile" type="tel" class="login-form-input" placeholder="请输入手机号"
maxlength="11" />
</view>
<view class="login-form-item">
<text class="login-form-label">验证码</text>
<view class="login-form-code">
<input v-model="registerForm.code" type="tel" class="login-form-input" placeholder="请输入验证码"
maxlength="6" />
<button class="login-code-btn" :disabled="!canSendCode || countdown > 0"
@click="handleSendCode('register')">
<text class="login-code-text">
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
</text>
</button>
</view>
</view>
<view class="login-form-item">
<text class="login-form-label">密码</text>
<input v-model="registerForm.password" :type="showPassword ? 'text' : 'password'" class="login-form-input"
placeholder="请输入密码6-20位" />
<view class="login-form-eye" @click="showPassword = !showPassword">
<text>{{ showPassword ? '👁️' : '👁️‍🗨️' }}</text>
</view>
</view>
<view class="login-form-item">
<text class="login-form-label">确认密码</text>
<input v-model="registerForm.repassword" :type="showPassword ? 'text' : 'password'" class="login-form-input"
placeholder="请再次输入密码" />
</view>
<button class="login-btn login-btn-primary" :disabled="loading" @click="handleRegister">
<text class="login-btn-text">{{ loading ? '注册中...' : '注册' }}</text>
</button>
</view>
<!-- 忘记密码表单 -->
<view v-if="currentTab === 'forgot'" class="login-form">
<view class="login-form-item">
<text class="login-form-label">手机号</text>
<input v-model="forgotForm.mobile" type="tel" class="login-form-input" placeholder="请输入手机号"
maxlength="11" />
</view>
<view class="login-form-item">
<text class="login-form-label">验证码</text>
<view class="login-form-code">
<input v-model="forgotForm.code" type="tel" class="login-form-input" placeholder="请输入验证码" maxlength="6" />
<button class="login-code-btn" :disabled="!canSendCodeForgot || countdown > 0"
@click="handleSendCode('forgot')">
<text class="login-code-text">
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
</text>
</button>
</view>
</view>
<view class="login-form-item">
<text class="login-form-label">新密码</text>
<input v-model="forgotForm.password" :type="showPassword ? 'text' : 'password'" class="login-form-input"
placeholder="请输入新密码6-20位" />
<view class="login-form-eye" @click="showPassword = !showPassword">
<text>{{ showPassword ? '👁️' : '👁️‍🗨️' }}</text>
</view>
</view>
<view class="login-form-item">
<text class="login-form-label">确认密码</text>
<input v-model="forgotForm.repassword" :type="showPassword ? 'text' : 'password'" class="login-form-input"
placeholder="请再次输入新密码" />
</view>
<button class="login-btn login-btn-primary" :disabled="!canResetPassword || loading"
@click="handleResetPassword">
<text class="login-btn-text">{{ loading ? '重置中...' : '重置密码' }}</text>
</button>
<view class="login-back" @click="currentTab = 'login'">
<text class="login-back-text">返回登录</text>
</view>
</view>
</view>
<!-- 协议 -->
<view class="login-agreement">
<text class="login-agreement-text">
登录即表示同意
<text class="login-agreement-link" @click="handleNavigateToAgreement">用户协议</text>
<text class="login-agreement-link" @click="handleNavigateToPrivacy">隐私政策</text>
</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue';
import { useRouter } from 'vue-router';
import { userApi } from '@/api';
import type { MobileLoginResponse, MobileRegisterResponse, ForgotPasswordResponse } from '@/api/types';
import { showToast } from '@/utils/uni-compat';
const router = useRouter();
const emit = defineEmits<{
success: [data: MobileLoginResponse | MobileRegisterResponse | ForgotPasswordResponse];
}>();
const loading = ref(false);
const currentTab = ref<'login' | 'register' | 'forgot'>('login');
const showPassword = ref(false);
const countdown = ref(0);
let countdownTimer: number | null = null;
// 登录表单
const loginForm = reactive({
mobile: '',
password: '',
});
// 注册表单
const registerForm = reactive({
mobile: '',
code: '',
password: '',
repassword: '',
});
// 忘记密码表单
const forgotForm = reactive({
mobile: '',
code: '',
password: '',
repassword: '',
});
// 验证手机号
const isValidMobile = (mobile: string) => {
return /^1[3-9]\d{9}$/.test(mobile);
};
// 验证密码
const isValidPassword = (password: string) => {
return password.length >= 6 && password.length <= 20;
};
// 是否可以登录(仅根据 loading 控制按钮,去掉其他前置校验)
const canLogin = computed(() => {
return true;
});
// 是否可以注册
const canRegister = computed(() => {
return (
isValidMobile(registerForm.mobile) &&
registerForm.code.length === 6 &&
isValidPassword(registerForm.password) &&
registerForm.password === registerForm.repassword
);
});
// 是否可以发送验证码(注册)
const canSendCode = computed(() => {
return isValidMobile(registerForm.mobile);
});
// 是否可以发送验证码(忘记密码)
const canSendCodeForgot = computed(() => {
return isValidMobile(forgotForm.mobile);
});
// 是否可以重置密码
const canResetPassword = computed(() => {
return (
isValidMobile(forgotForm.mobile) &&
forgotForm.code.length === 6 &&
isValidPassword(forgotForm.password) &&
forgotForm.password === forgotForm.repassword
);
});
// 开始倒计时
const startCountdown = () => {
countdown.value = 60;
if (countdownTimer) {
clearInterval(countdownTimer);
}
countdownTimer = window.setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
}
}, 1000);
};
// 发送验证码
const handleSendCode = async (type: 'register' | 'forgot') => {
const mobile = type === 'register' ? registerForm.mobile : forgotForm.mobile;
if (!isValidMobile(mobile)) {
showToast({ title: '请输入正确的手机号', icon: 'none' });
return;
}
try {
await userApi.sendSmsCode(mobile);
showToast({ title: '验证码已发送', icon: 'success' });
startCountdown();
} catch (error: any) {
showToast({ title: error.msg || '发送失败,请重试', icon: 'none' });
}
};
// 登录
const handleLogin = async () => {
if (!canLogin.value || loading.value) return;
try {
loading.value = true;
const result = await userApi.mobileLogin({
mobile: loginForm.mobile,
password: loginForm.password,
});
showToast({ title: '登录成功', icon: 'success' });
emit('success', result);
} catch (error: any) {
showToast({ title: error.msg || '登录失败,请重试', icon: 'none' });
} finally {
loading.value = false;
}
};
// 注册
const handleRegister = async () => {
if (!canRegister.value || loading.value) return;
if (registerForm.password !== registerForm.repassword) {
showToast({ title: '两次密码输入不一致', icon: 'none' });
return;
}
try {
loading.value = true;
const result = await userApi.mobileRegister({
mobile: registerForm.mobile,
password: registerForm.password,
repassword: registerForm.repassword,
verification_code: registerForm.code,
});
showToast({ title: '注册成功', icon: 'success' });
emit('success', result);
} catch (error: any) {
showToast({ title: error.msg || '注册失败,请重试', icon: 'none' });
} finally {
loading.value = false;
}
};
// 重置密码
const handleResetPassword = async () => {
if (!canResetPassword.value || loading.value) return;
if (forgotForm.password !== forgotForm.repassword) {
showToast({ title: '两次密码输入不一致', icon: 'none' });
return;
}
try {
loading.value = true;
const result = await userApi.forgotPassword({
mobile: forgotForm.mobile,
password: forgotForm.password,
repassword: forgotForm.repassword,
verification_code: forgotForm.code,
});
showToast({ title: '密码重置成功', icon: 'success' });
emit('success', result);
} catch (error: any) {
showToast({ title: error.msg || '重置失败,请重试', icon: 'none' });
} finally {
loading.value = false;
}
};
// 导航到用户协议
const handleNavigateToAgreement = () => {
router.push('/user-agreement');
};
// 导航到隐私政策
const handleNavigateToPrivacy = () => {
router.push('/privacy-policy');
};
</script>
<style scoped>
.login-screen {
position: relative;
width: 100%;
min-height: 100vh;
background: #fdfbf7 url("https://www.transparenttextures.com/patterns/rice-paper.png");
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.login-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 0;
overflow: hidden;
}
.login-bg-pattern {
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle at 30% 30%, rgba(139, 35, 35, 0.03) 0%, transparent 50%);
animation: rotate 60s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.login-content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 30px;
width: 100%;
max-width: 500px;
}
.login-header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40px;
}
.login-logo {
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(135deg, #8b2323 0%, #9c2a2a 100%);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
box-shadow: 0 4px 12px rgba(139, 35, 35, 0.3);
}
.login-logo-text {
font-size: 32px;
font-weight: bold;
color: #fdfbf7;
font-family: SimSun, "Songti SC", serif;
}
.login-title {
font-size: 28px;
font-weight: 500;
color: #2c2c2c;
margin-bottom: 8px;
font-family: SimSun, "Songti SC", serif;
letter-spacing: 0.2em;
}
.login-subtitle {
font-size: 14px;
color: #8a8a8a;
font-family: SimSun, "Songti SC", serif;
}
.login-form-wrapper {
width: 100%;
background: rgba(255, 255, 255, 0.8);
border-radius: 16px;
padding: 30px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #eaddcf;
}
.login-tabs {
display: flex;
margin-bottom: 30px;
border-bottom: 2px solid #eaddcf;
}
.login-tab {
flex: 1;
padding: 12px 0;
text-align: center;
cursor: pointer;
position: relative;
transition: all 0.3s;
}
.login-tab.active {
color: #8b2323;
}
.login-tab.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background: #8b2323;
}
.login-tab-text {
font-size: 16px;
font-weight: 500;
font-family: SimSun, "Songti SC", serif;
}
.login-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.login-form-item {
display: flex;
flex-direction: column;
gap: 8px;
position: relative;
}
.login-form-label {
font-size: 14px;
color: #2c2c2c;
font-weight: 500;
font-family: SimSun, "Songti SC", serif;
}
.login-form-input {
width: 100%;
height: 44px;
padding: 0 16px;
background: #fff;
border: 1px solid #dcd3c9;
border-radius: 8px;
font-size: 14px;
color: #2c2c2c;
font-family: SimSun, "Songti SC", serif;
box-sizing: border-box;
}
.login-form-input:focus {
border-color: #8b2323;
outline: none;
}
.login-form-eye {
position: absolute;
right: 16px;
bottom: 12px;
cursor: pointer;
font-size: 18px;
}
.login-form-code {
display: flex;
gap: 10px;
align-items: center;
}
.login-form-code .login-form-input {
flex: 1;
}
.login-code-btn {
flex-shrink: 0;
height: 44px;
padding: 0 16px;
background: #8b2323;
color: #fff;
border: none;
border-radius: 8px;
font-size: 13px;
cursor: pointer;
white-space: nowrap;
font-family: SimSun, "Songti SC", serif;
}
.login-code-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.login-code-text {
font-size: 13px;
}
.login-forgot {
text-align: right;
margin-top: -10px;
cursor: pointer;
}
.login-forgot-text {
font-size: 13px;
color: #8b2323;
font-family: SimSun, "Songti SC", serif;
}
.login-back {
text-align: center;
margin-top: 10px;
cursor: pointer;
}
.login-back-text {
font-size: 14px;
color: #8b2323;
font-family: SimSun, "Songti SC", serif;
}
.login-btn {
width: 100%;
height: 48px;
border-radius: 24px;
border: none;
font-size: 16px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
font-family: SimSun, "Songti SC", serif;
cursor: pointer;
margin-top: 10px;
}
.login-btn-primary {
background: linear-gradient(135deg, #8b2323 0%, #9c2a2a 100%);
color: #fff;
}
.login-btn-primary:active {
background: linear-gradient(135deg, #701c1c 0%, #8b2323 100%);
transform: scale(0.98);
}
.login-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.login-btn-text {
font-size: 16px;
}
.login-agreement {
margin-top: 30px;
text-align: center;
}
.login-agreement-text {
font-size: 12px;
color: #8a8a8a;
font-family: SimSun, "Songti SC", serif;
}
.login-agreement-link {
color: #8b2323;
cursor: pointer;
text-decoration: underline;
}
.login-agreement-link:hover {
opacity: 0.8;
}
</style>

View File

@@ -0,0 +1,916 @@
<template>
<view class="plans-screen">
<view class="plans-bg"></view>
<view class="status-bar-placeholder"></view>
<!-- 固定导航栏 -->
<view class="plans-header">
<view class="plans-back-btn" @click="handleBack">
<text class="plans-back-icon"></text>
</view>
<text class="plans-title">我的方案</text>
<view class="plans-refresh-btn" @click="handleRefresh">
<text class="plans-refresh-icon" :class="{ rotating: loading }"></text>
</view>
</view>
<!-- 固定Tab切换 -->
<view class="plans-tabs">
<view v-for="tab in tabs" :key="tab.value"
:class="['plans-tab', { 'plans-tab-active': currentTab === tab.value }]"
@click="switchTab(tab.value as 'naming' | 'affinity' | 'zeji')">
<text class="plans-tab-text">{{ tab.label }}</text>
</view>
</view>
<!-- 滚动列表内容 -->
<scroll-view class="plans-scroll" scroll-y="true">
<view class="plans-content">
<!-- 初始加载状态 -->
<view v-if="loading && list.length === 0" class="initial-loading">
<text class="loading-text">加载中...</text>
</view>
<!-- 列表 -->
<template v-else-if="list.length > 0">
<!-- 名字方案列表 -->
<template v-if="currentTab === 'naming'">
<view v-for="item in list" :key="item.id" class="list-item" @click="viewNamingDetail(item)">
<view :class="['item-icon', item.category === 'company' ? 'icon-gold' : 'icon-red']">
<text class="icon-text">{{ item.category === 'company' ? '企' : '名' }}</text>
</view>
<view class="item-content">
<view class="item-header">
<text class="item-title">{{ item.title || '未命名方案' }}</text>
<text class="item-date">{{ item.relative_time }}</text>
</view>
<text class="item-subtitle">{{ item.subtitle }}</text>
<view class="item-tags">
<text :class="['item-tag', item.category === 'company' ? 'tag-gold' : 'tag-red']">
{{ item.category === 'company' ? '公司' : '个人' }}
</text>
<text class="item-status">
<text class="status-icon">{{ item.has_solutions ? '✓' : '⏱' }}</text>
{{ item.has_solutions ? '已生成方案' : '生成中' }}
</text>
</view>
</view>
<view class="item-arrow">
<text class="arrow-icon"></text>
</view>
</view>
</template>
<!-- 缘分合盘列表 -->
<template v-else-if="currentTab === 'affinity'">
<view v-for="item in list" :key="item.id" class="list-item" @click="viewAffinityDetail(item)">
<view class="item-icon icon-pink">
<text class="icon-text"></text>
</view>
<view class="item-content">
<view class="item-header">
<text class="item-title">{{ item.person1_name }} & {{ item.person2_name }}</text>
<text class="item-date">{{ formatDate(item.created_time) }}</text>
</view>
<text class="item-subtitle">
{{ getRelationshipLabel(item.relationship) }}
<template v-if="item.score">· 缘分指数 {{ item.score }}</template>
</text>
<view class="item-tags">
<text v-if="item.score_badge" class="item-tag tag-pink">{{ item.score_badge }}</text>
<text class="item-status">
<text class="status-icon">{{ getTaskStatusIcon(item.task_status) }}</text>
{{ getTaskStatusText(item.task_status) }}
</text>
</view>
</view>
<view class="item-arrow">
<text class="arrow-icon"></text>
</view>
</view>
</template>
<!-- 八字择吉列表 -->
<template v-else-if="currentTab === 'zeji'">
<view v-for="item in list" :key="item.id" class="list-item" @click="viewZejiDetail(item)">
<view class="item-icon icon-purple">
<text class="icon-text"></text>
</view>
<view class="item-content">
<view class="item-header">
<text class="item-title">{{ item.event_name || '择吉事项' }}</text>
<text class="item-date">{{ formatDate(item.created_time) }}</text>
</view>
<text class="item-subtitle">
{{ item.event_type_label }}
<template v-if="item.selected_date">· {{ item.selected_date }}</template>
</text>
<view class="item-tags">
<text v-if="item.score_badge" class="item-tag tag-purple">{{ item.score_badge }}</text>
<text class="item-status">
<text class="status-icon">{{ getTaskStatusIcon(item.task_status) }}</text>
{{ getTaskStatusText(item.task_status) }}
</text>
</view>
</view>
<view class="item-arrow">
<text class="arrow-icon"></text>
</view>
</view>
</template>
<!-- 加载更多按钮 -->
<view class="load-more-wrapper">
<view v-if="loading" class="loading-more">
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="hasMore" class="load-more-btn" @click="loadNextPage">
<text class="load-more-text">点击加载更多</text>
</view>
<view v-else class="no-more">
<text class="no-more-text"> 已加载全部 </text>
</view>
</view>
</template>
<!-- 空状态 -->
<template v-else>
<view class="empty-state">
<text class="empty-icon">📋</text>
<text class="empty-text">{{ getEmptyText() }}</text>
<view class="empty-btn" @click="handleEmptyAction">
<text class="empty-btn-text">{{ getEmptyButtonText() }}</text>
</view>
</view>
</template>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from "vue";
import { userApi, namingApi, affinityApi, baziZejiApi } from "@/api";
import type { MyReportItem } from "@/api/types";
import type { AffinityListItem } from "@/api";
import type { BaziZejiListItem } from "@/api";
declare const uni: any;
const props = defineProps<{
/** 从详情页返回时父级传入,用于选中「名字 / 缘分合盘 / 八字择吉」子 tab */
focusListTab?: 'naming' | 'affinity' | 'zeji' | null;
}>();
const emit = defineEmits<{
back: [];
navigate: [screen: string];
showDetail: [data: any, category?: string, serviceType?: string];
showNamingSolutionsList: [payload: { reportId: number; solutions: any[]; category?: string; serviceType?: string }];
showAffinityResult: [data: any, fromMyPlans?: boolean];
showAuspiciousResult: [data: any, fromMyPlans?: boolean];
focusListTabConsumed: [];
}>();
// Tab配置
const tabs = [
{ label: '名字', value: 'naming' },
{ label: '缘分合盘', value: 'affinity' },
{ label: '八字择吉', value: 'zeji' },
];
// 响应式数据
const currentTab = ref<'naming' | 'affinity' | 'zeji'>('naming');
const loading = ref(false);
const list = ref<any[]>([]);
const pageNo = ref(1);
const pageSize = 10;
const total = ref(0);
const hasMore = ref(true);
// 重置分页状态(须在 switchTab / watch 之前声明,避免暂时性死区)
const resetPagination = () => {
pageNo.value = 1;
list.value = [];
hasMore.value = true;
total.value = 0;
};
// 加载列表数据
const loadList = async (refresh = false) => {
if (loading.value) return;
if (!refresh && !hasMore.value) return;
loading.value = true;
try {
let res: any;
if (currentTab.value === 'naming') {
res = await userApi.getMyReports({
page_no: pageNo.value,
page_size: pageSize,
});
const items = res?.items || (Array.isArray(res) ? res : []);
if (refresh) {
list.value = items;
} else {
list.value = [...list.value, ...items];
}
total.value = res?.total || items.length;
hasMore.value = list.value.length < total.value;
} else if (currentTab.value === 'affinity') {
res = await affinityApi.getAffinityList({
page_no: pageNo.value,
page_size: pageSize,
});
if (res && res.data) {
const items = Array.isArray(res.data.data) ? res.data.data : [];
if (refresh) {
list.value = items;
} else {
list.value = [...list.value, ...items];
}
total.value = res.data.total || 0;
hasMore.value = res.data.has_next || false;
}
} else if (currentTab.value === 'zeji') {
res = await baziZejiApi.getBaziZejiList({
page_no: pageNo.value,
page_size: pageSize,
});
if (res && res.data) {
const items = Array.isArray(res.data.data) ? res.data.data : [];
if (refresh) {
list.value = items;
} else {
list.value = [...list.value, ...items];
}
total.value = res.data.total || 0;
hasMore.value = res.data.has_next || false;
}
}
pageNo.value++;
} catch (error) {
console.error('加载失败:', error);
uni.showToast({ title: '加载失败,请重试', icon: 'none' });
} finally {
loading.value = false;
}
};
// 点击加载下一页
const loadNextPage = () => {
if (!loading.value && hasMore.value) {
loadList();
}
};
// 刷新
const handleRefresh = async () => {
if (loading.value) return;
resetPagination();
await loadList(true);
uni.showToast({ title: '刷新成功', icon: 'success', duration: 1000 });
};
// 切换Tab依赖 resetPagination、loadList须放在二者之后
const switchTab = (tab: 'naming' | 'affinity' | 'zeji') => {
if (currentTab.value === tab) return;
currentTab.value = tab;
resetPagination();
loadList(true);
};
watch(
() => props.focusListTab,
(v) => {
if (!v) return;
if (v === currentTab.value) {
emit('focusListTabConsumed');
return;
}
switchTab(v);
emit('focusListTabConsumed');
},
{ immediate: true }
);
// 查看名字方案详情
const viewNamingDetail = async (item: MyReportItem) => {
if (!item.has_solutions) {
uni.showToast({ title: "方案生成中,请稍后查看", icon: "none" });
return;
}
const parseMaybeJson = (value: any) => {
if (value == null) return value;
if (typeof value === "object") return value;
if (typeof value !== "string") return value;
const raw = value.trim();
if (!raw) return value;
try {
return JSON.parse(raw);
} catch {
return value;
}
};
try {
uni.showLoading({ title: "加载中..." });
const solutionsResult = await namingApi.getSolutionsByReportId(item.id);
const solutions = solutionsResult?.solutions || solutionsResult?.items || solutionsResult;
// 新规则:接口返回 solutions 数组且有数据 -> 跳转到新的方案列表页
if (Array.isArray(solutions) && solutions.length > 0) {
uni.hideLoading();
emit("showNamingSolutionsList", {
reportId: item.id,
solutions,
category: item.category,
serviceType: String(item.service_type || ''),
});
return;
}
// 否则:回退到旧版详情页(接口可能直接返回详情对象)
const parsedLegacy = parseMaybeJson(solutionsResult);
uni.hideLoading();
if (parsedLegacy && typeof parsedLegacy === "object") {
emit("showDetail", parsedLegacy as any, item.category, String(item.service_type || ''));
return;
}
uni.showToast({ title: "暂无方案数据", icon: "none" });
return;
} catch (e: any) {
uni.hideLoading();
console.error('Error in viewNamingDetail:', e);
uni.showToast({ title: e.msg || e.message || "获取详情失败", icon: "none" });
}
};
// 查看缘分合盘详情
const viewAffinityDetail = async (item: AffinityListItem) => {
if (item.task_status !== 5) {
uni.showToast({ title: "生成中,请稍后查看", icon: "none" });
return;
}
uni.showLoading({ title: "加载中..." });
const detailData = await affinityApi.getAffinityDetail(item.id);
uni.hideLoading();
if (!detailData) {
uni.showToast({ title: "获取详情失败", icon: "none" });
return;
}
const transformedData = {
relationship: detailData.relationship,
relationshipLabel: getRelationshipLabel(detailData.relationship),
person1: {
name: detailData.person1_name,
gender: detailData.person1_gender,
birthDate: detailData.person1_birth_date,
},
person2: {
name: detailData.person2_name,
gender: detailData.person2_gender,
birthDate: detailData.person2_birth_date,
},
score: detailData.score,
scoreBadge: detailData.score_badge,
sixDimension: detailData.six_dimension ? JSON.parse(detailData.six_dimension) : null,
radarDesc: detailData.radar_desc,
analysisCards: detailData.analysis_cards ? JSON.parse(detailData.analysis_cards).map((card: any, index: number) => ({
id: `card-${index}`,
icon: ['💗', '💬', '🛡️', '⚡', '🎯', '🏠'][index] || '✨',
iconBg: ['rgba(236, 72, 153, 0.2)', 'rgba(59, 130, 246, 0.2)', 'rgba(34, 197, 94, 0.2)', 'rgba(234, 179, 8, 0.2)', 'rgba(168, 85, 247, 0.2)', 'rgba(249, 115, 22, 0.2)'][index] || 'rgba(156, 163, 175, 0.2)',
title: card.title,
score: String(card.score),
summary: card.content.substring(0, 50) + '...',
content: card.content,
})) : [],
unlocked: detailData.unlocked_content ? JSON.parse(detailData.unlocked_content) : null,
isUnlocked: detailData.is_unlocked === 1,
unlockPrice: detailData.unlock_price,
unlockStats: {
unlockCount: 12392,
accuracy: '98%',
},
};
emit('showAffinityResult', transformedData, true);
};
// 查看八字择吉详情
const viewZejiDetail = async (item: BaziZejiListItem) => {
uni.showLoading({ title: "加载中..." });
const detailData = await baziZejiApi.getBaziZejiDetail(item.id);
uni.hideLoading();
if (!detailData) {
uni.showToast({ title: "获取详情失败", icon: "none" });
return;
}
emit('showAuspiciousResult', detailData, true);
};
// 工具函数:相对时间(刚刚 / N分钟前 / N小时前 / …)
const formatDate = (dateStr: string) => {
if (!dateStr) return '';
const date = new Date(dateStr);
if (Number.isNaN(date.getTime())) return String(dateStr).trim();
const now = Date.now();
const diff = now - date.getTime();
if (diff < 0) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
const h = String(date.getHours()).padStart(2, '0');
const min = String(date.getMinutes()).padStart(2, '0');
return `${y}-${m}-${d} ${h}:${min}`;
}
const minutes = Math.floor(diff / (1000 * 60));
if (minutes < 1) return '刚刚';
if (minutes < 60) return `${minutes}分钟前`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}小时前`;
const days = Math.floor(hours / 24);
if (days === 1) return '昨天';
if (days < 7) return `${days}天前`;
if (days < 30) return `${Math.floor(days / 7)}周前`;
if (days < 365) return `${Math.floor(days / 30)}月前`;
return `${Math.floor(days / 365)}年前`;
};
const getRelationshipLabel = (relationship: string) => {
const labels: Record<string, string> = {
'couple': '恋人',
'married': '夫妻',
'crush': '暗恋',
'partner': '合作伙伴',
'friend': '朋友',
'family': '家人',
};
return labels[relationship] || relationship;
};
const getTaskStatusIcon = (status: number) => {
if (status === 5) return '✓';
if (status === 0) return '⏱';
return '⏱';
};
const getTaskStatusText = (status: number) => {
if (status === 5) return '已生成';
if (status === 0) return '生成中';
return '生成中';
};
const getEmptyText = () => {
if (currentTab.value === 'naming') return '暂无起名方案';
if (currentTab.value === 'affinity') return '暂无缘分合盘记录';
if (currentTab.value === 'zeji') return '暂无八字择吉记录';
return '暂无数据';
};
const getEmptyButtonText = () => {
if (currentTab.value === 'naming') return '去起名';
if (currentTab.value === 'affinity') return '去测算';
if (currentTab.value === 'zeji') return '去择吉';
return '去测算';
};
const handleEmptyAction = () => {
if (currentTab.value === 'naming') {
emit('navigate', 'naming');
} else if (currentTab.value === 'affinity') {
emit('navigate', 'affinity');
} else if (currentTab.value === 'zeji') {
emit('navigate', 'calendar');
}
};
const handleBack = () => {
emit('back');
};
// 初始化
onMounted(() => {
loadList(true);
});
</script>
<style scoped>
.plans-screen {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f0efe9;
position: relative;
font-family: SimSun, "Songti SC", "Songti TC", "Noto Serif SC", STSong, serif;
}
.status-bar-placeholder {
height: var(--status-bar-height, 0);
width: 100%;
flex-shrink: 0;
}
.plans-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
opacity: 0.3;
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
}
.plans-header {
position: sticky;
top: var(--status-bar-height, 0);
z-index: 100;
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
border-bottom: 1rpx solid #dcd3c9;
background-color: rgba(253, 251, 247, 0.95);
backdrop-filter: blur(10rpx);
}
.plans-back-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
padding: 0;
margin-left: -16rpx;
}
.plans-back-icon {
font-size: 48rpx;
color: #5a5a5a;
font-weight: 300;
}
.plans-title {
font-size: 32rpx;
font-weight: 700;
color: #2c2c2c;
letter-spacing: 0.2em;
}
.plans-refresh-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
padding: 0;
margin-right: -16rpx;
}
.plans-refresh-icon {
font-size: 40rpx;
color: #5a5a5a;
font-weight: 300;
transition: transform 0.3s ease;
}
.plans-refresh-icon.rotating {
animation: rotate 1s linear infinite;
}
.plans-tabs {
position: sticky;
top: calc(var(--status-bar-height, 0) + 88rpx);
z-index: 99;
display: flex;
background-color: rgba(253, 251, 247, 0.95);
border-bottom: 1rpx solid #dcd3c9;
padding: 0 32rpx;
backdrop-filter: blur(10rpx);
}
.plans-tab {
flex: 1;
padding: 24rpx 0;
text-align: center;
position: relative;
transition: all 0.3s ease;
}
.plans-tab-text {
font-size: 28rpx;
color: #999;
transition: all 0.3s ease;
}
.plans-tab-active .plans-tab-text {
color: #8b2323;
font-weight: 700;
}
.plans-tab-active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 48rpx;
height: 4rpx;
background-color: #8b2323;
border-radius: 2rpx;
}
.plans-scroll {
flex: 1;
height: calc(100vh - var(--status-bar-height, 0) - 88rpx - 76rpx);
}
.plans-content {
padding: 32rpx;
}
.initial-loading {
display: flex;
align-items: center;
justify-content: center;
height: 400rpx;
}
.loading-text {
font-size: 28rpx;
color: #999;
}
.list-item {
display: flex;
align-items: flex-start;
background-color: #fffdf9;
padding: 32rpx;
border-radius: 24rpx;
border: 1rpx solid #e5e5e5;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
margin-bottom: 32rpx;
transition: all 0.3s ease;
}
.list-item:active {
transform: scale(0.98);
background-color: #f8f6f2;
}
.item-icon {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-right: 32rpx;
}
.icon-red {
background-color: rgba(139, 35, 35, 0.1);
}
.icon-gold {
background-color: rgba(212, 175, 55, 0.1);
}
.icon-pink {
background-color: rgba(236, 72, 153, 0.1);
}
.icon-purple {
background-color: rgba(147, 51, 234, 0.1);
}
.icon-text {
font-size: 36rpx;
font-weight: 500;
}
.icon-red .icon-text {
color: #8b2323;
}
.icon-gold .icon-text {
color: #d4af37;
}
.icon-pink .icon-text {
color: #ec4899;
}
.icon-purple .icon-text {
color: #9333ea;
}
.item-content {
flex: 1;
min-width: 0;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8rpx;
}
.item-title {
font-size: 28rpx;
font-weight: 700;
color: #2c2c2c;
flex: 1;
margin-right: 16rpx;
}
.item-date {
font-size: 20rpx;
color: #999;
background-color: #f5f5f5;
padding: 4rpx 12rpx;
border-radius: 999rpx;
flex-shrink: 0;
}
.item-subtitle {
font-size: 24rpx;
color: #5a5a5a;
margin-bottom: 24rpx;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-tags {
display: flex;
align-items: center;
gap: 16rpx;
}
.item-tag {
font-size: 20rpx;
padding: 2rpx 12rpx;
border-width: 1rpx;
border-style: solid;
border-radius: 4rpx;
}
.tag-red {
border-color: rgba(139, 35, 35, 0.3);
color: #8b2323;
}
.tag-gold {
border-color: rgba(212, 175, 55, 0.3);
color: #d4af37;
}
.tag-pink {
border-color: rgba(236, 72, 153, 0.3);
color: #ec4899;
}
.tag-purple {
border-color: rgba(147, 51, 234, 0.3);
color: #9333ea;
}
.item-status {
font-size: 20rpx;
color: #ccc;
display: flex;
align-items: center;
}
.status-icon {
font-size: 20rpx;
margin-right: 8rpx;
}
.item-arrow {
align-self: center;
margin-left: 16rpx;
}
.arrow-icon {
font-size: 32rpx;
color: #ccc;
}
.load-more-wrapper {
padding: 32rpx 0;
text-align: center;
}
.loading-more {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx;
}
.load-more-btn {
background: #8b2323;
color: white;
padding: 20rpx 40rpx;
border-radius: 12rpx;
display: inline-block;
}
.load-more-text {
font-size: 28rpx;
color: white;
}
.no-more {
padding: 20rpx;
}
.no-more-text {
font-size: 24rpx;
color: #999;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 500rpx;
}
.empty-icon {
font-size: 96rpx;
opacity: 0.3;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-bottom: 32rpx;
}
.empty-btn {
background-color: #8b2323;
padding: 16rpx 64rpx;
border-radius: 8rpx;
border: none;
}
.empty-btn-text {
font-size: 28rpx;
color: white;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
<template>
<view class="h-full flex flex-col items-center justify-center bg-[#fdfbf7] text-[#2c2c2c] font-serif relative overflow-hidden">
<view class="absolute inset-0 opacity-10 pointer-events-none bg-[url('https://www.transparenttextures.com/patterns/rice-paper.png')]"></view>
<view class="relative z-10 text-center px-8">
<text class="block text-4xl mb-3 text-[#8b2323]"></text>
<text class="block text-xl font-bold tracking-[0.3em] mb-2">智能起名模块</text>
<text class="block text-sm text-[#5a5a5a]">稍后将迁移完整表单与生成结果</text>
</view>
</view>
</template>

View File

@@ -0,0 +1,391 @@
<template>
<view class="solutions-screen">
<view class="solutions-bg"></view>
<view class="status-bar-placeholder"></view>
<view class="solutions-header">
<view class="solutions-back-btn" @click="$emit('back')">
<text class="solutions-back-icon"></text>
</view>
<view class="solutions-header-center">
<text class="solutions-title">起名方案列表</text>
</view>
<view class="solutions-header-placeholder"></view>
</view>
<scroll-view scroll-y class="solutions-scroll">
<view class="solutions-content">
<view v-if="!solutions.length" class="solutions-empty">
<text class="solutions-empty-icon"></text>
<text class="solutions-empty-text">暂无方案</text>
</view>
<view
v-for="(it, idx) in solutions"
:key="String(it?.id || it?.solution_id || idx)"
class="solutions-item"
:style="{ animationDelay: (idx * 0.04) + 's' }"
@click="open(it)"
>
<view class="solutions-item-main">
<view class="solutions-item-header">
<view>
<text class="solutions-item-name">{{ titleOf(it) }}</text>
<text class="solutions-item-pinyin">{{ pinyinOf(it) }}</text>
</view>
<view class="solutions-item-actions">
<view class="solutions-item-view-btn" @click.stop="open(it)">
<text class="solutions-item-view-icon"></text>
<text class="solutions-item-view-text">查看</text>
</view>
<view class="solutions-item-badge">
<text class="solutions-item-badge-text">{{ idx + 1 }}</text>
</view>
</view>
</view>
<view class="solutions-item-meta">
<text class="solutions-tag">{{ tagAOf(it) }}</text>
<text class="solutions-tag">{{ tagBOf(it) }}</text>
</view>
<view class="solutions-item-meta-sub">
<text v-if="scoreOf(it)" class="solutions-chip">评分 {{ scoreOf(it) }}</text>
</view>
<view v-if="poetryOf(it)" class="solutions-item-poetry">
<text class="solutions-item-poetry-label">出处</text>
<text class="solutions-item-poetry-text">{{ poetryOf(it) }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { namingApi } from "@/api/naming";
import { parseMaybeJson } from "@/utils/poll-test-solution-detail";
declare const uni: any;
const props = defineProps<{
reportId: number;
solutions: any[];
category?: string;
serviceType?: string;
}>();
const emit = defineEmits<{
back: [];
showDetail: [data: any, category?: string, serviceType?: string];
}>();
const solutions = computed(() => (Array.isArray(props.solutions) ? props.solutions : []));
const titleOf = (it: any) => {
const name = String(it?.name || it?.solution_name || it?.title || it?.label || "").trim();
if (name) return name;
const fallback = String(it?.given_name || it?.full_name || it?.company_name || "").trim();
return fallback || "方案";
};
const pinyinOf = (it: any) => String(it?.pinyin || it?.name_pinyin || "").trim() || "Pīn Yīn";
const tagAOf = (it: any) => String(it?.wuxing || it?.wuxing_tag || it?.element_tag || "五行均衡");
const tagBOf = (it: any) => String(it?.style || it?.style_tag || it?.feature_tag || "温文尔雅");
const poetryOf = (it: any) => String(it?.poetry_source || it?.poetry || it?.source || "").trim();
const scoreOf = (it: any) => {
const v = it?.total_score ?? it?.score;
if (v === null || v === undefined || v === "") return "";
return String(v);
};
const open = async (it: any) => {
const id = it?.id || it?.solution_id;
if (!id) {
uni.showToast({ title: "方案ID不存在", icon: "none" });
return;
}
try {
uni.showLoading({ title: "加载中..." });
const detailRaw: any = await namingApi.getSolutionDetail(id);
uni.hideLoading();
const parsed = parseMaybeJson(detailRaw);
if (!parsed || typeof parsed !== "object") {
uni.showToast({ title: "详情数据格式异常", icon: "none" });
return;
}
emit("showDetail", parsed, props.category, String(props.serviceType || ""));
} catch (e: any) {
uni.hideLoading();
uni.showToast({ title: e?.msg || e?.message || "加载失败", icon: "none" });
}
};
</script>
<style scoped>
.solutions-screen{
min-height: 100%;
position: relative;
overflow: hidden;
}
.solutions-bg{
position: fixed;
inset: 0;
background:
/* 纸张底色 */
radial-gradient(1200px 900px at 30% 20%, rgba(255,255,255,.92), rgba(245,241,232,.88) 45%, rgba(235,229,214,.92) 100%),
/* 纸纹颗粒 */
radial-gradient(2px 2px at 12% 18%, rgba(0,0,0,.06), transparent 55%),
radial-gradient(2px 2px at 48% 62%, rgba(0,0,0,.05), transparent 55%),
radial-gradient(2px 2px at 78% 34%, rgba(0,0,0,.04), transparent 55%),
radial-gradient(2px 2px at 32% 84%, rgba(0,0,0,.04), transparent 55%),
/* 墨韵晕染 */
radial-gradient(900px 520px at 12% 8%, rgba(25,28,33,.10), transparent 60%),
radial-gradient(760px 520px at 92% 22%, rgba(120,75,40,.08), transparent 62%),
radial-gradient(900px 640px at 40% 92%, rgba(90,35,35,.08), transparent 62%),
linear-gradient(180deg, #f6f2e8 0%, #efe7d6 60%, #f6f2e8 100%);
z-index: -1;
}
.status-bar-placeholder{
height: 24px;
}
.solutions-header{
display:flex;
align-items:center;
justify-content:space-between;
padding: 10px 14px 12px;
}
.solutions-back-btn{
width: 36px;
height: 36px;
border-radius: 12px;
border: 1px solid rgba(120,90,40,.26);
background: linear-gradient(180deg, rgba(255,255,255,.70), rgba(244,236,220,.65));
box-shadow: 0 6px 16px rgba(40,30,20,.10);
display:flex;
align-items:center;
justify-content:center;
}
.solutions-back-icon{
font-size: 22px;
color: rgba(82,60,28,.92);
line-height: 1;
}
.solutions-header-center{
display:flex;
flex-direction:column;
align-items:center;
gap: 3px;
}
.solutions-title{
font-size: 17px;
font-weight: 900;
color: rgba(28,24,20,.92);
letter-spacing: .08em;
font-family: "STSong","Songti SC","SimSun","STSong","Noto Serif SC",serif;
}
.solutions-subtitle{
font-size: 11px;
color: rgba(70,58,44,.72);
font-family: "STSong","Songti SC","SimSun","STSong","Noto Serif SC",serif;
}
.solutions-header-placeholder{
width: 36px;
height: 36px;
}
.solutions-scroll{
height: calc(100vh - 24px - 58px);
}
.solutions-content{
padding: 2px 14px 24px;
box-sizing:border-box;
}
.solutions-empty{
padding: 26px 10px;
border-radius: 18px;
border: 1px solid rgba(120,90,40,.18);
background: linear-gradient(180deg, rgba(255,255,255,.76), rgba(247,239,224,.66));
box-shadow: 0 10px 26px rgba(40,30,20,.10);
text-align:center;
}
.solutions-empty-icon{
display:block;
font-size: 18px;
color: rgba(132,98,52,.85);
margin-bottom: 6px;
}
.solutions-empty-text{
font-size: 13px;
color: rgba(50,40,28,.78);
font-family: "STSong","Songti SC","SimSun","STSong","Noto Serif SC",serif;
}
.solutions-item{
position: relative;
display:flex;
align-items:stretch;
justify-content:space-between;
border-radius: 18px;
border: 1px solid rgba(178,120,120,.45);
background:
linear-gradient(180deg, rgba(255,255,255,.84), rgba(249,243,232,.78));
overflow:hidden;
padding: 14px 14px 13px 14px;
margin-bottom: 14px;
animation: solFadeUp .22s ease both;
box-shadow:
0 14px 30px rgba(40,30,20,.14),
inset 0 1px 0 rgba(255,255,255,.55);
}
.solutions-item-main{
flex: 1;
}
.solutions-item-header{
display:flex;
align-items:flex-start;
justify-content:space-between;
gap: 10px;
}
.solutions-item-name{
font-size: 18px;
font-weight: 900;
color: #20252e;
letter-spacing: .04em;
font-family: "STSong","Songti SC","SimSun","STSong","Noto Serif SC",serif;
display: block;
}
.solutions-item-pinyin{
display: block;
margin-top: 4px;
font-size: 12px;
color: rgba(32,37,46,.72);
letter-spacing: .08em;
}
.solutions-item-badge{
width: 24px;
height: 24px;
border-radius: 999px;
border: 1px solid rgba(202,166,96,.62);
background:
linear-gradient(180deg, rgba(255,245,220,.90), rgba(248,229,184,.82));
display:flex;
align-items:center;
justify-content:center;
flex: 0 0 24px;
}
.solutions-item-badge-text{
font-size: 11px;
font-weight: 900;
color: rgba(128,96,42,.92);
}
.solutions-item-actions{
display: flex;
align-items: center;
gap: 8px;
}
.solutions-item-view-btn{
height: 26px;
padding: 0 10px;
border-radius: 999px;
border: 1px solid rgba(188,170,136,.78);
background: rgba(255,255,255,.70);
display:flex;
align-items:center;
gap: 4px;
box-shadow: 0 2px 6px rgba(40,30,20,.08);
}
.solutions-item-view-icon{
font-size: 12px;
color: rgba(55,60,68,.75);
}
.solutions-item-view-text{
font-size: 12px;
font-weight: 700;
color: rgba(55,60,68,.85);
}
.solutions-item-meta{
display:flex;
gap: 8px;
margin-top: 8px;
flex-wrap: wrap;
}
.solutions-tag{
font-size: 11px;
padding: 3px 8px;
border-radius: 7px;
border: 1px solid rgba(188,114,114,.52);
color: rgba(130,58,58,.90);
background: rgba(255,250,248,.78);
font-family: "STSong","Songti SC","SimSun","STSong","Noto Serif SC",serif;
}
.solutions-item-meta-sub{
display:flex;
gap: 8px;
margin-top: 9px;
flex-wrap: wrap;
}
.solutions-chip{
font-size: 10px;
padding: 3px 8px;
border-radius: 999px;
border: 1px solid rgba(196,168,106,.42);
color: rgba(128,96,42,.88);
background: rgba(250,239,211,.65);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.solutions-item-poetry{
margin-top: 8px;
display:flex;
align-items:flex-start;
gap: 8px;
}
.solutions-item-poetry-label{
flex: 0 0 auto;
font-size: 11px;
line-height: 1.5;
color: rgba(130,58,58,.90);
border: 1px solid rgba(188,114,114,.50);
border-radius: 6px;
padding: 1px 6px;
background: rgba(255,250,248,.78);
}
.solutions-item-poetry-text{
flex: 1;
font-size: 12px;
line-height: 1.6;
color: rgba(40,34,26,.82);
font-family: "STSong","Songti SC","SimSun","STSong","Noto Serif SC",serif;
}
@keyframes solFadeUp{
from{opacity:0;transform:translateY(6px)}
to{opacity:1;transform:translateY(0)}
}
/* 细节:内侧金线与“卷轴边” */
.solutions-item::before{
content:"";
position:absolute;
inset: 9px 9px 9px 9px;
border-radius: 12px;
border: 1px solid rgba(188,170,136,.28);
pointer-events:none;
}
.solutions-item::after{
content:"";
position:absolute;
top:-40px;
right:-60px;
width: 160px;
height: 160px;
background: radial-gradient(closest-side, rgba(188,114,114,.08), transparent 70%);
transform: rotate(18deg);
pointer-events:none;
}
/* 轻微按压反馈 */
.solutions-item:active{
transform: translateY(1px);
box-shadow:
0 10px 22px rgba(40,30,20,.12),
inset 0 1px 0 rgba(255,255,255,.55);
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,351 @@
<template>
<view class="privacy-screen">
<view class="privacy-header">
<view class="privacy-back" @click="handleBack">
<text class="privacy-back-icon"></text>
</view>
<text class="privacy-title">隐私政策</text>
</view>
<view v-if="loading" class="privacy-loading">
<text class="privacy-loading-text">加载中...</text>
</view>
<view v-else-if="error" class="privacy-error">
<text class="privacy-error-text">{{ error }}</text>
<button class="privacy-retry-btn" @click="loadPrivacyPolicy">
<text class="privacy-retry-text">重试</text>
</button>
</view>
<view v-else class="privacy-content">
<view v-if="policy" class="privacy-info">
<text class="privacy-version">版本{{ policy.version }}</text>
<text class="privacy-date">生效日期{{ policy.effective_date }}</text>
</view>
<view v-if="policy" class="privacy-body" v-html="policy.content"></view>
<view v-if="!policy" class="privacy-default">
<view class="privacy-section">
<text class="privacy-section-title">引言</text>
<text class="privacy-text">
壹梵起名以下简称"我们"非常重视用户的隐私保护本隐私政策旨在向您说明我们如何收集使用存储和保护您的个人信息
</text>
</view>
<view class="privacy-section">
<text class="privacy-section-title">我们收集的信息</text>
<text class="privacy-text">
1. 账号信息手机号码密码等注册信息
</text>
<text class="privacy-text">
2. 服务信息您在使用起名测名等服务时提供的姓名出生日期性别等信息
</text>
<text class="privacy-text">
3. 设备信息设备型号操作系统版本设备标识符等
</text>
<text class="privacy-text">
4. 日志信息IP地址访问时间浏览记录等
</text>
</view>
<view class="privacy-section">
<text class="privacy-section-title">信息的使用</text>
<text class="privacy-text">
我们收集的信息将用于
</text>
<text class="privacy-text">
1. 提供维护和改进我们的服务
</text>
<text class="privacy-text">
2. 处理您的订单和支付
</text>
<text class="privacy-text">
3. 向您发送服务通知和更新
</text>
<text class="privacy-text">
4. 保护服务安全防止欺诈
</text>
<text class="privacy-text">
5. 遵守法律法规要求
</text>
</view>
<view class="privacy-section">
<text class="privacy-section-title">信息的共享</text>
<text class="privacy-text">
我们不会向第三方出售出租或以其他方式披露您的个人信息除非
</text>
<text class="privacy-text">
1. 获得您的明确同意
</text>
<text class="privacy-text">
2. 法律法规要求
</text>
<text class="privacy-text">
3. 为提供服务所必需如支付服务提供商
</text>
<text class="privacy-text">
4. 保护我们或他人的合法权益
</text>
</view>
<view class="privacy-section">
<text class="privacy-section-title">信息的存储</text>
<text class="privacy-text">
您的个人信息将存储在中华人民共和国境内的服务器上我们将采取合理的安全措施保护您的信息包括加密存储访问控制等
</text>
</view>
<view class="privacy-section">
<text class="privacy-section-title">您的权利</text>
<text class="privacy-text">
您有权
</text>
<text class="privacy-text">
1. 访问更正或删除您的个人信息
</text>
<text class="privacy-text">
2. 撤回您的同意
</text>
<text class="privacy-text">
3. 注销您的账号
</text>
<text class="privacy-text">
4. 投诉或举报
</text>
</view>
<view class="privacy-section">
<text class="privacy-section-title">未成年人保护</text>
<text class="privacy-text">
我们非常重视未成年人的个人信息保护如果您是未成年人请在监护人的陪同下阅读本政策并在监护人同意的情况下使用我们的服务
</text>
</view>
<view class="privacy-section">
<text class="privacy-section-title">政策更新</text>
<text class="privacy-text">
我们可能会不时更新本隐私政策更新后的政策将在平台上公布并在您继续使用服务时生效
</text>
</view>
<view class="privacy-section">
<text class="privacy-section-title">联系我们</text>
<text class="privacy-text">
如果您对本隐私政策有任何疑问或建议请通过平台内的反馈功能联系我们
</text>
</view>
<view class="privacy-footer">
<text class="privacy-footer-text">壹梵起名</text>
<text class="privacy-footer-text">生效日期2024年1月1日</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { userApi } from '@/api';
import type { PrivacyPolicyResponse } from '@/api/types';
const router = useRouter();
const loading = ref(true);
const error = ref('');
const policy = ref<PrivacyPolicyResponse | null>(null);
const handleBack = () => {
router.back();
};
const loadPrivacyPolicy = async () => {
loading.value = true;
error.value = '';
try {
const result = await userApi.getPrivacyPolicy();
policy.value = result;
} catch (err: any) {
console.error('加载隐私政策失败:', err);
error.value = err.msg || '加载失败,请重试';
} finally {
loading.value = false;
}
};
onMounted(() => {
loadPrivacyPolicy();
});
</script>
<style scoped>
.privacy-screen {
min-height: 100vh;
width: 100%;
background: #fdfbf7 url("https://www.transparenttextures.com/patterns/rice-paper.png");
display: flex;
flex-direction: column;
overflow-x: hidden;
}
.privacy-header {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
padding: 16px 20px;
background: rgba(253, 251, 247, 0.95);
border-bottom: 1px solid #eaddcf;
backdrop-filter: blur(10px);
flex-shrink: 0;
}
.privacy-back {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin-right: 12px;
flex-shrink: 0;
}
.privacy-back-icon {
font-size: 24px;
color: #8b2323;
font-weight: bold;
}
.privacy-title {
font-size: 18px;
font-weight: 500;
color: #2c2c2c;
font-family: SimSun, "Songti SC", serif;
}
.privacy-loading,
.privacy-error {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
}
.privacy-loading-text,
.privacy-error-text {
font-size: 14px;
color: #8a8a8a;
font-family: SimSun, "Songti SC", serif;
margin-bottom: 16px;
}
.privacy-retry-btn {
padding: 8px 24px;
background: #8b2323;
color: #fff;
border: none;
border-radius: 20px;
font-size: 14px;
cursor: pointer;
font-family: SimSun, "Songti SC", serif;
}
.privacy-content {
flex: 1;
padding: 24px 20px 40px;
overflow-y: auto;
overflow-x: hidden;
width: 100%;
box-sizing: border-box;
}
.privacy-info {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: rgba(139, 35, 35, 0.05);
border-radius: 8px;
margin-bottom: 24px;
flex-wrap: wrap;
gap: 8px;
}
.privacy-version,
.privacy-date {
font-size: 13px;
color: #8b2323;
font-family: SimSun, "Songti SC", serif;
}
.privacy-body {
font-size: 14px;
line-height: 1.8;
color: #4a4a4a;
font-family: SimSun, "Songti SC", serif;
word-wrap: break-word;
word-break: break-word;
overflow-wrap: break-word;
}
.privacy-default {
display: flex;
flex-direction: column;
width: 100%;
}
.privacy-section {
margin-bottom: 24px;
width: 100%;
}
.privacy-section-title {
display: block;
font-size: 16px;
font-weight: 600;
color: #8b2323;
margin-bottom: 12px;
font-family: SimSun, "Songti SC", serif;
word-wrap: break-word;
word-break: break-word;
}
.privacy-text {
display: block;
font-size: 14px;
line-height: 1.8;
color: #4a4a4a;
margin-bottom: 8px;
font-family: SimSun, "Songti SC", serif;
word-wrap: break-word;
word-break: break-word;
white-space: pre-wrap;
overflow-wrap: break-word;
}
.privacy-footer {
margin-top: 40px;
padding-top: 24px;
border-top: 1px solid #eaddcf;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
width: 100%;
}
.privacy-footer-text {
display: block;
font-size: 13px;
color: #8a8a8a;
font-family: SimSun, "Songti SC", serif;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,416 @@
<template>
<view class="faq-screen">
<view class="faq-bg"></view>
<!-- 状态栏占位 -->
<view class="status-bar-placeholder"></view>
<!-- Header -->
<view class="faq-header">
<view class="faq-back-btn" @click="handleBack">
<text class="faq-back-icon"></text>
</view>
<text class="faq-title">常见问题</text>
<view class="faq-header-placeholder"></view>
</view>
<!-- Content -->
<scroll-view scroll-y class="faq-content">
<view class="faq-content-inner">
<view v-if="loading" class="faq-loading">
<text class="faq-loading-text">加载中...</text>
</view>
<template v-else-if="groups.length > 0">
<view v-for="(group, groupIndex) in groups" :key="groupIndex" class="faq-group">
<view class="faq-group-header">
<text class="faq-group-title">{{ group.category_name }}</text>
</view>
<view class="faq-list">
<view v-for="(item, itemIndex) in group.items" :key="item.id" class="faq-item"
:class="{ 'faq-item-expanded': expandedItems[item.id] }" @click="toggleItem(item.id)">
<view class="faq-question">
<text class="faq-question-icon">Q</text>
<view class="faq-question-content">
<text class="faq-question-text">{{ item.question }}</text>
<text v-if="item.is_hot === 1" class="faq-hot-badge">HOT</text>
</view>
<text class="faq-question-arrow"
:class="{ 'faq-question-arrow-expanded': expandedItems[item.id] }"></text>
</view>
<view v-if="expandedItems[item.id]" class="faq-answer">
<text class="faq-answer-icon">A</text>
<text class="faq-answer-text">{{ item.answer }}</text>
</view>
</view>
</view>
</view>
</template>
<view v-else class="faq-empty">
<text class="faq-empty-icon">📋</text>
<text class="faq-empty-text">暂无常见问题</text>
</view>
<!-- 联系客服 -->
<view class="faq-contact">
<text class="faq-contact-title">没有找到答案</text>
<button class="faq-contact-btn" @click="handleContact">
<text class="faq-contact-btn-text">联系客服</text>
</button>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
import { userApi } from "@/api";
import type { FAQGroup } from "@/api/types";
declare const uni: any;
const emit = defineEmits<{
back: [];
}>();
const loading = ref(false);
const groups = ref<FAQGroup[]>([]);
const expandedItems = reactive<Record<number, boolean>>({});
const loadFAQ = async () => {
loading.value = true;
try {
const res = await userApi.getFAQ();
console.log('getFAQ response:', res);
// API返回的是data数组不是groups
groups.value = res?.data || (Array.isArray(res) ? res : []);
} catch (e: any) {
console.error('loadFAQ error:', e);
uni.showToast({ title: e.msg || "加载失败", icon: "none" });
} finally {
loading.value = false;
}
};
const toggleItem = (id: number) => {
expandedItems[id] = !expandedItems[id];
};
const handleContact = () => {
// Web环境使用alertuni-app环境使用showModal
if (typeof uni?.showModal === 'function') {
uni.showModal({
title: '联系客服',
content: '客服微信yifan_service\n工作时间9:00-18:00',
showCancel: false,
});
} else {
// Web环境使用原生alert
alert('联系客服\n\n客服微信yifan_service\n工作时间9:00-18:00');
}
};
const handleBack = () => {
emit('back');
};
onMounted(() => loadFAQ());
</script>
<style scoped>
.faq-screen {
height: 100%;
display: flex;
flex-direction: column;
background-color: #f0efe9;
position: relative;
}
.status-bar-placeholder {
height: var(--status-bar-height, 0);
width: 100%;
flex-shrink: 0;
}
.faq-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
opacity: 0.3;
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
}
/* Header */
.faq-header {
position: relative;
z-index: 10;
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
border-bottom: 1rpx solid #dcd3c9;
background-color: rgba(253, 251, 247, 0.8);
backdrop-filter: blur(10rpx);
}
.faq-back-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
padding: 0;
margin-left: -16rpx;
}
.faq-back-icon {
font-size: 48rpx;
color: #5a5a5a;
font-weight: 300;
}
.faq-title {
font-size: 32rpx;
font-weight: 700;
color: #2c2c2c;
letter-spacing: 0.2em;
}
.faq-header-placeholder {
width: 64rpx;
}
/* Content */
.faq-content {
flex: 1;
height: 0;
position: relative;
z-index: 10;
}
.faq-content-inner {
padding: 32rpx;
}
/* Loading */
.faq-loading {
display: flex;
align-items: center;
justify-content: center;
height: 400rpx;
}
.faq-loading-text {
font-size: 28rpx;
color: #999;
}
/* Group */
.faq-group {
margin-bottom: 32rpx;
}
.faq-group-header {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 16rpx;
padding: 0 8rpx;
}
.faq-group-icon {
font-size: 28rpx;
}
.faq-group-title {
font-size: 28rpx;
font-weight: 700;
color: #8b2323;
}
/* List */
.faq-list {
background-color: #fffdf9;
border-radius: 24rpx;
border: 1rpx solid #e5e5e5;
overflow: hidden;
}
.faq-item {
border-bottom: 1rpx solid #f0f0f0;
transition: background-color 0.2s;
}
.faq-item:last-child {
border-bottom: none;
}
.faq-item:active {
background-color: #fafafa;
}
.faq-item-expanded {
background-color: #fafafa;
}
/* Question */
.faq-question {
display: flex;
align-items: center;
padding: 24rpx 32rpx;
gap: 16rpx;
}
.faq-question-icon {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background-color: #8b2323;
color: #fff;
font-size: 24rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.faq-question-text {
flex: 1;
font-size: 28rpx;
color: #2c2c2c;
font-weight: 500;
}
.faq-question-content {
flex: 1;
display: flex;
align-items: center;
gap: 12rpx;
}
.faq-hot-badge {
font-size: 18rpx;
background: linear-gradient(135deg, #ff6b6b 0%, #ff4757 100%);
color: #fff;
padding: 4rpx 12rpx;
border-radius: 8rpx;
font-weight: 700;
flex-shrink: 0;
}
.faq-question-arrow {
font-size: 32rpx;
color: #ccc;
transition: transform 0.3s;
flex-shrink: 0;
}
.faq-question-arrow-expanded {
transform: rotate(90deg);
}
/* Answer */
.faq-answer {
display: flex;
padding: 0 32rpx 24rpx 32rpx;
gap: 16rpx;
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.faq-answer-icon {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background-color: #d4af37;
color: #fff;
font-size: 24rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.faq-answer-text {
flex: 1;
font-size: 26rpx;
color: #5a5a5a;
line-height: 1.8;
padding-top: 8rpx;
white-space: pre-line;
}
/* Empty */
.faq-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400rpx;
}
.faq-empty-icon {
font-size: 96rpx;
opacity: 0.3;
margin-bottom: 24rpx;
}
.faq-empty-text {
font-size: 28rpx;
color: #999;
}
/* Contact */
.faq-contact {
margin-top: 32rpx;
padding: 32rpx;
background-color: #fffdf9;
border-radius: 24rpx;
border: 1rpx solid #e5e5e5;
text-align: center;
}
.faq-contact-title {
font-size: 28rpx;
color: #2c2c2c;
margin-bottom: 24rpx;
display: block;
}
.faq-contact-btn {
width: 100%;
padding: 24rpx 0;
background-color: #8b2323;
border-radius: 16rpx;
border: none;
}
.faq-contact-btn-text {
font-size: 28rpx;
font-weight: 700;
color: #d4af37;
}
</style>

View File

@@ -0,0 +1,556 @@
<template>
<view class="favorites-screen">
<view class="favorites-bg"></view>
<!-- 状态栏占位 -->
<view class="status-bar-placeholder"></view>
<!-- Header -->
<view class="favorites-header">
<view class="favorites-back-btn" @click="handleBack">
<text class="favorites-back-icon"></text>
</view>
<text class="favorites-title">我的收藏</text>
<view class="favorites-header-placeholder"></view>
</view>
<!-- Tabs -->
<view class="favorites-tabs">
<view class="favorites-tabs-container">
<view v-for="tab in tabs" :key="tab.value" class="favorites-tab-btn"
:class="{ 'favorites-tab-btn-active': activeTab === tab.value }" @click="switchTab(tab.value)">
<text class="favorites-tab-text" :class="{ 'favorites-tab-text-active': activeTab === tab.value }">
{{ tab.label }}
</text>
</view>
</view>
</view>
<!-- List -->
<scroll-view scroll-y class="favorites-list" @scrolltolower="loadMore">
<view class="favorites-list-inner">
<view v-if="loading && filteredList.length === 0" class="favorites-loading">
<text class="favorites-loading-text">加载中...</text>
</view>
<template v-else-if="filteredList.length > 0">
<view v-for="(item, index) in filteredList" :key="item.id" class="favorites-item"
:style="{ animationDelay: (index * 0.05) + 's' }" @click="viewDetail(item)">
<!-- Left Border Accent -->
<view class="favorites-item-border"
:class="item.category === 'company' ? 'favorites-item-border-gold' : 'favorites-item-border-red'"></view>
<view class="favorites-item-main">
<view class="favorites-item-header">
<text class="favorites-item-name">{{ item.name || '未命名' }}</text>
<view class="favorites-item-type-icon">
<text class="favorites-item-type-text">{{ item.category === 'company' ? '企' : '名' }}</text>
</view>
</view>
<text v-if="item.pinyin" class="favorites-item-pinyin">{{ item.pinyin }}</text>
<view class="favorites-item-bottom">
<view class="favorites-item-tags">
<text v-for="(tag, idx) in item.tags" :key="idx" class="favorites-item-tag"
:class="item.category === 'company' ? 'favorites-item-tag-gold' : 'favorites-item-tag-red'">
{{ tag }}
</text>
</view>
<text class="favorites-item-date-inline">{{ formatDate(item.created_time) }}</text>
</view>
</view>
<view class="favorites-item-right">
<view class="favorites-item-heart">
<text class="favorites-item-heart-icon"></text>
</view>
</view>
</view>
<view v-if="hasMore" class="favorites-load-more">
<text class="favorites-load-more-text">{{ loading ? '加载中...' : '上拉加载更多' }}</text>
</view>
<view v-else class="favorites-no-more">
<text class="favorites-no-more-text"> 已加载全部 </text>
</view>
</template>
<template v-else>
<view class="favorites-empty">
<text class="favorites-empty-icon"></text>
<text class="favorites-empty-text">暂无收藏</text>
</view>
</template>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { userApi } from "@/api";
import type { MyFavoriteItem } from "@/api/types";
declare const uni: any;
const emit = defineEmits<{
back: [];
navigate: [screen: string];
showDetail: [data: any, category?: string, serviceType?: string];
}>();
const loading = ref(false);
const list = ref<MyFavoriteItem[]>([]);
const pageNo = ref(1);
const pageSize = 10;
const total = ref(0);
const hasMore = ref(true);
const activeTab = ref<'all' | 'personal' | 'company'>('all');
const tabs = [
{ value: 'all' as const, label: '全部' },
{ value: 'personal' as const, label: '个人名' },
{ value: 'company' as const, label: '商号' },
];
// 根据tab过滤列表
const filteredList = computed(() => {
if (activeTab.value === 'all') return list.value;
return list.value.filter(item => item.category === activeTab.value);
});
const loadList = async (refresh = false) => {
if (loading.value) return;
if (!refresh && !hasMore.value) return;
loading.value = true;
try {
if (refresh) {
pageNo.value = 1;
list.value = [];
}
const res = await userApi.getMyFavorites({
page_no: pageNo.value,
page_size: pageSize,
category: activeTab.value === 'all' ? undefined : activeTab.value,
});
console.log('getMyFavorites response:', res);
const items = res?.items || (Array.isArray(res) ? res : []);
list.value = refresh ? items : [...list.value, ...items];
total.value = res?.total || items.length;
hasMore.value = list.value.length < total.value;
pageNo.value++;
} catch (e: any) {
console.error('loadList error:', e);
uni.showToast({ title: e.msg || "加载失败", icon: "none" });
} finally {
loading.value = false;
}
};
const switchTab = (tab: 'all' | 'personal' | 'company') => {
activeTab.value = tab;
loadList(true);
};
const loadMore = () => {
if (!loading.value && hasMore.value) loadList();
};
const parseMaybeJson = (value: any): any => {
if (value == null) return value;
if (typeof value === 'object') return value;
if (typeof value !== 'string') return value;
const raw = value.trim();
if (!raw) return value;
try {
return JSON.parse(raw);
} catch {
return value;
}
};
const viewDetail = async (item: MyFavoriteItem) => {
if (!item.solution_id) {
uni.showToast({ title: "方案ID不存在", icon: "none" });
return;
}
try {
const detailData = await userApi.getSolutionDetail(item.solution_id);
const parsedData = parseMaybeJson(detailData);
const serviceType = String((item as any)?.service_type || '');
emit('showDetail', parsedData, item.category, serviceType);
} catch (e: any) {
uni.showToast({ title: e.msg || "加载详情失败", icon: "none" });
}
};
const handleBack = () => {
emit('back');
};
const formatDate = (dateStr: string) => {
if (!dateStr) return "";
// 处理 created_time 格式 "2026-01-13"
return dateStr.replace(/-/g, ".");
};
onMounted(() => loadList(true));
</script>
<style scoped>
.favorites-screen {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
background-color: #f0efe9;
position: relative;
font-family: SimSun, "Songti SC", "Songti TC", "Noto Serif SC", STSong, serif;
}
/* 强制所有容器全宽 */
:deep(*) {
box-sizing: border-box;
}
:deep(.shell) {
width: 100% !important;
max-width: 100% !important;
padding: 0 !important;
margin: 0 !important;
}
.status-bar-placeholder {
height: var(--status-bar-height, 0);
width: 100%;
flex-shrink: 0;
}
.favorites-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
opacity: 0.3;
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
}
/* Header */
.favorites-header {
position: relative;
z-index: 10;
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
border-bottom: 1px solid #dcd3c9;
background-color: rgba(253, 251, 247, 0.8);
backdrop-filter: blur(5px);
}
.favorites-back-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
padding: 0;
margin-left: -8px;
}
.favorites-back-icon {
font-size: 24px;
color: #5a5a5a;
font-weight: 300;
}
.favorites-title {
font-size: 16px;
font-weight: 700;
color: #2c2c2c;
letter-spacing: 0.2em;
}
.favorites-header-placeholder {
width: 32px;
}
/* Tabs */
.favorites-tabs {
position: relative;
z-index: 10;
padding: 12px 16px;
background-color: #fdfbf7;
}
.favorites-tabs-container {
display: flex;
background-color: #f0efe9;
padding: 4px;
border-radius: 8px;
}
.favorites-tab-btn {
flex: 1;
padding: 8px 0;
text-align: center;
border-radius: 6px;
transition: all 0.3s;
}
.favorites-tab-btn-active {
background-color: #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
}
.favorites-tab-text {
font-size: 12px;
font-weight: 700;
color: #999;
}
.favorites-tab-text-active {
color: #8b2323;
}
/* List */
.favorites-list {
flex: 1;
height: 0;
position: relative;
z-index: 10;
margin-top: 8px;
}
.favorites-list-inner {
padding: 16px;
padding-top: 0;
}
.favorites-item {
position: relative;
display: flex;
align-items: center;
background-color: #fffdf9;
padding: 16px;
border-radius: 12px;
border: 1px solid #e5e5e5;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
margin-bottom: 12px;
animation: fadeInUp 0.3s ease-out forwards;
opacity: 0;
overflow: hidden;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.favorites-item-border {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
}
.favorites-item-border-red {
background-color: #8b2323;
}
.favorites-item-border-gold {
background-color: #d4af37;
}
.favorites-item-main {
flex: 1;
min-width: 0;
padding-left: 8px;
}
.favorites-item-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.favorites-item-name {
font-size: 18px;
font-weight: 700;
color: #2c2c2c;
font-family: "SimSun", "Songti SC", serif;
}
.favorites-item-type-icon {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
}
.favorites-item-type-text {
font-size: 9px;
color: #999;
}
.favorites-item-pinyin {
font-size: 11px;
color: #999;
margin-bottom: 8px;
display: block;
font-style: italic;
}
.favorites-item-bottom {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
}
.favorites-item-category {
font-size: 11px;
color: #999;
margin-bottom: 8px;
display: block;
}
.favorites-item-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
flex: 1;
}
.favorites-item-tag {
font-size: 9px;
padding: 3px 8px;
border-radius: 4px;
background-color: #f5f5f5;
color: #5a5a5a;
white-space: nowrap;
}
.favorites-item-tag-red {
background-color: rgba(139, 35, 35, 0.05);
color: #8b2323;
}
.favorites-item-tag-gold {
background-color: rgba(212, 175, 55, 0.05);
color: #d4af37;
}
.favorites-item-date-inline {
font-size: 9px;
color: #ccc;
display: flex;
align-items: center;
white-space: nowrap;
flex-shrink: 0;
margin-left: 8px;
}
.favorites-item-right {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
margin-left: 12px;
}
.favorites-item-date {
font-size: 9px;
color: #ccc;
display: flex;
align-items: center;
}
.favorites-item-heart {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.favorites-item-heart-icon {
font-size: 16px;
color: #8b2323;
}
.favorites-loading {
display: flex;
align-items: center;
justify-content: center;
height: 100px;
}
.favorites-loading-text {
font-size: 14px;
color: #999;
}
.favorites-load-more,
.favorites-no-more {
text-align: center;
padding: 16px 0;
}
.favorites-load-more-text,
.favorites-no-more-text {
font-size: 12px;
color: #999;
}
.favorites-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 250px;
}
.favorites-empty-icon {
font-size: 48px;
opacity: 0.3;
margin-bottom: 12px;
}
.favorites-empty-text {
font-size: 14px;
color: #999;
margin-bottom: 16px;
}
.favorites-empty-btn {
background-color: #8b2323;
padding: 10px 32px;
border-radius: 4px;
border: none;
}
.favorites-empty-btn-text {
font-size: 14px;
color: #d4af37;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,477 @@
<template>
<view class="feedback-screen">
<view class="feedback-bg"></view>
<!-- 状态栏占位 -->
<view class="status-bar-placeholder"></view>
<!-- Header -->
<view class="feedback-header">
<view class="feedback-back-btn" @click="handleBack">
<text class="feedback-back-icon"></text>
</view>
<text class="feedback-title">意见反馈</text>
<view class="feedback-header-placeholder"></view>
</view>
<!-- Content -->
<scroll-view scroll-y class="feedback-content">
<view class="feedback-content-inner">
<!-- 反馈类型 -->
<view class="feedback-section">
<text class="feedback-section-title">反馈类型</text>
<view class="feedback-type-list">
<view v-for="type in feedbackTypes" :key="type.value" class="feedback-type-item"
:class="{ 'feedback-type-item-active': selectedType === type.value }"
@click="selectType(type.value)">
<text class="feedback-type-label">{{ type.label }}</text>
</view>
</view>
</view>
<!-- 反馈内容 -->
<view class="feedback-section">
<text class="feedback-section-title">反馈内容</text>
<textarea class="feedback-textarea" v-model="feedbackContent"
placeholder="请详细描述您遇到的问题或建议,我们会认真对待每一条反馈..." :maxlength="500"
placeholder-class="feedback-textarea-placeholder" />
<view class="feedback-textarea-counter">
<text class="feedback-textarea-counter-text">{{ feedbackContent.length }}/500</text>
</view>
</view>
<!-- 上传图片 -->
<view class="feedback-section">
<text class="feedback-section-title">上传图片选填</text>
<view class="feedback-images">
<view v-for="(img, index) in uploadedImages" :key="index" class="feedback-image-item">
<image :src="img" class="feedback-image" mode="aspectFill" />
<view class="feedback-image-delete" @click="deleteImage(index)">
<text class="feedback-image-delete-icon">×</text>
</view>
</view>
<view v-if="uploadedImages.length < 3" class="feedback-image-upload" @click="chooseImage">
<text class="feedback-image-upload-icon">+</text>
<text class="feedback-image-upload-text">添加图片</text>
</view>
</view>
<text class="feedback-images-tip">最多上传3张图片每张不超过5MB</text>
</view>
<!-- 联系方式 -->
<view class="feedback-section">
<text class="feedback-section-title">联系方式选填</text>
<input class="feedback-input" v-model="contactInfo" placeholder="请输入手机号或微信号,方便我们联系您"
placeholder-class="feedback-input-placeholder" />
</view>
<!-- 提交按钮 -->
<view class="feedback-submit-btn" @click="submitFeedback">
<text class="feedback-submit-btn-text">提交反馈</text>
</view>
<!-- 温馨提示 -->
<view class="feedback-tips">
<text class="feedback-tips-title">温馨提示</text>
<text class="feedback-tips-text"> 我们会在1-3个工作日内处理您的反馈</text>
<text class="feedback-tips-text"> 如需回复请留下您的联系方式</text>
<text class="feedback-tips-text"> 感谢您对壹梵起名的支持与建议</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { userApi } from "@/api";
declare const uni: any;
const emit = defineEmits<{
back: [];
}>();
// 反馈类型
const feedbackTypes = [
{ value: 'suggestion', label: '功能建议', icon: '💡' },
{ value: 'bug', label: '问题反馈', icon: '🐛' },
{ value: 'complaint', label: '投诉建议', icon: '📢' },
{ value: 'other', label: '其他', icon: '💬' },
];
const selectedType = ref<'suggestion' | 'bug' | 'complaint' | 'other'>('suggestion');
const feedbackContent = ref("");
const uploadedImages = ref<string[]>([]);
const contactInfo = ref("");
const selectType = (type: 'suggestion' | 'bug' | 'complaint' | 'other') => {
selectedType.value = type;
};
const chooseImage = async () => {
uni.chooseImage({
count: 3 - uploadedImages.value.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res: any) => {
const tempFilePaths = res.tempFilePaths;
uni.showLoading({ title: '上传中...' });
try {
// 逐个上传图片到服务器
for (const filePath of tempFilePaths) {
const result = await userApi.uploadImage(filePath);
// 使用服务器返回的file_url
uploadedImages.value.push(result.file_url);
}
uni.hideLoading();
} catch (error: any) {
uni.hideLoading();
uni.showToast({
title: error.msg || '图片上传失败',
icon: 'none'
});
}
}
});
};
const deleteImage = (index: number) => {
uploadedImages.value.splice(index, 1);
};
const submitFeedback = async () => {
if (!feedbackContent.value.trim()) {
uni.showToast({ title: "请输入反馈内容", icon: "none" });
return;
}
try {
await userApi.submitFeedback({
content: feedbackContent.value.trim(),
images: uploadedImages.value.join(','),
contact: contactInfo.value.trim(),
feedback_type: selectedType.value
});
uni.showToast({
title: "提交成功,感谢您的反馈!",
icon: "success",
duration: 2000
});
// 延迟返回,让用户看到成功提示
setTimeout(() => {
handleBack();
}, 2000);
} catch (error: any) {
uni.showToast({
title: error.msg || "提交失败,请稍后重试",
icon: "none"
});
}
};
const handleBack = () => {
emit('back');
};
</script>
<style scoped>
.feedback-screen {
height: 100%;
display: flex;
flex-direction: column;
background-color: #f0efe9;
position: relative;
}
.status-bar-placeholder {
height: var(--status-bar-height, 0);
width: 100%;
flex-shrink: 0;
}
.feedback-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
opacity: 0.3;
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
}
/* Header */
.feedback-header {
position: relative;
z-index: 10;
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
border-bottom: 1rpx solid #dcd3c9;
background-color: rgba(253, 251, 247, 0.8);
backdrop-filter: blur(10rpx);
}
.feedback-back-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
padding: 0;
margin-left: -16rpx;
}
.feedback-back-icon {
font-size: 48rpx;
color: #5a5a5a;
font-weight: 300;
}
.feedback-title {
font-size: 32rpx;
font-weight: 700;
color: #2c2c2c;
letter-spacing: 0.2em;
}
.feedback-header-placeholder {
width: 64rpx;
}
/* Content */
.feedback-content {
flex: 1;
height: 0;
position: relative;
z-index: 10;
}
.feedback-content-inner {
padding: 32rpx;
padding-bottom: calc(32rpx + env(safe-area-inset-bottom, 0px));
}
/* Section */
.feedback-section {
margin-bottom: 32rpx;
}
.feedback-section-title {
font-size: 28rpx;
font-weight: 700;
color: #2c2c2c;
margin-bottom: 16rpx;
display: block;
}
/* Type List */
.feedback-type-list {
display: flex;
gap: 16rpx;
flex-wrap: wrap;
}
.feedback-type-item {
flex: 1;
min-width: 150rpx;
padding: 20rpx 16rpx;
background-color: #fffdf9;
border: 2rpx solid #e5e5e5;
border-radius: 16rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
transition: all 0.3s;
}
.feedback-type-item-active {
background-color: #8b2323;
border-color: #8b2323;
}
.feedback-type-icon {
font-size: 32rpx;
}
.feedback-type-label {
font-size: 24rpx;
color: #2c2c2c;
}
.feedback-type-item-active .feedback-type-label {
color: #f2e6d8;
}
/* Textarea */
.feedback-textarea {
width: 100%;
min-height: 240rpx;
background-color: #fffdf9;
border: 1rpx solid #e5e5e5;
border-radius: 16rpx;
padding: 24rpx;
font-size: 28rpx;
color: #2c2c2c;
box-sizing: border-box;
line-height: 1.6;
}
.feedback-textarea-placeholder {
color: #ccc;
}
.feedback-textarea-counter {
margin-top: 8rpx;
text-align: right;
}
.feedback-textarea-counter-text {
font-size: 20rpx;
color: #999;
}
/* Images */
.feedback-images {
display: flex;
gap: 16rpx;
flex-wrap: wrap;
margin-bottom: 8rpx;
}
.feedback-image-item {
position: relative;
width: 160rpx;
height: 160rpx;
}
.feedback-image {
width: 100%;
height: 100%;
border-radius: 16rpx;
border: 1rpx solid #e5e5e5;
}
.feedback-image-delete {
position: absolute;
top: -8rpx;
right: -8rpx;
width: 40rpx;
height: 40rpx;
background-color: #8b2323;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
}
.feedback-image-delete-icon {
font-size: 32rpx;
color: #fff;
line-height: 1;
}
.feedback-image-upload {
width: 160rpx;
height: 160rpx;
background-color: #fffdf9;
border: 2rpx dashed #e5e5e5;
border-radius: 16rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
}
.feedback-image-upload-icon {
font-size: 48rpx;
color: #ccc;
line-height: 1;
}
.feedback-image-upload-text {
font-size: 20rpx;
color: #999;
}
.feedback-images-tip {
font-size: 20rpx;
color: #999;
display: block;
}
/* Input */
.feedback-input {
width: 100%;
height: 88rpx;
background-color: #fffdf9;
border: 1rpx solid #e5e5e5;
border-radius: 16rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #2c2c2c;
box-sizing: border-box;
}
.feedback-input-placeholder {
color: #ccc;
}
/* Submit Button */
.feedback-submit-btn {
width: 100%;
padding: 28rpx 0;
background-color: #8b2323;
border-radius: 16rpx;
display: flex;
justify-content: center;
margin-bottom: 32rpx;
transition: opacity 0.3s;
}
.feedback-submit-btn:active {
opacity: 0.8;
}
.feedback-submit-btn-text {
font-size: 32rpx;
font-weight: 700;
color: #f2e6d8;
}
/* Tips */
.feedback-tips {
background-color: rgba(255, 253, 249, 0.6);
border-radius: 16rpx;
padding: 24rpx;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.feedback-tips-title {
font-size: 24rpx;
font-weight: 700;
color: #8b2323;
margin-bottom: 4rpx;
}
.feedback-tips-text {
font-size: 20rpx;
color: #666;
line-height: 1.6;
}
</style>

View File

@@ -0,0 +1,470 @@
<template>
<view class="order-detail-screen">
<view class="order-detail-bg"></view>
<view class="status-bar-placeholder"></view>
<view class="order-detail-header">
<view class="order-detail-back-btn" @click="emit('back')">
<text class="order-detail-back-icon"></text>
</view>
<text class="order-detail-title">订单详情</text>
<view class="order-detail-header-placeholder"></view>
</view>
<scroll-view scroll-y class="order-detail-content">
<view class="order-detail-inner">
<view class="detail-card detail-card-highlight">
<view class="detail-row">
<text class="detail-label">订单状态</text>
<text class="detail-status" :class="`status-${order.status}`">{{ getStatusText(order.status) }}</text>
</view>
<view class="detail-price-wrap">
<text class="detail-price-symbol">¥</text>
<text class="detail-price">{{ displayAmount }}</text>
</view>
<text class="detail-desc">{{ order.description || getBusinessTypeName(order.business_type) }}</text>
</view>
<view class="detail-card">
<view class="section-title">订单信息</view>
<view class="detail-item">
<text class="detail-item-label">订单号</text>
<text class="detail-item-value detail-item-mono">{{ order.out_trade_no || '-' }}</text>
</view>
<view class="detail-item">
<text class="detail-item-label">微信订单号</text>
<text class="detail-item-value detail-item-mono">{{ order.transaction_id || '-' }}</text>
</view>
<view class="detail-item">
<text class="detail-item-label">业务类型</text>
<text class="detail-item-value">{{ getBusinessTypeName(order.business_type) }}</text>
</view>
<view class="detail-item">
<text class="detail-item-label">业务ID</text>
<text class="detail-item-value">{{ order.business_id ?? '-' }}</text>
</view>
</view>
<view class="detail-card">
<view class="section-title">支付信息</view>
<view class="detail-item">
<text class="detail-item-label">应付金额</text>
<text class="detail-item-value">¥{{ order.total_amount ?? '-' }}</text>
</view>
<view class="detail-item">
<text class="detail-item-label">实付金额</text>
<text class="detail-item-value">¥{{ order.paid_amount ?? order.total_amount ?? '-' }}</text>
</view>
<view class="detail-item">
<text class="detail-item-label">支付时间</text>
<text class="detail-item-value">{{ order.paid_at || '待支付' }}</text>
</view>
</view>
</view>
</scroll-view>
<view class="order-action-bar" v-if="showActionBar">
<view v-if="isPending" class="order-action-btn order-action-cancel" @click="handleCancelOrder">
<text class="order-action-text">取消订单</text>
</view>
<view v-if="isPending" class="order-action-btn order-action-pay" @click="handlePayOrder">
<text class="order-action-text order-action-text-light">{{ actionLoading ? '处理中...' : '继续支付' }}</text>
</view>
<view v-if="isPending" class="order-action-btn order-action-plain" @click="refreshOrderStatus()">
<text class="order-action-text">刷新状态</text>
</view>
<template v-else-if="order.status === 'paid'">
<view class="order-action-btn order-action-pay" @click="handleOpenBusiness">
<text class="order-action-text order-action-text-light">查看对应业务</text>
</view>
<view class="order-action-btn order-action-plain" @click="emit('back')">
<text class="order-action-text">返回订单列表</text>
</view>
<view class="order-action-btn order-action-pay" @click="refreshOrderStatus()">
<text class="order-action-text order-action-text-light">刷新状态</text>
</view>
</template>
<view v-else class="order-action-btn order-action-plain" @click="emit('back')">
<text class="order-action-text">返回订单列表</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { computed, ref, watch, onMounted } from 'vue';
import type { QueryOrderResponse } from '@/api/types';
import { closeOrder, wxPay } from '@/utils/payment';
import { paymentApi } from '@/api/payment';
declare const uni: any;
const props = defineProps<{
data?: QueryOrderResponse | null;
}>();
const emit = defineEmits<{
back: [];
openBusiness: [order: QueryOrderResponse];
}>();
const buildFallbackOrder = (): QueryOrderResponse => ({
out_trade_no: '',
status: 'pending',
total_amount: 0,
business_type: '',
business_id: 0,
});
const localOrder = ref<QueryOrderResponse>(props.data || buildFallbackOrder());
watch(
() => props.data,
(next) => {
localOrder.value = next || buildFallbackOrder();
},
{ immediate: true }
);
const order = computed(() => localOrder.value);
const displayAmount = computed(() => order.value.paid_amount ?? order.value.total_amount ?? 0);
const isPending = computed(() => order.value.status === 'pending');
const showActionBar = computed(() => Boolean(order.value.out_trade_no));
const actionLoading = ref(false);
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
pending: '待支付',
paid: '已支付',
cancelled: '已关闭',
refunded: '已退款',
};
return statusMap[status] || status;
};
const getBusinessTypeName = (type: string) => {
const typeMap: Record<string, string> = {
naming_report: '命名报告',
partner_apply: '推广合伙人',
test_report: '测名报告',
fortune_report: '财运报告',
test: '测试商品',
};
return typeMap[type] || type || '-';
};
const refreshOrderStatus = async (silent = false) => {
if (!order.value.out_trade_no || actionLoading.value) return;
actionLoading.value = true;
try {
const latest = await paymentApi.queryOrder(order.value.out_trade_no);
if (latest) {
localOrder.value = latest;
if (!silent) {
uni.showToast({ title: `状态已更新:${getStatusText(latest.status)}`, icon: 'none' });
}
}
} catch (e: any) {
if (!silent) {
uni.showToast({ title: e?.msg || '刷新失败', icon: 'none' });
}
} finally {
actionLoading.value = false;
}
};
const handleCancelOrder = () => {
if (!order.value.out_trade_no || actionLoading.value) return;
uni.showModal({
title: '提示',
content: '确定要取消该订单吗?',
success: async (res: any) => {
if (!res.confirm) return;
actionLoading.value = true;
try {
const success = await closeOrder(order.value.out_trade_no);
if (success) {
uni.showToast({ title: '订单已取消', icon: 'success' });
await refreshOrderStatus(true);
}
} finally {
actionLoading.value = false;
}
},
});
};
const handlePayOrder = async () => {
if (actionLoading.value) return;
actionLoading.value = true;
try {
const result = await wxPay({
description: order.value.description || getBusinessTypeName(order.value.business_type),
total_amount: order.value.total_amount,
business_type: order.value.business_type,
business_id: order.value.business_id,
});
if (result.success) {
uni.showToast({ title: '支付成功', icon: 'success' });
await refreshOrderStatus(true);
if (localOrder.value.status === 'paid') {
handleOpenBusiness();
}
return;
}
uni.showToast({ title: result.msg || '支付失败', icon: 'none' });
} catch (e: any) {
uni.showToast({ title: e?.msg || '支付失败', icon: 'none' });
} finally {
actionLoading.value = false;
}
};
const handleOpenBusiness = () => {
emit('openBusiness', order.value);
};
onMounted(() => {
if (order.value.out_trade_no && order.value.status === 'pending') {
refreshOrderStatus(true);
}
});
</script>
<style scoped>
.order-detail-screen {
height: 100%;
display: flex;
flex-direction: column;
background-color: #f0efe9;
position: relative;
}
.status-bar-placeholder {
height: var(--status-bar-height, 0);
width: 100%;
flex-shrink: 0;
}
.order-detail-bg {
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.3;
background-image: url('https://www.transparenttextures.com/patterns/rice-paper.png');
}
.order-detail-header {
position: relative;
z-index: 10;
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
border-bottom: 1rpx solid #dcd3c9;
background-color: rgba(253, 251, 247, 0.8);
backdrop-filter: blur(10rpx);
}
.order-detail-back-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: -16rpx;
}
.order-detail-back-icon {
font-size: 48rpx;
color: #5a5a5a;
font-weight: 300;
}
.order-detail-title {
font-size: 32rpx;
font-weight: 700;
color: #2c2c2c;
letter-spacing: 0.2em;
}
.order-detail-header-placeholder {
width: 64rpx;
}
.order-detail-content {
flex: 1;
height: 0;
position: relative;
z-index: 10;
}
.order-detail-inner {
padding: 32rpx;
padding-bottom: calc(180rpx + env(safe-area-inset-bottom, 0px));
display: flex;
flex-direction: column;
gap: 24rpx;
}
.detail-card {
background-color: #fffdf9;
border-radius: 16rpx;
border: 1rpx solid #e5e5e5;
padding: 24rpx;
}
.detail-card-highlight {
border-color: #e5d6c4;
background: linear-gradient(135deg, #fffdf9 0%, #f9f4ee 100%);
}
.section-title {
font-size: 28rpx;
font-weight: 700;
color: #2c2c2c;
margin-bottom: 16rpx;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.detail-label {
font-size: 24rpx;
color: #999;
}
.detail-status {
font-size: 24rpx;
padding: 4rpx 16rpx;
border-radius: 8rpx;
}
.status-paid {
background-color: #e8f5e9;
color: #4caf50;
}
.status-pending {
background-color: #fff3e0;
color: #ff9800;
}
.status-cancelled {
background-color: #f5f5f5;
color: #999;
}
.status-refunded {
background-color: #ffebee;
color: #f44336;
}
.detail-price-wrap {
margin-top: 16rpx;
display: flex;
align-items: baseline;
}
.detail-price-symbol {
font-size: 28rpx;
color: #8b2323;
font-weight: 700;
}
.detail-price {
font-size: 56rpx;
color: #8b2323;
font-weight: 700;
}
.detail-desc {
display: block;
margin-top: 10rpx;
font-size: 24rpx;
color: #666;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16rpx;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.detail-item:last-child {
border-bottom: none;
}
.detail-item-label {
font-size: 24rpx;
color: #999;
}
.detail-item-value {
flex: 1;
text-align: right;
font-size: 24rpx;
color: #333;
word-break: break-all;
}
.detail-item-mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
}
.order-action-bar {
position: relative;
z-index: 12;
display: flex;
flex-wrap: wrap;
gap: 16rpx;
padding: 20rpx 24rpx calc(20rpx + env(safe-area-inset-bottom, 0px));
border-top: 1rpx solid #e5e5e5;
background-color: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(8rpx);
}
.order-action-btn {
flex: 1 1 220rpx;
height: 80rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
border: 1rpx solid #e5e5e5;
}
.order-action-text {
font-size: 26rpx;
color: #666;
}
.order-action-text-light {
color: #fff;
}
.order-action-cancel,
.order-action-plain {
background-color: #fff;
}
.order-action-pay {
background-color: #8b2323;
border-color: #8b2323;
}
</style>

View File

@@ -0,0 +1,576 @@
<template>
<view class="orders-screen">
<view class="orders-bg"></view>
<!-- 状态栏占位 -->
<view class="status-bar-placeholder"></view>
<!-- Header -->
<view class="orders-header">
<view class="orders-back-btn" @click="handleBack">
<text class="orders-back-icon"></text>
</view>
<text class="orders-title">我的订单</text>
<view class="orders-header-placeholder"></view>
</view>
<!-- Tabs -->
<view class="orders-tabs">
<view v-for="tab in tabs" :key="tab.value" class="orders-tab"
:class="{ 'orders-tab-active': currentTab === tab.value }" @click="switchTab(tab.value)">
<text class="orders-tab-text">{{ tab.label }}</text>
</view>
</view>
<!-- Content -->
<scroll-view scroll-y class="orders-content" refresher-enabled :refresher-triggered="refreshing"
@refresherrefresh="onRefresh" @scrolltolower="onLoadMore">
<view class="orders-content-inner">
<!-- Loading -->
<view v-if="loading && orders.length === 0" class="orders-loading">
<text class="orders-loading-text">加载中...</text>
</view>
<!-- Empty -->
<view v-else-if="orders.length === 0" class="orders-empty">
<text class="orders-empty-icon">📦</text>
<text class="orders-empty-text">暂无订单</text>
</view>
<!-- List -->
<view v-else class="orders-list">
<view v-for="order in orders" :key="order.out_trade_no" class="order-item">
<!-- Header -->
<view class="order-header">
<text class="order-time">{{ order.paid_at || '待支付' }}</text>
<text class="order-status" :class="`status-${order.status}`">
{{ getStatusText(order.status) }}
</text>
</view>
<!-- Content -->
<view class="order-content">
<view class="order-info">
<text class="order-name">{{ order.description ||
getBusinessTypeName(order.business_type) }}</text>
<text class="order-no">订单号{{ order.out_trade_no }}</text>
</view>
<view class="order-price">
<text class="order-price-symbol">¥</text>
<text class="order-price-amount">{{ order.total_amount }}</text>
</view>
</view>
<!-- Actions -->
<view class="order-actions">
<view v-if="order.status === 'pending'" class="order-action-btn order-action-cancel"
@click="handleCancelOrder(order.out_trade_no)">
<text class="order-action-text">取消订单</text>
</view>
<view v-if="order.status === 'pending'" class="order-action-btn order-action-pay"
@click="handlePayOrder(order)">
<text class="order-action-text">继续支付</text>
</view>
<view v-if="order.status === 'paid'" class="order-action-btn order-action-view"
@click="handleViewOrder(order)">
<text class="order-action-text">查看详情</text>
</view>
</view>
</view>
</view>
<!-- Load More -->
<view v-if="hasMore && !loading" class="orders-loadmore">
<text class="orders-loadmore-text">加载更多...</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { closeOrder, wxPay } from "@/utils/payment";
import type { QueryOrderResponse } from "@/api/types";
import { paymentApi } from "@/api/payment";
declare const uni: any;
const emit = defineEmits<{
back: [];
showOrderDetail: [order: QueryOrderResponse];
}>();
const tabs = [
{ label: '全部', value: 'all' },
{ label: '待支付', value: 'pending' },
{ label: '已支付', value: 'paid' },
{ label: '已关闭', value: 'cancelled' },
];
const currentTab = ref('all');
const loading = ref(false);
const refreshing = ref(false);
const orders = ref<QueryOrderResponse[]>([]);
const page = ref(1);
const pageSize = 10;
const hasMore = ref(true);
const mapTradeStateToStatus = (tradeState: string): QueryOrderResponse['status'] => {
const state = (tradeState || '').toUpperCase();
if (state === 'SUCCESS') return 'paid';
if (state === 'NOTPAY') return 'pending';
if (state === 'CLOSED') return 'cancelled';
if (state === 'REFUND') return 'refunded';
return 'pending';
};
// 切换标签
const switchTab = (tab: string) => {
currentTab.value = tab;
page.value = 1;
orders.value = [];
hasMore.value = true;
loadOrders();
};
// 加载订单列表
const loadOrders = async () => {
if (loading.value) return;
loading.value = true;
try {
const res = await paymentApi.listOrders({
page_no: page.value,
page_size: pageSize,
});
const mapped: QueryOrderResponse[] = (res?.items || []).map((item: any) => ({
out_trade_no: item.out_trade_no,
transaction_id: item.transaction_id,
status: mapTradeStateToStatus(item.trade_state),
total_amount: item.total_amount,
paid_amount: item.total_amount,
paid_at: item.success_time,
business_type: item.business_type,
business_id: item.id,
description: item.description,
}));
const filtered = currentTab.value === 'all'
? mapped
: mapped.filter(o => o.status === currentTab.value);
if (page.value === 1) {
orders.value = filtered;
} else {
orders.value.push(...filtered);
}
hasMore.value = (res?.items || []).length >= pageSize;
} catch (error: any) {
console.error('加载订单失败:', error);
uni.showToast({ title: '加载失败', icon: 'none' });
} finally {
loading.value = false;
refreshing.value = false;
}
};
// 下拉刷新
const onRefresh = () => {
refreshing.value = true;
page.value = 1;
orders.value = [];
hasMore.value = true;
loadOrders();
};
// 加载更多
const onLoadMore = () => {
if (!hasMore.value || loading.value) return;
page.value++;
loadOrders();
};
// 取消订单
const handleCancelOrder = (outTradeNo: string) => {
// Web环境使用confirmuni-app环境使用showModal
if (typeof uni?.showModal === 'function') {
uni.showModal({
title: '提示',
content: '确定要取消该订单吗?',
success: async (res: any) => {
if (res.confirm) {
const success = await closeOrder(outTradeNo);
if (success) {
// 刷新列表
onRefresh();
}
}
}
});
} else {
// Web环境使用原生confirm
const confirmed = confirm('确定要取消该订单吗?');
if (confirmed) {
(async () => {
const success = await closeOrder(outTradeNo);
if (success) {
// 刷新列表
onRefresh();
}
})();
}
}
};
// 继续支付
const handlePayOrder = (order: QueryOrderResponse) => {
(async () => {
try {
const result = await wxPay({
description: order.description || getBusinessTypeName(order.business_type),
total_amount: order.total_amount,
business_type: order.business_type as any,
business_id: order.business_id,
pay_type: 'jsapi',
});
if (result.success) {
uni.showToast({ title: '支付成功', icon: 'success' });
onRefresh();
return;
}
uni.showToast({ title: result.msg || '支付失败', icon: 'none' });
} catch (e: any) {
uni.showToast({ title: e?.msg || '支付失败', icon: 'none' });
}
})();
};
// 查看订单详情
const handleViewOrder = (order: QueryOrderResponse) => {
emit('showOrderDetail', order);
};
// 获取状态文本
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
pending: '待支付',
paid: '已支付',
cancelled: '已关闭',
refunded: '已退款'
};
return statusMap[status] || status;
};
// 获取业务类型名称
const getBusinessTypeName = (type: string) => {
const typeMap: Record<string, string> = {
naming_report: '命名报告',
partner_apply: '推广合伙人',
test_report: '测名报告',
fortune_report: '财运报告',
test: '测试商品'
};
return typeMap[type] || type;
};
// 返回
const handleBack = () => {
emit('back');
};
onMounted(() => {
loadOrders();
});
</script>
<style scoped>
.orders-screen {
height: 100%;
display: flex;
flex-direction: column;
background-color: #f0efe9;
position: relative;
}
.status-bar-placeholder {
height: var(--status-bar-height, 0);
width: 100%;
flex-shrink: 0;
}
.orders-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
opacity: 0.3;
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
}
/* Header */
.orders-header {
position: relative;
z-index: 10;
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
border-bottom: 1rpx solid #dcd3c9;
background-color: rgba(253, 251, 247, 0.8);
backdrop-filter: blur(10rpx);
}
.orders-back-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
padding: 0;
margin-left: -16rpx;
}
.orders-back-icon {
font-size: 48rpx;
color: #5a5a5a;
font-weight: 300;
}
.orders-title {
font-size: 32rpx;
font-weight: 700;
color: #2c2c2c;
letter-spacing: 0.2em;
}
.orders-header-placeholder {
width: 64rpx;
}
/* Tabs */
.orders-tabs {
display: flex;
background-color: #fff;
border-bottom: 1rpx solid #e5e5e5;
position: relative;
z-index: 10;
}
.orders-tab {
flex: 1;
padding: 24rpx 0;
text-align: center;
position: relative;
}
.orders-tab-text {
font-size: 28rpx;
color: #666;
transition: all 0.3s;
}
.orders-tab-active .orders-tab-text {
color: #8b2323;
font-weight: 700;
}
.orders-tab-active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background-color: #8b2323;
border-radius: 2rpx;
}
/* Content */
.orders-content {
flex: 1;
height: 0;
position: relative;
z-index: 10;
}
.orders-content-inner {
padding: 32rpx;
padding-bottom: calc(32rpx + env(safe-area-inset-bottom, 0px));
}
/* Loading & Empty */
.orders-loading,
.orders-empty {
text-align: center;
padding: 120rpx 0;
}
.orders-loading-text {
font-size: 28rpx;
color: #999;
}
.orders-empty-icon {
font-size: 120rpx;
display: block;
margin-bottom: 24rpx;
}
.orders-empty-text {
font-size: 28rpx;
color: #999;
}
/* Order List */
.orders-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.order-item {
background-color: #fffdf9;
border-radius: 16rpx;
border: 1rpx solid #e5e5e5;
overflow: hidden;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.order-time {
font-size: 24rpx;
color: #999;
}
.order-status {
font-size: 24rpx;
padding: 4rpx 16rpx;
border-radius: 8rpx;
}
.status-paid {
background-color: #e8f5e9;
color: #4caf50;
}
.status-pending {
background-color: #fff3e0;
color: #ff9800;
}
.status-cancelled {
background-color: #f5f5f5;
color: #999;
}
.status-refunded {
background-color: #ffebee;
color: #f44336;
}
.order-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
}
.order-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.order-name {
font-size: 28rpx;
color: #2c2c2c;
font-weight: 500;
}
.order-no {
font-size: 20rpx;
color: #999;
}
.order-price {
display: flex;
align-items: baseline;
}
.order-price-symbol {
font-size: 24rpx;
color: #8b2323;
font-weight: 700;
}
.order-price-amount {
font-size: 36rpx;
color: #8b2323;
font-weight: 700;
}
/* Actions */
.order-actions {
display: flex;
justify-content: flex-end;
gap: 16rpx;
padding: 0 24rpx 24rpx;
}
.order-action-btn {
padding: 12rpx 32rpx;
border-radius: 8rpx;
border: 1rpx solid #e5e5e5;
}
.order-action-text {
font-size: 24rpx;
color: #666;
}
.order-action-cancel {
background-color: #fff;
}
.order-action-pay {
background-color: #8b2323;
border-color: #8b2323;
}
.order-action-pay .order-action-text {
color: #fff;
}
.order-action-view {
background-color: #fff;
}
/* Load More */
.orders-loadmore {
text-align: center;
padding: 32rpx 0;
}
.orders-loadmore-text {
font-size: 24rpx;
color: #999;
}
</style>

View File

@@ -0,0 +1,6 @@
<template>
<view class="h-full flex items-center justify-center text-[#5a5a5a]">
<text>已替换为 ProfileScreen.vue</text>
</view>
</template>

View File

@@ -0,0 +1,284 @@
<template>
<view class="privacy-screen">
<view class="privacy-bg"></view>
<!-- 状态栏占位 -->
<view class="status-bar-placeholder"></view>
<!-- Header -->
<view class="privacy-header">
<view class="privacy-back-btn" @click="handleBack">
<text class="privacy-back-icon"></text>
</view>
<text class="privacy-title">隐私政策</text>
<view class="privacy-header-placeholder"></view>
</view>
<!-- Content -->
<scroll-view scroll-y class="privacy-content">
<view class="privacy-content-inner">
<view v-if="loading" class="privacy-loading">
<text class="privacy-loading-text">加载中...</text>
</view>
<template v-else-if="policy">
<!-- 标题和版本信息 -->
<view class="privacy-header-info">
<text class="privacy-doc-title">{{ policy.title }}</text>
<view class="privacy-meta">
<text class="privacy-meta-item">版本{{ policy.version }}</text>
<text class="privacy-meta-item">生效日期{{ formatDate(policy.effective_date) }}</text>
<text class="privacy-meta-item">更新时间{{ formatDate(policy.updated_at) }}</text>
</view>
</view>
<!-- 内容 -->
<view class="privacy-body">
<rich-text :nodes="formattedContent" class="privacy-rich-text"></rich-text>
</view>
</template>
<view v-else class="privacy-empty">
<text class="privacy-empty-icon">📄</text>
<text class="privacy-empty-text">暂无隐私政策</text>
</view>
<!-- 底部提示 -->
<view v-if="policy" class="privacy-footer">
<text class="privacy-footer-text">如有疑问请联系客服</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { userApi } from "@/api";
declare const uni: any;
const emit = defineEmits<{
back: [];
}>();
const loading = ref(false);
const policy = ref<any>(null);
// 格式化内容将换行符转换为HTML
const formattedContent = computed(() => {
if (!policy.value?.content) return '';
// 简单的文本格式化:将换行符转换为<br>,段落添加样式
let content = policy.value.content;
// 如果内容包含HTML标签直接返回
if (content.includes('<')) {
return content;
}
// 否则进行简单格式化
content = content
.replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>')
.replace(/^(.+)$/, '<p>$1</p>');
return content;
});
const loadPrivacyPolicy = async () => {
loading.value = true;
try {
const res = await userApi.getPrivacyPolicy();
console.log('getPrivacyPolicy response:', res);
policy.value = res;
} catch (e: any) {
console.error('loadPrivacyPolicy error:', e);
uni.showToast({ title: e.msg || "加载失败", icon: "none" });
} finally {
loading.value = false;
}
};
const formatDate = (dateStr: string) => {
if (!dateStr) return "";
return dateStr.split("T")[0].replace(/-/g, ".");
};
const handleBack = () => {
emit('back');
};
onMounted(() => loadPrivacyPolicy());
</script>
<style scoped>
.privacy-screen {
height: 100%;
display: flex;
flex-direction: column;
background-color: #f0efe9;
position: relative;
}
.status-bar-placeholder {
height: var(--status-bar-height, 0);
width: 100%;
flex-shrink: 0;
}
.privacy-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
opacity: 0.3;
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
}
/* Header */
.privacy-header {
position: relative;
z-index: 10;
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
border-bottom: 1rpx solid #dcd3c9;
background-color: rgba(253, 251, 247, 0.8);
backdrop-filter: blur(10rpx);
}
.privacy-back-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
padding: 0;
margin-left: -16rpx;
}
.privacy-back-icon {
font-size: 48rpx;
color: #5a5a5a;
font-weight: 300;
}
.privacy-title {
font-size: 32rpx;
font-weight: 700;
color: #2c2c2c;
letter-spacing: 0.2em;
}
.privacy-header-placeholder {
width: 64rpx;
}
/* Content */
.privacy-content {
flex: 1;
height: 0;
position: relative;
z-index: 10;
}
.privacy-content-inner {
padding: 32rpx;
}
/* Loading */
.privacy-loading {
display: flex;
align-items: center;
justify-content: center;
height: 400rpx;
}
.privacy-loading-text {
font-size: 28rpx;
color: #999;
}
/* Header Info */
.privacy-header-info {
background-color: #fffdf9;
border-radius: 24rpx;
border: 1rpx solid #e5e5e5;
padding: 32rpx;
margin-bottom: 24rpx;
}
.privacy-doc-title {
font-size: 36rpx;
font-weight: 700;
color: #2c2c2c;
display: block;
margin-bottom: 24rpx;
text-align: center;
}
.privacy-meta {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.privacy-meta-item {
font-size: 24rpx;
color: #999;
text-align: center;
}
/* Body */
.privacy-body {
background-color: #fffdf9;
border-radius: 24rpx;
border: 1rpx solid #e5e5e5;
padding: 32rpx;
margin-bottom: 24rpx;
}
.privacy-rich-text {
font-size: 28rpx;
color: #2c2c2c;
line-height: 1.8;
}
/* Empty */
.privacy-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400rpx;
}
.privacy-empty-icon {
font-size: 96rpx;
opacity: 0.3;
margin-bottom: 24rpx;
}
.privacy-empty-text {
font-size: 28rpx;
color: #999;
}
/* Footer */
.privacy-footer {
text-align: center;
padding: 32rpx 0;
}
.privacy-footer-text {
font-size: 24rpx;
color: #999;
}
</style>

View File

@@ -0,0 +1,415 @@
<template>
<view class="reports-screen">
<view class="reports-bg"></view>
<!-- 状态栏占位 -->
<view class="status-bar-placeholder"></view>
<!-- Header -->
<view class="reports-header">
<view class="reports-back-btn" @click="$emit('back')">
<text class="reports-back-icon"></text>
</view>
<text class="reports-title">已解锁报告</text>
<view class="reports-header-placeholder"></view>
</view>
<!-- Content -->
<scroll-view scroll-y class="reports-list">
<template v-if="reports.length > 0">
<view v-for="(item, index) in reports" :key="item.id" class="reports-item"
:style="{ animationDelay: index * 0.1 + 's' }">
<!-- Card Header accent -->
<view class="reports-item-accent"
:class="item.type === 'fortune' ? 'reports-item-accent-gold' : 'reports-item-accent-red'"></view>
<view class="reports-item-body">
<!-- Icon -->
<view class="reports-item-icon"
:class="item.type === 'fortune' ? 'reports-item-icon-gold' : 'reports-item-icon-red'">
<text class="reports-item-icon-text"></text>
</view>
<view class="reports-item-content">
<text class="reports-item-title">{{ item.title }}</text>
<view class="reports-item-meta">
<text class="reports-item-date">{{ item.date }}</text>
<view class="reports-item-divider"></view>
<text class="reports-item-price">已支付 {{ item.price }}</text>
</view>
<view class="reports-item-actions">
<view class="reports-btn-download">
<text class="reports-btn-download-icon"></text>
<text class="reports-btn-download-text">下载PDF</text>
</view>
<view class="reports-btn-preview">
<text class="reports-btn-preview-text">在线预览</text>
</view>
</view>
</view>
</view>
</view>
</template>
<template v-else>
<view class="reports-empty">
<text class="reports-empty-icon">🔒</text>
<text class="reports-empty-text">暂无解锁报告</text>
</view>
</template>
<!-- Upsell Banner -->
<view class="reports-upsell">
<view class="reports-upsell-glow"></view>
<view class="reports-upsell-content">
<view class="reports-upsell-left">
<view class="reports-upsell-title-row">
<text class="reports-upsell-crown">👑</text>
<text class="reports-upsell-title">解锁更多财运玄机</text>
</view>
<text class="reports-upsell-desc">助您掌握流年运势趋吉避凶</text>
</view>
<view class="reports-upsell-btn" @click="$emit('navigate', 'test')">
<text class="reports-upsell-btn-text">去测算</text>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
interface ReportItem {
id: string;
title: string;
type: 'fortune' | 'naming' | 'renaming';
date: string;
price: string;
}
defineEmits<{
back: [];
navigate: [screen: string];
}>();
const reports: ReportItem[] = [
{ id: '1', title: '2024年度个人财运深度解析', type: 'fortune', date: '2023-12-12', price: '¥666' },
{ id: '2', title: '"宏图科技"品牌改名运势报告', type: 'renaming', date: '2023-11-20', price: '¥888' },
];
</script>
<style scoped>
.reports-screen {
height: 100%;
display: flex;
flex-direction: column;
background-color: #f0efe9;
position: relative;
}
.status-bar-placeholder {
height: var(--status-bar-height, 0);
width: 100%;
flex-shrink: 0;
}
.reports-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
opacity: 0.3;
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
}
/* Header */
.reports-header {
position: relative;
z-index: 10;
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
border-bottom: 1rpx solid #dcd3c9;
background-color: rgba(253, 251, 247, 0.8);
}
.reports-back-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
padding: 0;
margin-left: -16rpx;
}
.reports-back-icon {
font-size: 48rpx;
color: #5a5a5a;
font-weight: 300;
}
.reports-title {
font-size: 32rpx;
font-weight: 700;
color: #2c2c2c;
letter-spacing: 0.2em;
}
.reports-header-placeholder {
width: 64rpx;
}
/* List */
.reports-list {
flex: 1;
height: 0;
position: relative;
z-index: 10;
}
.reports-item {
background-color: #fffdf9;
border-radius: 24rpx;
border: 1rpx solid #e5e5e5;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
margin-bottom: 24rpx;
overflow: hidden;
animation: fadeInUp 0.3s ease-out forwards;
opacity: 0;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.reports-item-accent {
height: 8rpx;
width: 100%;
}
.reports-item-accent-gold {
background-color: #d4af37;
}
.reports-item-accent-red {
background-color: #8b2323;
}
.reports-item-body {
padding: 32rpx;
display: flex;
align-items: flex-start;
gap: 24rpx;
}
.reports-item-icon {
width: 96rpx;
height: 112rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.reports-item-icon-gold {
background: linear-gradient(180deg, #d4af37 0%, #c4a130 100%);
}
.reports-item-icon-red {
background: linear-gradient(180deg, #8b2323 0%, #701c1c 100%);
}
.reports-item-icon-text {
font-size: 40rpx;
color: #fff;
font-weight: 700;
font-family: SimSun, "Songti SC", serif;
}
.reports-item-content {
flex: 1;
min-width: 0;
}
.reports-item-title {
font-size: 28rpx;
font-weight: 700;
color: #2c2c2c;
line-height: 1.4;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.reports-item-meta {
display: flex;
align-items: center;
gap: 16rpx;
margin-top: 8rpx;
margin-bottom: 24rpx;
}
.reports-item-date,
.reports-item-price {
font-size: 20rpx;
color: #999;
}
.reports-item-divider {
width: 2rpx;
height: 20rpx;
background-color: #e5e5e5;
}
.reports-item-actions {
display: flex;
gap: 16rpx;
}
.reports-btn-download {
flex: 1;
background-color: #2c2c2c;
color: #f2e6d8;
padding: 16rpx 0;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
border: none;
}
.reports-btn-download-icon {
font-size: 24rpx;
}
.reports-btn-download-text {
font-size: 24rpx;
}
.reports-btn-preview {
flex: 1;
background-color: transparent;
border: 1rpx solid #e5e5e5;
padding: 16rpx 0;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
}
.reports-btn-preview-text {
font-size: 24rpx;
color: #5a5a5a;
}
/* Empty State */
.reports-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400rpx;
color: #999;
}
.reports-empty-icon {
font-size: 96rpx;
opacity: 0.2;
margin-bottom: 24rpx;
}
.reports-empty-text {
font-size: 28rpx;
}
/* Upsell Banner */
.reports-upsell {
margin-top: 48rpx;
padding: 32rpx;
background-color: #2c2c2c;
border-radius: 24rpx;
position: relative;
overflow: hidden;
}
.reports-upsell-glow {
position: absolute;
top: 0;
right: 0;
width: 200rpx;
height: 200rpx;
background-color: #d4af37;
border-radius: 50%;
filter: blur(100rpx);
opacity: 0.2;
pointer-events: none;
}
.reports-upsell-content {
position: relative;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
}
.reports-upsell-left {
flex: 1;
}
.reports-upsell-title-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 8rpx;
}
.reports-upsell-crown {
font-size: 32rpx;
}
.reports-upsell-title {
font-size: 28rpx;
font-weight: 700;
color: #d4af37;
}
.reports-upsell-desc {
font-size: 24rpx;
color: rgba(242, 230, 216, 0.7);
}
.reports-upsell-btn {
background-color: #d4af37;
padding: 16rpx 32rpx;
border-radius: 8rpx;
border: none;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
}
.reports-upsell-btn-text {
font-size: 24rpx;
font-weight: 700;
color: #2c2c2c;
}
</style>

View File

@@ -0,0 +1,433 @@
<template>
<view class="settings-screen">
<view class="settings-bg"></view>
<!-- 状态栏占位 -->
<view class="status-bar-placeholder"></view>
<!-- Header -->
<view class="settings-header">
<view class="settings-back-btn" @click="handleBack">
<text class="settings-back-icon"></text>
</view>
<text class="settings-title">设置与反馈</text>
<view class="settings-header-placeholder"></view>
</view>
<!-- Content -->
<scroll-view scroll-y class="settings-content">
<view class="settings-content-inner">
<!-- Section 1: 偏好设置 -->
<!-- <view class="settings-section">
<view class="settings-item settings-item-border">
<view class="settings-item-left">
<text class="settings-item-icon">🔔</text>
<text class="settings-item-label">推送通知</text>
</view>
<view class="settings-switch" :class="{ 'settings-switch-active': pushEnabled }"
@click="togglePush">
<view class="settings-switch-thumb"
:class="{ 'settings-switch-thumb-active': pushEnabled }"></view>
</view>
</view>
<view class="settings-item">
<view class="settings-item-left">
<text class="settings-item-icon">🔊</text>
<text class="settings-item-label">音效反馈</text>
</view>
<view class="settings-switch" :class="{ 'settings-switch-active': soundEnabled }"
@click="toggleSound">
<view class="settings-switch-thumb"
:class="{ 'settings-switch-thumb-active': soundEnabled }"></view>
</view>
</view>
</view> -->
<!-- Section 2: 支持 -->
<view class="settings-section">
<view class="settings-item settings-item-border settings-item-clickable" @click="handleFeedback">
<view class="settings-item-left">
<text class="settings-item-icon">💬</text>
<text class="settings-item-label">意见反馈</text>
</view>
<text class="settings-item-arrow"></text>
</view>
<view class="settings-item settings-item-border settings-item-clickable" @click="handleFAQ">
<view class="settings-item-left">
<text class="settings-item-icon"></text>
<text class="settings-item-label">常见问题</text>
</view>
<text class="settings-item-arrow"></text>
</view>
<view class="settings-item settings-item-clickable" @click="handlePrivacy">
<view class="settings-item-left">
<text class="settings-item-icon">🛡</text>
<text class="settings-item-label">隐私政策</text>
</view>
<text class="settings-item-arrow"></text>
</view>
</view>
<!-- Section 3: 快速反馈 -->
<view class="settings-feedback-section">
<text class="settings-feedback-title">快速反馈</text>
<textarea class="settings-feedback-textarea" v-model="feedbackText" placeholder="您遇到的问题或建议..."
:maxlength="500" />
<view class="settings-feedback-btn" @click="submitFeedback">
<text class="settings-feedback-btn-text">提交反馈</text>
</view>
</view>
<!-- 退出登录 -->
<view class="settings-logout-btn" @click="handleLogout">
<text class="settings-logout-text">退出登录</text>
</view>
<!-- 版本信息 -->
<view class="settings-version">
<text class="settings-version-text">当前版本 v1.0.2</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { userApi } from "@/api";
import { logout } from "@/utils/auth";
declare const uni: any;
const emit = defineEmits<{
back: [];
navigate: [screen: string];
}>();
const pushEnabled = ref(true);
const soundEnabled = ref(true);
const feedbackText = ref("");
const togglePush = () => {
pushEnabled.value = !pushEnabled.value;
uni.showToast({
title: pushEnabled.value ? "已开启推送通知" : "已关闭推送通知",
icon: "none"
});
};
const toggleSound = () => {
soundEnabled.value = !soundEnabled.value;
uni.showToast({
title: soundEnabled.value ? "已开启音效反馈" : "已关闭音效反馈",
icon: "none"
});
};
const handleFeedback = () => {
emit('navigate', 'feedback');
};
const handleFAQ = () => {
emit('navigate', 'faq');
};
const handlePrivacy = () => {
emit('navigate', 'privacy');
};
const submitFeedback = async () => {
if (!feedbackText.value.trim()) {
uni.showToast({ title: "请输入反馈内容", icon: "none" });
return;
}
try {
await userApi.submitFeedback({
content: feedbackText.value.trim(),
feedback_type: 'other'
});
uni.showToast({ title: "感谢您的反馈!", icon: "success" });
feedbackText.value = "";
} catch (error: any) {
uni.showToast({
title: error.msg || "提交失败,请稍后重试",
icon: "none"
});
}
};
const handleLogout = () => {
// Web 环境使用 confirmuni-app 环境使用 showModal
if (typeof uni?.showModal === "function") {
uni.showModal({
title: "提示",
content: "确定要退出登录吗?",
success: (res: any) => {
if (res.confirm) {
logout();
uni.showToast({ title: "已退出登录", icon: "success" });
}
},
});
} else {
const confirmed = confirm("确定要退出登录吗?");
if (confirmed) {
logout();
if (typeof uni?.showToast === "function") {
uni.showToast({ title: "已退出登录", icon: "success" });
}
}
}
};
const handleBack = () => {
emit('back');
};
</script>
<style scoped>
.settings-screen {
height: 100%;
display: flex;
flex-direction: column;
background-color: #f0efe9;
position: relative;
}
.status-bar-placeholder {
height: var(--status-bar-height, 0);
width: 100%;
flex-shrink: 0;
}
.settings-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
opacity: 0.3;
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
}
/* Header */
.settings-header {
position: relative;
z-index: 10;
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
border-bottom: 1rpx solid #dcd3c9;
background-color: rgba(253, 251, 247, 0.8);
backdrop-filter: blur(10rpx);
}
.settings-back-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
padding: 0;
margin-left: -16rpx;
}
.settings-back-icon {
font-size: 48rpx;
color: #5a5a5a;
font-weight: 300;
}
.settings-title {
font-size: 32rpx;
font-weight: 700;
color: #2c2c2c;
letter-spacing: 0.2em;
}
.settings-header-placeholder {
width: 64rpx;
}
/* Content */
.settings-content {
flex: 1;
height: 0;
position: relative;
z-index: 10;
}
.settings-content-inner {
padding: 32rpx;
}
/* Section */
.settings-section {
background-color: #fffdf9;
border-radius: 24rpx;
border: 1rpx solid #e5e5e5;
overflow: hidden;
margin-bottom: 32rpx;
}
.settings-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 32rpx;
}
.settings-item-border {
border-bottom: 1rpx solid #f0f0f0;
}
.settings-item-clickable {
transition: background-color 0.2s;
}
.settings-item-clickable:active {
background-color: #fafafa;
}
.settings-item-left {
display: flex;
align-items: center;
gap: 24rpx;
}
.settings-item-icon {
font-size: 32rpx;
}
.settings-item-label {
font-size: 28rpx;
color: #2c2c2c;
}
.settings-item-arrow {
font-size: 32rpx;
color: #ccc;
}
/* Switch */
.settings-switch {
width: 80rpx;
height: 40rpx;
border-radius: 40rpx;
background-color: #ccc;
position: relative;
transition: background-color 0.3s;
}
.settings-switch-active {
background-color: #8b2323;
}
.settings-switch-thumb {
position: absolute;
top: 4rpx;
left: 4rpx;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
background-color: #fff;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
transition: left 0.3s;
}
.settings-switch-thumb-active {
left: 44rpx;
}
/* Feedback Section */
.settings-feedback-section {
margin-bottom: 32rpx;
}
.settings-feedback-title {
font-size: 24rpx;
font-weight: 700;
color: #8b2323;
margin-bottom: 16rpx;
display: block;
padding: 0 8rpx;
}
.settings-feedback-textarea {
width: 100%;
height: 192rpx;
background-color: #fffdf9;
border: 1rpx solid #e5e5e5;
border-radius: 24rpx;
padding: 24rpx;
font-size: 28rpx;
color: #2c2c2c;
box-sizing: border-box;
}
.settings-feedback-btn {
width: 100%;
margin-top: 16rpx;
padding: 24rpx 0;
background-color: #2c2c2c;
border-radius: 16rpx;
border: none;
display: flex;
justify-content: center;
}
.settings-feedback-btn-text {
font-size: 24rpx;
font-weight: 700;
color: #f2e6d8;
}
/* Logout Button */
.settings-logout-btn {
width: 100%;
padding: 24rpx 0;
background-color: #fffdf9;
border: 1rpx solid #e5e5e5;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
margin-bottom: 32rpx;
transition: background-color 0.2s;
}
.settings-logout-btn:active {
background-color: #fff5f5;
}
.settings-logout-icon {
font-size: 28rpx;
}
.settings-logout-text {
font-size: 28rpx;
font-weight: 700;
color: #8b2323;
}
/* Version */
.settings-version {
text-align: center;
padding: 32rpx 0;
}
.settings-version-text {
font-size: 20rpx;
color: #ccc;
}
</style>

View File

@@ -0,0 +1,248 @@
<template>
<view class="userinfo-screen">
<view class="userinfo-bg"></view>
<view class="status-bar-placeholder"></view>
<view class="userinfo-header">
<view class="userinfo-back-btn" @click="handleBack">
<text class="userinfo-back-icon"></text>
</view>
<text class="userinfo-title">我的信息</text>
<view class="userinfo-header-placeholder"></view>
</view>
<scroll-view scroll-y class="userinfo-content">
<view class="userinfo-inner">
<view class="userinfo-section">
<text class="userinfo-label">用户名</text>
<input
v-model="username"
class="userinfo-input"
type="text"
maxlength="32"
placeholder="请输入用户名"
/>
</view>
<view class="userinfo-section userinfo-section-mt">
<text class="userinfo-label">手机号</text>
<input
v-model="mobile"
class="userinfo-input"
type="tel"
maxlength="11"
placeholder="请输入手机号"
/>
</view>
<view class="userinfo-save" @click="handleSave">
<text class="userinfo-save-text">{{ saving ? "保存中…" : "保存" }}</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { getUserInfo, setUserInfo } from "@/utils/auth";
import { userApi } from "@/api";
declare const uni: any;
const emit = defineEmits<{
back: [];
}>();
const username = ref("");
const mobile = ref("");
const saving = ref(false);
const isValidPhone = (phone: string) => /^1[3-9]\d{9}$/.test(phone);
const loadFromStorage = () => {
const info = getUserInfo();
if (!info || typeof info !== "object") return;
const u = info as Record<string, unknown>;
username.value = String(
u.username ?? u.name ?? ""
).trim();
mobile.value = String(u.mobile ?? u.phone ?? "").trim();
};
onMounted(() => {
loadFromStorage();
});
const handleBack = () => {
emit("back");
};
const handleSave = async () => {
const name = username.value.trim();
const tel = mobile.value.trim();
if (!name) {
uni.showToast({ title: "请输入用户名", icon: "none" });
return;
}
if (!tel) {
uni.showToast({ title: "请输入手机号", icon: "none" });
return;
}
if (!isValidPhone(tel)) {
uni.showToast({ title: "请输入正确的手机号", icon: "none" });
return;
}
if (saving.value) return;
saving.value = true;
try {
await userApi.updateCurrentUserUsernameMobile({
username: name,
mobile: tel,
});
// 仅把表单结果合并进已有 userInfo不把接口 data 整包写入,避免覆盖/清空头像、id 等字段
const prev = getUserInfo();
if (!prev || typeof prev !== "object") {
uni.showToast({ title: "登录状态异常,请重新登录", icon: "none" });
return;
}
setUserInfo({
...prev,
nickname: name,
name: name,
username: name,
mobile: tel,
phone: tel,
});
uni.showToast({ title: "已保存", icon: "success" });
} catch (e: any) {
uni.showToast({
title: e?.msg || e?.message || "保存失败,请稍后重试",
icon: "none",
});
} finally {
saving.value = false;
}
};
</script>
<style scoped>
.userinfo-screen {
height: 100%;
display: flex;
flex-direction: column;
background-color: #f0efe9;
position: relative;
}
.status-bar-placeholder {
height: var(--status-bar-height, 0);
width: 100%;
flex-shrink: 0;
}
.userinfo-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
opacity: 0.3;
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
}
.userinfo-header {
position: relative;
z-index: 10;
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
border-bottom: 1rpx solid #dcd3c9;
background-color: rgba(253, 251, 247, 0.8);
backdrop-filter: blur(10rpx);
}
.userinfo-back-btn {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: -16rpx;
}
.userinfo-back-icon {
font-size: 48rpx;
color: #5a5a5a;
font-weight: 300;
}
.userinfo-title {
font-size: 32rpx;
font-weight: 700;
color: #2c2c2c;
letter-spacing: 0.2em;
}
.userinfo-header-placeholder {
width: 64rpx;
}
.userinfo-content {
flex: 1;
height: 0;
position: relative;
z-index: 10;
}
.userinfo-inner {
padding: 32rpx;
}
.userinfo-section {
background-color: #fffdf9;
border-radius: 24rpx;
border: 1rpx solid #e5e5e5;
padding: 24rpx 32rpx;
}
.userinfo-section-mt {
margin-top: 24rpx;
}
.userinfo-label {
display: block;
font-size: 24rpx;
color: #888;
margin-bottom: 16rpx;
}
.userinfo-input {
width: 100%;
font-size: 30rpx;
color: #2c2c2c;
border: none;
background: transparent;
padding: 0;
}
.userinfo-save {
margin-top: 48rpx;
height: 88rpx;
border-radius: 44rpx;
background: linear-gradient(135deg, #8b7355, #6b5344);
display: flex;
align-items: center;
justify-content: center;
}
.userinfo-save-text {
font-size: 30rpx;
color: #fffdf9;
font-weight: 600;
letter-spacing: 0.15em;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,190 @@
<template>
<view class="renaming-detail">
<view class="detail-header">
<view class="detail-header-back" @click="$emit('back')">
<text class="back-icon"></text>
<text class="back-text">返回</text>
</view>
<text class="detail-header-title">改名详解</text>
<view class="detail-header-placeholder"></view>
</view>
<scroll-view scroll-y class="detail-content">
<view class="hero-card">
<text class="hero-name">{{ data?.name || '新名' }}</text>
<text class="hero-pinyin">{{ data?.pinyin || '' }}</text>
</view>
<view class="section">
<view class="section-title">
<text class="section-icon">📖</text>
<text class="section-text">寓意</text>
</view>
<view class="card">
<text class="card-text">{{ data?.meaning || '' }}</text>
</view>
</view>
<view class="section">
<view class="section-title">
<text class="section-icon">🪶</text>
<text class="section-text">出处</text>
</view>
<view class="card">
<text class="card-text">{{ data?.source || '' }}</text>
</view>
</view>
<view class="section" v-if="data?.tags?.length">
<view class="section-title">
<text class="section-icon">🏷</text>
<text class="section-text">标签</text>
</view>
<view class="tag-row">
<text v-for="(t, i) in data.tags" :key="i" class="tag">{{ t }}</text>
</view>
</view>
<view class="bottom-spacer"></view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
type Mode = 'personal' | 'company';
defineProps<{
data: any;
mode?: Mode;
}>();
defineEmits<{
back: [];
}>();
</script>
<style scoped>
.renaming-detail {
height: 100%;
display: flex;
flex-direction: column;
background: #f0efe9;
font-family: SimSun, "Songti SC", "Songti TC", "Noto Serif SC", STSong, serif;
}
.detail-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 32rpx;
border-bottom: 1px solid #eaddcf;
background: rgba(253, 251, 247, 0.9);
}
.detail-header-back {
display: flex;
align-items: center;
gap: 10rpx;
color: #8b2323;
}
.back-icon {
font-size: 22px;
line-height: 1;
}
.back-text {
font-size: 14px;
font-weight: 700;
}
.detail-header-title {
font-size: 16px;
font-weight: 700;
color: #2c2c2c;
letter-spacing: 0.2em;
}
.detail-header-placeholder {
width: 80rpx;
}
.detail-content {
flex: 1;
height: 0;
}
.hero-card {
margin: 32rpx;
padding: 32rpx;
border-radius: 16rpx;
border: 1px solid #dcd3c9;
background: #fdfbf7;
}
.hero-name {
font-size: 28px;
font-weight: 700;
color: #8b2323;
display: block;
margin-bottom: 8rpx;
}
.hero-pinyin {
font-size: 12px;
color: #5a5a5a;
}
.section {
margin: 0 32rpx 28rpx;
}
.section-title {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 12rpx;
}
.section-icon {
font-size: 14px;
}
.section-text {
font-size: 14px;
font-weight: 700;
color: #2c2c2c;
}
.card {
padding: 24rpx;
border-radius: 16rpx;
border: 1px solid #dcd3c9;
background: #f9f7f2;
}
.card-text {
font-size: 14px;
color: #2c2c2c;
line-height: 1.7;
}
.tag-row {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.tag {
font-size: 10px;
padding: 6rpx 10rpx;
border-radius: 8rpx;
border: 1px solid rgba(139, 35, 35, 0.2);
color: #8b2323;
background: rgba(139, 35, 35, 0.04);
}
.bottom-spacer {
height: 80rpx;
}
</style>

View File

@@ -0,0 +1,11 @@
<template>
<view class="h-full flex flex-col items-center justify-center bg-[#fdfbf7] text-[#2c2c2c] font-serif relative overflow-hidden">
<view class="absolute inset-0 opacity-10 pointer-events-none bg-[url('https://www.transparenttextures.com/patterns/rice-paper.png')]"></view>
<view class="relative z-10 text-center px-8">
<text class="block text-4xl mb-3 text-[#d4af37]"></text>
<text class="block text-xl font-bold tracking-[0.3em] mb-2">改名/重塑模块</text>
<text class="block text-sm text-[#5a5a5a]">稍后将迁移完整改名流程与报告</text>
</view>
</view>
</template>

View File

@@ -0,0 +1,917 @@
<template>
<view class="testname-screen">
<!-- 背景纹理 -->
<view class="testname-bg-texture"></view>
<!-- 状态栏占位 -->
<view class="status-bar-placeholder"></view>
<!-- 顶部装饰 -->
<view class="testname-top-bar"></view>
<view class="testname-container">
<!-- 标题区 -->
<view class="testname-header">
<text class="testname-title">八字测名</text>
<!-- Mode Toggle -->
<view class="testname-mode-toggle">
<view class="testname-mode-toggle-bg">
<!-- Active Indicator -->
<view class="testname-mode-toggle-slider"
:style="{ left: mode === 'personal' ? '4px' : 'calc(50% + 2px)', width: 'calc(50% - 6px)' }" />
<view class="testname-mode-toggle-btn" :class="{ 'testname-mode-toggle-btn-active': mode === 'personal' }"
@click="mode = 'personal'">
<text>个人</text>
</view>
<view class="testname-mode-toggle-btn" :class="{ 'testname-mode-toggle-btn-active': mode === 'company' }"
@click="mode = 'company'">
<text>公司</text>
</view>
</view>
</view>
</view>
<!-- 主表单卡片 -->
<view class="testname-form-card" :class="{ 'testname-form-card-company': mode === 'company' }">
<!-- 四角装饰纹样 -->
<view v-for="(corner, i) in corners" :key="i" class="testname-corner" :style="corner"></view>
<!-- 个人表单 -->
<view v-if="mode === 'personal'" class="testname-form-personal">
<view class="testname-name-row">
<view class="testname-name-group">
<text class="testname-label">姓氏</text>
<view class="testname-input-wrapper testname-input-wrapper-focus">
<input v-model="personalData.lastName" type="text" class="testname-input-name" placeholder="李" />
</view>
</view>
<view class="testname-name-group">
<text class="testname-label">名字</text>
<view class="testname-input-wrapper testname-input-wrapper-focus">
<input v-model="personalData.firstName" type="text" class="testname-input-name" placeholder="逍遥" />
</view>
</view>
</view>
<view class="testname-gender-section">
<text class="testname-label-center">性别</text>
<view class="testname-gender-group">
<view class="testname-gender-btn"
:class="{ 'testname-gender-btn-active': personalData.gender === 'male' }"
@click="personalData.gender = 'male'">
<text class="testname-gender-symbol"></text>
<text class="testname-gender-label"></text>
</view>
<view class="testname-gender-btn"
:class="{ 'testname-gender-btn-active': personalData.gender === 'female' }"
@click="personalData.gender = 'female'">
<text class="testname-gender-symbol"></text>
<text class="testname-gender-label"></text>
</view>
</view>
</view>
<view class="testname-date-section">
<view class="testname-label-with-icon">
<CalendarIcon :size="14" class="testname-icon" />
<text class="testname-label" style="margin-bottom: 0px;">生辰</text>
</view>
<view class="testname-date-picker-trigger" @click="activeDateField = 'personal'">
<text class="testname-date-picker-text"
:class="{ 'testname-date-picker-text-filled': personalData.birthDateDisplay }">
{{ personalData.birthDateDisplay || '请择生辰' }}
</text>
<view class="testname-date-picker-arrow">
<ChevronDownIcon :size="16" />
</view>
</view>
</view>
</view>
<!-- 公司表单 -->
<view v-else class="testname-form-company">
<!-- 基础信息 -->
<view class="testname-company-basic">
<view class="testname-company-field">
<view class="testname-label-with-icon">
<HomeIcon :size="14" class="testname-icon" />
<text class="testname-label" style="margin-bottom: 0px;">公司名称</text>
</view>
<input v-model="companyData.companyName" type="text" class="testname-input-company"
placeholder="例:鼎盛科技" />
</view>
<view class="testname-company-field">
<view class="testname-label-with-icon">
<HomeIcon :size="14" class="testname-icon" />
<text class="testname-label" style="margin-bottom: 0px;">主营业务 / 行业</text>
</view>
<input v-model="companyData.industry" type="text" class="testname-input-company"
placeholder="例:科技、餐饮、文化..." />
</view>
<view class="testname-company-row">
<view class="testname-company-field testname-company-field-half">
<text class="testname-label">经营地址</text>
<input v-model="companyData.address" type="text" class="testname-input-company" placeholder="城市/方位" />
</view>
<view class="testname-company-field testname-company-field-half">
<text class="testname-label">服务群体</text>
<input v-model="companyData.targetAudience" type="text" class="testname-input-company"
placeholder="年轻人、高端..." />
</view>
</view>
</view>
<view class="testname-divider"></view>
<!-- 核心成员 -->
<view class="testname-members-section">
<view class="testname-members-header">
<view class="testname-label-with-icon">
<ProfileIcon :size="14" class="testname-icon" />
<text class="testname-label" style="margin-bottom: 0px;">核心成员 (五行匹配)</text>
</view>
<text class="testname-members-tip">至少需填一位</text>
</view>
<scroll-view scroll-y class="testname-members-list">
<view v-for="(member, idx) in companyData.members" :key="idx" class="testname-member-item">
<view class="testname-member-number">{{ chNum[Number(idx) + 1] }}</view>
<input v-model="member.name" type="text" class="testname-member-name" placeholder="姓名" />
<view class="testname-member-divider"></view>
<view class="testname-member-date" :class="{ 'testname-member-date-filled': member.birthDate }"
@click="activeDateField = `member-${idx}`">
<text>{{ member.birthDate ? member.birthDate.split('年')[0] + '年...' : '选择诞辰' }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
<!-- 底部按钮 -->
<view class="testname-submit-section">
<button class="testname-submit-btn" :class="{ 'testname-submit-btn-disabled': !isValid }" @click="handleStart">
<view class="testname-submit-btn-content">
<SearchIcon :size="18" class="testname-submit-icon" />
<text class="testname-submit-text">立即排盘</text>
</view>
<view class="testname-submit-btn-border"></view>
</button>
</view>
<view class="testname-footer-tip">
<text class="testname-footer-text">
易经数理 · 五行生克 · {{ mode === 'personal' ? '三才五格' : '商号吉凶' }}
<text class="testname-footer-subtext">隐私保护您的信息仅用于本次测算不做留存</text>
</text>
</view>
</view>
<!-- 自定义日期选择器 Modal -->
<MysticDatePicker :is-open="!!activeDateField" :title="activeDateField === 'personal' ? '请择良辰' : '核心成员诞辰'"
:default-value="getDefaultValue()" @close="activeDateField = null" @confirm="handleDateConfirm" />
<!-- 加载界面 -->
<MysticCompass
v-if="isLoading"
:title="mode === 'personal' ? '正在推演命盘' : '正在测算商号'"
:subtitle="mode === 'personal' ? '易经数理 · 五行生克 · 三才五格' : '易经数理 · 五行生克 · 商号吉凶'"
:desktop="isDesktopLayout"
@back="handleLoadingBack"
/>
</view>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue';
import { getIsDesktopLayout } from '../../utils/device-layout';
import MysticDatePicker from '../MysticDatePicker.vue';
import MysticCompass from '../MysticCompass.vue';
import CalendarIcon from '../icons/CalendarIcon.vue';
import ProfileIcon from '../icons/ProfileIcon.vue';
import HomeIcon from '../icons/HomeIcon.vue';
import ChevronDownIcon from '../icons/ChevronDownIcon.vue';
import SearchIcon from '../icons/SearchIcon.vue';
interface CoreMember {
name: string;
birthDate: string;
birthDateApi: string;
}
interface PersonalTestParams {
lastName: string;
firstName: string;
gender: 'male' | 'female';
birthDate: string;
}
interface CompanyTestParams {
industry: string;
address: string;
target_audience: string;
members: Array<{ name: string; birth_date: string }>;
}
const emit = defineEmits<{
test: [mode: 'personal' | 'company', params: PersonalTestParams | CompanyTestParams];
}>();
type TestMode = 'personal' | 'company';
const mode = ref<TestMode>('personal');
const isLoading = ref(false);
const isDesktopLayout = ref(
typeof window !== 'undefined' ? getIsDesktopLayout() : false,
);
const syncDesktopLayout = () => {
if (typeof window === 'undefined') return;
isDesktopLayout.value = getIsDesktopLayout();
};
onMounted(() => {
syncDesktopLayout();
if (typeof window !== 'undefined') {
window.addEventListener('resize', syncDesktopLayout, { passive: true });
}
});
onUnmounted(() => {
if (typeof window !== 'undefined') {
window.removeEventListener('resize', syncDesktopLayout);
}
});
// 个人表单数据
const personalData = reactive({
lastName: '',
firstName: '',
gender: 'male' as 'male' | 'female',
birthDateDisplay: '',
birthDateApi: '' // 接口格式
});
// 公司表单数据
const companyData = reactive({
companyName: '',
industry: '',
address: '',
targetAudience: '',
members: Array(5).fill(null).map(() => ({ name: '', birthDate: '', birthDateApi: '' } as CoreMember))
});
// 日期选择器状态
const activeDateField = ref<string | null>(null);
// 中文数字
const chNum = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
// 四角装饰样式
const corners = [
{ top: '8px', left: '8px', borderTopWidth: '1px', borderLeftWidth: '1px', borderRightWidth: '0', borderBottomWidth: '0' },
{ top: '8px', right: '8px', borderTopWidth: '1px', borderRightWidth: '1px', borderLeftWidth: '0', borderBottomWidth: '0' },
{ bottom: '8px', left: '8px', borderBottomWidth: '1px', borderLeftWidth: '1px', borderTopWidth: '0', borderRightWidth: '0' },
{ bottom: '8px', right: '8px', borderBottomWidth: '1px', borderRightWidth: '1px', borderTopWidth: '0', borderLeftWidth: '0' }
];
const handleDateConfirm = (displayVal: string, apiVal: string) => {
if (!activeDateField.value) return;
if (activeDateField.value === 'personal') {
personalData.birthDateDisplay = displayVal;
personalData.birthDateApi = apiVal;
} else if (activeDateField.value.startsWith('member-')) {
const index = parseInt(activeDateField.value.split('-')[1]);
companyData.members[index].birthDate = displayVal;
companyData.members[index].birthDateApi = apiVal;
}
activeDateField.value = null;
};
const getDefaultValue = () => {
if (!activeDateField.value) return '';
if (activeDateField.value === 'personal') {
return personalData.birthDateDisplay || '';
} else if (activeDateField.value.startsWith('member-')) {
const index = parseInt(activeDateField.value.split('-')[1]);
return companyData.members[index].birthDate || '';
}
return '';
};
const isValid = computed(() => {
if (mode.value === 'personal') {
return personalData.lastName && personalData.firstName && personalData.birthDateDisplay;
} else {
return companyData.industry && companyData.address &&
companyData.members.some((m: CoreMember) => m.name && m.birthDate);
}
});
const handleStart = () => {
if (!isValid.value) {
uni.showToast({
title: mode.value === 'personal' ? '请填写完整个人信息以获取准确命盘' : '请至少填写主营业务、地址及一位核心成员信息',
icon: 'none'
});
return;
}
// 显示加载界面
isLoading.value = true;
// 触发提交事件,由父组件处理接口调用
if (mode.value === 'personal') {
emit('test', 'personal', {
lastName: personalData.lastName,
firstName: personalData.firstName,
gender: personalData.gender,
birthDate: personalData.birthDateApi
});
} else {
emit('test', 'company', {
companyName: companyData.companyName,
industry: companyData.industry,
address: companyData.address,
target_audience: companyData.targetAudience,
members: companyData.members
.filter((m: CoreMember) => m.name && m.birthDateApi)
.map((m: CoreMember) => ({
name: m.name,
birth_date: m.birthDateApi
}))
});
}
};
// 暴露方法供父组件调用
defineExpose({
closeLoading: () => {
isLoading.value = false;
}
});
// 处理 loading 页面的返回按钮
const handleLoadingBack = () => {
isLoading.value = false;
uni.showToast({
title: '测算结果可在"我的方案"中查看',
icon: 'none',
duration: 2000
});
};
</script>
<style scoped>
.testname-screen {
min-height: 100vh;
width: 100%;
font-family: SimSun, "Songti SC", "Songti TC", "Noto Serif SC", STSong, serif;
display: flex;
flex-direction: column;
align-items: center;
background: #fdfbf7;
position: relative;
overflow-x: hidden;
overflow-y: auto;
}
/* 状态栏占位 */
.status-bar-placeholder {
height: var(--status-bar-height, 0);
width: 100%;
flex-shrink: 0;
}
.testname-bg-texture {
position: absolute;
inset: 0;
opacity: 0.1;
pointer-events: none;
background-image: url("https://www.transparenttextures.com/patterns/rice-paper.png");
}
.testname-top-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: #8b2323;
opacity: 0.8;
}
.testname-container {
width: 100%;
max-width: 100%;
flex: 1;
display: flex;
flex-direction: column;
padding: 40px 20px 32px;
z-index: 10;
overflow-y: auto;
box-sizing: border-box;
}
/* Header */
.testname-header {
text-align: center;
margin-bottom: 32px;
}
.testname-title {
font-size: 28px;
font-weight: bold;
color: #2c2c2c;
letter-spacing: 0.3em;
margin-bottom: 20px;
font-family: SimSun, serif;
display: block;
}
.testname-mode-toggle {
display: flex;
justify-content: center;
margin-top: 0;
margin-bottom: 0;
}
.testname-mode-toggle-bg {
background: rgba(234, 221, 207, 0.5);
padding: 4px;
border-radius: 999px;
display: flex;
align-items: center;
position: relative;
border: 1px solid #dcd3c9;
}
.testname-mode-toggle-slider {
position: absolute;
top: 4px;
bottom: 4px;
background: #fffdf9;
border-radius: 999px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #dcd3c9;
transition: left 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.testname-mode-toggle-btn {
position: relative;
z-index: 10;
padding: 8px 32px;
font-size: 14px;
font-weight: bold;
letter-spacing: 0.24em;
color: #8a8a8a;
transition: color 0.3s;
cursor: pointer;
}
.testname-mode-toggle-btn-active {
color: #8b2323;
}
/* Form Card */
.testname-form-card {
background: #fffdf9;
padding: 24px;
border: 1px solid #eaddcf;
box-shadow: 0 4px 20px -10px rgba(0, 0, 0, 0.1);
position: relative;
margin-bottom: 32px;
}
.testname-corner {
position: absolute;
width: 16px;
height: 16px;
border-color: #8b2323;
opacity: 0.4;
border-style: solid;
}
/* Personal Form */
.testname-form-personal {
display: flex;
flex-direction: column;
gap: 32px;
margin-top: 8px;
}
.testname-name-row {
display: flex;
gap: 16px;
}
.testname-name-group {
flex: 1;
}
.testname-label {
display: block;
font-size: 13px;
color: #8a8a8a;
letter-spacing: 0.24em;
margin-bottom: 8px;
text-align: center;
}
.testname-label-center {
text-align: center;
font-size: 13px;
color: #8a8a8a;
letter-spacing: 0.24em;
margin-bottom: 12px;
}
.testname-label-with-icon {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
font-size: 12px;
color: #8a8a8a;
letter-spacing: 0.2em;
margin-bottom: 8px;
}
.testname-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
}
.testname-input-wrapper {
position: relative;
border-bottom: 2px solid #e5e5e5;
padding-bottom: 4px;
transition: border-color 0.3s;
}
.testname-input-wrapper-focus {
border-bottom-color: #8b2323;
}
.testname-input-name {
width: 100%;
background: transparent;
text-align: center;
font-size: 24px;
color: #2c2c2c;
font-family: SimSun, serif;
border: none;
outline: none;
}
.testname-input-name::placeholder {
color: #dcd3c9;
}
/* Gender Section */
.testname-gender-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.testname-gender-group {
display: flex;
align-items: center;
gap: 32px;
}
.testname-gender-btn {
width: 80px;
height: 80px;
border-radius: 50%;
border: 1px solid #dcd3c9;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: all 0.3s;
color: #5a5a5a;
cursor: pointer;
}
.testname-gender-btn-active {
border-color: #8b2323;
background: #8b2323;
color: #fdfbf7;
box-shadow: 0 2px 8px rgba(139, 35, 35, 0.2);
transform: scale(1.05);
}
.testname-gender-symbol {
font-size: 24px;
font-family: SimSun, serif;
font-weight: bold;
margin-bottom: 4px;
}
.testname-gender-label {
font-size: 14px;
letter-spacing: 0.24em;
opacity: 0.8;
}
/* Date Section */
.testname-date-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.testname-date-picker-trigger {
position: relative;
border: 1px solid #eaddcf;
background: #fcfaf5;
padding: 12px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: border-color 0.3s;
}
.testname-date-picker-trigger:active {
border-color: rgba(139, 35, 35, 0.5);
}
.testname-date-picker-text {
font-family: SimSun, serif;
font-size: 15px;
letter-spacing: 0.1em;
color: #dcd3c9;
}
.testname-date-picker-text-filled {
color: #2c2c2c;
font-weight: bold;
}
.testname-date-picker-arrow {
position: absolute;
right: 12px;
opacity: 0.5;
transition: opacity 0.3s;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.testname-date-picker-trigger:active .testname-date-picker-arrow {
opacity: 1;
}
/* Company Form */
.testname-form-company {
display: flex;
flex-direction: column;
gap: 24px;
margin-top: 8px;
}
.testname-company-basic {
display: flex;
flex-direction: column;
gap: 16px;
}
.testname-company-field {
display: flex;
flex-direction: column;
gap: 8px;
}
.testname-company-row {
display: flex;
gap: 12px;
}
.testname-company-field-half {
flex: 1;
}
.testname-input-company {
width: 100%;
background: #fcfaf5;
border: 1px solid #eaddcf;
padding: 10px;
font-size: 15px;
color: #2c2c2c;
outline: none;
transition: border-color 0.3s;
box-sizing: border-box;
}
.testname-input-company:focus {
border-color: #8b2323;
}
.testname-input-company::placeholder {
color: #dcd3c9;
}
.testname-divider {
width: 100%;
height: 1px;
background: #eaddcf;
opacity: 0.5;
margin: 8px 0;
}
/* Members Section */
.testname-members-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.testname-members-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.testname-members-tip {
font-size: 11px;
color: rgba(139, 35, 35, 0.6);
}
.testname-members-list {
max-height: 200px;
overflow-y: auto;
}
.testname-member-item {
display: flex;
align-items: center;
gap: 8px;
background: #fcfaf5;
border: 1px solid #eaddcf;
padding: 8px;
border-radius: 4px;
margin-bottom: 8px;
}
.testname-member-number {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(234, 221, 207, 0.3);
border-radius: 50%;
font-size: 11px;
color: #8a8a8a;
font-family: SimSun, serif;
flex-shrink: 0;
}
.testname-member-name {
flex: 1;
background: transparent;
font-size: 14px;
color: #2c2c2c;
border: none;
outline: none;
}
.testname-member-name::placeholder {
color: #dcd3c9;
}
.testname-member-divider {
height: 16px;
width: 1px;
background: #eaddcf;
flex-shrink: 0;
}
.testname-member-date {
cursor: pointer;
font-size: 12px;
padding: 4px 8px;
border-radius: 2px;
color: #dcd3c9;
transition: all 0.3s;
flex-shrink: 0;
}
.testname-member-date:active {
background: rgba(234, 221, 207, 0.3);
}
.testname-member-date-filled {
color: #2c2c2c;
}
/* Submit Button */
.testname-submit-section {
margin-top: 24px;
}
.testname-submit-btn {
width: 100%;
padding: 16px 0;
background: #2c2c2c;
color: #fdfbf7;
letter-spacing: 0.4em;
font-weight: bold;
font-size: 18px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
position: relative;
overflow: hidden;
border: none;
border-radius: 4px;
transition: all 0.3s;
cursor: pointer;
}
.testname-submit-btn:active:not(.testname-submit-btn-disabled) {
background: #1a1a1a;
transform: scale(0.98);
}
.testname-submit-btn-disabled {
background: #dcd3c9;
cursor: not-allowed;
opacity: 0.7;
}
.testname-submit-btn-content {
position: relative;
z-index: 10;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 12px;
}
.testname-submit-icon {
width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: #fdfbf7;
}
.testname-submit-btn-disabled .testname-submit-icon {
color: #fdfbf7;
}
.testname-submit-text {
font-size: 18px;
display: inline-block;
vertical-align: middle;
}
.testname-submit-btn-border {
position: absolute;
top: 4px;
bottom: 4px;
left: 4px;
right: 4px;
border: 1px solid rgba(255, 255, 255, 0.1);
pointer-events: none;
}
/* Footer Tip */
.testname-footer-tip {
margin-top: 32px;
text-align: center;
padding: 0 16px;
}
.testname-footer-text {
font-size: 12px;
color: rgba(138, 138, 138, 0.8);
line-height: 1.8;
font-family: SimSun, serif;
display: block;
margin-bottom: 8px;
}
.testname-footer-subtext {
display: block;
font-size: 11px;
color: rgba(138, 138, 138, 0.6);
line-height: 1.6;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,162 @@
<template>
<view class="test-result-screen">
<!-- 背景 -->
<view class="test-result-bg"></view>
<!-- 头部 -->
<view class="test-result-header">
<view class="header-left" @click="emit('back')">
<text class="header-back"></text>
</view>
<view class="header-title-wrap">
<text class="header-title">{{ mode === 'personal' ? '个人测名结果' : '公司测名结果' }}</text>
<text class="header-subtitle">接口原始数据展示无任何模拟与加工</text>
</view>
<view class="header-right" />
</view>
<!-- 内容 -->
<scroll-view scroll-y class="test-result-body">
<view class="test-result-card">
<text class="section-title">原始返回 JSON</text>
<view class="json-box">
<text class="json-text">
{{ formattedJson }}
</text>
</view>
<text class="tip-text">
当前为后端返回数据的直接展示用于确保与接口保持 100% 一致后续可以在此基础上做更精细的可视化拆解
</text>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
mode: 'personal' | 'company';
data: any;
}>();
const emit = defineEmits<{
back: [];
}>();
const formattedJson = computed(() => {
try {
return JSON.stringify(props.data ?? {}, null, 2);
} catch (e) {
return String(props.data ?? '');
}
});
</script>
<style scoped>
.test-result-screen {
height: 100%;
display: flex;
flex-direction: column;
position: relative;
background-color: #050508;
color: #f5f5f5;
}
.test-result-bg {
position: absolute;
inset: 0;
background: radial-gradient(circle at top, #1a1a2e 0, #050508 40%, #000 100%);
opacity: 0.9;
}
.test-result-header {
position: relative;
z-index: 10;
padding: 24rpx 32rpx;
padding-top: calc(24rpx + env(safe-area-inset-top, 0px));
display: flex;
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
background: rgba(5, 5, 8, 0.9);
backdrop-filter: blur(10px);
}
.header-left,
.header-right {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
}
.header-back {
font-size: 40rpx;
color: #a0a0a0;
}
.header-title-wrap {
flex: 1;
align-items: center;
justify-content: center;
text-align: center;
}
.header-title {
font-size: 30rpx;
font-weight: 700;
letter-spacing: 0.2em;
color: #f2e6d8;
}
.header-subtitle {
margin-top: 6rpx;
font-size: 20rpx;
color: rgba(226, 232, 240, 0.7);
}
.test-result-body {
position: relative;
z-index: 10;
flex: 1;
padding: 32rpx;
}
.test-result-card {
background: rgba(15, 23, 42, 0.9);
border-radius: 20rpx;
padding: 28rpx;
border: 1px solid rgba(148, 163, 184, 0.4);
box-shadow: 0 20rpx 40rpx rgba(0, 0, 0, 0.5);
}
.section-title {
font-size: 26rpx;
font-weight: 700;
letter-spacing: 0.16em;
margin-bottom: 20rpx;
color: #e5e7eb;
}
.json-box {
background: #020617;
border-radius: 16rpx;
padding: 20rpx;
border: 1px solid rgba(15, 23, 42, 0.9);
}
.json-text {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 22rpx;
color: #e5e7eb;
line-height: 1.7;
white-space: pre-wrap;
}
.tip-text {
margin-top: 20rpx;
font-size: 20rpx;
color: #9ca3af;
}
</style>

View File

@@ -0,0 +1,211 @@
<template>
<view class="agreement-screen">
<view class="agreement-header">
<view class="agreement-back" @click="handleBack">
<text class="agreement-back-icon"></text>
</view>
<text class="agreement-title">用户协议</text>
</view>
<view class="agreement-content">
<view class="agreement-section">
<text class="agreement-section-title">协议的接受与修改</text>
<text class="agreement-text">
欢迎使用壹梵起名服务本协议是您与壹梵起名之间关于使用壹梵起名服务所订立的协议请您仔细阅读本协议您点击"同意"按钮后本协议即构成对双方有约束力的法律文件
</text>
</view>
<view class="agreement-section">
<text class="agreement-section-title">服务说明</text>
<text class="agreement-text">
壹梵起名向用户提供包括但不限于宝宝起名姓名测试公司起名改名建议择吉日等服务具体服务内容以平台实际提供为准
</text>
</view>
<view class="agreement-section">
<text class="agreement-section-title">用户账号</text>
<text class="agreement-text">
1. 用户需要注册账号才能使用本服务用户应当提供真实准确完整的个人信息
</text>
<text class="agreement-text">
2. 用户应妥善保管账号和密码对账号下的所有行为负责
</text>
<text class="agreement-text">
3. 用户不得将账号转让出借或以其他方式提供给第三方使用
</text>
</view>
<view class="agreement-section">
<text class="agreement-section-title">用户行为规范</text>
<text class="agreement-text">
用户在使用本服务时应遵守国家法律法规不得利用本服务从事违法违规活动包括但不限于
</text>
<text class="agreement-text">
1. 发布传播违法违规信息
</text>
<text class="agreement-text">
2. 侵犯他人知识产权或其他合法权益
</text>
<text class="agreement-text">
3. 干扰或破坏服务的正常运行
</text>
<text class="agreement-text">
4. 其他违反法律法规或本协议的行为
</text>
</view>
<view class="agreement-section">
<text class="agreement-section-title">知识产权</text>
<text class="agreement-text">
本服务中的所有内容包括但不限于文字图片软件程序等其知识产权均归壹梵起名或相关权利人所有未经授权用户不得擅自使用
</text>
</view>
<view class="agreement-section">
<text class="agreement-section-title">免责声明</text>
<text class="agreement-text">
1. 本服务提供的起名测名等内容仅供参考不构成任何承诺或保证
</text>
<text class="agreement-text">
2. 因不可抗力网络故障等原因导致的服务中断或数据丢失壹梵起名不承担责任
</text>
<text class="agreement-text">
3. 用户因使用本服务产生的任何纠纷应依法解决
</text>
</view>
<view class="agreement-section">
<text class="agreement-section-title">协议的变更与终止</text>
<text class="agreement-text">
壹梵起名有权根据需要修改本协议修改后的协议将在平台上公布用户继续使用服务即视为接受修改后的协议
</text>
</view>
<view class="agreement-section">
<text class="agreement-section-title">其他</text>
<text class="agreement-text">
本协议的解释效力及纠纷的解决适用中华人民共和国法律如有争议双方应友好协商解决协商不成的任何一方均可向壹梵起名所在地人民法院提起诉讼
</text>
</view>
<view class="agreement-footer">
<text class="agreement-footer-text">壹梵起名</text>
<text class="agreement-footer-text">生效日期2024年1月1日</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter();
const handleBack = () => {
router.back();
};
</script>
<style scoped>
.agreement-screen {
min-height: 100vh;
width: 100%;
background: #fdfbf7 url("https://www.transparenttextures.com/patterns/rice-paper.png");
display: flex;
flex-direction: column;
overflow-x: hidden;
}
.agreement-header {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
padding: 16px 20px;
background: rgba(253, 251, 247, 0.95);
border-bottom: 1px solid #eaddcf;
backdrop-filter: blur(10px);
flex-shrink: 0;
}
.agreement-back {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin-right: 12px;
flex-shrink: 0;
}
.agreement-back-icon {
font-size: 24px;
color: #8b2323;
font-weight: bold;
}
.agreement-title {
font-size: 18px;
font-weight: 500;
color: #2c2c2c;
font-family: SimSun, "Songti SC", serif;
}
.agreement-content {
flex: 1;
padding: 24px 20px 40px;
overflow-y: auto;
overflow-x: hidden;
width: 100%;
box-sizing: border-box;
}
.agreement-section {
margin-bottom: 24px;
width: 100%;
}
.agreement-section-title {
display: block;
font-size: 16px;
font-weight: 600;
color: #8b2323;
margin-bottom: 12px;
font-family: SimSun, "Songti SC", serif;
word-wrap: break-word;
word-break: break-word;
}
.agreement-text {
display: block;
font-size: 14px;
line-height: 1.8;
color: #4a4a4a;
margin-bottom: 8px;
font-family: SimSun, "Songti SC", serif;
word-wrap: break-word;
word-break: break-word;
white-space: pre-wrap;
overflow-wrap: break-word;
}
.agreement-footer {
margin-top: 40px;
padding-top: 24px;
border-top: 1px solid #eaddcf;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
width: 100%;
}
.agreement-footer-text {
display: block;
font-size: 13px;
color: #8a8a8a;
font-family: SimSun, "Songti SC", serif;
}
</style>