mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
feat(app): 桌面端体验优化 — 界面缩放(80%-150%, 修复缩放黑条)/每小时检查更新/侧栏橘黄色呼吸灯下载图标(点击定位并自动检查)/发现新版本徽章带版本号/官网入口/顶部工具栏不折行/页面标题渐变字效; build: 安装包文件名带版本号 + 安装后自动刷新图标缓存
This commit is contained in:
@@ -18,7 +18,7 @@ on:
|
||||
version:
|
||||
description: '版本号 (如 v0.1.33, 用作 Release tag 和标题)'
|
||||
required: true
|
||||
default: 'v0.3.2'
|
||||
default: 'v0.3.3'
|
||||
platforms:
|
||||
description: '要构建的平台 (逗号分隔: windows,macos,linux)'
|
||||
required: true
|
||||
@@ -37,6 +37,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.filter.outputs.matrix }}
|
||||
ver: ${{ steps.filter.outputs.ver }}
|
||||
steps:
|
||||
- id: filter
|
||||
run: |
|
||||
@@ -52,11 +53,17 @@ jobs:
|
||||
import json, sys
|
||||
requested = '${{ github.event.inputs.platforms }}'.split(',')
|
||||
requested = [r.strip() for r in requested if r.strip()]
|
||||
ver = '${{ github.event.inputs.version }}'.lstrip('v') # v0.3.3 → 0.3.3
|
||||
all_platforms = json.loads('''$ALL''')
|
||||
selected = [p for p in all_platforms if p['platform'] in requested]
|
||||
# 产物文件名带版本号 (TSP-Setup-x64-0.3.3.exe): 用户下载后一眼可辨版本
|
||||
for p in selected:
|
||||
stem, _, ext = p['artifact'].rpartition('.')
|
||||
p['artifact'] = f'{stem}-{ver}.{ext}'
|
||||
print('matrix=' + json.dumps({'include': selected}))
|
||||
with open('$GITHUB_OUTPUT', 'a') as f:
|
||||
f.write('matrix=' + json.dumps({'include': selected}) + '\n')
|
||||
f.write('ver=' + ver + '\n')
|
||||
"
|
||||
|
||||
# 第二步: 各平台并行构建
|
||||
@@ -207,9 +214,9 @@ jobs:
|
||||
|
||||
| 平台 | 文件 | 下载 |
|
||||
| :--- | :--- | :--- |
|
||||
| **Windows x64** | `TSP-Setup-x64.exe` | [⬇️ 立即下载](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TSP-Setup-x64.exe) |
|
||||
| **macOS (Apple Silicon / M 系列芯片)** | `TSP-macos-arm64.dmg` | [⬇️ 立即下载](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TSP-macos-arm64.dmg) |
|
||||
| **Linux** | `TSP-linux-x64.tar.gz` | [⬇️ 立即下载](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TSP-linux-x64.tar.gz) |
|
||||
| **Windows x64** | `TSP-Setup-x64-${{ needs.prepare.outputs.ver }}.exe` | [⬇️ 立即下载](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TSP-Setup-x64-${{ needs.prepare.outputs.ver }}.exe) |
|
||||
| **macOS (Apple Silicon / M 系列芯片)** | `TSP-macos-arm64-${{ needs.prepare.outputs.ver }}.dmg` | [⬇️ 立即下载](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TSP-macos-arm64-${{ needs.prepare.outputs.ver }}.dmg) |
|
||||
| **Linux** | `TSP-linux-x64-${{ needs.prepare.outputs.ver }}.tar.gz` | [⬇️ 立即下载](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TSP-linux-x64-${{ needs.prepare.outputs.ver }}.tar.gz) |
|
||||
|
||||
> 💡 点击「立即下载」直接获取, 无需滚动到底部附件区。
|
||||
> 🍎 **macOS 仅提供 Apple Silicon (M 系列芯片) 版本**: 点左上角 → "关于本机", 看"芯片"一项是否为 Apple; Intel 芯片 Mac 暂不支持 (上游 polars 已停止发布 Intel wheel)。
|
||||
@@ -240,11 +247,12 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p assets && cd assets
|
||||
# 只拉已知的三个安装包 (latest.json 自身等其他资产不进清单)
|
||||
# 只拉已知的三个安装包 (latest.json 自身等其他资产不进清单);
|
||||
# 产物文件名带版本号, 故用通配匹配当前版本
|
||||
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \
|
||||
--pattern 'TSP-Setup-x64.exe' \
|
||||
--pattern 'TSP-macos-arm64.dmg' \
|
||||
--pattern 'TSP-linux-x64.tar.gz' \
|
||||
--pattern 'TSP-Setup-x64-*.exe' \
|
||||
--pattern 'TSP-macos-arm64-*.dmg' \
|
||||
--pattern 'TSP-linux-x64-*.tar.gz' \
|
||||
--clobber || true
|
||||
cd ..
|
||||
RELEASE_JSON=$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$TAG")
|
||||
@@ -256,11 +264,15 @@ jobs:
|
||||
repo = os.environ["REPO"]
|
||||
rel = json.loads(os.environ["RELEASE_JSON"])
|
||||
|
||||
# 资产文件名 → 渠道名 (客户端按渠道取下载地址)
|
||||
# version 去掉 v 前缀, 与 package.json 语义版本一致
|
||||
# (产物文件名 TSP-Setup-x64-0.3.3.exe 用的就是不带 v 的版本)
|
||||
version = tag[1:] if tag.startswith("v") else tag
|
||||
|
||||
# 资产文件名 → 渠道名 (客户端按渠道取下载地址); 文件名带版本号, 按当前 tag 拼
|
||||
channels_cfg = {
|
||||
"TSP-Setup-x64.exe": "windows",
|
||||
"TSP-macos-arm64.dmg": "macos-arm64",
|
||||
"TSP-linux-x64.tar.gz": "linux-x64",
|
||||
f"TSP-Setup-x64-{version}.exe": "windows",
|
||||
f"TSP-macos-arm64-{version}.dmg": "macos-arm64",
|
||||
f"TSP-linux-x64-{version}.tar.gz": "linux-x64",
|
||||
}
|
||||
by_name = {a["name"]: a for a in rel.get("assets", [])}
|
||||
|
||||
@@ -281,8 +293,6 @@ jobs:
|
||||
print("::warning::Release 上没有任何安装包资产, 跳过 latest.json 生成")
|
||||
sys.exit(0)
|
||||
|
||||
# version 去掉 v 前缀, 与 package.json 语义版本一致
|
||||
version = tag[1:] if tag.startswith("v") else tag
|
||||
manifest = {
|
||||
"version": version,
|
||||
"tag": tag,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<script>
|
||||
// 预渲染主题脚本: 首屏前设置 html.dark, 避免亮暗闪烁 (FOUC)。
|
||||
// 默认暗色; 用户切过亮色后 (localStorage tf-theme=light) 保持亮色。
|
||||
// 同步应用页面缩放 (tf-page-zoom, 设置→系统→界面缩放), 避免加载后尺寸跳变。
|
||||
(function () {
|
||||
try {
|
||||
if (localStorage.getItem('tf-theme') !== 'light') {
|
||||
@@ -13,6 +14,12 @@
|
||||
} catch (e) {
|
||||
document.documentElement.classList.add('dark')
|
||||
}
|
||||
try {
|
||||
var z = JSON.parse(localStorage.getItem('tf-page-zoom') || '')
|
||||
if (typeof z === 'number' && z >= 0.8 && z <= 1.5) {
|
||||
document.documentElement.style.zoom = String(z)
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
})()
|
||||
</script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tickflow-stock-panel-frontend",
|
||||
"private": true,
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
PanelLeft,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Download,
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
IconDashboard,
|
||||
@@ -642,7 +643,7 @@ export function Layout() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-screen grid bg-base text-foreground overflow-hidden transition-[grid-template-columns] duration-200 ease-smooth"
|
||||
className="h-full grid bg-base text-foreground overflow-hidden transition-[grid-template-columns] duration-200 ease-smooth"
|
||||
style={{ gridTemplateColumns: isDesktop && !overlayPreview ? (navState === 'expanded' ? '14rem 1fr' : navState === 'rail' ? '3.5rem 1fr' : '0 1fr') : '1fr' }}
|
||||
>
|
||||
{/* 移动端抽屉遮罩 */}
|
||||
@@ -1023,30 +1024,30 @@ export function Layout() {
|
||||
)}
|
||||
/>
|
||||
<Settings className={cn('h-4 w-4 shrink-0 transition-colors', isActive ? 'text-accent' : 'text-foreground/60 group-hover:text-foreground/85')} />
|
||||
{!railMode && <span>设置</span>}
|
||||
{!railMode && <span className="whitespace-nowrap">设置</span>}
|
||||
{!railMode && version && (
|
||||
<span className="ml-auto font-mono text-[10px] text-muted/50 select-none shrink-0">
|
||||
<span className="ml-auto flex items-center gap-1.5 font-mono text-[10px] text-muted/50 select-none shrink-0">
|
||||
{version}
|
||||
{hasUpdate && update.info && (
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title={`发现新版本 ${update.info.latest},点击前往检查更新`}
|
||||
title={`发现新版本 ${update.info.latest},点击检查更新`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
navigate('/settings?tab=system')
|
||||
navigate('/settings?tab=system&autoupdate=1')
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
navigate('/settings?tab=system')
|
||||
navigate('/settings?tab=system&autoupdate=1')
|
||||
}
|
||||
}}
|
||||
className="ml-1.5 inline-flex cursor-pointer items-center rounded-full bg-accent px-1.5 py-px text-[9px] font-semibold leading-none text-white transition-colors hover:bg-accent/90"
|
||||
className="inline-flex cursor-pointer text-amber-400 transition-transform hover:scale-110 animate-breath"
|
||||
>
|
||||
NEW
|
||||
<Download className="h-3 w-3" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -17,10 +17,16 @@ export function PageHeader({ title, subtitle, titleExtra, right, className }: Pr
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-lg font-semibold tracking-tight">{title}</h1>
|
||||
{titleExtra}
|
||||
{subtitle && <span className="text-xs text-muted">{subtitle}</span>}
|
||||
<div className="flex min-w-0 flex-col justify-center gap-0.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="shrink-0 whitespace-nowrap bg-gradient-to-b from-foreground via-foreground to-accent/80 bg-clip-text text-xl font-semibold tracking-normal text-transparent">
|
||||
{title}
|
||||
</h1>
|
||||
{titleExtra}
|
||||
</div>
|
||||
{subtitle && (
|
||||
<span className="truncate whitespace-nowrap text-[10px] leading-tight text-muted">{subtitle}</span>
|
||||
)}
|
||||
</div>
|
||||
{right}
|
||||
</header>
|
||||
|
||||
@@ -68,6 +68,11 @@ html.dark {
|
||||
/* 默认 1px 细分隔线(暗色用边框区分层次,不用 shadow) */
|
||||
* { border-color: hsl(var(--border)); }
|
||||
|
||||
/* 页面缩放 (设置→系统→界面缩放) 用 CSS zoom 作用于 <html>:
|
||||
* vh 单位不随 zoom 缩放 (zoom<1 底部露底 / zoom>1 溢出), 百分比高度链才会 —
|
||||
* 根链路统一 % 高度, 主布局与加载占位用 h-full/min-h-full 而非 h-screen。 */
|
||||
html, body, #root { height: 100%; }
|
||||
|
||||
body {
|
||||
background: hsl(var(--base));
|
||||
color: hsl(var(--fg-primary));
|
||||
@@ -84,3 +89,18 @@ body {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
/* 呼吸灯: 透明度 + 橘黄色光晕同步起伏 (侧栏"有新版本"下载图标) */
|
||||
@keyframes breath-glow {
|
||||
0%, 100% {
|
||||
opacity: 0.5;
|
||||
filter: drop-shadow(0 0 1px rgba(251, 191, 36, 0.35));
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
filter: drop-shadow(0 0 5px rgba(251, 191, 36, 0.9));
|
||||
}
|
||||
}
|
||||
.animate-breath {
|
||||
animation: breath-glow 2.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 页面整体缩放 — 桌面客户端 (pywebview) 里没有浏览器缩放快捷键,
|
||||
* 由 设置→系统→界面缩放 控制。CSS zoom 作用于 <html>, 全组件等比缩放。
|
||||
*
|
||||
* - 值域 0.8–1.5, 步进 0.05, localStorage 持久化 (storage.pageZoom)。
|
||||
* - index.html 的预渲染脚本会在首屏前应用, 避免加载后尺寸跳变。
|
||||
* - 应用后派发 window resize, echarts 等监听 resize 的图表随之重排。
|
||||
*/
|
||||
import { storage } from '@/lib/storage'
|
||||
|
||||
export const ZOOM_MIN = 0.8
|
||||
export const ZOOM_MAX = 1.5
|
||||
export const ZOOM_STEP = 0.05
|
||||
|
||||
function clamp(v: number): number {
|
||||
if (!Number.isFinite(v)) return 1
|
||||
return Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, Math.round(v * 100) / 100))
|
||||
}
|
||||
|
||||
/** 把缩放应用到 <html> 并通知图表重排, 返回 clamp 后的实际值。 */
|
||||
export function applyPageZoom(v: number): number {
|
||||
const z = clamp(v)
|
||||
document.documentElement.style.zoom = String(z)
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
return z
|
||||
}
|
||||
|
||||
/** 读取持久化的缩放值 (越界/缺失回落 1)。 */
|
||||
export function getPageZoom(): number {
|
||||
return clamp(storage.pageZoom.get(1))
|
||||
}
|
||||
|
||||
/** 持久化并立即生效。 */
|
||||
export function setPageZoom(v: number): number {
|
||||
const z = clamp(v)
|
||||
storage.pageZoom.set(z)
|
||||
return applyPageZoom(z)
|
||||
}
|
||||
@@ -100,6 +100,9 @@ export const storage = {
|
||||
/** Screener 卡片尺寸 */
|
||||
screenerCardSize: kv<string>('screener-card-size'),
|
||||
|
||||
/** 界面整体缩放 (0.8–1.5, 设置→系统→界面缩放; 桌面客户端无浏览器缩放快捷键) */
|
||||
pageZoom: kv<number>('tf-page-zoom'),
|
||||
|
||||
/** 连板梯队板块筛选 */
|
||||
limitLadderBoard: kv<string[]>('limit-ladder-board-filter'),
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
* 静默检查结果按天缓存 localStorage (GitHub 未认证 API 限额 60 次/时):
|
||||
* 同一版本 24h 内只发一次真实请求; 手动检查 (force) 绕过缓存立即刷新。
|
||||
* 检查失败不落缓存, 下次启动仍会重试, 但单次会话只静默检查一次。
|
||||
* 使用期间每小时静默复检一次 (silent+force): 新版本发布后最长 1 小时亮 NEW;
|
||||
* 复检失败不打掉已亮的 found 状态。
|
||||
*/
|
||||
import { useCallback, useEffect, useSyncExternalStore } from 'react'
|
||||
import { useVersion } from '@/lib/useSharedQueries'
|
||||
@@ -128,7 +130,7 @@ let inFlight = false
|
||||
|
||||
export async function checkForUpdate(
|
||||
current: string,
|
||||
opts: { force?: boolean } = {},
|
||||
opts: { force?: boolean; silent?: boolean } = {},
|
||||
): Promise<void> {
|
||||
const cur = current.trim()
|
||||
if (!cur || inFlight) return
|
||||
@@ -144,7 +146,7 @@ export async function checkForUpdate(
|
||||
}
|
||||
}
|
||||
inFlight = true
|
||||
setState({ status: 'checking' })
|
||||
if (!opts.silent) setState({ status: 'checking' })
|
||||
const checkedAt = Date.now()
|
||||
try {
|
||||
const info = await fetchLatest()
|
||||
@@ -152,18 +154,26 @@ export async function checkForUpdate(
|
||||
setState({ status: found ? 'found' : 'latest', info, checkedAt })
|
||||
writeCache({ current: cur, latest: info.latest, url: info.url, found, checkedAt })
|
||||
} catch {
|
||||
// 不落缓存: 下次启动重试 (静默检查每次会话最多一次, 不会打爆限额)
|
||||
setState({ status: 'error', checkedAt })
|
||||
// 不落缓存: 下次启动重试 (静默检查每次会话最多一次, 不会打爆限额)。
|
||||
// 每小时复检 (silent) 失败时若已亮 NEW, 保留 found 不降级为 error。
|
||||
if (!(opts.silent && state.status === 'found')) {
|
||||
setState({ status: 'error', checkedAt })
|
||||
}
|
||||
} finally {
|
||||
inFlight = false
|
||||
}
|
||||
}
|
||||
|
||||
let silentStarted = false
|
||||
let hourlyStarted = false
|
||||
|
||||
/** 每小时静默复检间隔 (ms)。 */
|
||||
const HOURLY_MS = 60 * 60 * 1000
|
||||
|
||||
/**
|
||||
* 侧栏与设置页共用的更新检查 hook。
|
||||
* 版本号就绪后自动做一次静默检查 (每次会话最多一次);
|
||||
* 此后每小时静默复检 (silent+force 绕过 24h 缓存, 捕捉使用期间发布的新版本);
|
||||
* `check` 为手动入口, 绕过缓存立即刷新。
|
||||
*/
|
||||
export function useUpdateCheck(): {
|
||||
@@ -180,6 +190,15 @@ export function useUpdateCheck(): {
|
||||
silentStarted = true
|
||||
void checkForUpdate(current)
|
||||
}, [current])
|
||||
// 每小时复检: 模块级单例 (Layout 常驻持有; 多消费者不重复起定时器)
|
||||
useEffect(() => {
|
||||
if (!current || hourlyStarted) return
|
||||
hourlyStarted = true
|
||||
const id = setInterval(() => {
|
||||
void checkForUpdate(current, { force: true, silent: true })
|
||||
}, HOURLY_MS)
|
||||
return () => clearInterval(id)
|
||||
}, [current])
|
||||
const check = useCallback(() => {
|
||||
void checkForUpdate(current, { force: true })
|
||||
}, [current])
|
||||
|
||||
@@ -107,9 +107,9 @@ export function AbnormalMoves() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* tab 条: 交易时间线 竞价(盘前) → 盘中 → 偏移(多日) */}
|
||||
<div className="flex shrink-0 flex-wrap items-center gap-3 px-5 pt-3">
|
||||
<div className="inline-flex items-center gap-0.5 rounded-full border border-border/50 bg-base/70 p-0.5">
|
||||
{/* tab 条: 交易时间线 竞价(盘前) → 盘中 → 偏移(多日) — 单行, 描述超宽截断 */}
|
||||
<div className="flex shrink-0 flex-nowrap items-center gap-3 px-5 pt-3">
|
||||
<div className="inline-flex shrink-0 items-center gap-0.5 rounded-full border border-border/50 bg-base/70 p-0.5">
|
||||
{TAB_META.map(t => {
|
||||
const Icon = t.icon
|
||||
const active = tab === t.key
|
||||
@@ -131,7 +131,7 @@ export function AbnormalMoves() {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<span className="text-[10px] text-muted">{TAB_META.find(t => t.key === tab)?.desc}</span>
|
||||
<span className="min-w-0 truncate text-[10px] text-muted">{TAB_META.find(t => t.key === tab)?.desc}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col px-5 pb-4 pt-3">
|
||||
@@ -424,8 +424,8 @@ function IntradayView({ onPreview }: {
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-3">
|
||||
{/* 信号筛选 chips (带各类型计数) + 工具行 */}
|
||||
<div className="flex shrink-0 flex-wrap items-center gap-1.5">
|
||||
{/* 信号筛选 chips (带各类型计数) + 工具行 — 单行横向滚动, 不折行 */}
|
||||
<div className="flex shrink-0 flex-nowrap items-center gap-1.5 overflow-x-auto pb-0.5 [&>*]:shrink-0">
|
||||
<SigChip active={sigFilter === 'all'} onClick={() => setSigFilter('all')} label="全部" count={total} />
|
||||
{SIGNAL_KEYS.map(k => (
|
||||
<SigChip
|
||||
|
||||
@@ -222,8 +222,8 @@ export function Monitor() {
|
||||
<section className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl border border-border bg-surface/40 shadow-lg shadow-black/5">
|
||||
<div className="flex items-center gap-3 border-b border-border/60 bg-surface/60 px-4 py-2.5">
|
||||
<SectionHeader icon={BellRing} title="触发记录" />
|
||||
{/* 过滤标签 */}
|
||||
<div className="flex flex-wrap items-center gap-0.5">
|
||||
{/* 过滤标签 — 单行横向滚动, 不折行 (缩放/窄窗口下保持一行) */}
|
||||
<div className="flex min-w-0 flex-nowrap items-center gap-0.5 overflow-x-auto [&>button]:shrink-0">
|
||||
{(['all', 'strategy', 'signal', 'price', 'market', 'sector', 'abnormal', 'volume_delta', 'date'] as const).map(f => (
|
||||
<button
|
||||
key={f}
|
||||
|
||||
@@ -74,7 +74,7 @@ export function Onboarding() {
|
||||
const finish = () => complete.mutate()
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen bg-base overflow-hidden flex flex-col">
|
||||
<div className="relative min-h-full bg-base overflow-hidden flex flex-col">
|
||||
{/* 背景光晕 —— 品牌 + 主色渐变 */}
|
||||
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
||||
<div
|
||||
|
||||
@@ -653,7 +653,7 @@ function MarketSummaryBar({ data }: { data: OverviewMarket }) {
|
||||
const indices = (data.indices ?? []).slice(0, 4)
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-x-5 gap-y-2 rounded-card border border-border bg-surface/80 px-4 py-2.5">
|
||||
<div className="flex flex-nowrap items-center gap-x-5 overflow-x-auto rounded-card border border-border bg-surface/80 px-4 py-2.5 [&>*]:shrink-0">
|
||||
{/* 情绪分(带色徽章)—— 复盘的核心定调 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
@@ -671,7 +671,7 @@ function MarketSummaryBar({ data }: { data: OverviewMarket }) {
|
||||
<div className="hidden h-7 w-px bg-border sm:block" />
|
||||
|
||||
{/* 四大指数(简称:上深创科)*/}
|
||||
<div className="flex flex-wrap items-center gap-x-2.5 gap-y-1">
|
||||
<div className="flex flex-nowrap items-center gap-x-2.5 [&>*]:shrink-0">
|
||||
{indices.map(idx => (
|
||||
<div key={idx.symbol} className="flex items-center gap-1">
|
||||
<span className="text-[11px] text-secondary">{indexShort(idx.name, idx.symbol)}</span>
|
||||
|
||||
@@ -749,7 +749,7 @@ export function Screener() {
|
||||
title="策略"
|
||||
subtitle="基于本地 enriched 表 · 毫秒级 SQL"
|
||||
right={
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-nowrap overflow-x-auto max-w-full [&>*]:shrink-0 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
{/* 资产类型切换: 股票 / ETF (分钟策略 asset_types 仅股票, ETF 列表自然不含) */}
|
||||
<div className="flex items-center h-7 rounded-btn border border-border overflow-hidden">
|
||||
{(['stock', 'etf'] as const).map(t => (
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
*
|
||||
* 独立于实时监控, 放置影响整体应用行为的开关项。
|
||||
*/
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { Settings2, Trash2, RefreshCw, Bell, Volume2, Info, ExternalLink, CheckCircle2, Download } from 'lucide-react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { Settings2, Trash2, RefreshCw, Bell, Volume2, Info, ExternalLink, CheckCircle2, Download, ZoomIn, Minus, Plus, Sparkles } from 'lucide-react'
|
||||
import { usePreferences, useVersion } from '@/lib/useSharedQueries'
|
||||
import { api } from '@/lib/api'
|
||||
import { QK } from '@/lib/queryKeys'
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
} from '@/lib/voiceBroadcast'
|
||||
import { loadStockExternalTemplate, saveStockExternalTemplate } from '@/lib/stock-external-link'
|
||||
import { useUpdateCheck } from '@/lib/updateCheck'
|
||||
import { ZOOM_MIN, ZOOM_MAX, ZOOM_STEP, getPageZoom, setPageZoom } from '@/lib/pageZoom'
|
||||
|
||||
export function SettingsSystemPanel() {
|
||||
const qc = useQueryClient()
|
||||
@@ -34,6 +36,25 @@ export function SettingsSystemPanel() {
|
||||
info: updateInfo,
|
||||
check: checkUpdate,
|
||||
} = useUpdateCheck()
|
||||
|
||||
// ===== 界面缩放 (桌面客户端无浏览器缩放快捷键, CSS zoom 作用于 <html>) =====
|
||||
const [pageZoom, setPageZoomState] = useState(() => getPageZoom())
|
||||
const changeZoom = useCallback((delta: number) => {
|
||||
setPageZoomState(setPageZoom(getPageZoom() + delta))
|
||||
}, [])
|
||||
|
||||
// ===== 侧栏下载图标深链 (?autoupdate=1): 滚动定位到本行并自动执行一次检查 =====
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const updateRowRef = useRef<HTMLDivElement>(null)
|
||||
const currentVersion = (versionData?.version ?? '').trim()
|
||||
useEffect(() => {
|
||||
if (!currentVersion || searchParams.get('autoupdate') !== '1') return
|
||||
updateRowRef.current?.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||||
checkUpdate()
|
||||
const next = new URLSearchParams(searchParams)
|
||||
next.delete('autoupdate')
|
||||
setSearchParams(next, { replace: true })
|
||||
}, [currentVersion, searchParams, checkUpdate, setSearchParams])
|
||||
const [toastEnabled, setToastEnabled] = useState(() => {
|
||||
try { return localStorage.getItem('alert_toast_enabled') !== '0' } catch { return true }
|
||||
})
|
||||
@@ -352,6 +373,56 @@ export function SettingsSystemPanel() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-card border border-border bg-surface p-5 mt-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<ZoomIn className="h-4 w-4 text-accent" />
|
||||
<h3 className="text-sm font-medium text-foreground">界面</h3>
|
||||
</div>
|
||||
|
||||
<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-[11px] text-muted truncate">
|
||||
整个界面等比缩放, 立即生效并记忆 (80%–150%)
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{pageZoom !== 1 && (
|
||||
<button
|
||||
onClick={() => setPageZoomState(setPageZoom(1))}
|
||||
className="px-2.5 py-1.5 rounded-btn text-xs bg-elevated text-secondary
|
||||
hover:text-foreground transition-colors"
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => changeZoom(-ZOOM_STEP)}
|
||||
disabled={pageZoom <= ZOOM_MIN}
|
||||
aria-label="缩小界面"
|
||||
className="inline-flex items-center justify-center h-7 w-7 rounded-btn
|
||||
bg-elevated text-secondary hover:text-foreground transition-colors
|
||||
disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Minus className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<span className="font-mono text-xs text-secondary w-11 text-center select-none">
|
||||
{Math.round(pageZoom * 100)}%
|
||||
</span>
|
||||
<button
|
||||
onClick={() => changeZoom(ZOOM_STEP)}
|
||||
disabled={pageZoom >= ZOOM_MAX}
|
||||
aria-label="放大界面"
|
||||
className="inline-flex items-center justify-center h-7 w-7 rounded-btn
|
||||
bg-elevated text-secondary hover:text-foreground transition-colors
|
||||
disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-card border border-border bg-surface p-5 mt-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Info className="h-4 w-4 text-accent" />
|
||||
@@ -368,20 +439,37 @@ export function SettingsSystemPanel() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-4 py-2">
|
||||
<div ref={updateRowRef} className="flex scroll-mt-24 items-center justify-between gap-4 py-2">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm text-foreground">检查更新</div>
|
||||
<div className="text-[11px] text-muted truncate">
|
||||
{updateState === 'found' && updateInfo
|
||||
? `发现新版本 ${updateInfo.latest} (当前 ${versionData?.version ?? '—'})`
|
||||
: updateState === 'latest'
|
||||
? `已是最新版本 (${versionData?.version ?? '—'})`
|
||||
{updateState === 'found' && updateInfo ? (
|
||||
<div className="mt-0.5 inline-flex items-center gap-1.5 rounded-full border border-accent/30 bg-accent/10 px-2 py-0.5 text-[11px] text-accent">
|
||||
<Sparkles className="h-3 w-3" />
|
||||
发现新版本
|
||||
<span className="font-semibold">v{updateInfo.latest.replace(/^v/, '')}</span>
|
||||
<span className="text-accent/60">· 当前 v{versionData?.version?.replace(/^v/, '') ?? '—'}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[11px] text-muted truncate">
|
||||
{updateState === 'latest'
|
||||
? `已是最新版本 (v${versionData?.version?.replace(/^v/, '') ?? '—'})`
|
||||
: updateState === 'error'
|
||||
? '检查失败 (网络受限?), 可直接前往 Releases'
|
||||
: '对比 GitHub Releases 最新 Release, 提示新版本'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<a
|
||||
href="https://tsp.shy313.com/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-btn text-xs
|
||||
bg-elevated text-secondary hover:text-foreground transition-colors"
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
官网
|
||||
</a>
|
||||
{updateState === 'found' && updateInfo && (
|
||||
<a
|
||||
href={updateInfo.url}
|
||||
@@ -391,7 +479,7 @@ export function SettingsSystemPanel() {
|
||||
bg-accent text-white hover:bg-accent/90 transition-colors"
|
||||
>
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
前往下载
|
||||
前往下载 v{updateInfo.latest.replace(/^v/, '')}
|
||||
</a>
|
||||
)}
|
||||
<button
|
||||
|
||||
@@ -96,7 +96,7 @@ function OnboardingGuard({ children }: { children: React.ReactNode }) {
|
||||
// 防误重定向已由 Onboarding/AI 等处 invalidate 前的 setQueryData 同步缓存兜底。
|
||||
if (settings.isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-base grid place-items-center">
|
||||
<div className="min-h-full bg-base grid place-items-center">
|
||||
<div className="flex flex-col items-center gap-3 text-muted">
|
||||
<Logo size={28} className="text-foreground" />
|
||||
<div className="text-xs">加载中…</div>
|
||||
@@ -116,7 +116,7 @@ function OnboardingGuard({ children }: { children: React.ReactNode }) {
|
||||
// 按需路由的加载占位 (与 OnboardingGuard 的加载态同风格)
|
||||
function RouteFallback() {
|
||||
return (
|
||||
<div className="min-h-screen bg-base grid place-items-center">
|
||||
<div className="min-h-full bg-base grid place-items-center">
|
||||
<Logo size={28} className="text-foreground" />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -93,6 +93,11 @@ Name: "{group}\卸载 {#MyAppName}"; Filename: "{uninstallexe}"
|
||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
; 刷新 Windows 图标缓存: 覆盖安装时 exe 路径不变 (D:\TSP\TSP.exe), 资源管理器
|
||||
; 会一直显示缓存里的旧图标 (首个版本为透明底)。ie4uinit -show 强制重建缓存,
|
||||
; 装完快捷方式立即显示当前 exe 内嵌图标, 无需用户重启资源管理器。
|
||||
Filename: "{sys}\ie4uinit.exe"; Parameters: "-show"; Flags: runhidden
|
||||
|
||||
; 安装完成后启动应用
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#MyAppName}}"; Flags: nowait postinstall skipifsilent
|
||||
|
||||
|
||||
Reference in New Issue
Block a user