feat: 实现生辰信息存储与真太阳时计算

- 新增 ADR 文档记录生辰信息存储设计决策
- 新增真太阳时计算模块 (solarTime.ts),基于 Spencer 1971 时差方程
- 新增城市经纬度内置数据库,覆盖全国主要区县,系统自动匹配坐标
- 更新紫微页面:移除手动经纬度输入,改为 500ms 防抖自动定位
- 更新所有 9 个语言文件,替换经纬度提示为定位提示
This commit is contained in:
2026-08-12 20:44:45 +08:00
parent 3a428d8977
commit 34e9c3721d
13 changed files with 869 additions and 76 deletions
@@ -24,16 +24,9 @@
<view class="form-group">
<text class="label">{{ $t('ziwei.birthPlace') }}</text>
<input class="text-input" :value="birthPlace" @input="onBirthPlaceChange" :placeholder="$t('ziwei.birthPlaceHint')" />
</view>
<view class="form-row">
<view class="form-group form-half">
<text class="label">{{ $t('ziwei.longitude') }}</text>
<input class="text-input" type="digit" :value="longitude" @input="onLongitudeChange" :placeholder="$t('ziwei.longitudeHint')" />
</view>
<view class="form-group form-half">
<text class="label">{{ $t('ziwei.latitude') }}</text>
<input class="text-input" type="digit" :value="latitude" @input="onLatitudeChange" :placeholder="$t('ziwei.latitudeHint')" />
</view>
<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>
@@ -89,6 +82,7 @@ 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()
@@ -101,11 +95,13 @@ const birthDate = ref('')
const hourIndex = ref(0)
const gender = ref<'male' | 'female'>('male')
const birthPlace = ref('')
const longitude = ref('')
const latitude = 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)',
@@ -123,14 +119,28 @@ const onHourChange = (e: any) => {
const onBirthPlaceChange = (e: any) => {
birthPlace.value = e.detail.value
detectedLocation.value = ''
autoLookupCoordinates(birthPlace.value)
}
const onLongitudeChange = (e: any) => {
longitude.value = e.detail.value
}
const onLatitudeChange = (e: any) => {
latitude.value = e.detail.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 => {
@@ -164,15 +174,25 @@ const generateChart = async () => {
try {
const hour = hourIndex.value * 2
const birthTime = `${birthDate.value}T${String(hour).padStart(2, '0')}:00:00`
const lng = longitude.value ? parseFloat(longitude.value) : undefined
const lat = latitude.value ? parseFloat(latitude.value) : undefined
// 从检测结果中解析经纬度
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: lng,
latitude: lat,
longitude,
latitude,
})
} catch (e) {
console.error('排盘失败:', e)
@@ -245,15 +265,6 @@ const generateChart = async () => {
background-color: rgba(44, 24, 16, 255);
}
.form-row {
display: flex;
gap: 12px;
}
.form-half {
flex: 1;
}
.text-input {
font-size: 15px;
color: #333;
@@ -265,6 +276,24 @@ const generateChart = async () => {
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;
}