feat(k8s): 新增 core 真应用(前端+Django API+Celery worker)构建与部署

- 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>
This commit is contained in:
zyc
2026-06-05 10:21:41 +08:00
co-authored by Claude Opus 4.8
parent cfdcd84a30
commit d41e487f08
37 changed files with 4108 additions and 456 deletions
+96 -39
View File
@@ -1,63 +1,120 @@
import { Check, Search } from "lucide-react";
import type { Product, User } from "../types";
import type { NavItem, Notice, Page } from "../routes/route-config";
import { mainNav, parentPage, supplementNav } from "../routes/route-config";
import { Check } from "lucide-react";
import { IconKitSvg } from "./IconKitSvg";
import type { Product, Project, Team, User } from "../types";
import type { Notice, Page } from "../routes/route-config";
type Navigate = (page: Page) => void;
export function Sidebar({ page, navigate, user, products }: { page: Page; navigate: Navigate; user: User; products: Product[] }) {
const activePage = parentPage(page);
type NavDef = { id: string; page: Page; label: string; icon: string; badge?: number };
const NAV: NavDef[] = [
{ id: "dashboard", page: "dashboard", label: "工作台", icon: "dashboard" },
{ id: "products", page: "products", label: "商品库", icon: "package" },
{ id: "projects", page: "projects", label: "视频项目", icon: "clapperboard" },
{ id: "asset-factory", page: "assetFactory", label: "图片生成", icon: "sparkles" },
{ id: "library", page: "library", label: "资产库", icon: "library" },
{ id: "team", page: "team", label: "团队", icon: "users" },
{ id: "account", page: "account", label: "消费", icon: "creditCard" },
{ id: "settings", page: "settings", label: "设置", icon: "settings" }
];
const PAGE_TO_NAV: Partial<Record<Page, Page>> = {
dashboard: "dashboard",
products: "products",
productDetail: "products",
productCreateUpload: "products",
projects: "projects",
projectWizard: "projects",
pipeline: "projects",
assetFactory: "assetFactory",
imageOptimize: "assetFactory",
modelPhoto: "assetFactory",
modelPhotoDemoA: "assetFactory",
modelPhotoDemoB: "assetFactory",
platformCover: "assetFactory",
library: "library",
team: "team",
account: "account",
settings: "settings",
settingsNotify: "settings"
};
export function Sidebar({ page, navigate, user, team, products, projects }: {
page: Page;
navigate: Navigate;
user: User;
team: Team | null;
products: Product[];
projects: Project[];
}) {
const activeNav = PAGE_TO_NAV[page];
const badges: Partial<Record<string, number>> = { products: products.length, projects: projects.length };
const avatar = (team?.name || user.username || "A").slice(0, 1).toUpperCase();
return (
<aside className="sidebar">
<div className="brand">
<div className="flame">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 2c1 3 4 5 4 9a4 4 0 0 1-4 4 4 4 0 0 1-4-4 5 5 0 0 1 1.5-3.5C10.5 6 11.5 4 12 2zm-1 13c0 2 1 3 1 5 1-1 3-2 3-5 0-1.5-1-2-2-3-1 1-2 2-2 3z" />
</svg>
</div>
<div><div className="name">·Studio</div></div>
<div className="sidebar-head">
<a className="brand" href="/dashboard" aria-label="Airshelf 工作台" onClick={(event) => { event.preventDefault(); navigate("dashboard"); }}>
<span className="brand-clip"><img className="brand-logo" src="/assets/logo.png" alt="Airshelf" /></span>
</a>
</div>
<div className="search-box">
<Search size={14} />
<input id="global-search" placeholder="搜索" />
<span className="kbd">K</span>
<button className="sidebar-toggle" type="button" aria-label="收窄导航" title="收窄导航">
<span className="sidebar-toggle-icon sidebar-toggle-icon--collapse"><IconKitSvg name="chevronLeft" size={18} strokeWidth={1.8} /></span>
<span className="sidebar-toggle-icon sidebar-toggle-icon--expand"><IconKitSvg name="chevronRight" size={18} strokeWidth={1.8} /></span>
</button>
<div className="search-box" title="搜索">
<IconKitSvg name="search" />
<input id="global-search" placeholder="搜索" readOnly aria-label="打开全局搜索" />
<span className="kbd">Ctrl K</span>
</div>
<div className="nav-section"></div>
<nav>
{mainNav.map((item) => (
<NavButton key={item.page} item={{ ...item, badge: item.page === "products" ? String(products.length) : item.badge }} active={activePage === item.page} navigate={navigate} />
{NAV.map((item) => (
<a
key={item.id}
href={`/${item.id}`}
className={activeNav === item.page ? "active" : ""}
title={item.label}
aria-label={item.label}
onClick={(event) => { event.preventDefault(); navigate(item.page); }}
>
<IconKitSvg name={item.icon} />
<span>{item.label}</span>
{badges[item.id] !== undefined && <span className="pill-mini">{badges[item.id]}</span>}
</a>
))}
</nav>
<div className="nav-section"></div>
<nav>
{supplementNav.map((item) => <NavButton key={item.page} item={item} active={activePage === item.page} navigate={navigate} />)}
</nav>
<div className="aside-foot">
<button className="user" type="button">
<div className="av">{user.username.slice(0, 1).toUpperCase()}</div>
<div className="em">{user.username}</div>
</button>
<div className="user">
<div className="av">{avatar}</div>
<div className="em">{team?.name || user.username}</div>
</div>
</div>
</aside>
);
}
export function NavButton({ item, active, navigate }: { item: NavItem; active: boolean; navigate: Navigate }) {
const Icon = item.icon;
export function Decorations() {
// 与设计稿 shell.js 一致:grid-bg 底纹 + 4 个 sq-mark(写死坐标,不压内容)
return (
<button className={active ? "active" : ""} type="button" onClick={() => navigate(item.page)}>
<Icon size={14} />
<span>{item.label}</span>
{item.badge && <span className="pill-mini">{item.badge}</span>}
</button>
<>
<div className="grid-bg" />
<span className="sq-mark" style={{ top: 238, left: 478 }} />
<span className="sq-mark" style={{ top: 478, left: 1198 }} />
<span className="sq-mark" style={{ bottom: 300, left: 238 }} />
<span className="sq-mark" style={{ top: 718, right: 240 }} />
</>
);
}
export function Decorations() {
// Exact 设计稿只用 grid-bg 做底纹;旧版散点/角标是绝对定位写死坐标,会压在
// topbar 和 page-head 上(重叠 bug),且不在设计稿里。品牌 mono 签名由各页面 inline
// 的 // 注释、[ ALL ] 等承载,这里只保留底纹。
return <div className="grid-bg" />;
export function CornerMarks() {
return (
<>
<CornerMark pos="tl" />
<CornerMark pos="tr" />
<CornerMark pos="bl" />
<CornerMark pos="br" />
</>
);
}
export function ToastLike({ notice }: { notice: NonNullable<Notice> }) {