feat(app): 桌面端体验优化 — 界面缩放(80%-150%, 修复缩放黑条)/每小时检查更新/侧栏橘黄色呼吸灯下载图标(点击定位并自动检查)/发现新版本徽章带版本号/官网入口/顶部工具栏不折行/页面标题渐变字效; build: 安装包文件名带版本号 + 安装后自动刷新图标缓存

This commit is contained in:
shy3130
2026-10-06 12:10:32 +08:00
parent 30fcade330
commit 2b73e89b81
17 changed files with 252 additions and 55 deletions
+24 -14
View File
@@ -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,
+7
View File
@@ -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 -1
View File
@@ -1,7 +1,7 @@
{
"name": "tickflow-stock-panel-frontend",
"private": true,
"version": "0.3.2",
"version": "0.3.3",
"type": "module",
"scripts": {
"dev": "vite",
+9 -8
View File
@@ -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>
+10 -4
View File
@@ -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>
+20
View File
@@ -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;
}
+38
View File
@@ -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)
}
+3
View File
@@ -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'),
+23 -4
View File
@@ -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])
+6 -6
View File
@@ -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
+2 -2
View File
@@ -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}
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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>
+1 -1
View File
@@ -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 => (
+98 -10
View File
@@ -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
+2 -2
View File
@@ -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>
)
+5
View File
@@ -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