KNOWN-007: solarTime.ts 单元测试 KNOWN-008: 消除生产代码中 as any 类型断言 KNOWN-002: 工具层测试覆盖 KNOWN-003: lruCache 边界测试 KNOWN-004: templateService 分支覆盖(100%) KNOWN-001: 6个业务组件测试 额外修复: searchOptimizer memoize bug, templateService 常量修改 bug 测试覆盖率: 75.71% -> 90.22%, 测试用例: 412 -> 671
408 lines
9.8 KiB
Vue
408 lines
9.8 KiB
Vue
<template>
|
||
<view class="ziwei-page">
|
||
<view class="section" data-test="birth-form">
|
||
<text class="section-title">{{ $t('ziwei.birthInfo') }}</text>
|
||
<view class="form-group">
|
||
<text class="label">{{ $t('ziwei.birthDate') }}</text>
|
||
<picker mode="date" :value="birthDate" @change="onBirthDateChange">
|
||
<text class="picker-value">{{ birthDate || $t('ziwei.pleaseSelect') }}</text>
|
||
</picker>
|
||
</view>
|
||
<view class="form-group">
|
||
<text class="label">{{ $t('ziwei.birthHour') }}</text>
|
||
<picker :range="hourOptions" :value="hourIndex" @change="onHourChange">
|
||
<text class="picker-value">{{ hourOptions[hourIndex] }}</text>
|
||
</picker>
|
||
</view>
|
||
<view class="form-group">
|
||
<text class="label">{{ $t('ziwei.gender') }}</text>
|
||
<view class="gender-switch">
|
||
<text :class="['gender-option', gender === 'male' ? 'active' : '']" @tap="gender = 'male'">{{ $t('common.male') }}</text>
|
||
<text :class="['gender-option', gender === 'female' ? 'active' : '']" @tap="gender = 'female'">{{ $t('common.female') }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="form-group">
|
||
<text class="label">{{ $t('ziwei.birthPlace') }}</text>
|
||
<input class="text-input" :value="birthPlace" @input="onBirthPlaceChange" :placeholder="$t('ziwei.birthPlaceHint')" />
|
||
<text v-if="detectedLocation" class="location-hint detected">{{ detectedLocation }}</text>
|
||
<text v-else-if="birthPlace && birthPlace.trim().length > 2 && !isLookingUp" class="location-hint not-found">{{ $t('ziwei.locationNotFound') }}</text>
|
||
<text v-if="isLookingUp" class="location-hint looking-up">{{ $t('ziwei.lookingUp') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="action-bar">
|
||
<button data-test="generate-btn" class="generate-btn" @tap="generateChart" :disabled="isCalculating">
|
||
{{ isCalculating ? $t('ziwei.generating') : $t('ziwei.generate') }}
|
||
</button>
|
||
</view>
|
||
|
||
<view v-if="chart" class="section chart-result">
|
||
<text class="section-title">{{ $t('ziwei.chartTitle') }}</text>
|
||
<view class="chart-grid">
|
||
<view v-for="palace in chart.palaces" :key="palace.palaceType" class="palace-card">
|
||
<text class="palace-name">{{ getPalaceName(palace.palaceType) }}</text>
|
||
<text class="palace-branch">{{ getBranchName(palace.earthlyBranch) }}</text>
|
||
<text class="palace-score">{{ palace.score }}{{ $t('ziwei.scoreUnit') }}</text>
|
||
<view class="star-list">
|
||
<text v-for="star in palace.majorStars" :key="star.star" class="star-item">
|
||
{{ getStarName(star.star) }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="chart?.sanFangSiZheng" class="section analysis-section">
|
||
<text class="section-title">{{ $t('ziwei.sanFangAnalysis') }}</text>
|
||
<view class="analysis-item">
|
||
<text class="analysis-label">{{ $t('ziwei.sanFangScore') }}</text>
|
||
<text class="analysis-value">{{ chart.sanFangSiZheng.sanFangScore }}{{ $t('ziwei.scoreUnit') }}</text>
|
||
</view>
|
||
<view class="analysis-item">
|
||
<text class="analysis-label">{{ $t('ziwei.siZhengScore') }}</text>
|
||
<text class="analysis-value">{{ chart.sanFangSiZheng.siZhengScore }}{{ $t('ziwei.scoreUnit') }}</text>
|
||
</view>
|
||
<view class="analysis-item">
|
||
<text class="analysis-label">{{ $t('ziwei.overallEval') }}</text>
|
||
<text class="analysis-value">{{ chart.overallLuck }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="chart" class="section summary-section">
|
||
<text class="section-title">{{ $t('ziwei.summary') }}</text>
|
||
<text class="summary-text">{{ chart.summary }}</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
import { onShow } from '@dcloudio/uni-app'
|
||
import { ZiweiService } from '../../services/ziweiService'
|
||
import { PalaceType, EarthlyBranch, MajorStar } from '../../algorithms/enums'
|
||
import type { ZiweiChart } from '../../algorithms/types'
|
||
import { lookupCityCoordinates } from '../../data/cityCoordinates'
|
||
|
||
const ziweiService = new ZiweiService()
|
||
const { t } = useI18n()
|
||
|
||
onShow(() => {
|
||
uni.setNavigationBarTitle({ title: t('ziwei.pageTitle') })
|
||
})
|
||
|
||
const birthDate = ref('')
|
||
const hourIndex = ref(0)
|
||
const gender = ref<'male' | 'female'>('male')
|
||
const birthPlace = ref('')
|
||
const isCalculating = ref(false)
|
||
const isLookingUp = ref(false)
|
||
const detectedLocation = ref('')
|
||
const chart = ref<ZiweiChart | null>(null)
|
||
|
||
let lookupTimer: ReturnType<typeof setTimeout> | null = null
|
||
|
||
const hourOptions = [
|
||
'子时 (23:00-01:00)', '丑时 (01:00-03:00)', '寅时 (03:00-05:00)',
|
||
'卯时 (05:00-07:00)', '辰时 (07:00-09:00)', '巳时 (09:00-11:00)',
|
||
'午时 (11:00-13:00)', '未时 (13:00-15:00)', '申时 (15:00-17:00)',
|
||
'酉时 (17:00-19:00)', '戌时 (19:00-21:00)', '亥时 (21:00-23:00)',
|
||
]
|
||
|
||
const onBirthDateChange = (e: any) => {
|
||
birthDate.value = e.detail.value
|
||
}
|
||
|
||
const onHourChange = (e: any) => {
|
||
hourIndex.value = e.detail.value
|
||
}
|
||
|
||
const onBirthPlaceChange = (e: any) => {
|
||
birthPlace.value = e.detail.value
|
||
detectedLocation.value = ''
|
||
autoLookupCoordinates(birthPlace.value)
|
||
}
|
||
|
||
/** 自动查找出生地坐标 */
|
||
function autoLookupCoordinates(place: string): void {
|
||
if (lookupTimer) {
|
||
clearTimeout(lookupTimer)
|
||
}
|
||
if (!place || place.trim().length < 2) {
|
||
isLookingUp.value = false
|
||
return
|
||
}
|
||
isLookingUp.value = true
|
||
lookupTimer = setTimeout(() => {
|
||
const result = lookupCityCoordinates(place)
|
||
isLookingUp.value = false
|
||
if (result) {
|
||
detectedLocation.value = `${result.displayName}(${result.longitude}°E, ${result.latitude}°N)`
|
||
}
|
||
lookupTimer = null
|
||
}, 500)
|
||
}
|
||
|
||
const getPalaceName = (palaceType: string): string => {
|
||
try {
|
||
return PalaceType.name(palaceType as PalaceType)
|
||
} catch {
|
||
return palaceType
|
||
}
|
||
}
|
||
|
||
const getBranchName = (branch: any): string => {
|
||
try {
|
||
return EarthlyBranch.name(branch)
|
||
} catch {
|
||
return String(branch)
|
||
}
|
||
}
|
||
|
||
const getStarName = (star: any): string => {
|
||
try {
|
||
return MajorStar.name(star)
|
||
} catch {
|
||
return String(star)
|
||
}
|
||
}
|
||
|
||
const generateChart = async () => {
|
||
if (!birthDate.value) return
|
||
|
||
isCalculating.value = true
|
||
try {
|
||
const hour = hourIndex.value * 2
|
||
const birthTime = `${birthDate.value}T${String(hour).padStart(2, '0')}:00:00`
|
||
|
||
// 从检测结果中解析经纬度
|
||
let longitude: number | undefined
|
||
let latitude: number | undefined
|
||
if (detectedLocation.value) {
|
||
const coordResult = lookupCityCoordinates(birthPlace.value)
|
||
if (coordResult) {
|
||
longitude = coordResult.longitude
|
||
latitude = coordResult.latitude
|
||
}
|
||
}
|
||
|
||
chart.value = await ziweiService.generateChart({
|
||
birthTime,
|
||
gender: gender.value,
|
||
timezone: 'Asia/Shanghai',
|
||
birthPlace: birthPlace.value || undefined,
|
||
longitude,
|
||
latitude,
|
||
})
|
||
} catch (e) {
|
||
console.error('排盘失败:', e)
|
||
} finally {
|
||
isCalculating.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.ziwei-page {
|
||
min-height: 100vh;
|
||
background-color: rgba(244, 244, 245, 0.3);
|
||
padding: 16px;
|
||
padding-bottom: 40px;
|
||
}
|
||
|
||
.section {
|
||
background-color: #fff;
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: rgba(44, 24, 16, 255);
|
||
margin-bottom: 12px;
|
||
display: block;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.label {
|
||
font-size: 14px;
|
||
color: #666;
|
||
margin-bottom: 6px;
|
||
display: block;
|
||
}
|
||
|
||
.picker-value {
|
||
font-size: 15px;
|
||
color: #333;
|
||
padding: 8px 12px;
|
||
background-color: #f5f5f5;
|
||
border-radius: 8px;
|
||
display: block;
|
||
}
|
||
|
||
.gender-switch {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.gender-option {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: 8px 0;
|
||
border-radius: 8px;
|
||
font-size: 15px;
|
||
color: #666;
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.gender-option.active {
|
||
color: #fff;
|
||
background-color: rgba(44, 24, 16, 255);
|
||
}
|
||
|
||
.text-input {
|
||
font-size: 15px;
|
||
color: #333;
|
||
padding: 8px 12px;
|
||
background-color: #f5f5f5;
|
||
border-radius: 8px;
|
||
display: block;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.location-hint {
|
||
font-size: 12px;
|
||
margin-top: 4px;
|
||
display: block;
|
||
}
|
||
|
||
.location-hint.detected {
|
||
color: #52c41a;
|
||
}
|
||
|
||
.location-hint.not-found {
|
||
color: #faad14;
|
||
}
|
||
|
||
.location-hint.looking-up {
|
||
color: #999;
|
||
}
|
||
|
||
.action-bar {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.generate-btn {
|
||
width: 100%;
|
||
height: 48px;
|
||
line-height: 48px;
|
||
background-color: rgba(44, 24, 16, 255);
|
||
color: #fff;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
border-radius: 12px;
|
||
border: none;
|
||
}
|
||
|
||
.generate-btn[disabled] {
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.chart-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 8px;
|
||
}
|
||
|
||
.palace-card {
|
||
background-color: #fafafa;
|
||
border-radius: 8px;
|
||
padding: 8px;
|
||
text-align: center;
|
||
}
|
||
|
||
.palace-name {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: rgba(44, 24, 16, 255);
|
||
display: block;
|
||
}
|
||
|
||
.palace-branch {
|
||
font-size: 12px;
|
||
color: #999;
|
||
display: block;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.palace-score {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
color: #e74c3c;
|
||
display: block;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.star-list {
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.star-item {
|
||
font-size: 11px;
|
||
color: #666;
|
||
display: block;
|
||
}
|
||
|
||
.analysis-section {
|
||
.analysis-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.analysis-label {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.analysis-value {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: rgba(44, 24, 16, 255);
|
||
}
|
||
}
|
||
|
||
.summary-section {
|
||
.summary-text {
|
||
font-size: 14px;
|
||
line-height: 22px;
|
||
color: #333;
|
||
}
|
||
}
|
||
|
||
@media screen and (max-width: 375px) {
|
||
.chart-grid {
|
||
grid-template-columns: repeat(3, 1fr);
|
||
}
|
||
|
||
.palace-card {
|
||
padding: 6px;
|
||
}
|
||
|
||
.palace-name {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.palace-score {
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
</style>
|