Artifact 18852fc199fb26bbf4a54bbb8741c80e92d214334e481dcfb2c3a80eefaf1e36:
- File
web/src/pages/Dashboard.tsx
— part of check-in
[dfa747c1ad]
at
2026-10-23 06:08:28
on branch trunk
— Web 管理控制台
- 用 Vite 和 React 做完整的 Web 端:登录、按权限显示菜单、切换当前作业仓库 - 覆盖来肯同步、仓库、库位规划、商品、销售订单、采购入库与批次码、采购订单、库存、用户授权和审计日志 - 开发时由 Vite 把 /api 代理到本机后端 - SQLite 写入和页面读取冲突时自动重试,避免请求进程因数据库忙碌直接崩溃 (user: pluto size: 4421)
import { useEffect, useState } from 'react' import { api, can } from '../api' import { useSession } from '../session' import { Banner, Empty, Page } from '../ui' type Run = { id: string entity: string trigger: string started_at: string finished_at: string status: string fetched: number upserted: number error_message: string } type Status = { credentials_configured: boolean ql361_username: string session_cached: boolean order_poll_seconds: number request_gap_ms: number stock_warehouses: string[] recent_runs: Run[] } const syncs = [ { path: '/api/sync/products', label: '同步商品' }, { path: '/api/sync/orders', label: '同步销售订单' }, { path: '/api/sync/inbounds', label: '同步采购订单' }, { path: '/api/sync/purchase-receipts', label: '同步采购入库单' }, ] export function Dashboard() { const { user } = useSession() const [status, setStatus] = useState<Status | null>(null) const [error, setError] = useState('') const [ok, setOk] = useState('') const [pending, setPending] = useState('') function load() { if (!can(user, 'sync.view')) return api<Status>('/api/ql361/status') .then(setStatus) .catch((err: unknown) => setError(err instanceof Error ? err.message : '加载失败')) } useEffect(load, [user]) async function run(path: string, label: string) { if (!window.confirm(`确定${label}?这会向来肯发起只读查询。`)) return setPending(label) setError('') setOk('') try { const report = await api<{ fetched: number; upserted: number }>(path, { method: 'POST' }) setOk(`${label}完成,拉取 ${report.fetched},写入 ${report.upserted}`) load() } catch (err) { setError(err instanceof Error ? err.message : '同步失败') } finally { setPending('') } } return ( <Page title="概览"> <Banner kind="error" text={error} /> <Banner kind="ok" text={ok} /> {status ? ( <div className="cards"> <div className="card"> <span>来肯账号</span> <strong>{status.credentials_configured ? status.ql361_username : '未配置'}</strong> </div> <div className="card"> <span>来肯会话</span> <strong>{status.session_cached ? '已缓存' : '未登录'}</strong> </div> <div className="card"> <span>单据轮询</span> <strong>每 {status.order_poll_seconds} 秒</strong> </div> <div className="card"> <span>请求间隔</span> <strong>{status.request_gap_ms} 毫秒</strong> </div> <div className="card"> <span>采集库存的仓库</span> <strong>{status.stock_warehouses.join('、')}</strong> </div> </div> ) : ( <p className="muted">当前账号不能查看来肯同步状态。</p> )} {can(user, 'sync.run') && ( <div className="actions"> {syncs.map((item) => ( <button key={item.path} type="button" disabled={pending !== ''} onClick={() => void run(item.path, item.label)} > {pending === item.label ? '同步中…' : item.label} </button> ))} </div> )} <h2>最近同步</h2> {!status || status.recent_runs.length === 0 ? ( <Empty text="还没有同步记录" /> ) : ( <div className="table-wrap"> <table> <thead> <tr> <th>时间</th> <th>对象</th> <th>触发</th> <th>状态</th> <th>拉取</th> <th>写入</th> <th>说明</th> </tr> </thead> <tbody> {status.recent_runs.map((run) => ( <tr key={run.id}> <td>{run.finished_at || run.started_at}</td> <td>{run.entity}</td> <td>{run.trigger}</td> <td>{run.status}</td> <td>{run.fetched}</td> <td>{run.upserted}</td> <td>{run.error_message}</td> </tr> ))} </tbody> </table> </div> )} </Page> ) }