diff options
Diffstat (limited to 'src/FundLab.Web/App.fs')
| -rw-r--r-- | src/FundLab.Web/App.fs | 120 |
1 files changed, 120 insertions, 0 deletions
diff --git a/src/FundLab.Web/App.fs b/src/FundLab.Web/App.fs index 9b4bed9..7a0e56c 100644 --- a/src/FundLab.Web/App.fs +++ b/src/FundLab.Web/App.fs @@ -408,6 +408,20 @@ type RawStockQuote = currency: string } +type RawStockDailyObservation = + { + date: string + close: string + volume: obj + amount: obj + } + +type RawStockDaily = + { + code: string + observations: RawStockDailyObservation array + } + type CreateAttempt = { idempotencyKey: string @@ -724,6 +738,14 @@ type StockQuoteView = currency: string } +type StockDailyBar = + { + date: string + close: string + volume: string option + amount: string option + } + module Api = [<Import("searchInstruments", "./src/api.js")>] let searchInstruments (token: string) (query: string) : JS.Promise<RawSearchResponse> = jsNative @@ -779,6 +801,9 @@ module Api = [<Import("getStockQuote", "./src/api.js")>] let getStockQuote (token: string) (code: string) : JS.Promise<RawStockQuote> = jsNative + [<Import("getStockDaily", "./src/api.js")>] + let getStockDaily (token: string) (code: string) (days: int) : JS.Promise<RawStockDaily> = jsNative + [<Import("createCapitalDeposit", "./src/api.js")>] let createCapitalDeposit (token: string) (fundId: string) (payload: CreateCapitalPayload) : JS.Promise<obj> = jsNative @@ -1067,6 +1092,14 @@ module Api = currency = raw.currency } + let decodeStockDailyBar (raw: RawStockDailyObservation) : StockDailyBar = + { + date = raw.date + close = raw.close + volume = decodeOptionalText raw.volume + amount = decodeOptionalText raw.amount + } + type Model = { token: string @@ -1160,6 +1193,10 @@ type Model = stockQuoteInFlight: bool stockQuote: StockQuoteView option stockQuoteMessage: string option + stockDailySeq: int + stockDailyInFlight: bool + stockDaily: StockDailyBar list + stockDailyMessage: string option returnsSelectedDate: string option planReadSeq: int planInFlight: bool @@ -1268,6 +1305,9 @@ type Msg = | StockQuoteRequested | StockQuoteCompleted of requestId: int * quote: RawStockQuote | StockQuoteFailed of requestId: int * message: string + | StockDailyRequested + | StockDailyCompleted of requestId: int * daily: RawStockDaily + | StockDailyFailed of requestId: int * message: string | ReturnsDateChanged of string | InvestmentPlansReadRequested | InvestmentPlansReadCompleted of requestId: int * plans: RawInvestmentPlan array @@ -1397,6 +1437,10 @@ let init () = stockQuoteInFlight = false stockQuote = None stockQuoteMessage = None + stockDailySeq = 0 + stockDailyInFlight = false + stockDaily = [] + stockDailyMessage = None returnsSelectedDate = None planReadSeq = 0 planInFlight = false @@ -1595,6 +1639,13 @@ let private readStockQuoteCommand token code requestId = (fun quote -> StockQuoteCompleted(requestId, quote)) (fun error -> StockQuoteFailed(requestId, errorText error)) +let private readStockDailyCommand token code days requestId = + Cmd.OfPromise.either + (fun () -> Api.getStockDaily token code days) + () + (fun daily -> StockDailyCompleted(requestId, daily)) + (fun error -> StockDailyFailed(requestId, errorText error)) + let private readInvestmentPlansCommand token fundId requestId = Cmd.OfPromise.either (fun () -> Api.getInvestmentPlans token fundId) @@ -2909,6 +2960,48 @@ let update message model = Cmd.none else model, Cmd.none + | StockDailyRequested -> + let code = model.stockCode.Trim() + + if String.IsNullOrWhiteSpace model.token then + { model with stockDailyMessage = Some "请输入 API token"; stockDaily = [] }, Cmd.none + elif String.IsNullOrWhiteSpace code then + { model with stockDailyMessage = Some "请输入六位股票代码"; stockDaily = [] }, Cmd.none + else + let requestId = model.stockDailySeq + 1 + + { + model with + stockDailySeq = requestId + stockDailyInFlight = true + stockDailyMessage = None + stockDaily = [] + }, + readStockDailyCommand model.token code 5 requestId + | StockDailyCompleted (requestId, daily) -> + if requestId = model.stockDailySeq then + let rows = daily.observations |> Array.toList |> List.map Api.decodeStockDailyBar + + { + model with + stockDaily = rows + stockDailyMessage = (if List.isEmpty rows then Some "股票日线不可用 — 暂无数据" else None) + stockDailyInFlight = false + }, + Cmd.none + else + model, Cmd.none + | StockDailyFailed (requestId, message) -> + if requestId = model.stockDailySeq then + { + model with + stockDaily = [] + stockDailyMessage = Some(sprintf "股票日线不可用 — %s" message) + stockDailyInFlight = false + }, + Cmd.none + else + model, Cmd.none | ReturnsDateChanged value -> { model with returnsSelectedDate = (if String.IsNullOrWhiteSpace value then None else Some value) }, Cmd.none | InvestmentPlansReadRequested -> @@ -4709,6 +4802,12 @@ let private stocksPanel model dispatch = prop.onClick (fun _ -> dispatch StockQuoteRequested) prop.text ((if model.stockQuoteInFlight then "查询中..." else "查询行情"): string) ] + Html.button [ + prop.className "secondary-action stock-daily-action" + prop.disabled model.stockDailyInFlight + prop.onClick (fun _ -> dispatch StockDailyRequested) + prop.text ((if model.stockDailyInFlight then "读取中..." else "查询近5日日线"): string) + ] ] ] Html.p [ @@ -4733,6 +4832,27 @@ let private stocksPanel model dispatch = ] | None -> Html.p [ prop.className "hint"; prop.text "查询后显示名称/价格/币种;缺失字段以「—」表示。" ] + Html.div [ + prop.className "stock-daily" + prop.hidden (List.isEmpty model.stockDaily) + prop.children ( + Html.p [ prop.className "returns-subheading"; prop.text "近 5 个交易日收盘" ] + :: (model.stockDaily + |> List.map (fun bar -> + Html.div [ + prop.className "order-row stock-daily-row" + prop.children [ + Html.span [ prop.className "fund-detail-label"; prop.text bar.date ] + Html.span [ prop.className "fund-detail-value"; prop.text bar.close ] + ] + ])) + ) + ] + Html.p [ + prop.className "stock-daily-unavailable" + prop.hidden (model.stockDailyMessage.IsNone) + prop.text (model.stockDailyMessage |> Option.defaultValue "") + ] ] ] |
