Spaces:
Sleeping
Sleeping
| '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> | |
| ) | |
| } | |