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
+206 -17
View File
@@ -1,7 +1,22 @@
import { useState } from "react";
import { CreditCard } from "lucide-react";
import type { BillingSummary, Ledger, Project, TeamMember } from "../types";
import { money, stageMeta, statusPill } from "./stage-config";
import { money } from "./stage-config";
type Tab = "overview" | "by-project" | "by-member" | "bills";
const RECHARGE: Array<{ amt: number; gift: string; bonus: boolean; ribbon?: string }> = [
{ amt: 100, gift: "无赠送", bonus: false },
{ amt: 500, gift: "+ ¥30 赠送", bonus: true, ribbon: "推荐" },
{ amt: 1000, gift: "+ ¥80 赠送", bonus: true },
{ amt: 3000, gift: "+ ¥300 赠送", bonus: true }
];
const STAGES: Array<{ k: string; color: string }> = [
{ k: "视频片段(Seedance)", color: "var(--heat)" },
{ k: "故事板(image-2)", color: "var(--accent-forest)" },
{ k: "基础资产", color: "var(--black-alpha-56)" },
{ k: "脚本 LLM", color: "var(--black-alpha-32)" }
];
export function AccountPage({ billing, ledgers, projects, teamMembers }: {
billing: BillingSummary | null;
@@ -9,21 +24,195 @@ export function AccountPage({ billing, ledgers, projects, teamMembers }: {
projects: Project[];
teamMembers: TeamMember[];
}) {
const [tab, setTab] = useState<"overview" | "projects" | "members" | "bills">("overview");
const [amount, setAmount] = useState(500);
const [tab, setTab] = useState<Tab>("overview");
const [recharge, setRecharge] = useState(500);
const balance = Number(billing?.account.balance || 0);
const used = Number(billing?.charged_total || 0);
const memberLimit = teamMembers.reduce((sum, m) => sum + Math.max(0, Number(m.monthly_credit_limit || 0)), 0);
const limit = memberLimit || balance;
const left = Math.max(0, limit - used);
const pct = limit > 0 ? Math.min(100, (used / limit) * 100) : 0;
return (
<>
<div className="page-head"><div><h1></h1><div className="sub"><span className="mono">// 余额 · 充值 · 4 维消费视图 + 账单流水</span></div></div></div>
<div className="top-grid"><div className="balance-banner"><span className="corner-tr">+</span><span className="corner-bl">+</span><div className="balance-hero"><div className="lbl"></div><div className="v">{money(billing?.account.balance)}</div><div className="meta">// 冻结 {money(billing?.account.reserved_balance)} · 已消费 {money(billing?.charged_total)}</div></div></div><div className="pane topup-pane"><div className="topup-head"><div><h3>快速充值</h3><div className="desc">// 充值后立刻到账,可开发票</div></div><div className="topup-selected">已选 ¥{amount}</div></div><div className="recharge-row">{[100, 500, 1000, 3000].map((item) => <button className={`recharge-card ${amount === item ? "selected" : ""}`} type="button" key={item} onClick={() => setAmount(item)}><div className="amt">¥{item}</div><div className="gift">+ 赠送</div></button>)}</div><button className="btn pay-method-btn" type="button"><CreditCard size={13} />微信支付</button></div></div>
<div className="billing-tabs">{[["overview", "总览"], ["projects", "项目"], ["members", "成员"], ["bills", "账单流水"]].map(([key, label]) => <button className={`tab ${tab === key ? "active" : ""}`} type="button" key={key} onClick={() => setTab(key as typeof tab)}>{label}</button>)}</div>
{tab === "overview" && <div className="overview-grid"><div className="pane trend-pane"><h3></h3><div className="bars">{Array.from({ length: 14 }).map((_, index) => <span key={index} style={{ height: `${18 + (index % 6) * 10}%` }} />)}</div></div><div className="pane stage-pane"><h3></h3><UsageLine label="视频片段" value="¥98.40" width="60%" color="var(--orange)" /><UsageLine label="故事板" value="¥36.00" width="22%" color="var(--green)" /></div></div>}
{tab === "projects" && <table className="billing-table"><thead><tr><th></th><th></th><th></th><th></th></tr></thead><tbody>{projects.map((project) => <tr key={project.id}><td>{project.name}</td><td>{stageMeta[project.current_stage]?.label || project.current_stage}</td><td><span className={`pill ${statusPill(project.status)}`}><span className="dot" />{project.status}</span></td><td>¥0.00</td></tr>)}</tbody></table>}
{tab === "members" && <table className="billing-table"><thead><tr><th></th><th></th><th></th><th></th></tr></thead><tbody>{teamMembers.map((member) => <tr key={member.id}><td>{member.user.username}</td><td>{member.role}</td><td>{money(member.monthly_credit_limit)}</td><td><span className={`pill ${statusPill(member.status)}`}><span className="dot" />{member.status}</span></td></tr>)}</tbody></table>}
{tab === "bills" && <table className="billing-table bills"><thead><tr><th></th><th></th><th></th><th></th></tr></thead><tbody>{ledgers.map((item) => <tr key={item.id}><td>{new Date(item.created_at).toLocaleString("zh-CN")}</td><td>{item.ledger_type}</td><td>{item.reason}</td><td>{item.amount}</td></tr>)}</tbody></table>}
</>
<section className="account-page">
<div className="page-head">
<div>
<h1></h1>
<div className="sub"><span className="mono">// 余额 · 充值 · 4 维消费视图 + 账单流水</span></div>
</div>
</div>
<div className="top-grid">
<div className="balance-banner">
<span className="corner-tr" aria-hidden="true"></span><span className="corner-bl" aria-hidden="true"></span>
<div className="balance-hero">
<div className="lbl"></div>
<div className="v">{money(balance)}</div>
<div className="meta">// 充值累加 · 不重置</div>
</div>
<div className="balance-sub">
<div className="col">
<div className="lbl"></div>
<div className="v">{money(limit)}</div>
<div className="meta">// 按自然月重置</div>
</div>
<div className="col">
<div className="lbl"></div>
<div className="v">{money(used)}</div>
<div className="meta">// 占比 {pct.toFixed(1)}% · {pct >= 80 ? "注意" : "健康"}</div>
</div>
</div>
<div className="balance-foot">
<div className="balance-meter" aria-label={`本月额度使用率 ${pct.toFixed(1)}%`}><span style={{ width: `${pct}%` }} /></div>
<div className="balance-foot-meta">
<span> {money(left)}</span>
<span>使 {pct.toFixed(1)}%</span>
</div>
</div>
</div>
<div className="pane topup-pane">
<div className="topup-head">
<div>
<h3></h3>
<div className="desc">// 充值后立刻到账,可开发票 · 仅超管可操作</div>
</div>
<div className="topup-selected"> ¥{recharge}</div>
</div>
<div className="recharge-row">
{RECHARGE.map((item) => (
<div
key={item.amt}
className={`recharge-card ${recharge === item.amt ? "selected" : ""}`}
role="button"
tabIndex={0}
aria-pressed={recharge === item.amt}
onClick={() => setRecharge(item.amt)}
>
{item.ribbon && <span className="ribbon">{item.ribbon}</span>}
<div className="amt">¥{item.amt}</div>
<div className={`gift ${item.bonus ? "bonus" : ""}`}>{item.gift}</div>
</div>
))}
</div>
<div className="pay-row">
<div className="pay-title"></div>
<input className="input" placeholder="最低 ¥50,可输入任意金额" />
<div className="pay-btn-row">
<button className="btn pay-method-btn pay-wechat" type="button" aria-label="微信支付">
<span className="pay-logo" aria-hidden="true"><img src="/assets/pay-wechat.png" alt="" /></span>
</button>
<button className="btn pay-method-btn pay-alipay" type="button" aria-label="支付宝">
<span className="pay-logo" aria-hidden="true"><img src="/assets/pay-alipay.png" alt="" /></span>
</button>
</div>
</div>
</div>
</div>
<div className="billing-tabs" role="tablist">
<button className={`tab ${tab === "overview" ? "active" : ""}`} type="button" onClick={() => setTab("overview")}></button>
<button className={`tab ${tab === "by-project" ? "active" : ""}`} type="button" onClick={() => setTab("by-project")}> <span className="count">{projects.length}</span></button>
<button className={`tab ${tab === "by-member" ? "active" : ""}`} type="button" onClick={() => setTab("by-member")}> <span className="count">{teamMembers.length}</span></button>
<button className={`tab ${tab === "bills" ? "active" : ""}`} type="button" onClick={() => setTab("bills")}> <span className="count">{ledgers.length}</span></button>
</div>
<div className={`tab-panel ${tab === "overview" ? "active" : ""}`}>
<div className="overview-grid">
<div className="pane trend-pane">
<div className="trend-head">
<h3></h3>
<span className="sub">// 近 14 天 · 单位 ¥</span>
<span className="spacer"></span>
<button className="chip active" type="button"></button>
<button className="chip" type="button"></button>
<button className="chip" type="button"></button>
</div>
<div className="trend-chart">
<div className="bars"></div>
<div className="x-axis"></div>
</div>
<div className="trend-foot">
<div className="item"><span className="k">14 </span><span className="v">{money(used)}</span></div>
<div className="item"><span className="k"></span><span className="v">{money(used / 14)}</span></div>
<div className="item"><span className="k"></span><span className="v warn">{money(used)}</span></div>
</div>
</div>
<div className="pane stage-pane">
<h3></h3>
<div className="desc">// PRD §5.3.5 扣费规则 · 仅确认后扣</div>
{STAGES.map((s) => (
<div key={s.k}>
<div className="usage-line"><span className="k">{s.k}</span><span className="v">{money(0)}</span></div>
<div className="usage-bar"><span style={{ width: "0%", background: s.color }} /></div>
</div>
))}
<div className="total"><span></span><span className="v">{money(used)}</span></div>
</div>
</div>
<div className="pane rule-pane" style={{ marginTop: 16 }}>
<h3> + </h3>
<div className="desc">// PRD §5.3.5 + §10.3 · 对接团队请以此页为准</div>
<div className="rule-list">
<strong> </strong>: / / <br />
<strong> </strong>:,<br />
<strong> <span className="mono-acc">[ ]</span> </strong><br />
<strong> </strong>, token
</div>
<div className="quota-rules">
<div className="qr-head">// 任务确认前 · 四层额度预检(任一不通过即拦截)</div>
<div className="step"><span className="num">1</span><span><strong></strong> × <span className="formula">1.2</span></span></div>
<div className="step"><span className="num">2</span><span><strong></strong> </span></div>
<div className="step"><span className="num">3</span><span><strong></strong> </span></div>
<div className="step"><span className="num">4</span><span><strong></strong> </span></div>
</div>
</div>
</div>
<div className={`tab-panel ${tab === "bills" ? "active" : ""}`}>
<table className="billing-table">
<thead><tr><th></th><th> / </th><th></th><th></th><th></th><th style={{ textAlign: "right" }}></th></tr></thead>
<tbody>
{ledgers.map((l) => (
<tr key={l.id}>
<td className="ts">{new Date(l.created_at).toLocaleString("zh-CN")}</td>
<td>{l.ledger_type}</td>
<td className="muted">{l.reason}</td>
<td></td>
<td><span className="status-tag ok">OK</span></td>
<td className="neg">{l.amount}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className={`tab-panel ${tab === "by-project" ? "active" : ""}`}>
<table className="billing-table">
<thead><tr><th></th><th></th><th></th><th style={{ textAlign: "right" }}></th></tr></thead>
<tbody>
{projects.map((p) => (
<tr key={p.id}><td>{p.name}</td><td>{p.current_stage}</td><td>{p.status}</td><td className="zero">{money(0)}</td></tr>
))}
</tbody>
</table>
</div>
<div className={`tab-panel ${tab === "by-member" ? "active" : ""}`}>
<table className="billing-table">
<thead><tr><th></th><th></th><th> / </th><th></th></tr></thead>
<tbody>
{teamMembers.map((m) => (
<tr key={m.id}><td className="who"><span className="av">{m.user.username.slice(0, 1).toUpperCase()}</span>{m.user.username}</td><td>{m.role}</td><td className="zero">{money(m.monthly_credit_limit)}</td><td>{m.status}</td></tr>
))}
</tbody>
</table>
</div>
</section>
);
}
export function UsageLine({ label, value, width, color }: { label: string; value: string; width: string; color: string }) {
return <><div className="usage-line"><span>{label}</span><span className="v">{value}</span></div><div className="usage-bar"><span style={{ width, background: color }} /></div></>;
}