feat(orders): 登录保护的生产订单标签页 + 一键导出多层 ZIP(小型程序 WCD 任务)
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
import { useEffect, useState, type FormEvent } from 'react';
|
||||
import AppSettingsWindow, { type ThemeMode } from '../components/AppSettingsWindow';
|
||||
import {
|
||||
IconCloud,
|
||||
IconExport,
|
||||
IconGrid,
|
||||
IconLock,
|
||||
IconTrash,
|
||||
} from '../components/Icons';
|
||||
import { apiUrl, ensureOk } from '../lib/api';
|
||||
|
||||
interface OrderItem {
|
||||
order_id: string;
|
||||
order_no: string;
|
||||
job_id: string;
|
||||
template_id: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const TOKEN_KEY = 'wordcloud-orders-token';
|
||||
|
||||
interface OrdersPageProps {
|
||||
themeMode: ThemeMode;
|
||||
systemTheme: 'light' | 'dark';
|
||||
onThemeModeChange: (mode: ThemeMode) => void;
|
||||
onOpenHome: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生产订单页:独立标签页,需登录(POST /api/login → token)后才能查看
|
||||
* 小程序下单派发到 wordcloud 的 WCD 生产任务,支持一键导出多层 ZIP。
|
||||
*/
|
||||
export default function OrdersPage({
|
||||
themeMode,
|
||||
systemTheme,
|
||||
onThemeModeChange,
|
||||
onOpenHome,
|
||||
}: OrdersPageProps) {
|
||||
const [token, setToken] = useState<string>(() => localStorage.getItem(TOKEN_KEY) || '');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loginError, setLoginError] = useState('');
|
||||
const [orders, setOrders] = useState<OrderItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [exporting, setExporting] = useState(false);
|
||||
|
||||
const loadOrders = async (t: string) => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const res = await fetch(apiUrl('/api/orders'), {
|
||||
headers: { Authorization: `Bearer ${t}` },
|
||||
});
|
||||
if (res.status === 403 || res.status === 401) {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
setToken('');
|
||||
return;
|
||||
}
|
||||
await ensureOk(res, '读取订单失败');
|
||||
setOrders(await res.json());
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : '读取订单失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (token) loadOrders(token);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
|
||||
const onLogin = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoginError('');
|
||||
try {
|
||||
const res = await fetch(apiUrl('/api/login'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password }),
|
||||
});
|
||||
if (res.status === 401) {
|
||||
setLoginError('口令错误');
|
||||
return;
|
||||
}
|
||||
await ensureOk(res, '登录失败');
|
||||
const data = await res.json();
|
||||
localStorage.setItem(TOKEN_KEY, data.token);
|
||||
setToken(data.token);
|
||||
setPassword('');
|
||||
} catch (err) {
|
||||
setLoginError(err instanceof Error ? err.message : '登录失败');
|
||||
}
|
||||
};
|
||||
|
||||
const onExport = async () => {
|
||||
setExporting(true);
|
||||
setError('');
|
||||
try {
|
||||
const res = await fetch(apiUrl('/api/orders/export'), {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
await ensureOk(res, '导出失败');
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'orders-export.zip';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '导出失败');
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onLogout = () => {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
setToken('');
|
||||
setOrders([]);
|
||||
};
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<div className="orders-page">
|
||||
<nav className="navbar">
|
||||
<div className="navbar-brand">
|
||||
<div className="navbar-brand-icon"><IconLock /></div>
|
||||
<span className="navbar-brand-name">生产订单 · 登录</span>
|
||||
</div>
|
||||
<div className="navbar-actions" />
|
||||
<div className="navbar-end">
|
||||
<button className="nav-btn" onClick={onOpenHome}>
|
||||
<span className="nav-btn-icon"><IconGrid /></span>
|
||||
<span className="nav-btn-label">模板</span>
|
||||
</button>
|
||||
<AppSettingsWindow themeMode={themeMode} systemTheme={systemTheme} onThemeModeChange={onThemeModeChange} />
|
||||
</div>
|
||||
</nav>
|
||||
<main className="orders-main">
|
||||
<form className="login-card" onSubmit={onLogin}>
|
||||
<h2 className="login-title">生产订单后台</h2>
|
||||
<p className="orders-hint">查看小程序下单派发到本平台的 WCD 生产任务,请输入管理口令。</p>
|
||||
<input
|
||||
type="password"
|
||||
className="orders-input"
|
||||
placeholder="管理口令"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
{loginError && <div className="orders-error">{loginError}</div>}
|
||||
<button type="submit" className="btn btn-primary btn-block">
|
||||
登录并查看
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="orders-page">
|
||||
<nav className="navbar">
|
||||
<div className="navbar-brand">
|
||||
<div className="navbar-brand-icon"><IconCloud /></div>
|
||||
<span className="navbar-brand-name">生产订单</span>
|
||||
</div>
|
||||
<div className="navbar-actions" />
|
||||
<div className="navbar-end">
|
||||
<button className="nav-btn" onClick={onOpenHome}>
|
||||
<span className="nav-btn-icon"><IconGrid /></span>
|
||||
<span className="nav-btn-label">模板</span>
|
||||
</button>
|
||||
<button className="nav-btn" onClick={() => loadOrders(token)}>
|
||||
<span className="nav-btn-icon"><IconLock /></span>
|
||||
<span className="nav-btn-label">刷新</span>
|
||||
</button>
|
||||
<button className="nav-btn" onClick={onLogout}>
|
||||
<span className="nav-btn-icon"><IconTrash /></span>
|
||||
<span className="nav-btn-label">退出</span>
|
||||
</button>
|
||||
<AppSettingsWindow themeMode={themeMode} systemTheme={systemTheme} onThemeModeChange={onThemeModeChange} />
|
||||
</div>
|
||||
</nav>
|
||||
<main className="orders-main">
|
||||
<div className="orders-toolbar">
|
||||
<span className="orders-count">共 {orders.length} 单 · 登录进入</span>
|
||||
<button className="btn btn-primary btn-sm" onClick={onExport} disabled={exporting || !orders.length}>
|
||||
<span className="nav-btn-icon"><IconExport /></span>
|
||||
{exporting ? '导出中…' : '一键导出多层 ZIP'}
|
||||
</button>
|
||||
</div>
|
||||
{error && <div className="orders-error">{error}</div>}
|
||||
{loading ? (
|
||||
<div className="table-empty">正在加载订单…</div>
|
||||
) : orders.length === 0 ? (
|
||||
<div className="table-empty">
|
||||
暂无生产订单。小程序下单派单后,这里会显示从你平台收到的 WCD 生产任务。
|
||||
</div>
|
||||
) : (
|
||||
<table className="orders-table">
|
||||
<thead>
|
||||
<tr><th>订单号</th><th>状态</th><th>任务ID</th><th>模板ID</th><th>创建时间</th><th>产物</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{orders.map((o) => (
|
||||
<tr key={o.order_id}>
|
||||
<td className="mono">{o.order_no}</td>
|
||||
<td><span className={`orders-status status-${o.status || 'unknown'}`}>{o.status || '-'}</span></td>
|
||||
<td className="mono">{o.job_id}</td>
|
||||
<td className="mono">{o.template_id}</td>
|
||||
<td>{o.created_at}</td>
|
||||
<td>
|
||||
{o.status === 'success' && o.job_id ? (
|
||||
<a className="orders-preview" href={apiUrl(`/api/jobs/${o.job_id}/files/png`)} target="_blank" rel="noreferrer">查看产物</a>
|
||||
) : '—'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user