Files
everything-is-suitable/everything-is-suitable-uniapp/src/pages/ziwei/index.vue
T
zhangxiang 69da92b58b test: 修复所有已知问题并全面提高测试覆盖率至 90%
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
2026-08-13 00:09:46 +08:00

408 lines
9.8 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="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>