AI-Astrology / frontend /app /create /CreateClient.tsx
luoleyuan's picture
Deploy minimal app (runtime only)
5960fdf verified
Raw
History Blame Contribute Delete
9.65 kB
'use client'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useRouter } from 'next/navigation'
import {
createBirthProfile,
createNatalChart,
getCnCities,
getCnCounties,
getCnProvinces,
getHealth,
PlaceItem,
} from '../../lib/apiClient'
import { Button, Card, Field, InlineMessage, Input, PageShell, Select } from '../../components/ui'
type TimeConfidence = 'exact' | 'approximate' | 'unknown' | 'rectified'
type HouseSystem = 'whole_sign' | 'equal' | 'porphyry' | 'placidus'
export default function CreateClient() {
const router = useRouter()
const [error, setError] = useState<string | null>(null)
const [backendError, setBackendError] = useState<string | null>(null)
const [submitting, setSubmitting] = useState(false)
const [name, setName] = useState('我')
const [birthDate, setBirthDate] = useState('2000-01-01')
const [birthTime, setBirthTime] = useState('12:00:00')
const [timeConfidence, setTimeConfidence] = useState<TimeConfidence>('exact')
const [houseSystem, setHouseSystem] = useState<HouseSystem>('placidus')
const [provinces, setProvinces] = useState<PlaceItem[]>([])
const [cities, setCities] = useState<PlaceItem[]>([])
const [counties, setCounties] = useState<PlaceItem[]>([])
const [loadingCities, setLoadingCities] = useState(false)
const [loadingCounties, setLoadingCounties] = useState(false)
const [provinceCode, setProvinceCode] = useState('')
const [cityCode, setCityCode] = useState('')
const [countyCode, setCountyCode] = useState('')
const [countyFilter, setCountyFilter] = useState('')
const filteredCounties = useMemo(() => {
const q = countyFilter.trim()
if (!q) return counties
return counties.filter((x) => x.name.includes(q))
}, [counties, countyFilter])
const selectCity = useCallback(async (code: string) => {
setCityCode(code)
setCounties([])
setCountyCode('')
setCountyFilter('')
if (!code) return
setLoadingCounties(true)
try {
const xs = await getCnCounties(code)
setCounties(xs)
setCountyCode(xs[0]?.code ?? '')
} catch (e: unknown) {
setError(e instanceof Error ? e.message : 'load_counties_failed')
} finally {
setLoadingCounties(false)
}
}, [])
const selectProvince = useCallback(
async (code: string) => {
setProvinceCode(code)
setCities([])
setCityCode('')
setCounties([])
setCountyCode('')
setCountyFilter('')
if (!code) return
setLoadingCities(true)
try {
const xs = await getCnCities(code)
setCities(xs)
const firstCity = xs[0]?.code ?? ''
await selectCity(firstCity)
} catch (e: unknown) {
setError(e instanceof Error ? e.message : 'load_cities_failed')
} finally {
setLoadingCities(false)
}
},
[selectCity]
)
useEffect(() => {
let aborted = false
getHealth()
.then(() => {
if (aborted) return
setBackendError(null)
})
.catch(() => {
if (aborted) return
setBackendError('后端不可访问:请确认后端服务已启动,或检查前端的后端地址配置。')
})
getCnProvinces()
.then((xs) => {
if (aborted) return
setProvinces(xs)
const first = xs[0]?.code ?? ''
if (first) selectProvince(first)
})
.catch((e: unknown) => {
if (aborted) return
setError(e instanceof Error ? e.message : 'load_provinces_failed')
})
return () => {
aborted = true
}
}, [selectProvince])
async function submit() {
setError(null)
setSubmitting(true)
try {
if (!countyCode) throw new Error('county_required')
const bp = await createBirthProfile({
name,
birth_date: birthDate,
birth_time: timeConfidence === 'unknown' ? null : birthTime,
time_confidence: timeConfidence,
birth_place_adcode: countyCode,
})
const chart = await createNatalChart({
profile_id: bp.profile_id,
calculation_profile: {
ephemeris: 'DE440',
zodiac: 'tropical',
house_system: houseSystem,
orb_profile: 'western_default_v1',
position_type: 'apparent',
ecliptic_epoch: 'date',
},
})
router.push(`/charts/${encodeURIComponent(chart.chart_id)}`)
} catch (e: unknown) {
setError(e instanceof Error ? e.message : 'submit_failed')
} finally {
setSubmitting(false)
}
}
return (
<PageShell title="创建本命盘" subtitle="选择出生地(省/市/县)并生成图表 SVG">
{backendError ? <InlineMessage variant="error">{backendError}</InlineMessage> : null}
<Card
title="基本信息"
subtitle="时间置信度为 unknown 时不会计算宫位/角点;建议使用 exact。"
>
<div
style={{
display: 'grid',
gap: 14,
gridTemplateColumns: '1fr',
}}
>
<Field label="姓名" required>
<Input value={name} onChange={(e) => setName(e.target.value)} />
</Field>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
gap: 12,
}}
>
<Field label="出生日期" required>
<Input value={birthDate} onChange={(e) => setBirthDate(e.target.value)} placeholder="YYYY-MM-DD" />
</Field>
<Field label="出生时间" helpText={timeConfidence === 'unknown' ? 'unknown 时不需要填写时间' : undefined}>
<Input
value={birthTime}
onChange={(e) => setBirthTime(e.target.value)}
placeholder="HH:MM:SS"
disabled={timeConfidence === 'unknown'}
/>
</Field>
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
gap: 12,
}}
>
<Field label="时间置信度" required>
<Select value={timeConfidence} onChange={(e) => setTimeConfidence(e.target.value as TimeConfidence)}>
<option value="exact">exact</option>
<option value="approximate">approximate</option>
<option value="unknown">unknown</option>
<option value="rectified">rectified</option>
</Select>
</Field>
<Field label="宫制" required>
<Select value={houseSystem} onChange={(e) => setHouseSystem(e.target.value as HouseSystem)}>
<option value="placidus">placidus</option>
<option value="whole_sign">whole_sign</option>
<option value="equal">equal</option>
<option value="porphyry">porphyry</option>
</Select>
</Field>
</div>
</div>
</Card>
<Card title="出生地(中国大陆,县/区)" subtitle="省/市/县为级联选择;县区支持关键字筛选。">
<div style={{ display: 'grid', gap: 14 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 12 }}>
<Field label="省" required helpText={provinces.length ? undefined : '加载中...'}>
<Select
value={provinceCode}
onChange={(e) => selectProvince(e.target.value)}
disabled={!provinces.length}
>
{provinces.map((p) => (
<option key={p.code} value={p.code}>
{p.name}
</option>
))}
</Select>
</Field>
<Field
label="市"
required
helpText={!provinceCode ? '请先选择省' : loadingCities ? '加载中...' : undefined}
>
<Select value={cityCode} onChange={(e) => selectCity(e.target.value)} disabled={!cities.length || loadingCities}>
{cities.map((c) => (
<option key={c.code} value={c.code}>
{c.name}
</option>
))}
</Select>
</Field>
</div>
<Field label="县/区筛选" helpText={loadingCounties ? '加载县区中...' : undefined}>
<Input value={countyFilter} onChange={(e) => setCountyFilter(e.target.value)} placeholder="输入关键字筛选县/区" />
</Field>
<Field label="县/区" required helpText={!cityCode ? '请先选择市' : undefined}>
<Select
value={countyCode}
onChange={(e) => setCountyCode(e.target.value)}
disabled={!filteredCounties.length || loadingCounties}
>
{filteredCounties.map((d) => (
<option key={d.code} value={d.code}>
{d.name}
</option>
))}
</Select>
</Field>
</div>
</Card>
{error ? <InlineMessage variant="error">{error}</InlineMessage> : null}
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<Button onClick={submit} loading={submitting} disabled={!countyCode}>
创建并查看
</Button>
<div style={{ fontSize: 13, color: 'var(--color-text-3)' }}>
{countyCode ? `已选择:${countyCode}` : '请选择县/区后再创建'}
</div>
</div>
</PageShell>
)
}