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:
@@ -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> }) {
|
||||
|
||||
Reference in New Issue
Block a user