diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-22 01:54:55 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-22 01:54:55 +0800 |
| commit | 0d1af831c10683bb1e5a5853e5924c5ddfb1310d (patch) | |
| tree | 638c7a94fb213926aebaa443761313ddf7dcd499 /src/FundLab.Web/App.fs | |
| parent | aa4dba4f70749b459c3922ce68610c4d78e644e6 (diff) | |
| download | fund-lab-0d1af831c10683bb1e5a5853e5924c5ddfb1310d.tar.gz | |
Add bonds probe panel with live bond quote flow (3d-16)
Diffstat (limited to 'src/FundLab.Web/App.fs')
| -rw-r--r-- | src/FundLab.Web/App.fs | 159 |
1 files changed, 159 insertions, 0 deletions
diff --git a/src/FundLab.Web/App.fs b/src/FundLab.Web/App.fs index 1552e15..ca0c61c 100644 --- a/src/FundLab.Web/App.fs +++ b/src/FundLab.Web/App.fs @@ -392,6 +392,14 @@ type RawMarketNavSeries = points: RawMarketNavSeriesPoint array } +type RawBondQuote = + { + code: string + name: obj + price: obj + maturityDate: obj + } + type CreateAttempt = { idempotencyKey: string @@ -692,6 +700,14 @@ type MarketNavSeriesPoint = accumulatedNav: string option } +type BondQuoteView = + { + code: string + name: string option + price: string option + maturityDate: string option + } + module Api = [<Import("searchInstruments", "./src/api.js")>] let searchInstruments (token: string) (query: string) : JS.Promise<RawSearchResponse> = jsNative @@ -741,6 +757,9 @@ module Api = [<Import("getMarketNavSeries", "./src/api.js")>] let getMarketNavSeries (token: string) (code: string) : JS.Promise<RawMarketNavSeries> = jsNative + [<Import("getBondQuote", "./src/api.js")>] + let getBondQuote (token: string) (code: string) : JS.Promise<RawBondQuote> = jsNative + [<Import("createCapitalDeposit", "./src/api.js")>] let createCapitalDeposit (token: string) (fundId: string) (payload: CreateCapitalPayload) : JS.Promise<obj> = jsNative @@ -1013,6 +1032,14 @@ module Api = accumulatedNav = decodeOptionalText raw.accumulatedNav } + let decodeBondQuote (raw: RawBondQuote) : BondQuoteView = + { + code = raw.code + name = decodeOptionalText raw.name + price = decodeOptionalText raw.price + maturityDate = decodeOptionalText raw.maturityDate + } + type Model = { token: string @@ -1096,6 +1123,11 @@ type Model = navSeriesInFlight: bool navSeries: MarketNavSeriesPoint list navSeriesAvailable: bool + bondCode: string + bondQuoteSeq: int + bondQuoteInFlight: bool + bondQuote: BondQuoteView option + bondQuoteMessage: string option returnsSelectedDate: string option planReadSeq: int planInFlight: bool @@ -1196,6 +1228,10 @@ type Msg = | NavSeriesReadRequested | NavSeriesReadCompleted of requestId: int * series: RawMarketNavSeries | NavSeriesReadFailed of requestId: int * message: string + | BondCodeChanged of string + | BondQuoteRequested + | BondQuoteCompleted of requestId: int * quote: RawBondQuote + | BondQuoteFailed of requestId: int * message: string | ReturnsDateChanged of string | InvestmentPlansReadRequested | InvestmentPlansReadCompleted of requestId: int * plans: RawInvestmentPlan array @@ -1315,6 +1351,11 @@ let init () = navSeriesInFlight = false navSeries = [] navSeriesAvailable = false + bondCode = "" + bondQuoteSeq = 0 + bondQuoteInFlight = false + bondQuote = None + bondQuoteMessage = None returnsSelectedDate = None planReadSeq = 0 planInFlight = false @@ -1499,6 +1540,13 @@ let private readNavSeriesCommand token code requestId = (fun series -> NavSeriesReadCompleted(requestId, series)) (fun error -> NavSeriesReadFailed(requestId, errorText error)) +let private readBondQuoteCommand token code requestId = + Cmd.OfPromise.either + (fun () -> Api.getBondQuote token code) + () + (fun quote -> BondQuoteCompleted(requestId, quote)) + (fun error -> BondQuoteFailed(requestId, errorText error)) + let private readInvestmentPlansCommand token fundId requestId = Cmd.OfPromise.either (fun () -> Api.getInvestmentPlans token fundId) @@ -2725,6 +2773,47 @@ let update message model = Cmd.none else model, Cmd.none + | BondCodeChanged value -> { model with bondCode = value }, Cmd.none + | BondQuoteRequested -> + let code = model.bondCode.Trim() + + if String.IsNullOrWhiteSpace model.token then + { model with bondQuoteMessage = Some "请输入 API token"; bondQuote = None }, Cmd.none + elif String.IsNullOrWhiteSpace code then + { model with bondQuoteMessage = Some "请输入六位债券代码"; bondQuote = None }, Cmd.none + else + let requestId = model.bondQuoteSeq + 1 + + { + model with + bondQuoteSeq = requestId + bondQuoteInFlight = true + bondQuoteMessage = None + bondQuote = None + }, + readBondQuoteCommand model.token code requestId + | BondQuoteCompleted (requestId, quote) -> + if requestId = model.bondQuoteSeq then + { + model with + bondQuote = Some(Api.decodeBondQuote quote) + bondQuoteMessage = None + bondQuoteInFlight = false + }, + Cmd.none + else + model, Cmd.none + | BondQuoteFailed (requestId, message) -> + if requestId = model.bondQuoteSeq then + { + model with + bondQuote = None + bondQuoteMessage = Some(sprintf "债券行情不可用 — %s" message) + bondQuoteInFlight = false + }, + Cmd.none + else + model, Cmd.none | ReturnsDateChanged value -> { model with returnsSelectedDate = (if String.IsNullOrWhiteSpace value then None else Some value) }, Cmd.none | InvestmentPlansReadRequested -> @@ -4423,6 +4512,75 @@ let private investmentPlansPanel model dispatch = ] ] +let private bondQuoteMetric (label: string) (value: string option) = + Html.div [ + prop.className "metric" + prop.children [ + Html.span label + Html.strong (value |> Option.defaultValue "—") + ] + ] + +let private bondsPanel model dispatch = + Html.section [ + prop.className "panel bonds-panel" + prop.children [ + Html.div [ + prop.className "section-heading" + prop.children [ + Html.div [ + Html.p [ prop.className "eyebrow"; prop.text "13 / BONDS" ] + Html.h2 "债券行情探针" + ] + Html.span [ prop.className "section-note"; prop.text "AKShare · convertible spot" ] + ] + ] + Html.p [ + prop.className "bonds-note" + prop.text "数据来源实时 AKShare 可转债行情,仅查询单一债券代码,不做本地模拟。" + ] + Html.div [ + prop.className "search-row" + prop.children [ + Html.input [ + prop.className "text-input bond-code-input" + prop.placeholder "输入六位债券代码,如 110075" + prop.value model.bondCode + prop.onChange (fun value -> dispatch (BondCodeChanged value)) + ] + Html.button [ + prop.className "primary-action bond-quote-action" + prop.disabled model.bondQuoteInFlight + prop.onClick (fun _ -> dispatch BondQuoteRequested) + prop.text ((if model.bondQuoteInFlight then "查询中..." else "查询行情"): string) + ] + ] + ] + Html.p [ + prop.className "bond-quote-unavailable" + prop.hidden (model.bondQuoteMessage.IsNone) + prop.text (model.bondQuoteMessage |> Option.defaultValue "") + ] + match model.bondQuote with + | Some quote -> + Html.div [ + prop.className "nav-content bond-quote-result" + prop.children [ + Html.div [ + prop.className "metric-strip" + prop.children [ + bondQuoteMetric "名称" quote.name + bondQuoteMetric "价格" quote.price + bondQuoteMetric "到期日" quote.maturityDate + ] + ] + ] + ] + | None -> + Html.p [ prop.className "hint"; prop.text "查询后显示名称/价格/到期日;缺失字段以「—」表示,不补零。" ] + ] + ] + let view model dispatch = Html.main [ prop.className "app-shell" @@ -4486,6 +4644,7 @@ let view model dispatch = returnsPanel model dispatch investmentPlansPanel model dispatch rebalancePanel model dispatch + bondsPanel model dispatch Html.footer [ prop.className "footer-note"; prop.text "SOURCE · AKShare / STORAGE · PostgreSQL / LEDGER · CREATE & READ & SUBSCRIBE" ] ] ] |
