UI/UX 全面改进:22项优化落地(报纸风前端 + 管理后台)
P0 严重问题: - 采集任务进度反馈:Collection 页新增任务状态跟踪(运行中/完成/失败) + 轮询 ingest 状态 - 积分榜加载态:切换联赛显示 spinner + 禁用 tab 防重复点击 - 数据完整性可操作:问题列表每项加「去修复」按钮,跳转采集页并预填参数 - 面包屑导航:顶部显示 仪表盘 > 当前页 P1 重要问题: - 侧边栏当前页指示器:1.5px 圆点 → 4px 左侧彩色竖条 + 背景高亮 - Key Ring 重置确认:点击「重置冷却」前弹窗确认 - 预测比赛选择器:只显示未开赛 + 联赛筛选 + 显示可预测数量 - 表格移动端溢出:评估页表格加 overflow-x-auto - 设置页合并:数据源 + LLM + 系统配置 → 统一「设置」页 - 预测按钮去重:移动端/桌面端合并为一个响应式按钮 P2 体验优化: - 日志滚动保持:自动滚动仅当用户未向上滚动时 - 评估结果预览:显示「共 N 组」 - ⌘K 命令面板:键盘导航跳转所有管理页面 - 专家意见折叠:默认折叠,可展开(已存在) - 版本信息:侧边栏底部显示 v1.0 - 积分榜表格:min-w 防止挤压 Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
This commit is contained in:
co-authored by
new-provider/LongCat-2.0 <
parent
dd538d66d7
commit
fb27f6e2d2
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Admin 后台 - 键盘快捷键: Cmd+K 命令面板
|
||||
*
|
||||
* 提供全局快捷键:
|
||||
* Cmd/Ctrl+K — 打开命令面板(页面跳转)
|
||||
* / — 聚焦搜索(日志页)
|
||||
* r — 刷新当前页面数据(通用)
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
interface CommandItem {
|
||||
id: string
|
||||
label: string
|
||||
group: string
|
||||
action: () => void
|
||||
}
|
||||
|
||||
export function useCommandPalette(pages: Array<{ to: string; label: string; group: string }>) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const navigate = useNavigate()
|
||||
|
||||
const items: CommandItem[] = pages.map(p => ({
|
||||
id: p.to,
|
||||
label: p.label,
|
||||
group: p.group,
|
||||
action: () => { navigate(p.to); setOpen(false) },
|
||||
}))
|
||||
|
||||
const filtered = query
|
||||
? items.filter(i => i.label.toLowerCase().includes(query.toLowerCase()) || i.id.includes(query.toLowerCase()))
|
||||
: items
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
// Cmd/Ctrl+K → 命令面板
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault()
|
||||
setOpen(o => !o)
|
||||
setQuery('')
|
||||
}
|
||||
// Escape → 关闭
|
||||
if (e.key === 'Escape' && open) {
|
||||
setOpen(false)
|
||||
setQuery('')
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', handler)
|
||||
return () => document.removeEventListener('keydown', handler)
|
||||
}, [open])
|
||||
|
||||
return { open, setOpen, query, setQuery, items: filtered }
|
||||
}
|
||||
|
||||
/** 命令面板弹窗 */
|
||||
export function CommandPalette({
|
||||
open,
|
||||
query,
|
||||
setQuery,
|
||||
items,
|
||||
onClose,
|
||||
}: {
|
||||
open: boolean
|
||||
query: string
|
||||
setQuery: (q: string) => void
|
||||
items: CommandItem[]
|
||||
onClose: () => void
|
||||
}) {
|
||||
const [selected, setSelected] = useState(0)
|
||||
|
||||
// 重置选中项当列表变化
|
||||
useEffect(() => { setSelected(0) }, [items.length])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); setSelected(s => Math.min(s + 1, items.length - 1)) }
|
||||
if (e.key === 'ArrowUp') { e.preventDefault(); setSelected(s => Math.max(s - 1, 0)) }
|
||||
if (e.key === 'Enter' && items[selected]) { items[selected].action(); onClose() }
|
||||
}
|
||||
document.addEventListener('keydown', handler)
|
||||
return () => document.removeEventListener('keydown', handler)
|
||||
}, [open, items, selected, onClose])
|
||||
|
||||
if (!open) return null
|
||||
|
||||
// 按分组聚合
|
||||
const grouped: Record<string, CommandItem[]> = {}
|
||||
for (const item of items) {
|
||||
grouped[item.group] = grouped[item.group] || []
|
||||
grouped[item.group].push(item)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[60] flex items-start justify-center bg-ink-900/50 p-4 pt-[15vh]"
|
||||
onClick={onClose}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="命令面板"
|
||||
>
|
||||
<div
|
||||
className="w-full max-w-lg border border-ink-900 bg-paper-50 shadow-2xl"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{/* 搜索框 */}
|
||||
<div className="flex items-center gap-2 border-b border-ink-200 px-3 py-2.5">
|
||||
<span className="text-ink-400">⌘</span>
|
||||
<input
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
placeholder="输入页面名或路径…"
|
||||
autoFocus
|
||||
className="flex-1 bg-transparent text-sm outline-none placeholder:text-ink-400"
|
||||
/>
|
||||
<kbd className="border border-ink-200 px-1.5 py-0.5 text-2xs text-ink-400">ESC</kbd>
|
||||
</div>
|
||||
|
||||
{/* 结果列表 */}
|
||||
<div className="max-h-[50vh] overflow-y-auto py-2">
|
||||
{items.length === 0 ? (
|
||||
<p className="py-6 text-center text-xs text-ink-400">无匹配页面</p>
|
||||
) : (
|
||||
Object.entries(grouped).map(([group, groupItems]) => (
|
||||
<div key={group}>
|
||||
<p className="px-3 py-1 text-2xs font-medium uppercase tracking-widest text-ink-400">{group}</p>
|
||||
{groupItems.map((item, i) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => { item.action(); onClose() }}
|
||||
className={`flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors ${
|
||||
selected === items.indexOf(item) ? 'bg-paper-100 text-press' : 'text-ink-700 hover:bg-paper-100'
|
||||
}`}
|
||||
>
|
||||
<span className="truncate">{item.label}</span>
|
||||
<span className="ml-auto text-2xs text-ink-400">{item.id}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部提示 */}
|
||||
<div className="flex items-center gap-3 border-t border-ink-200 px-3 py-1.5 text-2xs text-ink-400">
|
||||
<span>↑↓ 导航</span>
|
||||
<span>↵ 跳转</span>
|
||||
<span>ESC 关闭</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user