upload project source code
This commit is contained in:
1025
前端源码/uni-app/components/screens/Affinity.vue
Normal file
1025
前端源码/uni-app/components/screens/Affinity.vue
Normal file
File diff suppressed because it is too large
Load Diff
1175
前端源码/uni-app/components/screens/AffinityResult.vue
Normal file
1175
前端源码/uni-app/components/screens/AffinityResult.vue
Normal file
File diff suppressed because it is too large
Load Diff
131
前端源码/uni-app/components/screens/Analysis.vue
Normal file
131
前端源码/uni-app/components/screens/Analysis.vue
Normal 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>
|
||||
12
前端源码/uni-app/components/screens/AnalysisPlaceholder.vue
Normal file
12
前端源码/uni-app/components/screens/AnalysisPlaceholder.vue
Normal 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>
|
||||
|
||||
590
前端源码/uni-app/components/screens/AuspiciousForm.vue
Normal file
590
前端源码/uni-app/components/screens/AuspiciousForm.vue
Normal 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>
|
||||
320
前端源码/uni-app/components/screens/AuspiciousLoading.vue
Normal file
320
前端源码/uni-app/components/screens/AuspiciousLoading.vue
Normal 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>
|
||||
1369
前端源码/uni-app/components/screens/AuspiciousResult.vue
Normal file
1369
前端源码/uni-app/components/screens/AuspiciousResult.vue
Normal file
File diff suppressed because it is too large
Load Diff
608
前端源码/uni-app/components/screens/Calendar.vue
Normal file
608
前端源码/uni-app/components/screens/Calendar.vue
Normal 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>
|
||||
1526
前端源码/uni-app/components/screens/CompanyAnalysis.vue
Normal file
1526
前端源码/uni-app/components/screens/CompanyAnalysis.vue
Normal file
File diff suppressed because it is too large
Load Diff
2116
前端源码/uni-app/components/screens/CompanyBusinessFortune.vue
Normal file
2116
前端源码/uni-app/components/screens/CompanyBusinessFortune.vue
Normal file
File diff suppressed because it is too large
Load Diff
2328
前端源码/uni-app/components/screens/CompanyNamingDetail.vue
Normal file
2328
前端源码/uni-app/components/screens/CompanyNamingDetail.vue
Normal file
File diff suppressed because it is too large
Load Diff
823
前端源码/uni-app/components/screens/CompanyNamingDetailDesktop.vue
Normal file
823
前端源码/uni-app/components/screens/CompanyNamingDetailDesktop.vue
Normal 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>
|
||||
1703
前端源码/uni-app/components/screens/Home.vue
Normal file
1703
前端源码/uni-app/components/screens/Home.vue
Normal file
File diff suppressed because it is too large
Load Diff
661
前端源码/uni-app/components/screens/Login.vue
Normal file
661
前端源码/uni-app/components/screens/Login.vue
Normal 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>
|
||||
916
前端源码/uni-app/components/screens/MyNamingPlansScreen.vue
Normal file
916
前端源码/uni-app/components/screens/MyNamingPlansScreen.vue
Normal 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>
|
||||
|
||||
1116
前端源码/uni-app/components/screens/Naming.vue
Normal file
1116
前端源码/uni-app/components/screens/Naming.vue
Normal file
File diff suppressed because it is too large
Load Diff
2949
前端源码/uni-app/components/screens/NamingDetail.vue
Normal file
2949
前端源码/uni-app/components/screens/NamingDetail.vue
Normal file
File diff suppressed because it is too large
Load Diff
11
前端源码/uni-app/components/screens/NamingPlaceholder.vue
Normal file
11
前端源码/uni-app/components/screens/NamingPlaceholder.vue
Normal 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>
|
||||
|
||||
391
前端源码/uni-app/components/screens/NamingSolutionsList.vue
Normal file
391
前端源码/uni-app/components/screens/NamingSolutionsList.vue
Normal 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>
|
||||
|
||||
4885
前端源码/uni-app/components/screens/PersonalWealthAnalysis.vue
Normal file
4885
前端源码/uni-app/components/screens/PersonalWealthAnalysis.vue
Normal file
File diff suppressed because it is too large
Load Diff
351
前端源码/uni-app/components/screens/PrivacyPolicy.vue
Normal file
351
前端源码/uni-app/components/screens/PrivacyPolicy.vue
Normal 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>
|
||||
1134
前端源码/uni-app/components/screens/Profile.vue
Normal file
1134
前端源码/uni-app/components/screens/Profile.vue
Normal file
File diff suppressed because it is too large
Load Diff
416
前端源码/uni-app/components/screens/ProfileFAQ.vue
Normal file
416
前端源码/uni-app/components/screens/ProfileFAQ.vue
Normal 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环境使用alert,uni-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>
|
||||
556
前端源码/uni-app/components/screens/ProfileFavorites.vue
Normal file
556
前端源码/uni-app/components/screens/ProfileFavorites.vue
Normal 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>
|
||||
477
前端源码/uni-app/components/screens/ProfileFeedbackScreen.vue
Normal file
477
前端源码/uni-app/components/screens/ProfileFeedbackScreen.vue
Normal 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>
|
||||
470
前端源码/uni-app/components/screens/ProfileOrderDetailScreen.vue
Normal file
470
前端源码/uni-app/components/screens/ProfileOrderDetailScreen.vue
Normal 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>
|
||||
576
前端源码/uni-app/components/screens/ProfileOrdersScreen.vue
Normal file
576
前端源码/uni-app/components/screens/ProfileOrdersScreen.vue
Normal 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环境使用confirm,uni-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>
|
||||
6
前端源码/uni-app/components/screens/ProfilePlaceholder.vue
Normal file
6
前端源码/uni-app/components/screens/ProfilePlaceholder.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<view class="h-full flex items-center justify-center text-[#5a5a5a]">
|
||||
<text>已替换为 ProfileScreen.vue</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
284
前端源码/uni-app/components/screens/ProfilePrivacy.vue
Normal file
284
前端源码/uni-app/components/screens/ProfilePrivacy.vue
Normal 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>
|
||||
415
前端源码/uni-app/components/screens/ProfileReports.vue
Normal file
415
前端源码/uni-app/components/screens/ProfileReports.vue
Normal 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>
|
||||
433
前端源码/uni-app/components/screens/ProfileSettings.vue
Normal file
433
前端源码/uni-app/components/screens/ProfileSettings.vue
Normal 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 环境使用 confirm,uni-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>
|
||||
248
前端源码/uni-app/components/screens/ProfileUserInfo.vue
Normal file
248
前端源码/uni-app/components/screens/ProfileUserInfo.vue
Normal 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>
|
||||
1297
前端源码/uni-app/components/screens/Renaming.vue
Normal file
1297
前端源码/uni-app/components/screens/Renaming.vue
Normal file
File diff suppressed because it is too large
Load Diff
190
前端源码/uni-app/components/screens/RenamingDetail.vue
Normal file
190
前端源码/uni-app/components/screens/RenamingDetail.vue
Normal 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>
|
||||
11
前端源码/uni-app/components/screens/RenamingPlaceholder.vue
Normal file
11
前端源码/uni-app/components/screens/RenamingPlaceholder.vue
Normal 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>
|
||||
|
||||
917
前端源码/uni-app/components/screens/TestName.vue
Normal file
917
前端源码/uni-app/components/screens/TestName.vue
Normal 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>
|
||||
2873
前端源码/uni-app/components/screens/TestNameDetail.vue
Normal file
2873
前端源码/uni-app/components/screens/TestNameDetail.vue
Normal file
File diff suppressed because it is too large
Load Diff
162
前端源码/uni-app/components/screens/TestResult.vue
Normal file
162
前端源码/uni-app/components/screens/TestResult.vue
Normal 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>
|
||||
211
前端源码/uni-app/components/screens/UserAgreement.vue
Normal file
211
前端源码/uni-app/components/screens/UserAgreement.vue
Normal 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>
|
||||
Reference in New Issue
Block a user