summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/FundLab.Web/App.fs159
-rw-r--r--src/FundLab.Web/src/api.js4
-rw-r--r--src/FundLab.Web/src/styles.css11
3 files changed, 174 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" ]
]
]
diff --git a/src/FundLab.Web/src/api.js b/src/FundLab.Web/src/api.js
index c94a9e0..ed49552 100644
--- a/src/FundLab.Web/src/api.js
+++ b/src/FundLab.Web/src/api.js
@@ -135,6 +135,10 @@ export function getMarketNavSeries(token, code) {
return requestJson(`/api/market/nav-series?code=${encodeURIComponent(code)}&limit=30`, token);
}
+export function getBondQuote(token, code) {
+ return requestJson(`/api/market/bond-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 b994a80..0f67e97 100644
--- a/src/FundLab.Web/src/styles.css
+++ b/src/FundLab.Web/src/styles.css
@@ -578,6 +578,17 @@ h2 {
font-size: 12px;
}
+.bonds-note {
+ color: #64748b;
+ font-size: 12px;
+ margin-bottom: 8px;
+}
+
+.bond-quote-unavailable {
+ color: #b45309;
+ font-size: 12px;
+}
+
.returns-pending {
color: #b45309;
font-size: 12px;