feat(orders): 登录保护的生产订单标签页 + 一键导出多层 ZIP(小型程序 WCD 任务)
This commit is contained in:
+71
-5
@@ -16,7 +16,7 @@ from datetime import datetime, timezone
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import FastAPI, File, Form, HTTPException, Query, UploadFile
|
from fastapi import FastAPI, File, Form, HTTPException, Query, Request, UploadFile
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import FileResponse, StreamingResponse
|
from fastapi.responses import FileResponse, StreamingResponse
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
@@ -895,12 +895,42 @@ def get_file(job_id: str, kind: str):
|
|||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
# 3.9 生产订单列表(下单派单投递的 WCD 生产任务)
|
# 3.9 生产订单列表(下单派单投递的 WCD 生产任务,需登录)
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# 生产订单管理后台口令(建议生产通过环境变量 ORDERS_ADMIN_PASSWORD 覆盖)
|
||||||
|
ADMIN_PASSWORD = os.environ.get("ORDERS_ADMIN_PASSWORD") or "zhihui2024"
|
||||||
|
_ORDER_TOKEN_SECRET = os.environ.get("ORDERS_TOKEN_SECRET") or "wordcloud-orders-demo"
|
||||||
|
|
||||||
|
|
||||||
|
def _orders_token() -> str:
|
||||||
|
return hashlib.sha256((ADMIN_PASSWORD + ":" + _ORDER_TOKEN_SECRET).encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _require_orders_auth(request: Request) -> None:
|
||||||
|
"""订单相关接口登录校验:Authorization: Bearer <token> 或 ?token= 需等于鉴权 token。"""
|
||||||
|
auth = request.headers.get("Authorization", "")
|
||||||
|
bearer = auth[7:] if auth.startswith("Bearer ") else ""
|
||||||
|
token = bearer or request.query_params.get("token", "")
|
||||||
|
if not token or token != _orders_token():
|
||||||
|
raise HTTPException(status_code=403, detail="需要登录") # noqa: S105
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/login")
|
||||||
|
async def login(request: Request) -> dict:
|
||||||
|
try:
|
||||||
|
payload = await request.json()
|
||||||
|
except Exception:
|
||||||
|
payload = {}
|
||||||
|
if (payload or {}).get("password") != ADMIN_PASSWORD:
|
||||||
|
raise HTTPException(status_code=401, detail="口令错误")
|
||||||
|
return {"token": _orders_token()}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/orders")
|
@app.get("/api/orders")
|
||||||
def list_orders() -> list[dict]:
|
def list_orders(request: Request) -> list[dict]:
|
||||||
"""生产订单列表:来自小程序下单派单投递到 wordcloud 的 WCD 生产任务。"""
|
"""生产订单列表:来自小程序下单派单投递到 wordcloud 的 WCD 生产任务(需登录)。"""
|
||||||
|
_require_orders_auth(request)
|
||||||
orders = []
|
orders = []
|
||||||
if ORDERS_DIR.exists():
|
if ORDERS_DIR.exists():
|
||||||
for item in list(ORDERS_DIR.iterdir()):
|
for item in list(ORDERS_DIR.iterdir()):
|
||||||
@@ -919,8 +949,44 @@ def list_orders() -> list[dict]:
|
|||||||
return list(reversed(orders))
|
return list(reversed(orders))
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/orders/export")
|
||||||
|
async def export_orders(request: Request) -> StreamingResponse:
|
||||||
|
"""一键导出多层 ZIP:每个订单一个嵌套文件夹(order.json + design + result)。"""
|
||||||
|
_require_orders_auth(request)
|
||||||
|
buf = io.BytesIO()
|
||||||
|
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
dirs = [d for d in ORDERS_DIR.iterdir() if d.is_dir() and (d / "order.json").exists()]
|
||||||
|
for item in sorted(dirs):
|
||||||
|
o = _read_order(item)
|
||||||
|
if not o:
|
||||||
|
continue
|
||||||
|
order_no = str(o.get("order_no") or "order")
|
||||||
|
folder = f"orders/{order_no}/"
|
||||||
|
zf.writestr(folder + "order.json", json.dumps(o, ensure_ascii=False, indent=2))
|
||||||
|
tid = o.get("template_id")
|
||||||
|
job_id = o.get("job_id")
|
||||||
|
if tid:
|
||||||
|
tjson = _design_template_dir(tid) / "template.json"
|
||||||
|
if tjson.exists():
|
||||||
|
zf.write(str(tjson), folder + "design/template.json")
|
||||||
|
if job_id:
|
||||||
|
try:
|
||||||
|
st = manager.get_status(str(job_id))
|
||||||
|
png = manager.resolve_artifact_path(st, "png")
|
||||||
|
zf.write(str(png), folder + "result/result.png")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
buf.seek(0)
|
||||||
|
return StreamingResponse(
|
||||||
|
buf,
|
||||||
|
media_type="application/zip",
|
||||||
|
headers={"Content-Disposition": 'attachment; filename="orders-export.zip"'},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/orders/{order_no}")
|
@app.get("/api/orders/{order_no}")
|
||||||
def get_order(order_no: str) -> dict:
|
def get_order(request: Request, order_no: str) -> dict:
|
||||||
|
_require_orders_auth(request)
|
||||||
path = Path(_order_dir(order_no)) / "order.json"
|
path = Path(_order_dir(order_no)) / "order.json"
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
raise HTTPException(status_code=404, detail="order not found")
|
raise HTTPException(status_code=404, detail="order not found")
|
||||||
|
|||||||
+15
-1
@@ -2,12 +2,13 @@ import { useEffect, useLayoutEffect, useState } from 'react';
|
|||||||
import type { ThemeMode } from './components/AppSettingsWindow';
|
import type { ThemeMode } from './components/AppSettingsWindow';
|
||||||
import CanvasStudio from './pages/CanvasStudio';
|
import CanvasStudio from './pages/CanvasStudio';
|
||||||
import HelpPage from './pages/HelpPage';
|
import HelpPage from './pages/HelpPage';
|
||||||
|
import OrdersPage from './pages/OrdersPage';
|
||||||
import TemplateHome from './pages/TemplateHome';
|
import TemplateHome from './pages/TemplateHome';
|
||||||
import TestWorkbench from './pages/TestWorkbench';
|
import TestWorkbench from './pages/TestWorkbench';
|
||||||
import { CanvasDocument, WordcloudReplaceSession, WordcloudStickerPayload } from './types';
|
import { CanvasDocument, WordcloudReplaceSession, WordcloudStickerPayload } from './types';
|
||||||
import { createDefaultDocument } from './lib/canvasDocument';
|
import { createDefaultDocument } from './lib/canvasDocument';
|
||||||
|
|
||||||
type AppPage = 'home' | 'canvas' | 'wordcloud' | 'help';
|
type AppPage = 'home' | 'canvas' | 'wordcloud' | 'orders' | 'help';
|
||||||
|
|
||||||
const getStoredTheme = (): ThemeMode => {
|
const getStoredTheme = (): ThemeMode => {
|
||||||
const stored = window.localStorage.getItem('wordcloud-theme');
|
const stored = window.localStorage.getItem('wordcloud-theme');
|
||||||
@@ -98,6 +99,18 @@ export default function App() {
|
|||||||
onOpenHome={() => setPage('home')}
|
onOpenHome={() => setPage('home')}
|
||||||
onOpenCanvas={() => setPage('canvas')}
|
onOpenCanvas={() => setPage('canvas')}
|
||||||
onOpenWordcloud={() => setPage('wordcloud')}
|
onOpenWordcloud={() => setPage('wordcloud')}
|
||||||
|
onOpenOrders={() => setPage('orders')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page === 'orders') {
|
||||||
|
return (
|
||||||
|
<OrdersPage
|
||||||
|
themeMode={themeMode}
|
||||||
|
systemTheme={systemTheme}
|
||||||
|
onThemeModeChange={setThemeMode}
|
||||||
|
onOpenHome={() => setPage('home')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -117,6 +130,7 @@ export default function App() {
|
|||||||
}}
|
}}
|
||||||
onOpenCanvas={() => setPage('canvas')}
|
onOpenCanvas={() => setPage('canvas')}
|
||||||
onOpenWordcloud={() => setPage('wordcloud')}
|
onOpenWordcloud={() => setPage('wordcloud')}
|
||||||
|
onOpenOrders={() => setPage('orders')}
|
||||||
onOpenHelp={openHelp}
|
onOpenHelp={openHelp}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ interface HelpPageProps {
|
|||||||
onOpenHome: () => void;
|
onOpenHome: () => void;
|
||||||
onOpenCanvas: () => void;
|
onOpenCanvas: () => void;
|
||||||
onOpenWordcloud: () => void;
|
onOpenWordcloud: () => void;
|
||||||
|
onOpenOrders: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const quickSteps = [
|
const quickSteps = [
|
||||||
@@ -60,6 +61,7 @@ export default function HelpPage({
|
|||||||
onOpenHome,
|
onOpenHome,
|
||||||
onOpenCanvas,
|
onOpenCanvas,
|
||||||
onOpenWordcloud,
|
onOpenWordcloud,
|
||||||
|
onOpenOrders,
|
||||||
}: HelpPageProps) {
|
}: HelpPageProps) {
|
||||||
return (
|
return (
|
||||||
<div className="help-page">
|
<div className="help-page">
|
||||||
@@ -82,6 +84,10 @@ export default function HelpPage({
|
|||||||
<span className="nav-btn-icon"><IconCloud /></span>
|
<span className="nav-btn-icon"><IconCloud /></span>
|
||||||
<span className="nav-btn-label">词云</span>
|
<span className="nav-btn-label">词云</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button className="nav-btn" onClick={onOpenOrders}>
|
||||||
|
<span className="nav-btn-icon"><IconCloud /></span>
|
||||||
|
<span className="nav-btn-label">生产订单</span>
|
||||||
|
</button>
|
||||||
<AppSettingsWindow
|
<AppSettingsWindow
|
||||||
themeMode={themeMode}
|
themeMode={themeMode}
|
||||||
systemTheme={systemTheme}
|
systemTheme={systemTheme}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ interface TemplateHomeProps {
|
|||||||
onUseTemplate: (template: CanvasTemplate) => void;
|
onUseTemplate: (template: CanvasTemplate) => void;
|
||||||
onOpenCanvas: () => void;
|
onOpenCanvas: () => void;
|
||||||
onOpenWordcloud: () => void;
|
onOpenWordcloud: () => void;
|
||||||
|
onOpenOrders: () => void;
|
||||||
onOpenHelp: () => void;
|
onOpenHelp: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ export default function TemplateHome({
|
|||||||
onUseTemplate,
|
onUseTemplate,
|
||||||
onOpenCanvas,
|
onOpenCanvas,
|
||||||
onOpenWordcloud,
|
onOpenWordcloud,
|
||||||
|
onOpenOrders,
|
||||||
onOpenHelp,
|
onOpenHelp,
|
||||||
}: TemplateHomeProps) {
|
}: TemplateHomeProps) {
|
||||||
const [templates, setTemplates] = useState<CanvasTemplate[]>([]);
|
const [templates, setTemplates] = useState<CanvasTemplate[]>([]);
|
||||||
@@ -146,6 +148,10 @@ export default function TemplateHome({
|
|||||||
<span className="nav-btn-icon"><IconCloud /></span>
|
<span className="nav-btn-icon"><IconCloud /></span>
|
||||||
<span className="nav-btn-label">词云</span>
|
<span className="nav-btn-label">词云</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button className="nav-btn" onClick={onOpenOrders}>
|
||||||
|
<span className="nav-btn-icon"><IconCloud /></span>
|
||||||
|
<span className="nav-btn-label">生产订单</span>
|
||||||
|
</button>
|
||||||
<button className="nav-btn" onClick={onOpenHelp}>
|
<button className="nav-btn" onClick={onOpenHelp}>
|
||||||
<span className="nav-btn-icon"><IconHelp /></span>
|
<span className="nav-btn-icon"><IconHelp /></span>
|
||||||
<span className="nav-btn-label">帮助</span>
|
<span className="nav-btn-label">帮助</span>
|
||||||
|
|||||||
@@ -2401,3 +2401,27 @@ body.resizing .side-panel,
|
|||||||
body.resizing .studio-panel {
|
body.resizing .studio-panel {
|
||||||
transition: none !important;
|
transition: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── OrdersPage(生产订单,登录后可见)────────────── */
|
||||||
|
.orders-page { min-height: 100vh; display: flex; flex-direction: column; background: var(--workspace-bg, var(--bg)); }
|
||||||
|
.orders-main { flex: 1; padding: 24px 32px; max-width: 1080px; width: 100%; margin: 0 auto; box-sizing: border-box; }
|
||||||
|
.orders-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
|
||||||
|
.orders-count { color: var(--text-secondary); font-size: 14px; }
|
||||||
|
.orders-error { color: var(--danger); background: var(--danger-light); padding: 10px 14px; border-radius: var(--radius-sm); margin: 12px 0; }
|
||||||
|
.orders-table { width: 100%; border-collapse: collapse; background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; box-shadow: var(--shadow-sm); }
|
||||||
|
.orders-table th, .orders-table td { text-align: left; padding: 10px 14px; border-bottom: 1px solid var(--border); font-size: 13px; color: var(--text-primary); }
|
||||||
|
.orders-table th { color: var(--text-secondary); font-weight: 600; background: var(--bg-panel-alt); }
|
||||||
|
.orders-table tr:last-child td { border-bottom: none; }
|
||||||
|
.orders-status { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; text-transform: capitalize; }
|
||||||
|
.status-success { background: var(--success-light); color: var(--success); }
|
||||||
|
.status-running { background: var(--accent-light); color: var(--accent); }
|
||||||
|
.status-queued { background: var(--bg-panel-alt); color: var(--text-secondary); }
|
||||||
|
.status-failed { background: var(--danger-light); color: var(--danger); }
|
||||||
|
.mono { font-family: var(--font-mono); font-size: 12px; }
|
||||||
|
.orders-preview { color: var(--accent); text-decoration: none; }
|
||||||
|
.orders-preview:hover { text-decoration: underline; }
|
||||||
|
.login-card { max-width: 400px; margin: 80px auto; background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 28px 32px; box-shadow: var(--shadow-md); }
|
||||||
|
.login-title { margin: 0 0 8px; font-size: 20px; color: var(--text-primary); }
|
||||||
|
.orders-hint { color: var(--text-secondary); font-size: 13px; margin: 0 0 18px; line-height: 1.5; }
|
||||||
|
.orders-input { width: 100%; box-sizing: border-box; padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg); color: var(--text-primary); font-size: 14px; margin-bottom: 12px; }
|
||||||
|
.orders-input:focus { outline: none; border-color: var(--border-focus); }
|
||||||
|
|||||||
Reference in New Issue
Block a user