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,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></>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user