mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 17:54:15 +08:00
- 后端: 数据集→能力映射增广 (daily/adj_factor/minute/financial),
非 tickflow provider 按声明数据集动态 grant; 数据源偏好更新与
删除数据源后刷新 app.state.capabilities 快照; 新增 8 项测试
- 前端: 通用界面去档位词, 缺能力统一「{能力名} · 不可用」徽章
(capability-labels 新增 MissingCapChip, 点击跳设置→数据源);
StatCard/深度配置/分钟同步/历史扩展/财务页等 20+ 文件对齐;
实时模式判定改用 quoteStatus.mode 与 realtime_allowed,
替代前端 tierRank 推断; 监控设置页放开整页拦截
- 档位词仅保留 TickFlow 专属界面 (Key 配置/端点测速/引导页),
docs/configuration.md 档位表加注解说明
148 lines
6.2 KiB
TypeScript
148 lines
6.2 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { api } from '@/lib/api'
|
|
import { QK } from '@/lib/queryKeys'
|
|
import { usePreferences, useCapabilities } from '@/lib/useSharedQueries'
|
|
import { MissingCapChip } from '@/lib/capability-labels'
|
|
|
|
/**
|
|
* 五档盘口 sealed(真假涨停) 配置内容(纯内容, 无外框, 由父级 Card 包裹)。
|
|
*
|
|
* - 轮询间隔: 常规 10~120s · 数据源具备实时推送能力时 3~120s
|
|
* - 盘后定版时间: 15:01~18:00, 默认 15:02
|
|
* - disabled 时(监控关闭)输入框禁用
|
|
*/
|
|
// 注: 文件名保留 DepthConfigCard.tsx, 导出 DepthConfigContent(纯内容无外框)
|
|
export function DepthConfigContent({ disabled }: { disabled?: boolean }) {
|
|
const qc = useQueryClient()
|
|
const prefs = usePreferences()
|
|
const caps = useCapabilities()
|
|
|
|
const hasDepth = !!caps.data?.capabilities?.['depth5.batch']
|
|
// 能力判定(非档位): 具备实时推送能力的数据源允许更快的盘口轮询
|
|
const fastPolling = !!caps.data?.capabilities?.['websocket']
|
|
const range = fastPolling ? { lo: 3, hi: 120 } : { lo: 10, hi: 120 }
|
|
|
|
const interval = prefs.data?.depth_polling_interval ?? 10
|
|
const finalizeTime = prefs.data?.depth_finalize_time ?? { hour: 15, minute: 2 }
|
|
|
|
const [intervalInput, setIntervalInput] = useState(String(Math.round(interval)))
|
|
const [finalizeHour, setFinalizeHour] = useState(String(finalizeTime.hour))
|
|
const [finalizeMinute, setFinalizeMinute] = useState(String(finalizeTime.minute))
|
|
|
|
useEffect(() => { setIntervalInput(String(Math.round(interval))) }, [interval])
|
|
useEffect(() => {
|
|
setFinalizeHour(String(finalizeTime.hour))
|
|
setFinalizeMinute(String(finalizeTime.minute))
|
|
}, [finalizeTime.hour, finalizeTime.minute])
|
|
|
|
const saveInterval = useMutation({
|
|
mutationFn: (v: number) => api.updateDepthPollingInterval(v),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: QK.preferences }),
|
|
})
|
|
const saveFinalize = useMutation({
|
|
mutationFn: ({ hour, minute }: { hour: number; minute: number }) =>
|
|
api.updateDepthFinalizeTime(hour, minute),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: QK.preferences }),
|
|
})
|
|
|
|
// 无能力: 显示能力缺失说明 + 去数据源配置入口
|
|
if (!hasDepth) {
|
|
return (
|
|
<div className="space-y-2">
|
|
<p className="text-xs text-muted leading-relaxed">
|
|
真假涨停判定依赖五档盘口实时快照,该数据当前不可用。
|
|
配置提供五档盘口的数据源后,连板梯队将自动区分真封板(显示封单量)与假涨停(归入炸板)。
|
|
</p>
|
|
<MissingCapChip capKey="depth5.batch" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const inputCls = `w-16 h-7 bg-elevated border border-border rounded text-xs text-center px-1 focus:outline-none focus:border-accent/50 ${disabled ? 'opacity-40 cursor-not-allowed' : ''}`
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{/* 盘中轮询间隔 */}
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className={disabled ? 'opacity-50' : ''}>
|
|
<div className="text-xs text-secondary">盘中轮询间隔</div>
|
|
<div className="text-[10px] text-muted">范围 {range.lo}~{range.hi} 秒 · 涨跌停过多时系统自动放慢</div>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<input
|
|
type="number"
|
|
min={range.lo}
|
|
max={range.hi}
|
|
value={intervalInput}
|
|
disabled={disabled}
|
|
onChange={e => setIntervalInput(e.target.value)}
|
|
onBlur={() => {
|
|
if (disabled) return
|
|
let v = Number(intervalInput)
|
|
if (!Number.isFinite(v)) v = range.lo
|
|
v = Math.max(range.lo, Math.min(range.hi, v))
|
|
saveInterval.mutate(v)
|
|
}}
|
|
className={inputCls}
|
|
/>
|
|
<span className="text-xs text-muted">秒</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 盘后定版时间 */}
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className={disabled ? 'opacity-50' : ''}>
|
|
<div className="text-xs text-secondary">盘后定版时间</div>
|
|
<div className="text-[10px] text-muted">范围 15:01~18:00 · 收盘后拉取最终盘口定版</div>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<input
|
|
type="number"
|
|
min={15}
|
|
max={18}
|
|
value={finalizeHour}
|
|
disabled={disabled}
|
|
onChange={e => setFinalizeHour(e.target.value)}
|
|
onBlur={() => {
|
|
if (disabled) return
|
|
let h = Number(finalizeHour)
|
|
if (!Number.isFinite(h)) h = 15
|
|
h = Math.max(15, Math.min(18, h))
|
|
let m = Number(finalizeMinute)
|
|
if (!Number.isFinite(m)) m = 2
|
|
m = Math.max(0, Math.min(59, m))
|
|
if (h * 60 + m < 15 * 60 + 1) { h = 15; m = 1 }
|
|
if (h * 60 + m > 18 * 60) { h = 18; m = 0 }
|
|
saveFinalize.mutate({ hour: h, minute: m })
|
|
}}
|
|
className={`w-12 h-7 bg-elevated border border-border rounded text-xs text-center px-1 focus:outline-none focus:border-accent/50 ${disabled ? 'opacity-40 cursor-not-allowed' : ''}`}
|
|
/>
|
|
<span className="text-xs text-muted">:</span>
|
|
<input
|
|
type="number"
|
|
min={0}
|
|
max={59}
|
|
value={finalizeMinute}
|
|
disabled={disabled}
|
|
onChange={e => setFinalizeMinute(e.target.value)}
|
|
onBlur={() => {
|
|
if (disabled) return
|
|
let h = Number(finalizeHour)
|
|
if (!Number.isFinite(h)) h = 15
|
|
h = Math.max(15, Math.min(18, h))
|
|
let m = Number(finalizeMinute)
|
|
if (!Number.isFinite(m)) m = 2
|
|
m = Math.max(0, Math.min(59, m))
|
|
if (h * 60 + m < 15 * 60 + 1) { h = 15; m = 1 }
|
|
if (h * 60 + m > 18 * 60) { h = 18; m = 0 }
|
|
saveFinalize.mutate({ hour: h, minute: m })
|
|
}}
|
|
className={`w-12 h-7 bg-elevated border border-border rounded text-xs text-center px-1 focus:outline-none focus:border-accent/50 ${disabled ? 'opacity-40 cursor-not-allowed' : ''}`}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|