summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/FundLab.Web/App.fs156
-rw-r--r--src/FundLab.Web/src/api.js4
-rw-r--r--src/FundLab.Web/src/styles.css5
3 files changed, 165 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" ]
]
]
diff --git a/src/FundLab.Web/src/api.js b/src/FundLab.Web/src/api.js
index ed49552..7b7e1b9 100644
--- a/src/FundLab.Web/src/api.js
+++ b/src/FundLab.Web/src/api.js
@@ -139,6 +139,10 @@ export function getBondQuote(token, code) {
return requestJson(`/api/market/bond-quote?code=${encodeURIComponent(code)}`, token);
}
+export function getStockQuote(token, code) {
+ return requestJson(`/api/market/stock-quote?code=${encodeURIComponent(code)}`, token);
+}
+
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, {
diff --git a/src/FundLab.Web/src/styles.css b/src/FundLab.Web/src/styles.css
index 0f67e97..17326b5 100644
--- a/src/FundLab.Web/src/styles.css
+++ b/src/FundLab.Web/src/styles.css
@@ -589,6 +589,11 @@ h2 {
font-size: 12px;
}
+.stock-quote-unavailable {
+ color: #b45309;
+ font-size: 12px;
+}
+
.returns-pending {
color: #b45309;
font-size: 12px;