diff options
Diffstat (limited to 'src/FundLab.Web/App.fs')
| -rw-r--r-- | src/FundLab.Web/App.fs | 156 |
1 files changed, 156 insertions, 0 deletions
diff --git a/src/FundLab.Web/App.fs b/src/FundLab.Web/App.fs index ca0c61c..9b4bed9 100644 --- a/src/FundLab.Web/App.fs +++ b/src/FundLab.Web/App.fs @@ -400,6 +400,14 @@ type RawBondQuote = maturityDate: obj } +type RawStockQuote = + { + code: string + name: obj + price: obj + currency: string + } + type CreateAttempt = { idempotencyKey: string @@ -708,6 +716,14 @@ type BondQuoteView = maturityDate: string option } +type StockQuoteView = + { + code: string + name: string option + price: string option + currency: string + } + module Api = [<Import("searchInstruments", "./src/api.js")>] let searchInstruments (token: string) (query: string) : JS.Promise<RawSearchResponse> = jsNative @@ -760,6 +776,9 @@ module Api = [<Import("getBondQuote", "./src/api.js")>] let getBondQuote (token: string) (code: string) : JS.Promise<RawBondQuote> = jsNative + [<Import("getStockQuote", "./src/api.js")>] + let getStockQuote (token: string) (code: string) : JS.Promise<RawStockQuote> = jsNative + [<Import("createCapitalDeposit", "./src/api.js")>] let createCapitalDeposit (token: string) (fundId: string) (payload: CreateCapitalPayload) : JS.Promise<obj> = jsNative @@ -1040,6 +1059,14 @@ module Api = maturityDate = decodeOptionalText raw.maturityDate } + let decodeStockQuote (raw: RawStockQuote) : StockQuoteView = + { + code = raw.code + name = decodeOptionalText raw.name + price = decodeOptionalText raw.price + currency = raw.currency + } + type Model = { token: string @@ -1128,6 +1155,11 @@ type Model = bondQuoteInFlight: bool bondQuote: BondQuoteView option bondQuoteMessage: string option + stockCode: string + stockQuoteSeq: int + stockQuoteInFlight: bool + stockQuote: StockQuoteView option + stockQuoteMessage: string option returnsSelectedDate: string option planReadSeq: int planInFlight: bool @@ -1232,6 +1264,10 @@ type Msg = | BondQuoteRequested | BondQuoteCompleted of requestId: int * quote: RawBondQuote | BondQuoteFailed of requestId: int * message: string + | StockCodeChanged of string + | StockQuoteRequested + | StockQuoteCompleted of requestId: int * quote: RawStockQuote + | StockQuoteFailed of requestId: int * message: string | ReturnsDateChanged of string | InvestmentPlansReadRequested | InvestmentPlansReadCompleted of requestId: int * plans: RawInvestmentPlan array @@ -1356,6 +1392,11 @@ let init () = bondQuoteInFlight = false bondQuote = None bondQuoteMessage = None + stockCode = "" + stockQuoteSeq = 0 + stockQuoteInFlight = false + stockQuote = None + stockQuoteMessage = None returnsSelectedDate = None planReadSeq = 0 planInFlight = false @@ -1547,6 +1588,13 @@ let private readBondQuoteCommand token code requestId = (fun quote -> BondQuoteCompleted(requestId, quote)) (fun error -> BondQuoteFailed(requestId, errorText error)) +let private readStockQuoteCommand token code requestId = + Cmd.OfPromise.either + (fun () -> Api.getStockQuote token code) + () + (fun quote -> StockQuoteCompleted(requestId, quote)) + (fun error -> StockQuoteFailed(requestId, errorText error)) + let private readInvestmentPlansCommand token fundId requestId = Cmd.OfPromise.either (fun () -> Api.getInvestmentPlans token fundId) @@ -2814,6 +2862,53 @@ let update message model = Cmd.none else model, Cmd.none + | StockCodeChanged value -> { model with stockCode = value }, Cmd.none + | StockQuoteRequested -> + let code = model.stockCode.Trim() + + if String.IsNullOrWhiteSpace model.token then + { model with stockQuoteMessage = Some "请输入 API token"; stockQuote = None }, Cmd.none + elif String.IsNullOrWhiteSpace code then + { model with stockQuoteMessage = Some "请输入六位股票代码"; stockQuote = None }, Cmd.none + else + let requestId = model.stockQuoteSeq + 1 + + { + model with + stockQuoteSeq = requestId + stockQuoteInFlight = true + stockQuoteMessage = None + stockQuote = None + }, + readStockQuoteCommand model.token code requestId + | StockQuoteCompleted (requestId, quote) -> + if requestId = model.stockQuoteSeq then + { + model with + stockQuote = Some(Api.decodeStockQuote quote) + stockQuoteMessage = None + stockQuoteInFlight = false + }, + Cmd.none + else + model, Cmd.none + | StockQuoteFailed (requestId, message) -> + if requestId = model.stockQuoteSeq then + let text = + if message.Contains("six digits") then + "无效代码 — 请输入六位股票代码" + else + sprintf "股票行情不可用 — %s" message + + { + model with + stockQuote = None + stockQuoteMessage = Some text + stockQuoteInFlight = false + }, + Cmd.none + else + model, Cmd.none | ReturnsDateChanged value -> { model with returnsSelectedDate = (if String.IsNullOrWhiteSpace value then None else Some value) }, Cmd.none | InvestmentPlansReadRequested -> @@ -4581,6 +4676,66 @@ let private bondsPanel model dispatch = ] ] +let private stocksPanel model dispatch = + Html.section [ + prop.className "panel stocks-panel" + prop.children [ + Html.div [ + prop.className "section-heading" + prop.children [ + Html.div [ + Html.p [ prop.className "eyebrow"; prop.text "14 / STOCKS" ] + Html.h2 "股票行情探针" + ] + Html.span [ prop.className "section-note"; prop.text "AKShare · A-share spot" ] + ] + ] + Html.p [ + prop.className "bonds-note" + prop.text "数据来源实时 AKShare A 股行情,仅查询单一六位代码,不做本地模拟,不补零。" + ] + Html.div [ + prop.className "search-row" + prop.children [ + Html.input [ + prop.className "text-input stock-code-input" + prop.placeholder "输入六位股票代码,如 600519" + prop.value model.stockCode + prop.onChange (fun value -> dispatch (StockCodeChanged value)) + ] + Html.button [ + prop.className "primary-action stock-quote-action" + prop.disabled model.stockQuoteInFlight + prop.onClick (fun _ -> dispatch StockQuoteRequested) + prop.text ((if model.stockQuoteInFlight then "查询中..." else "查询行情"): string) + ] + ] + ] + Html.p [ + prop.className "stock-quote-unavailable" + prop.hidden (model.stockQuoteMessage.IsNone) + prop.text (model.stockQuoteMessage |> Option.defaultValue "") + ] + match model.stockQuote with + | Some quote -> + Html.div [ + prop.className "nav-content stock-quote-result" + prop.children [ + Html.div [ + prop.className "metric-strip" + prop.children [ + bondQuoteMetric "名称" quote.name + bondQuoteMetric "价格" quote.price + bondQuoteMetric "币种" (Some quote.currency) + ] + ] + ] + ] + | None -> + Html.p [ prop.className "hint"; prop.text "查询后显示名称/价格/币种;缺失字段以「—」表示。" ] + ] + ] + let view model dispatch = Html.main [ prop.className "app-shell" @@ -4645,6 +4800,7 @@ let view model dispatch = investmentPlansPanel model dispatch rebalancePanel model dispatch bondsPanel model dispatch + stocksPanel model dispatch Html.footer [ prop.className "footer-note"; prop.text "SOURCE · AKShare / STORAGE · PostgreSQL / LEDGER · CREATE & READ & SUBSCRIBE" ] ] ] |
