Files
Veyra/apps/mobile/app/plans/create.tsx
T
Blizzard 92329ddce2
Checks / verify (push) Failing after 9m52s
feat: 完成模拟交易闭环、App 与 A 股行情接入
统一单入口 Go 工程与前端依赖目录,增加后台供应商配置、候选审核、计划预算、成交冲正、持仓复盘和站内通知。接入策略版本、Python 回测、可配置 A 股公开行情及 zap 文件轮转日志,补充迁移、契约与验证。
2026-10-08 22:58:52 +08:00

20 lines
4.6 KiB
TypeScript
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.
import {useState} from "react"
import {Text} from "react-native"
import {useLocalSearchParams,useRouter} from "expo-router"
import {useQuery,useQueryClient} from "@tanstack/react-query"
import {useSession} from "../../src/state/session"
import {useAccounts} from "../../src/state/accounts"
import {Body,Button,ErrorMessage,Field,Panel,Screen,Title,styles} from "../../src/components/ui"
import {publicationAvailable,type Candidate} from "../../src/lib/candidates"
import type {Budget,Preview} from "../../src/lib/plans"
export default function CreatePlan(){
const {candidate_id}=useLocalSearchParams<{candidate_id:string}>(),{session,client}=useSession(),{selected:account}=useAccounts(),router=useRouter(),cache=useQueryClient()
const [quantity,setQuantity]=useState(""),[fee,setFee]=useState(""),[preview,setPreview]=useState<Preview|null>(null),[busy,setBusy]=useState(false),[error,setError]=useState("")
const candidate=useQuery({queryKey:["candidate",session?.user.id,candidate_id],enabled:!!session&&typeof candidate_id==="string",queryFn:({signal})=>client<Candidate>(`/api/v1/candidates/${encodeURIComponent(candidate_id)}`,{signal})})
const budget=useQuery({queryKey:["budget",session?.user.id,account?.id],enabled:account?.mode==="simulation",queryFn:({signal})=>client<Budget>(`/api/v1/accounts/${account!.id}/budget`,{signal})})
const c=candidate.data,eligible=!!c&&publicationAvailable(c)&&account?.mode==="simulation",currentPreview=preview?.account_id===account?.id?preview:null
async function calculate(){if(!c||!account)return;setBusy(true);setError("");setPreview(null);try{setPreview(await client<Preview>("/api/v1/plan-previews",{method:"POST",body:{account_id:account.id,candidate_revision_id:c.id,expected_candidate_version:c.version,quantity,fee_budget:fee}}))}catch(e){setError(e instanceof Error?e.message:"预览失败");void candidate.refetch();void budget.refetch()}finally{setBusy(false)}}
async function confirm(){if(!currentPreview)return;setBusy(true);setError("");try{await client(`/api/v1/plan-previews/${currentPreview.id}/confirm`,{method:"POST",body:{}});await cache.invalidateQueries({queryKey:["plans"]});await cache.invalidateQueries({queryKey:["budget"]});router.replace("/(tabs)/plans")}catch(e){setError(e instanceof Error?e.message:"确认失败");setPreview(null);void candidate.refetch();void budget.refetch()}finally{setBusy(false)}}
return <Screen><Title>创建模拟买入计划</Title><Body>确认计划仅保留资金预算;提醒与计划不会产生交易或扣款。</Body>{candidate.error&&<ErrorMessage message={candidate.error.message}/>} {c&&<Panel><Text style={styles.subtitle}>{c.content.name} · {c.content.symbol}</Text><Body>候选修订 {c.revision} · 区间 ¥{c.content.reference_plan.entry_low}–{c.content.reference_plan.entry_high}</Body><Body>风险价格 ¥{c.content.reference_plan.stop_loss} · 有效截止 {new Date(c.valid_until).toLocaleString("zh-CN")}</Body></Panel>}{!account&&<><Body>请先创建或选择模拟账户。</Body><Button secondary title="账户管理" onPress={()=>router.push("/settings/accounts")}/></>}{account&&<Body>当前账户:{account.name} {account.mode!=="simulation"?"· 真实记录账户暂不支持模拟计划":""}</Body>}{c&&!publicationAvailable(c)&&<ErrorMessage message="此候选已失效,不能创建新计划。"/>}{budget.error&&<ErrorMessage message={budget.error.message}/>} {budget.data&&<Panel><Body>账本现金 ¥{budget.data.cash_balance}</Body><Body>其他计划保留 ¥{budget.data.reserved_amount} · 可用 ¥{budget.data.available_amount}</Body></Panel>}<Field label="计划数量(模拟规则:100 股整数倍)" keyboardType="number-pad" editable={!busy} value={quantity} onChangeText={v=>{setQuantity(v);setPreview(null)}}/><Field label="预计费用预算(元,允许 0)" keyboardType="decimal-pad" editable={!busy} value={fee} onChangeText={v=>{setFee(v);setPreview(null)}}/><ErrorMessage message={error}/><Button title="预览资金预算" disabled={!eligible||!quantity||!fee} busy={busy} onPress={()=>void calculate()}/>{currentPreview&&<Panel accent><Text style={styles.subtitle}>确认前核对</Text><Body>数量 {currentPreview.quantity} 股 · 预计费用 ¥{currentPreview.fee_budget}</Body><Body>按区间上限保留 ¥{currentPreview.reserved_amount},预览截止 {new Date(currentPreview.expires_at).toLocaleTimeString("zh-CN")}</Body><Body>确认时会重新检查候选版本与现金;确认后不代表成交,实际发生交易后另行记录。</Body><Button title="确认计划并保留预算" disabled={!eligible||Date.parse(currentPreview.expires_at)<=Date.now()} busy={busy} onPress={()=>void confirm()}/></Panel>}</Screen>
}