All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m40s
- core/frontend: Vite 多阶段镜像 + nginx 同源反代 /api,/admin,/static(零 CORS) - core/backend: Django gunicorn 镜像 + entrypoint(自动 migrate/collectstatic)+ WhiteNoise - k8s/core: api/worker/web Deployment+Service + ingress(airshelf-web.airlabs.art) - workflow: 追加 core 前后端 build/push,从 core/backend/.env 套生产覆盖生成 env Secret 后部署 - .gitignore 放行 core/backend/.env;.env 白名单加入 airshelf-web 域名 - 含前端 WIP 还原改动 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
120 lines
6.7 KiB
TypeScript
120 lines
6.7 KiB
TypeScript
import { useState } from "react";
|
|
import type { FormEvent } from "react";
|
|
import type { Asset } from "../types";
|
|
import { Drawer } from "../components/overlays";
|
|
|
|
type LibTab = "people" | "scenes" | "products" | "finals" | "uploads" | "unclassified";
|
|
|
|
const LIB_TABS: Array<{ key: LibTab; label: string }> = [
|
|
{ key: "people", label: "人物" }, { key: "scenes", label: "场景" }, { key: "products", label: "商品图" },
|
|
{ key: "finals", label: "成片" }, { key: "uploads", label: "我的上传" }, { key: "unclassified", label: "未分类" }
|
|
];
|
|
|
|
// 对齐 api-bridge:工具栏 chip 按 tab 显隐
|
|
const LIB_CHIPS: Array<{ key: string; label: string; tabs: LibTab[] }> = [
|
|
{ key: "gender", label: "性别", tabs: ["people"] },
|
|
{ key: "age", label: "年龄段", tabs: ["people"] },
|
|
{ key: "role", label: "角色标签", tabs: ["people"] },
|
|
{ key: "sceneType", label: "场景类型", tabs: ["scenes"] },
|
|
{ key: "product", label: "关联商品", tabs: ["products"] },
|
|
{ key: "project", label: "关联项目", tabs: ["finals"] },
|
|
{ key: "duration", label: "时长", tabs: ["finals"] },
|
|
{ key: "kind", label: "资产类型", tabs: ["uploads"] },
|
|
{ key: "source", label: "来源", tabs: ["people", "scenes", "products", "uploads"] }
|
|
];
|
|
|
|
function assetTab(asset: Asset): LibTab {
|
|
switch (asset.category) {
|
|
case "person": return "people";
|
|
case "scene": return "scenes";
|
|
case "product_image": return "products";
|
|
case "final_video": return "finals";
|
|
case "upload": return "uploads";
|
|
default: return asset.asset_type === "video" ? "finals" : "unclassified";
|
|
}
|
|
}
|
|
|
|
export function LibraryPage({ assets, onUpload }: { assets: Asset[]; onUpload: (formData: FormData) => Promise<unknown> | void }) {
|
|
const [tab, setTab] = useState<LibTab>("people");
|
|
const [query, setQuery] = useState("");
|
|
const [drawer, setDrawer] = useState(false);
|
|
const [file, setFile] = useState<File | null>(null);
|
|
const [name, setName] = useState("");
|
|
|
|
const counts = LIB_TABS.reduce((acc, t) => { acc[t.key] = assets.filter((a) => assetTab(a) === t.key).length; return acc; }, {} as Record<LibTab, number>);
|
|
const inTab = assets.filter((a) => assetTab(a) === tab);
|
|
const filtered = inTab.filter((a) => `${a.name} ${a.category}`.toLowerCase().includes(query.toLowerCase()));
|
|
|
|
async function submit(event: FormEvent) {
|
|
event.preventDefault();
|
|
if (!file) return;
|
|
const formData = new FormData();
|
|
formData.append("file", file);
|
|
formData.append("name", name || file.name);
|
|
formData.append("asset_type", "image");
|
|
formData.append("category", "upload");
|
|
await onUpload(formData);
|
|
setDrawer(false);
|
|
}
|
|
|
|
return (
|
|
<section className="library-page">
|
|
<div className="page-head">
|
|
<div>
|
|
<h1>资产库</h1>
|
|
<div className="sub"><span className="mono">// 跨项目复用 · <span id="sub-people">{counts.people}</span> 人 · <span id="sub-scenes">{counts.scenes}</span> 景 · <span id="sub-products">{counts.products}</span> 商 · <span id="sub-finals">{counts.finals}</span> 片</span></div>
|
|
</div>
|
|
<div className="actions">
|
|
<button className="btn" type="button" id="lib-manage-btn">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="m3 7 2 2 4-4" /><path d="m3 17 2 2 4-4" /><path d="M13 6h8" /><path d="M13 12h8" /><path d="M13 18h8" /></svg>
|
|
<span className="lib-manage-label">管理资产</span>
|
|
</button>
|
|
<button className="btn btn-primary" type="button" id="open-upload-btn" onClick={() => setDrawer(true)}>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12" /></svg>
|
|
上传资产
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="tabs" id="asset-tabs">
|
|
{LIB_TABS.map((t) => (
|
|
<div className={`tab${tab === t.key ? " active" : ""}`} key={t.key} data-tab={t.key} onClick={() => setTab(t.key)}>{t.label} <span className="count">{counts[t.key]}</span></div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="toolbar">
|
|
<div className="search-inline">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><circle cx="11" cy="11" r="7" /><path d="m21 21-4.3-4.3" /></svg>
|
|
<input className="input" id="search-input" placeholder="搜索资产名称、标签" value={query} onChange={(event) => setQuery(event.target.value)} />
|
|
</div>
|
|
{LIB_CHIPS.filter((chip) => chip.tabs.includes(tab)).map((chip) => (
|
|
<div className="chip-wrap" data-key={chip.key} key={chip.key}>
|
|
<button className="chip" type="button"><span className="chip-label">{chip.label}</span> <svg className="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M4 6l4 4 4-4" /></svg></button>
|
|
</div>
|
|
))}
|
|
<span className="spacer"></span>
|
|
<div className="chip-wrap" data-key="sort">
|
|
<button className="chip" type="button"><span className="chip-label">最近使用</span> <svg className="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M4 6l4 4 4-4" /></svg></button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="result-meta" id="result-meta">// 显示 <span className="count">{filtered.length}</span> / {inTab.length} 个资产</div>
|
|
|
|
{filtered.length ? (
|
|
<div className="asset-grid" id="asset-grid">
|
|
{filtered.map((asset) => (
|
|
<article className={`asset-card ${asset.asset_type}`} key={asset.id}>
|
|
<div className="placeholder asset-thumb"><span className="ph-frame">{asset.asset_type}</span></div>
|
|
<div className="asset-body"><div className="asset-name">{asset.name}</div><div className="asset-meta">{asset.category} · {asset.source}</div></div>
|
|
</article>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="empty-filter">// 当前分类暂无真实资产</div>
|
|
)}
|
|
|
|
<Drawer title="上传资产" open={drawer} close={() => setDrawer(false)}><form onSubmit={submit}><div className="field"><label className="field-label">文件</label><input className="input file-input" type="file" onChange={(event) => setFile(event.target.files?.[0] || null)} /></div><div className="field"><label className="field-label">资产名称</label><input className="input" value={name} onChange={(event) => setName(event.target.value)} /></div><div className="drawer-actions"><button className="btn btn-ghost" type="button" onClick={() => setDrawer(false)}>取消</button><button className="btn btn-primary" type="submit" disabled={!file}>上传资产</button></div></form></Drawer>
|
|
</section>
|
|
);
|
|
}
|