feat(orders): 订单页改双栏——左队列/右详情,含分层预览与整体预览、单订单/全部导出
This commit is contained in:
@@ -999,6 +999,88 @@ def get_order(request: Request, order_no: str) -> dict:
|
|||||||
return o
|
return o
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/orders/{order_no}/design")
|
||||||
|
def order_design(request: Request, order_no: str) -> dict:
|
||||||
|
"""订单设计详情:画布 + 各层元素(含素材 URL),供前端做分层/整体预览。"""
|
||||||
|
_require_orders_auth(request)
|
||||||
|
path = Path(_order_dir(order_no)) / "order.json"
|
||||||
|
if not path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="order not found")
|
||||||
|
o = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
tid = o.get("template_id")
|
||||||
|
tjson = _design_template_dir(tid) / "template.json" if tid else None
|
||||||
|
if not tjson or not tjson.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="订单设计不存在")
|
||||||
|
data = json.loads(tjson.read_text(encoding="utf-8"))
|
||||||
|
doc = data.get("document") or {}
|
||||||
|
|
||||||
|
elements = []
|
||||||
|
for e in doc.get("elements") or []:
|
||||||
|
if not isinstance(e, dict):
|
||||||
|
continue
|
||||||
|
item = dict(e)
|
||||||
|
asset_id = e.get("assetId")
|
||||||
|
if asset_id:
|
||||||
|
meta = _read_asset_meta(_asset_dir(str(asset_id))) or {}
|
||||||
|
item["assetUrl"] = meta.get("file_url") or f"/api/assets/{asset_id}/download"
|
||||||
|
item["assetName"] = meta.get("name") or str(asset_id)
|
||||||
|
else:
|
||||||
|
item["assetUrl"] = ""
|
||||||
|
elements.append(item)
|
||||||
|
|
||||||
|
try:
|
||||||
|
status = manager.get_status(str(o.get("job_id") or "")).status
|
||||||
|
except Exception:
|
||||||
|
status = o.get("status", "unknown")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"order_no": order_no,
|
||||||
|
"status": status,
|
||||||
|
"job_id": o.get("job_id"),
|
||||||
|
"template_id": tid,
|
||||||
|
"design": {
|
||||||
|
"width": doc.get("width"),
|
||||||
|
"height": doc.get("height"),
|
||||||
|
"background": doc.get("background", "#ffffff"),
|
||||||
|
"layers": doc.get("layers") or [],
|
||||||
|
"elements": elements,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/orders/{order_no}/export")
|
||||||
|
async def export_order(request: Request, order_no: str) -> StreamingResponse:
|
||||||
|
"""单订单导出多层 ZIP。"""
|
||||||
|
_require_orders_auth(request)
|
||||||
|
path = Path(_order_dir(order_no)) / "order.json"
|
||||||
|
if not path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="order not found")
|
||||||
|
o = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
buf = io.BytesIO()
|
||||||
|
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
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": f'attachment; filename="order-{order_no}.zip"'},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
# 4. 模板接口
|
# 4. 模板接口
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
IconExport,
|
IconExport,
|
||||||
IconGrid,
|
IconGrid,
|
||||||
IconLock,
|
IconLock,
|
||||||
|
IconLayers,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
} from '../components/Icons';
|
} from '../components/Icons';
|
||||||
import { apiUrl, ensureOk } from '../lib/api';
|
import { apiUrl, ensureOk } from '../lib/api';
|
||||||
@@ -18,6 +19,35 @@ interface OrderItem {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DesignElement {
|
||||||
|
id?: string;
|
||||||
|
type?: string;
|
||||||
|
name?: string;
|
||||||
|
assetId?: string;
|
||||||
|
assetUrl?: string;
|
||||||
|
assetName?: string;
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
rotation?: number;
|
||||||
|
opacity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrderDesign {
|
||||||
|
order_no: string;
|
||||||
|
status: string;
|
||||||
|
job_id?: string;
|
||||||
|
template_id?: string;
|
||||||
|
design: {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
background?: string;
|
||||||
|
layers?: unknown[];
|
||||||
|
elements: DesignElement[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const TOKEN_KEY = 'wordcloud-orders-token';
|
const TOKEN_KEY = 'wordcloud-orders-token';
|
||||||
|
|
||||||
interface OrdersPageProps {
|
interface OrdersPageProps {
|
||||||
@@ -27,10 +57,50 @@ interface OrdersPageProps {
|
|||||||
onOpenHome: () => void;
|
onOpenHome: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 在画布上按坐标渲染一组元素(用于分层/整体预览)。 */
|
||||||
* 生产订单页:独立标签页,需登录(POST /api/login → token)后才能查看
|
function LayerCanvas({ design, onlyIndex, maxWidth }: {
|
||||||
* 小程序下单派发到 wordcloud 的 WCD 生产任务,支持一键导出多层 ZIP。
|
design: NonNullable<OrderDesign['design']>;
|
||||||
*/
|
onlyIndex?: number;
|
||||||
|
maxWidth: number;
|
||||||
|
}) {
|
||||||
|
const width = design.width || 1200;
|
||||||
|
const height = design.height || 1200;
|
||||||
|
const scale = maxWidth / width;
|
||||||
|
const elements = design.elements.filter((_, i) => onlyIndex === undefined || i === onlyIndex);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="layer-canvas"
|
||||||
|
style={{
|
||||||
|
width: maxWidth,
|
||||||
|
height: Math.round(height * scale),
|
||||||
|
background: design.background || '#ffffff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{elements.map((el, i) => {
|
||||||
|
const w = (el.width ?? 100) * scale;
|
||||||
|
const h = (el.height ?? 100) * scale;
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
key={`${el.id}-${i}`}
|
||||||
|
className="layer-el"
|
||||||
|
src={apiUrl(el.assetUrl || '')}
|
||||||
|
alt={el.assetName || el.name || `层 ${i + 1}`}
|
||||||
|
style={{
|
||||||
|
left: (el.x ?? 0) * scale,
|
||||||
|
top: (el.y ?? 0) * scale,
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
transform: `rotate(${el.rotation ?? 0}deg)`,
|
||||||
|
opacity: el.opacity ?? 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function OrdersPage({
|
export default function OrdersPage({
|
||||||
themeMode,
|
themeMode,
|
||||||
systemTheme,
|
systemTheme,
|
||||||
@@ -41,6 +111,9 @@ export default function OrdersPage({
|
|||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [loginError, setLoginError] = useState('');
|
const [loginError, setLoginError] = useState('');
|
||||||
const [orders, setOrders] = useState<OrderItem[]>([]);
|
const [orders, setOrders] = useState<OrderItem[]>([]);
|
||||||
|
const [selectedNo, setSelectedNo] = useState<string | null>(null);
|
||||||
|
const [design, setDesign] = useState<OrderDesign | null>(null);
|
||||||
|
const [previewMode, setPreviewMode] = useState<'overall' | 'layers'>('overall');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [exporting, setExporting] = useState(false);
|
const [exporting, setExporting] = useState(false);
|
||||||
@@ -49,9 +122,7 @@ export default function OrdersPage({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const res = await fetch(apiUrl('/api/orders'), {
|
const res = await fetch(apiUrl('/api/orders'), { headers: { Authorization: `Bearer ${t}` } });
|
||||||
headers: { Authorization: `Bearer ${t}` },
|
|
||||||
});
|
|
||||||
if (res.status === 403 || res.status === 401) {
|
if (res.status === 403 || res.status === 401) {
|
||||||
localStorage.removeItem(TOKEN_KEY);
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
setToken('');
|
setToken('');
|
||||||
@@ -71,6 +142,44 @@ export default function OrdersPage({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
|
||||||
|
const selectOrder = async (o: OrderItem) => {
|
||||||
|
setSelectedNo(o.order_no);
|
||||||
|
setPreviewMode('overall');
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
const res = await fetch(apiUrl(`/api/orders/${o.order_no}/design`), {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
await ensureOk(res, '读取订单详情失败');
|
||||||
|
setDesign(await res.json());
|
||||||
|
} catch (e) {
|
||||||
|
setDesign(null);
|
||||||
|
setError(e instanceof Error ? e.message : '读取订单详情失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadZip = async (path: string) => {
|
||||||
|
setExporting(true);
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
const res = await fetch(apiUrl(path), { 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 = path.startsWith('/api/orders/export') ? 'orders-export.zip' : `${selectedNo}.zip`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : '导出失败');
|
||||||
|
} finally {
|
||||||
|
setExporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onLogin = async (e: FormEvent) => {
|
const onLogin = async (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoginError('');
|
setLoginError('');
|
||||||
@@ -94,34 +203,12 @@ export default function OrdersPage({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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 = () => {
|
const onLogout = () => {
|
||||||
localStorage.removeItem(TOKEN_KEY);
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
setToken('');
|
setToken('');
|
||||||
setOrders([]);
|
setOrders([]);
|
||||||
|
setSelectedNo(null);
|
||||||
|
setDesign(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@@ -186,44 +273,101 @@ export default function OrdersPage({
|
|||||||
<AppSettingsWindow themeMode={themeMode} systemTheme={systemTheme} onThemeModeChange={onThemeModeChange} />
|
<AppSettingsWindow themeMode={themeMode} systemTheme={systemTheme} onThemeModeChange={onThemeModeChange} />
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<main className="orders-main">
|
|
||||||
<div className="orders-toolbar">
|
<main className="orders-main orders-layout">
|
||||||
<span className="orders-count">共 {orders.length} 单 · 登录进入</span>
|
{/* 左:订单队列 */}
|
||||||
<button className="btn btn-primary btn-sm" onClick={onExport} disabled={exporting || !orders.length}>
|
<aside className="orders-queue">
|
||||||
<span className="nav-btn-icon"><IconExport /></span>
|
<div className="orders-queue-head">
|
||||||
{exporting ? '导出中…' : '一键导出多层 ZIP'}
|
<span>订单队列</span>
|
||||||
</button>
|
<span className="orders-count">{orders.length} 单</span>
|
||||||
</div>
|
|
||||||
{error && <div className="orders-error">{error}</div>}
|
|
||||||
{loading ? (
|
|
||||||
<div className="table-empty">正在加载订单…</div>
|
|
||||||
) : orders.length === 0 ? (
|
|
||||||
<div className="table-empty">
|
|
||||||
暂无生产订单。小程序下单派单后,这里会显示从你平台收到的 WCD 生产任务。
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<div className="orders-queue-list">
|
||||||
<table className="orders-table">
|
{loading && <div className="table-empty">加载中…</div>}
|
||||||
<thead>
|
{!loading && orders.length === 0 && (
|
||||||
<tr><th>订单号</th><th>状态</th><th>任务ID</th><th>模板ID</th><th>创建时间</th><th>产物</th></tr>
|
<div className="table-empty">暂无订单</div>
|
||||||
</thead>
|
)}
|
||||||
<tbody>
|
{orders.map((o) => (
|
||||||
{orders.map((o) => (
|
<button
|
||||||
<tr key={o.order_id}>
|
key={o.order_id}
|
||||||
<td className="mono">{o.order_no}</td>
|
className={`orders-q-item ${selectedNo === o.order_no ? 'active' : ''}`}
|
||||||
<td><span className={`orders-status status-${o.status || 'unknown'}`}>{o.status || '-'}</span></td>
|
onClick={() => selectOrder(o)}
|
||||||
<td className="mono">{o.job_id}</td>
|
>
|
||||||
<td className="mono">{o.template_id}</td>
|
<div className="orders-q-item-top">
|
||||||
<td>{o.created_at}</td>
|
<span className="mono">{o.order_no}</span>
|
||||||
<td>
|
<span className={`orders-status status-${o.status || 'unknown'}`}>{o.status || '-'}</span>
|
||||||
{o.status === 'success' && o.job_id ? (
|
</div>
|
||||||
<a className="orders-preview" href={apiUrl(`/api/jobs/${o.job_id}/files/png`)} target="_blank" rel="noreferrer">查看产物</a>
|
<div className="orders-q-item-sub">{o.created_at}</div>
|
||||||
) : '—'}
|
</button>
|
||||||
</td>
|
))}
|
||||||
</tr>
|
</div>
|
||||||
))}
|
</aside>
|
||||||
</tbody>
|
|
||||||
</table>
|
{/* 右:选中订单详情 */}
|
||||||
)}
|
<section className="orders-detail">
|
||||||
|
{!selectedNo || !design ? (
|
||||||
|
<div className="table-empty">在左侧选择订单查看详情</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="orders-detail-head">
|
||||||
|
<div>
|
||||||
|
<h2 className="orders-detail-title mono">{design.order_no}</h2>
|
||||||
|
<span className={`orders-status status-${design.status || 'unknown'}`}>{design.status || '-'}</span>
|
||||||
|
<span className="orders-detail-meta">
|
||||||
|
任务 {design.job_id} · 模板 {design.template_id} · 画布 {design.design.width}×{design.design.height}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="orders-detail-actions">
|
||||||
|
<button className="nav-btn" onClick={() => setPreviewMode('overall')}>
|
||||||
|
<span className="nav-btn-icon"><IconCloud /></span>
|
||||||
|
<span className={`nav-btn-label ${previewMode === 'overall' ? 'active' : ''}`}>整体预览</span>
|
||||||
|
</button>
|
||||||
|
<button className="nav-btn" onClick={() => setPreviewMode('layers')}>
|
||||||
|
<span className="nav-btn-icon"><IconLayers /></span>
|
||||||
|
<span className={`nav-btn-label ${previewMode === 'layers' ? 'active' : ''}`}>分层预览</span>
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-primary btn-sm" onClick={() => downloadZip(`/api/orders/${design.order_no}/export`)} disabled={exporting}>
|
||||||
|
<span className="nav-btn-icon"><IconExport /></span>
|
||||||
|
{exporting ? '导出中…' : '一键导出'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="orders-error">{error}</div>}
|
||||||
|
|
||||||
|
{previewMode === 'overall' ? (
|
||||||
|
<div className="orders-preview-block">
|
||||||
|
<div className="orders-preview-label">整体预览</div>
|
||||||
|
{design.job_id ? (
|
||||||
|
<img
|
||||||
|
className="orders-overall-img"
|
||||||
|
src={apiUrl(`/api/jobs/${design.job_id}/files/png`)}
|
||||||
|
alt="整体预览"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LayerCanvas design={design.design} maxWidth={560} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="orders-preview-block">
|
||||||
|
<div className="orders-preview-label">分层预览(每层单独展示)</div>
|
||||||
|
<div className="orders-layer-list">
|
||||||
|
{design.design.elements.map((el, i) => (
|
||||||
|
<div className="orders-layer-item" key={`${el.id}-${i}`}>
|
||||||
|
<div className="orders-layer-thumb">
|
||||||
|
<LayerCanvas design={design.design} onlyIndex={i} maxWidth={220} />
|
||||||
|
</div>
|
||||||
|
<div className="orders-layer-name">{el.assetName || el.name || `层 ${i + 1}`}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{design.design.elements.length === 0 && <div className="table-empty">该订单没有独立图层</div>}
|
||||||
|
</div>
|
||||||
|
<div className="orders-preview-label" style={{ marginTop: 18 }}>合成预览(全部层叠加)</div>
|
||||||
|
<LayerCanvas design={design.design} maxWidth={560} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2425,3 +2425,29 @@ body.resizing .studio-panel {
|
|||||||
.orders-hint { color: var(--text-secondary); font-size: 13px; margin: 0 0 18px; line-height: 1.5; }
|
.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 { 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); }
|
.orders-input:focus { outline: none; border-color: var(--border-focus); }
|
||||||
|
|
||||||
|
/* ── OrdersPage 双栏:左订单队列 / 右详情 ────────── */
|
||||||
|
.orders-layout { display: flex; gap: 20px; align-items: flex-start; max-width: 1280px; }
|
||||||
|
.orders-queue { width: 300px; flex-shrink: 0; background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; align-self: stretch; max-height: calc(100vh - 140px); display: flex; flex-direction: column; }
|
||||||
|
.orders-queue-head { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid var(--border); font-weight: 600; color: var(--text-primary); }
|
||||||
|
.orders-queue-list { flex: 1; overflow: auto; padding: 8px; }
|
||||||
|
.orders-q-item { display: block; width: 100%; text-align: left; background: transparent; border: 1px solid transparent; border-radius: var(--radius-sm); padding: 10px 12px; margin-bottom: 6px; cursor: pointer; }
|
||||||
|
.orders-q-item:hover { background: var(--bg-panel-alt); }
|
||||||
|
.orders-q-item.active { border-color: var(--accent); background: var(--accent-light); }
|
||||||
|
.orders-q-item-top { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
|
||||||
|
.orders-q-item-sub { color: var(--text-muted); font-size: 12px; margin-top: 4px; }
|
||||||
|
.orders-detail { flex: 1; min-width: 0; }
|
||||||
|
.orders-detail-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; flex-wrap: wrap; margin-bottom: 16px; }
|
||||||
|
.orders-detail-title { margin: 0 0 6px; font-size: 20px; color: var(--text-primary); }
|
||||||
|
.orders-detail-meta { color: var(--text-secondary); font-size: 12px; margin-left: 12px; }
|
||||||
|
.orders-detail-actions { display: flex; align-items: center; gap: 8px; }
|
||||||
|
.orders-preview-block { background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius-md); padding: 16px; }
|
||||||
|
.orders-preview-label { color: var(--text-secondary); font-size: 13px; font-weight: 600; margin-bottom: 12px; }
|
||||||
|
.orders-overall-img { max-width: 100%; max-height: 60vh; display: block; border-radius: var(--radius-sm); border: 1px solid var(--border); }
|
||||||
|
.layer-canvas { position: relative; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-sm); background-size: cover; }
|
||||||
|
.layer-el { position: absolute; object-fit: contain; transform-origin: center; pointer-events: none; }
|
||||||
|
.orders-layer-list { display: flex; flex-wrap: wrap; gap: 12px; }
|
||||||
|
.orders-layer-item { width: 220px; }
|
||||||
|
.orders-layer-thumb { box-shadow: var(--shadow-sm); }
|
||||||
|
.orders-layer-name { color: var(--text-secondary); font-size: 12px; margin-top: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.nav-btn-label.active { color: var(--accent); font-weight: 600; }
|
||||||
|
|||||||
Reference in New Issue
Block a user