mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 17:54:15 +08:00
feat(settings): 补全设置卡片定位闪烁锚点体系
此前「从业务页引导去设置」只有 depth-fix 一处孤例 (SealedBadge → 连板 梯队修正卡片闪烁), 其余 8 个引导链接只带 tab 参数跳转, 用户落地后要 自己在设置页里找目标卡片。 - 通用锚点机制: useCardFlash hook (/settings?tab=X&highlight=<anchor> 打开时目标卡片 scrollIntoView 居中 + ring 高亮 2s, 一次会话仅触发 一次防 StrictMode 重放) + AnchorWrap 包裹层 (无统一 Card 组件的面板用) - Settings 把 highlight 传给全部面板 (面板类型本就声明了 highlight?: string) - 锚点落位: 监控页 Card 组件 anchor 化 — quotes(行情轮询)/webhooks(推送 通知)/minute-refresh(盘中分钟增量)/intraday-refresh(分时图刷新)/ depth-fix(原实现迁移到统一机制); AI ai-connection; 数据源 data-sources; 信号库 signals - 8 个引导链接补 &highlight=: RuleEditor/Review webhook 未配置提示 → webhooks; Layout 实时无权限/监控入口 → data-sources/quotes; Data 页 数据源入口 → data-sources; 信号触发器 → signals 验证: pnpm build 通过; 浏览器实测 8 个锚点全部滚动定位+闪烁; 顺带补验 40e54ea 统一策略池欠的界面项 (双池→统一迁移 17/33、分钟徽章、三态筛选 只过滤显示不动池、ETF 视角无分钟策略、分钟卡片实时单跑)。
This commit is contained in:
@@ -754,7 +754,7 @@ export function Layout() {
|
||||
当前数据源无实时行情权限,
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/settings?tab=data-sources')}
|
||||
onClick={() => navigate('/settings?tab=data-sources&highlight=data-sources')}
|
||||
className="mx-0.5 text-accent/80 hover:text-accent hover:underline"
|
||||
>
|
||||
去配置数据源
|
||||
@@ -777,7 +777,7 @@ export function Layout() {
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<button
|
||||
onClick={() => navigate('/settings?tab=monitoring')}
|
||||
onClick={() => navigate('/settings?tab=monitoring&highlight=quotes')}
|
||||
aria-label="打开实时监控设置"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-btn text-muted transition-colors hover:bg-elevated hover:text-foreground"
|
||||
title="实时监控设置"
|
||||
|
||||
@@ -1402,7 +1402,7 @@ export function RuleEditor({ rule, preset, simple, onClose, onSaved }: Props) {
|
||||
return (
|
||||
<p className="text-[10px] leading-relaxed text-warning/80">
|
||||
{unconfigured.join('、')}尚未配置,
|
||||
<Link to="/settings?tab=monitoring" className="text-accent hover:text-accent/80">前往设置页配置 →</Link>
|
||||
<Link to="/settings?tab=monitoring&highlight=webhooks" className="text-accent hover:text-accent/80">前往设置页配置 →</Link>
|
||||
</p>
|
||||
)
|
||||
})()}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function SignalTriggerActions({ kind, signals, onChange, buttonClassName,
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/settings?tab=signals')}
|
||||
onClick={() => navigate('/settings?tab=signals&highlight=signals')}
|
||||
title="去信号库"
|
||||
className={`${btnCls} hover:border-amber-400/40 hover:text-amber-400`}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
/**
|
||||
* 设置卡片定位闪烁锚点。
|
||||
*
|
||||
* 用法: /settings?tab=monitoring&highlight=<key> 打开时, 声明了 anchor=key 的
|
||||
* 卡片滚动到视口中央并 ring 高亮 2 秒 — 从其他页面引导用户"去某张卡片开启/配置"
|
||||
* 时精确定位, 不再让用户在设置页里大海捞针。
|
||||
*
|
||||
* 每个锚点 key 一次会话只触发一次 (firedRef), 避免 StrictMode 双渲染重复闪烁。
|
||||
*/
|
||||
export function useCardFlash(highlight: string | undefined, key: string) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const firedRef = useRef(false)
|
||||
const [flash, setFlash] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (highlight !== key || firedRef.current || !ref.current) return
|
||||
firedRef.current = true
|
||||
ref.current.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
// 等滚动起一帧再点亮, 视觉上"滚过去 → 闪一下"更连贯
|
||||
requestAnimationFrame(() => {
|
||||
setFlash(true)
|
||||
window.setTimeout(() => setFlash(false), 2000)
|
||||
})
|
||||
}, [highlight, key])
|
||||
|
||||
return { ref, flash }
|
||||
}
|
||||
|
||||
/** 闪烁态样式: 与连板梯队修正卡片 (depth-fix) 原实现保持一致 */
|
||||
export function cardFlashCls(flash: boolean): string {
|
||||
return `rounded-card transition-all duration-500 ${
|
||||
flash ? 'ring-2 ring-accent/60 ring-offset-2 ring-offset-base scale-[1.01]' : 'ring-0 ring-transparent'
|
||||
}`
|
||||
}
|
||||
|
||||
/** 锚点包裹层: 面板没有统一 Card 组件时, 直接包住目标区块即可 */
|
||||
export function AnchorWrap({
|
||||
highlight,
|
||||
anchor,
|
||||
children,
|
||||
}: {
|
||||
highlight?: string
|
||||
anchor: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const { ref, flash } = useCardFlash(highlight, anchor)
|
||||
return (
|
||||
<div ref={ref} id={anchor} className={cardFlashCls(flash)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -633,7 +633,7 @@ export function Data() {
|
||||
</button>
|
||||
<div className="w-px h-4 bg-border" />
|
||||
<Link
|
||||
to="/settings?tab=data-sources"
|
||||
to="/settings?tab=data-sources&highlight=data-sources"
|
||||
className="inline-flex items-center gap-1 px-2 py-1 rounded-btn text-secondary hover:text-accent hover:bg-accent/8 text-xs transition-colors duration-150"
|
||||
title="切换数据源"
|
||||
>
|
||||
@@ -661,7 +661,7 @@ export function Data() {
|
||||
<span className="text-secondary leading-relaxed">
|
||||
当前无需 API Key,历史日K将使用免费通道获取。
|
||||
实时行情、分钟K等能力取决于所选数据源,可在
|
||||
<Link to="/settings?tab=data-sources" className="mx-0.5 font-medium text-accent hover:underline">
|
||||
<Link to="/settings?tab=data-sources&highlight=data-sources" className="mx-0.5 font-medium text-accent hover:underline">
|
||||
数据源设置
|
||||
</Link>
|
||||
中配置。
|
||||
|
||||
@@ -469,7 +469,7 @@ export function Review() {
|
||||
<p className="mt-1.5 text-[10px] leading-relaxed text-muted/70">
|
||||
手动或定时生成的复盘都会推送完整报告。复用「设置 → 实时监控」的 Webhook 配置。
|
||||
{((reviewPushChannels.includes('feishu') && !feishuConfigured) || (reviewPushChannels.includes('wecom') && !wecomConfigured)) && (
|
||||
<Link to="/settings?tab=monitoring" className="ml-1 text-accent hover:underline" onClick={() => setShowSchedule(false)}>
|
||||
<Link to="/settings?tab=monitoring&highlight=webhooks" className="ml-1 text-accent hover:underline" onClick={() => setShowSchedule(false)}>
|
||||
前往配置 →
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -125,7 +125,7 @@ export function Settings() {
|
||||
>
|
||||
{activeTab.key === 'monitoring'
|
||||
? <SettingsMonitoringPanel highlight={highlight} />
|
||||
: <activeTab.panel />}
|
||||
: <activeTab.panel highlight={highlight} />}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useState, useEffect, useRef, createContext, useContext } from 'react'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Save, Loader2, Check, Wifi, WifiOff, Eye, EyeOff, Shield,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { useSettings } from '@/lib/useSharedQueries'
|
||||
import { api, type SettingsState } from '@/lib/api'
|
||||
import { QK } from '@/lib/queryKeys'
|
||||
import { useCardFlash, cardFlashCls } from '@/lib/useCardFlash'
|
||||
|
||||
// 统一的输入框样式(与项目其他设置页一致)
|
||||
const INPUT_CLS =
|
||||
@@ -70,7 +71,7 @@ const findPreset = (provider: string, baseUrl: string, codexCommand: string) =>
|
||||
return provider === CODEX_PROVIDER ? p.codexCommand === codexCommand : p.url === baseUrl
|
||||
}) ?? PRESETS[0]
|
||||
|
||||
export function SettingsAIPanel() {
|
||||
export function SettingsAIPanel({ highlight }: { highlight?: string } = {}) {
|
||||
const qc = useQueryClient()
|
||||
const settings = useSettings()
|
||||
const s = settings.data
|
||||
@@ -306,8 +307,9 @@ export function SettingsAIPanel() {
|
||||
}
|
||||
|
||||
return (
|
||||
<HighlightContext.Provider value={highlight ?? ''}>
|
||||
<div className="space-y-5 max-w-2xl">
|
||||
<Card icon={Plug} title="连接状态" right={
|
||||
<Card icon={Plug} title="连接状态" anchor="ai-connection" right={
|
||||
configured && (
|
||||
<button onClick={handleTest} disabled={testing}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-btn bg-elevated hover:bg-elevated/80 text-xs text-secondary transition-colors duration-150 ease-smooth disabled:opacity-50">
|
||||
@@ -519,20 +521,27 @@ export function SettingsAIPanel() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</HighlightContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// ===== 通用卡片(与 Keys 页风格统一) =====
|
||||
|
||||
// 卡片定位锚点: highlight=<anchor> 时滚动到视口中央并闪烁 (见 useCardFlash)
|
||||
const HighlightContext = createContext('')
|
||||
|
||||
interface CardProps {
|
||||
icon: React.ComponentType<{ className?: string }>
|
||||
title: string
|
||||
right?: React.ReactNode
|
||||
children: React.ReactNode
|
||||
anchor?: string
|
||||
}
|
||||
|
||||
function Card({ icon: Icon, title, right, children }: CardProps) {
|
||||
return (
|
||||
function Card({ icon: Icon, title, right, children, anchor }: CardProps) {
|
||||
const highlight = useContext(HighlightContext)
|
||||
const { ref, flash } = useCardFlash(anchor ? highlight : undefined, anchor ?? '')
|
||||
const inner = (
|
||||
<section className="rounded-card border border-border bg-surface p-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2.5">
|
||||
@@ -544,6 +553,12 @@ function Card({ icon: Icon, title, right, children }: CardProps) {
|
||||
{children}
|
||||
</section>
|
||||
)
|
||||
if (!anchor) return inner
|
||||
return (
|
||||
<div ref={ref} id={anchor} className={cardFlashCls(flash)}>
|
||||
{inner}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ===== 表单字段(统一 label + 输入框样式) =====
|
||||
|
||||
@@ -6,6 +6,7 @@ import { QK } from '@/lib/queryKeys'
|
||||
import { BUILTIN_SIGNAL_DEFINITIONS, type SignalKind } from '@/lib/signals'
|
||||
import { CustomSignalDialog } from '@/components/signals/CustomSignalDialog'
|
||||
import { Skeleton } from '@/components/data/Skeleton'
|
||||
import { AnchorWrap } from '@/lib/useCardFlash'
|
||||
|
||||
type SignalSection = 'builtin' | 'custom'
|
||||
|
||||
@@ -16,7 +17,7 @@ const KIND_CLASS: Record<SignalKind, string> = {
|
||||
both: 'bg-muted/10 text-muted',
|
||||
}
|
||||
|
||||
export function SettingsCustomSignalsPanel() {
|
||||
export function SettingsCustomSignalsPanel({ highlight }: { highlight?: string } = {}) {
|
||||
const qc = useQueryClient()
|
||||
const list = useQuery({ queryKey: QK.customSignals, queryFn: api.customSignalsList })
|
||||
const options = useQuery({ queryKey: QK.customSignalsOptions, queryFn: api.customSignalsOptions })
|
||||
@@ -87,6 +88,7 @@ export function SettingsCustomSignalsPanel() {
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl space-y-6">
|
||||
<AnchorWrap highlight={highlight} anchor="signals">
|
||||
<section className="rounded-2xl border border-border bg-surface p-6 bg-[radial-gradient(circle_at_top_right,rgba(234,179,8,0.12),transparent_38%)]">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
@@ -133,6 +135,7 @@ export function SettingsCustomSignalsPanel() {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</AnchorWrap>
|
||||
|
||||
{activeSection === 'builtin' && (
|
||||
<section className="rounded-card border border-border bg-surface p-5 space-y-4">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Check, Database, Eye, EyeOff, KeyRound, Plus, RefreshCw, Zap, FileWarni
|
||||
import { api, type DataSourceItem, type PluginDataSourceItem } from '@/lib/api'
|
||||
import { QK } from '@/lib/queryKeys'
|
||||
import { useCapabilities, usePreferences } from '@/lib/useSharedQueries'
|
||||
import { AnchorWrap } from '@/lib/useCardFlash'
|
||||
import { TIER_RANK, tierRank, tierStyle } from '@/lib/capability-labels'
|
||||
import { toast } from '@/components/Toast'
|
||||
import { DataSourceEditor } from './DataSourceEditor'
|
||||
@@ -367,7 +368,7 @@ function PluginKeyConfig({ plugin }: { plugin: PluginDataSourceItem }) {
|
||||
)
|
||||
}
|
||||
|
||||
export function SettingsDataSourcesPanel() {
|
||||
export function SettingsDataSourcesPanel({ highlight }: { highlight?: string } = {}) {
|
||||
const qc = useQueryClient()
|
||||
const prefs = usePreferences()
|
||||
const sources = useQuery({ queryKey: QK.dataSources, queryFn: api.dataSources })
|
||||
@@ -585,6 +586,7 @@ export function SettingsDataSourcesPanel() {
|
||||
return (
|
||||
<div className="space-y-5 max-w-5xl">
|
||||
{/* ===== 顶部: 当前数据源 + 数据源选择 (一个大卡片) ===== */}
|
||||
<AnchorWrap highlight={highlight} anchor="data-sources">
|
||||
<section className="rounded-card border border-border bg-surface p-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2.5">
|
||||
@@ -779,6 +781,7 @@ export function SettingsDataSourcesPanel() {
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</AnchorWrap>
|
||||
|
||||
{/* ===== 下方: 编辑区 ===== */}
|
||||
<AnimatePresence mode="wait">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||
import { useState, useCallback, useEffect, createContext, useContext } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
@@ -19,9 +19,14 @@ import {
|
||||
import { useUpdateQuoteInterval, useToggleRealtimeQuotes } from '@/lib/useSharedMutations'
|
||||
import { api } from '@/lib/api'
|
||||
import { QK } from '@/lib/queryKeys'
|
||||
import { useCardFlash, cardFlashCls } from '@/lib/useCardFlash'
|
||||
import { toast } from '@/components/Toast'
|
||||
import { DepthConfigContent } from '@/components/data/DepthConfigCard'
|
||||
|
||||
// 卡片定位锚点: highlight=<anchor> 时该卡片滚动到视口中央并闪烁高亮。
|
||||
// 其他页面用 /settings?tab=monitoring&highlight=<anchor> 精确引导用户到某张卡片。
|
||||
const HighlightContext = createContext('')
|
||||
|
||||
// 页面 → 显示名
|
||||
const PAGE_LABELS: Record<string, string> = {
|
||||
'overview-market': '看板',
|
||||
@@ -288,27 +293,13 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
|
||||
return () => window.clearTimeout(t)
|
||||
}, [minuteRefreshIntervalDraft, minuteRefreshInterval, save])
|
||||
|
||||
// highlight=depth-fix 时闪烁高亮连板梯队修正卡片
|
||||
const [flash, setFlash] = useState(false)
|
||||
const flashedRef = useRef(false)
|
||||
useEffect(() => {
|
||||
if (highlight === 'depth-fix' && !flashedRef.current) {
|
||||
flashedRef.current = true
|
||||
// 延迟一帧确保 DOM 已渲染, 再触发闪烁
|
||||
requestAnimationFrame(() => {
|
||||
setFlash(true)
|
||||
const t = setTimeout(() => setFlash(false), 2000)
|
||||
return () => clearTimeout(t)
|
||||
})
|
||||
}
|
||||
}, [highlight])
|
||||
|
||||
return (
|
||||
<HighlightContext.Provider value={highlight ?? ''}>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-6 max-w-5xl">
|
||||
{/* ========== 左列 ========== */}
|
||||
<div className="space-y-6">
|
||||
{/* 行情状态 — 开关 + 间隔 */}
|
||||
<Card icon={Activity} title="行情轮询">
|
||||
<Card icon={Activity} title="行情轮询" anchor="quotes">
|
||||
<ToggleRow
|
||||
label="实时行情"
|
||||
desc={
|
||||
@@ -408,7 +399,7 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
|
||||
)}
|
||||
|
||||
{/* 自选列表分时图实时刷新 (默认关闭, 开启后盘中按设定间隔轮询刷新分时数据) */}
|
||||
<Card icon={Activity} title="分时图刷新">
|
||||
<Card icon={Activity} title="分时图刷新" anchor="intraday-refresh">
|
||||
<ToggleRow
|
||||
label="自选/策略分时图实时刷新"
|
||||
desc={`开启后自选与策略列表的分时图盘中每 ${intradayInterval} 秒自动刷新(依赖分钟K批量数据 + 实时行情运行)。关闭时仅打开页面时拉取一次, 可点表头刷新按钮手动更新。`}
|
||||
@@ -474,14 +465,11 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
|
||||
|
||||
{/* ========== 右列 ========== */}
|
||||
<div className="space-y-6">
|
||||
{/* 连板梯队降级修正 (移至右列顶部) */}
|
||||
<div
|
||||
id="depth-fix"
|
||||
className={`rounded-card transition-all duration-500 ${flash ? 'ring-2 ring-accent/60 ring-offset-2 ring-offset-base scale-[1.01]' : 'ring-0 ring-transparent'}`}
|
||||
>
|
||||
{/* 连板梯队降级修正 (右列顶部) */}
|
||||
<Card
|
||||
icon={Flame}
|
||||
title="连板梯队降级修正"
|
||||
anchor="depth-fix"
|
||||
badge={!hasDepth ? '五档盘口不可用' : undefined}
|
||||
right={hasDepth ? (
|
||||
<button
|
||||
@@ -519,10 +507,9 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
|
||||
<DepthConfigContent disabled />
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 盘中分钟增量落盘 (Expert 专有): 交易时段常驻服务, intraday.batch 独立配额 */}
|
||||
<Card icon={Zap} title="盘中分钟增量">
|
||||
<Card icon={Zap} title="盘中分钟增量" anchor="minute-refresh">
|
||||
<ToggleRow
|
||||
label="盘中分钟增量落盘"
|
||||
desc={
|
||||
@@ -575,7 +562,7 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
|
||||
{/* 推送通知 — 监控告警的外部推送渠道 (全局配置)。
|
||||
飞书 / 企业微信。
|
||||
每个渠道合并成一行: 勾选=新建规则默认推送, 点行展开地址配置。 */}
|
||||
<Card icon={Webhook} title="推送通知">
|
||||
<Card icon={Webhook} title="推送通知" anchor="webhooks">
|
||||
<p className="text-xs text-secondary mb-3">
|
||||
监控规则命中后,可把告警推送到外部。勾选渠道作为<b className="text-foreground/80">新建规则的默认推送</b>,
|
||||
单条规则仍可在编辑页独立修改。
|
||||
@@ -841,6 +828,7 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</HighlightContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -899,8 +887,10 @@ interface CardProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function Card({ icon: Icon, title, badge, right, children }: CardProps) {
|
||||
return (
|
||||
function Card({ icon: Icon, title, badge, right, children, anchor }: CardProps & { anchor?: string }) {
|
||||
const highlight = useContext(HighlightContext)
|
||||
const { ref, flash } = useCardFlash(anchor ? highlight : undefined, anchor ?? '')
|
||||
const inner = (
|
||||
<section className="rounded-card border border-border bg-surface p-5">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
@@ -917,4 +907,10 @@ function Card({ icon: Icon, title, badge, right, children }: CardProps) {
|
||||
{children}
|
||||
</section>
|
||||
)
|
||||
if (!anchor) return inner
|
||||
return (
|
||||
<div ref={ref} id={anchor} className={cardFlashCls(flash)}>
|
||||
{inner}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user