feat(ui): 看板无数据提示优化 + 数据页无 Key 引导 + 设置页文案

Dashboard:
- hasNoData 改用 trading_days 判断 (后端 status rows 恒为 0 导致提示无法消除)
- 无数据提示改 warning 黄色风格 + AlertTriangle 图标
- 无数据/无 Key 两提示互斥: 无 Key 优先, 有 Key 无数据才显示同步提示
- API Key 横幅: 邀请码加复制按钮, 文案调整

Data:
- 无 Key 时立即同步按钮置灰禁用 + 显示未配置提示
- 指数数据卡片设置按钮与其它卡片一致 (有数据才显示)

System:
- '清理浏览器缓存' 改名 '刷新前端缓存', 避免误导为清数据
This commit is contained in:
shy3130
2026-06-24 23:24:25 +08:00
parent 37f4d61e3a
commit 00a30597bd
6 changed files with 85 additions and 37 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
import sys
__version__ = "0.1.38"
__version__ = "0.1.39"
# Windows 默认 stdout/stderr 编码为 GBK(cp936),TickFlow SDK 内部输出含 emoji 的
# 指数/标的名称(如 \U0001f193)时会抛 UnicodeEncodeError,导致请求失败。
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "tickflow-stock-panel-backend"
version = "0.1.35"
version = "0.1.39"
description = "A 股选股 + 监控 + 回测面板 — TickFlow 适配"
readme = "../README.md"
requires-python = ">=3.11"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "tickflow-stock-panel-frontend",
"private": true,
"version": "0.1.38",
"version": "0.1.39",
"type": "module",
"scripts": {
"dev": "vite",
+38 -17
View File
@@ -2,7 +2,7 @@ import { useState, type ReactNode } from 'react'
import { Link } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import { motion } from 'framer-motion'
import { Activity, ArrowDownRight, ArrowUpRight, BarChart3, BellRing, Database, Flame, Gauge, LineChart, Loader2, RefreshCw, Sparkles, Target, Timer, ExternalLink, Gift } from 'lucide-react'
import { Activity, AlertTriangle, ArrowDownRight, ArrowUpRight, BarChart3, BellRing, Check, Copy, Flame, Gauge, LineChart, Loader2, RefreshCw, Sparkles, Target, Timer, ExternalLink } from 'lucide-react'
import { DatePicker } from '@/components/DatePicker'
import { api, type MarketSnapshotRow, type OverviewDimensionRankItem, type OverviewMarket, type AlertEvent } from '@/lib/api'
import { QK } from '@/lib/queryKeys'
@@ -471,6 +471,7 @@ function HotRankCard({ title, rank, configUrl }: { title: string; rank?: Overvie
export function Dashboard() {
const [selectedDate, setSelectedDate] = useState<string | undefined>()
const [manualFetching, setManualFetching] = useState(false)
const [copiedCode, setCopiedCode] = useState(false)
const dataStatus = useDataStatus({ staleTime: 60_000 })
const overview = useQuery({
queryKey: QK.overviewMarket(selectedDate),
@@ -487,8 +488,11 @@ export function Dashboard() {
// none 档(无 key / 无效 key)→ 显示升级提示横幅
const isNoKey = settings.data?.mode === 'none'
// 无本地数据(enriched/daily 都没有)→ 提示去数据页同步
// 注: 后端 status 的 rows 为性能刻意返回 0, 用 trading_days 判断是否有数据
const ds = dataStatus.data
const hasNoData = !!ds && (ds.enriched?.rows ?? 0) === 0 && (ds.daily?.rows ?? 0) === 0
const hasNoData = !!ds
&& (ds.enriched?.trading_days ?? 0) === 0
&& (ds.daily?.trading_days ?? 0) === 0
// 手动刷新: 显示旋转动画; SSE 自动刷新: 静默, 无体感
const handleRefresh = () => {
@@ -529,9 +533,9 @@ export function Dashboard() {
{/* none 档(无 key)提示横幅 —— 引导用户领取免费 Key 解锁完整能力 */}
{isNoKey && (
<div className="mb-3 flex items-center gap-2 rounded-card border border-warning/40 bg-warning/10 px-3 py-2 text-xs">
<Gift className="h-4 w-4 shrink-0 text-warning" />
<AlertTriangle className="h-4 w-4 shrink-0 text-warning" />
<span className="text-secondary leading-relaxed">
API Key,
API Key,
<a
href="https://tickflow.org/auth/register?ref=V3KDKGXPEA"
target="_blank"
@@ -542,24 +546,41 @@ export function Dashboard() {
<ExternalLink className="h-3 w-3 self-center" />
</a>
({' '}
<span className="font-mono font-semibold text-warning">V3KDKGXPEA</span>
) API Key,
<span className="font-mono font-semibold text-warning inline-flex items-baseline gap-1">
V3KDKGXPEA
<button
type="button"
onClick={() => {
navigator.clipboard?.writeText('V3KDKGXPEA').then(() => {
setCopiedCode(true)
setTimeout(() => setCopiedCode(false), 1500)
})
}}
className="text-warning/60 hover:text-warning transition-colors self-center"
aria-label="复制邀请码"
tabIndex={-1}
>
{copiedCode ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
</button>
</span>
) API Key,
</span>
</div>
)}
{/* 无本地数据提示 —— 引导用户去数据页同步 */}
{hasNoData && (
<div className="mb-3 flex items-center gap-2 rounded-card border border-accent/40 bg-accent/10 px-3 py-2 text-xs">
<Database className="h-4 w-4 shrink-0 text-accent" />
{/* 无本地数据提示 —— 引导用户去数据页同步 (仅当已配置 Key 时显示, 无 Key 时优先提示配置 Key) */}
{hasNoData && !isNoKey && (
<div className="mb-3 flex items-center gap-2 rounded-card border border-warning/40 bg-warning/10 px-3 py-2 text-xs">
<AlertTriangle className="h-4 w-4 shrink-0 text-warning" />
<span className="text-secondary leading-relaxed">
,
,
<Link
to="/data"
className="ml-1 shrink-0 inline-flex items-center gap-0.5 font-medium text-warning hover:underline"
>
<ArrowUpRight className="h-3 w-3 self-center" />
</Link>
</span>
<Link
to="/data"
className="ml-auto shrink-0 inline-flex items-center gap-1 rounded-btn bg-accent px-2.5 py-1 text-[11px] font-medium text-white hover:bg-accent/90 transition-colors"
>
</Link>
</div>
)}
<div className="mb-3 flex flex-wrap items-center justify-between gap-2 rounded-card border border-border bg-surface/85 px-3 py-2">
+40 -13
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { Link } from 'react-router-dom'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { motion, AnimatePresence } from 'framer-motion'
import {
@@ -214,6 +215,8 @@ export function Data() {
const isRunning = job.data?.status === 'running' || job.data?.status === 'pending'
const isStarting = startSync.isPending
const hasData = !!(s?.instruments?.rows || s?.daily?.rows)
// none 档(无 key / 无效 key) → 禁用立即同步 (同步依赖付费档的批量端点)
const isNoKey = settings.data?.mode === 'none'
const indexOverviewStats = s ? {
rows: 0,
earliest_date: s.index_daily?.earliest_date ?? s.index_enriched?.earliest_date ?? null,
@@ -290,21 +293,31 @@ export function Data() {
subtitle="本地数据画像 · 同步状态 · 历史记录"
right={
<div className="flex items-center gap-3">
{!hasData && !isLoading && (
{!hasData && !isLoading && !isNoKey && (
<span className="text-xs text-accent animate-pulse">使</span>
)}
<button
onClick={() => startSync.mutate()}
disabled={isStarting}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-btn bg-gradient-to-r from-accent/25 to-accent/10 border border-accent/30 text-accent text-xs font-medium hover:from-accent/35 hover:to-accent/20 disabled:opacity-40 transition-all duration-150"
>
{(isRunning || isStarting) ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
{isNoKey ? (
<button
disabled
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-btn bg-gradient-to-r from-accent/25 to-accent/10 border border-accent/30 text-accent text-xs font-medium opacity-40 cursor-not-allowed transition-all duration-150"
>
<Play className="h-3.5 w-3.5" />
)}
{isStarting ? '启动中…' : isRunning ? '同步中…' : '立即同步'}
</button>
</button>
) : (
<button
onClick={() => startSync.mutate()}
disabled={isStarting}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-btn bg-gradient-to-r from-accent/25 to-accent/10 border border-accent/30 text-accent text-xs font-medium hover:from-accent/35 hover:to-accent/20 disabled:opacity-40 transition-all duration-150"
>
{(isRunning || isStarting) ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Play className="h-3.5 w-3.5" />
)}
{isStarting ? '启动中…' : isRunning ? '同步中…' : '立即同步'}
</button>
)}
<div className="w-px h-4 bg-border" />
<div className="flex items-center gap-1.5">
<button
@@ -335,6 +348,20 @@ export function Data() {
/>
<div className="px-8 py-6 space-y-6 max-w-6xl">
{/* 未配置 API Key 告警条 —— 引导用户去配置 Key 后才能同步 */}
{isNoKey && (
<div className="flex items-center gap-2 rounded-card border border-warning/40 bg-warning/10 px-3 py-2 text-xs">
<AlertTriangle className="h-4 w-4 shrink-0 text-warning" />
<span className="text-secondary leading-relaxed">
API Key,,
<Link to="/settings?tab=account" className="mx-0.5 font-medium text-warning hover:underline">
</Link>
</span>
</div>
)}
{/* 实时进度 */}
<AnimatePresence>
{job.data && (
@@ -643,7 +670,7 @@ export function Data() {
{ label: '指标', table: 'index_enriched' },
] as FieldTab[]}
onShowFields={(t) => setSchemaTable(t ?? 'index_daily')}
onSettings={() => setOpenSettings(v => v === 'index' ? null : 'index')}
onSettings={hasData ? () => setOpenSettings(v => v === 'index' ? null : 'index') : undefined}
settingsOpen={openSettings === 'index'}
/>
<StatCard
+4 -4
View File
@@ -47,8 +47,8 @@ export function SettingsSystemPanel() {
}
}, [qc])
// 清理浏览器缓存: 清除 react-query 缓存 + 强制重载 (绕过浏览器缓存)
// 不动 localStorage (用户列配置/策略池等偏好保留)
// 刷新前端缓存: 清除 react-query 缓存 + 强制重载 (绕过浏览器缓存)
// 不动 localStorage (用户列配置/策略池等偏好保留), 也不影响后端的本地股票数据
const handleClearCache = useCallback(() => {
setClearing(true)
qc.clear()
@@ -171,9 +171,9 @@ export function SettingsSystemPanel() {
<div className="flex items-center justify-between gap-4 py-2">
<div className="min-w-0">
<div className="text-sm text-foreground"></div>
<div className="text-sm text-foreground"></div>
<div className="text-[11px] text-muted truncate">
()
()
</div>
</div>
<button