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

918 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="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>