diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-22 05:56:14 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-22 05:56:14 +0800 |
| commit | 79600c15aef9bd411abe88dcf36286a102b5abea (patch) | |
| tree | e5e3bfbff6f60a48a3c0ff8b78b48da98c8f238a /src/FundLab.Web | |
| parent | 6abd4c206fbfe8369b3fde4c905619d08f13c5d1 (diff) | |
| download | fund-lab-79600c15aef9bd411abe88dcf36286a102b5abea.tar.gz | |
Add daily market refresh endpoint, snapshot-first valuation and UI refresh (3d-25)
Diffstat (limited to 'src/FundLab.Web')
| -rw-r--r-- | src/FundLab.Web/App.fs | 89 | ||||
| -rw-r--r-- | src/FundLab.Web/src/api.js | 7 |
2 files changed, 96 insertions, 0 deletions
diff --git a/src/FundLab.Web/App.fs b/src/FundLab.Web/App.fs index 7dea8bb..fe16411 100644 --- a/src/FundLab.Web/App.fs +++ b/src/FundLab.Web/App.fs @@ -505,6 +505,29 @@ type RawFundValuation = positions: RawValuationPosition array } +type RawMarketRefreshTarget = + { + instrumentCode: string + assetClass: string + snapshotDate: string + price: string + } + +type RawMarketRefreshFailure = + { + instrumentCode: string + assetClass: string + reason: string + } + +type RawMarketRefresh = + { + fundId: string + asOfDate: string + refreshed: RawMarketRefreshTarget array + failures: RawMarketRefreshFailure array + } + type CreateAttempt = { idempotencyKey: string @@ -996,6 +1019,9 @@ module Api = [<Import("getFundValuation", "./src/api.js")>] let getFundValuation (token: string) (fundId: string) : JS.Promise<RawFundValuation> = jsNative + [<Import("refreshFundMarketData", "./src/api.js")>] + let refreshFundMarketData (token: string) (fundId: string) : JS.Promise<RawMarketRefresh> = jsNative + [<Import("createCapitalDeposit", "./src/api.js")>] let createCapitalDeposit (token: string) (fundId: string) (payload: CreateCapitalPayload) : JS.Promise<obj> = jsNative @@ -1492,6 +1518,9 @@ type Model = valuationInFlight: bool valuation: FundValuationView option valuationMessage: string option + marketRefreshSeq: int + marketRefreshInFlight: bool + marketRefreshMessage: string option returnsSelectedDate: string option planReadSeq: int planInFlight: bool @@ -1627,6 +1656,9 @@ type Msg = | ValuationReadRequested | ValuationReadCompleted of requestId: int * fundId: string * valuation: RawFundValuation | ValuationReadFailed of requestId: int * fundId: string * message: string + | MarketRefreshRequested + | MarketRefreshCompleted of requestId: int * fundId: string * response: RawMarketRefresh + | MarketRefreshFailed of requestId: int * fundId: string * message: string | ReturnsDateChanged of string | InvestmentPlansReadRequested | InvestmentPlansReadCompleted of requestId: int * plans: RawInvestmentPlan array @@ -1797,6 +1829,9 @@ let init () = valuationInFlight = false valuation = None valuationMessage = None + marketRefreshSeq = 0 + marketRefreshInFlight = false + marketRefreshMessage = None returnsSelectedDate = None planReadSeq = 0 planInFlight = false @@ -2058,6 +2093,13 @@ let private readValuationCommand token fundId requestId = (fun valuation -> ValuationReadCompleted(requestId, fundId, valuation)) (fun error -> ValuationReadFailed(requestId, fundId, errorText error)) +let private refreshMarketDataCommand token fundId requestId = + Cmd.OfPromise.either + (fun () -> Api.refreshFundMarketData token fundId) + () + (fun response -> MarketRefreshCompleted(requestId, fundId, response)) + (fun error -> MarketRefreshFailed(requestId, fundId, errorText error)) + let private readInvestmentPlansCommand token fundId requestId = Cmd.OfPromise.either (fun () -> Api.getInvestmentPlans token fundId) @@ -3710,6 +3752,42 @@ let update message model = Cmd.none else model, Cmd.none + | MarketRefreshRequested -> + match model.createdFund with + | Some fund when not (String.IsNullOrWhiteSpace model.token) -> + let requestId = model.marketRefreshSeq + 1 + + { + model with + marketRefreshSeq = requestId + marketRefreshInFlight = true + marketRefreshMessage = None + }, + refreshMarketDataCommand model.token fund.id requestId + | _ -> model, Cmd.none + | MarketRefreshCompleted (requestId, fundId, response) -> + if requestId = model.marketRefreshSeq + && (match model.createdFund with Some fund -> fund.id = fundId | None -> false) then + { + model with + marketRefreshInFlight = false + marketRefreshMessage = + Some(sprintf "行情刷新完成(%s):更新 %d 项,失败 %d 项" response.asOfDate response.refreshed.Length response.failures.Length) + }, + Cmd.ofMsg ValuationReadRequested + else + model, Cmd.none + | MarketRefreshFailed (requestId, fundId, message) -> + if requestId = model.marketRefreshSeq + && (match model.createdFund with Some fund -> fund.id = fundId | None -> false) then + { + model with + marketRefreshInFlight = false + marketRefreshMessage = Some(sprintf "行情刷新失败 — %s" message) + }, + Cmd.none + else + model, Cmd.none | ReturnsDateChanged value -> { model with returnsSelectedDate = (if String.IsNullOrWhiteSpace value then None else Some value) }, Cmd.none | InvestmentPlansReadRequested -> @@ -5743,8 +5821,19 @@ let private stocksPanel model dispatch = prop.onClick (fun _ -> dispatch ValuationReadRequested) prop.text ((if model.valuationInFlight then "估值中..." else "刷新估值"): string) ] + Html.button [ + prop.className "secondary-action market-refresh-action" + prop.disabled model.marketRefreshInFlight + prop.onClick (fun _ -> dispatch MarketRefreshRequested) + prop.text ((if model.marketRefreshInFlight then "刷新中..." else "刷新行情"): string) + ] ] ] + Html.p [ + prop.className "market-refresh-message" + prop.hidden (model.marketRefreshMessage.IsNone) + prop.text (model.marketRefreshMessage |> Option.defaultValue "") + ] match model.valuation with | Some valuation -> Html.div [ diff --git a/src/FundLab.Web/src/api.js b/src/FundLab.Web/src/api.js index a03b97f..eaa8978 100644 --- a/src/FundLab.Web/src/api.js +++ b/src/FundLab.Web/src/api.js @@ -183,6 +183,13 @@ export function getFundValuation(token, fundId) { return requestJson(`/api/funds/${encodeURIComponent(fundId)}/valuation`, token); } +export function refreshFundMarketData(token, fundId, asOfDate) { + const suffix = asOfDate ? `?asOfDate=${encodeURIComponent(asOfDate)}` : ""; + return requestJson(`/api/funds/${encodeURIComponent(fundId)}/market-data/refresh${suffix}`, token, { + method: "POST", + }); +} + export function createSipPlan(token, fundId, payload) { const body = `{"instrumentCode":${JSON.stringify(payload.instrumentCode)},"amount":${JSON.stringify(payload.amount)},"frequency":${JSON.stringify(payload.frequency)}}`; return requestJson(`/api/funds/${encodeURIComponent(fundId)}/sip/plans`, token, { |
