feat(core): notification inbox infinite scroll + command palette fix (+ pending WIP)
消息中心:全量渲染 → 真·后端分页滚动加载 - backend(ops/views): NotificationPagination(10/页,page_size 可覆盖)+ 响应回 type_counts(按收件人绝对计数,不受分页/搜索影响) - frontend(messages): 自管分页,滚到底加载下一批;tab/搜索走服务端并重置到第1页; 代号作废在途旧请求防切换卡空白;乐观标已读;「已加载 X / Y」分母用当前筛选总数 - api/App/types: listNotifications 支持 page/page_size/search;allNotifications 携带 type_counts 命令面板(侧边栏搜索):修复点开后 UI 错位 - app-shell: 遮罩 className 漏了基类 shell-command-bg(只有 .show)致无定位塌到左下; 补回基类 + header 类名对齐 .shell-command-h - messages-page.css: 工作台收进视口高度,收件箱在面板内滚动 本次提交一并带入此前若干未提交 WIP(account/ai-tools/library/pipeline/products/settings + accounts/ai/assets/billing/projects 后端),按用户要求整体推 dev。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,9 @@ import { api } from "../api";
|
||||
import type { BillingSummary, BillingTrend, Ledger, Project, TeamMember } from "../types";
|
||||
import { money } from "./stage-config";
|
||||
|
||||
const ROLE_LABEL: Record<string, string> = { owner: "超管", admin: "团管", member: "成员", viewer: "访客" };
|
||||
const STATUS_LABEL: Record<string, string> = { active: "活跃", invited: "待激活", disabled: "已停用" };
|
||||
|
||||
type TrendRange = "day" | "week" | "month";
|
||||
const RANGE_META: Record<TrendRange, { chip: string; sub: string; totalLabel: string; avgLabel: string }> = {
|
||||
day: { chip: "日", sub: "// 近 14 天 · 单位 ¥", totalLabel: "14 天合计", avgLabel: "日均" },
|
||||
@@ -12,6 +15,33 @@ const RANGE_META: Record<TrendRange, { chip: string; sub: string; totalLabel: st
|
||||
|
||||
type Tab = "overview" | "by-project" | "by-member" | "bills";
|
||||
|
||||
// 账单类型 / 详情 中文化(后端历史英文流水也一并映射;未知值原样透出)
|
||||
const LEDGER_TYPE_LABEL: Record<string, string> = {
|
||||
recharge: "充值", reserve: "预扣", release: "释放", charge: "扣费", adjustment: "调整", refund: "退款"
|
||||
};
|
||||
const LEDGER_REASON_LABEL: Record<string, string> = {
|
||||
"reserve ai task credit": "AI 任务预扣额度",
|
||||
"charge ai task credit": "AI 任务扣费",
|
||||
"release reserved credit": "释放预留额度",
|
||||
"release unused reserved credit": "释放未用预留额度",
|
||||
"release unused credit": "释放未用额度"
|
||||
};
|
||||
const ledgerTypeLabel = (t: string) => LEDGER_TYPE_LABEL[t] ?? t;
|
||||
const ledgerReasonLabel = (r: string) => LEDGER_REASON_LABEL[r] ?? r;
|
||||
|
||||
// 分页页码窗口:页数多时折叠成 1 … 当前±1 … 末页(≤7 页则全展开)
|
||||
function pageWindow(current: number, total: number): Array<number | "ellipsis"> {
|
||||
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
|
||||
const items: Array<number | "ellipsis"> = [1];
|
||||
const start = Math.max(2, current - 1);
|
||||
const end = Math.min(total - 1, current + 1);
|
||||
if (start > 2) items.push("ellipsis");
|
||||
for (let p = start; p <= end; p++) items.push(p);
|
||||
if (end < total - 1) items.push("ellipsis");
|
||||
items.push(total);
|
||||
return items;
|
||||
}
|
||||
|
||||
const RECHARGE: Array<{ amt: number; gift: string; bonus: boolean; bonusAmt: number; ribbon?: string }> = [
|
||||
{ amt: 100, gift: "无赠送", bonus: false, bonusAmt: 0 },
|
||||
{ amt: 500, gift: "+ ¥30 赠送", bonus: true, bonusAmt: 30, ribbon: "推荐" },
|
||||
@@ -38,6 +68,23 @@ export function AccountPage({ billing, ledgers, trend, projects, teamMembers, on
|
||||
const [recharge, setRecharge] = useState(500);
|
||||
const [customAmt, setCustomAmt] = useState("");
|
||||
|
||||
// 账单流水分页:服务端分页(总数随流水增长,不再写死 100),每页 10 条
|
||||
const BILLS_PER_PAGE = 10;
|
||||
const [billPage, setBillPage] = useState(1);
|
||||
const [ledgerRows, setLedgerRows] = useState<Ledger[]>(ledgers);
|
||||
const [ledgerCount, setLedgerCount] = useState<number>(ledgers.length);
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
api.ledgers(billPage, BILLS_PER_PAGE).then((data) => {
|
||||
if (!alive) return;
|
||||
setLedgerRows(data.results);
|
||||
setLedgerCount(data.count);
|
||||
}).catch(() => {});
|
||||
return () => { alive = false; };
|
||||
}, [billPage]);
|
||||
const billTotalPages = Math.max(1, Math.ceil(ledgerCount / BILLS_PER_PAGE));
|
||||
const safeBillPage = Math.min(billPage, billTotalPages);
|
||||
|
||||
const selectedCard = RECHARGE.find((item) => item.amt === recharge);
|
||||
const effectiveAmount = Number(customAmt) > 0 ? Number(customAmt) : recharge;
|
||||
const effectiveBonus = Number(customAmt) > 0 ? 0 : selectedCard?.bonusAmt || 0;
|
||||
@@ -158,7 +205,7 @@ export function AccountPage({ billing, ledgers, trend, projects, teamMembers, on
|
||||
<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>
|
||||
<button className={`tab ${tab === "bills" ? "active" : ""}`} type="button" onClick={() => setTab("bills")}>账单流水 <span className="count">{ledgerCount}</span></button>
|
||||
</div>
|
||||
|
||||
<div className={`tab-panel ${tab === "overview" ? "active" : ""}`}>
|
||||
@@ -238,18 +285,34 @@ export function AccountPage({ billing, ledgers, trend, projects, teamMembers, on
|
||||
<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) => (
|
||||
{ledgerRows.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>{ledgerTypeLabel(l.ledger_type)}</td>
|
||||
<td className="muted">{ledgerReasonLabel(l.reason)}</td>
|
||||
<td>{l.user_label
|
||||
? <span className="who"><span className="av">{l.user_label.slice(0, 1).toUpperCase()}</span>{l.user_label}</span>
|
||||
: <span className="sys">系统</span>}</td>
|
||||
<td><span className="status-tag ok">成功</span></td>
|
||||
<td className="neg">{l.amount}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{ledgerCount > BILLS_PER_PAGE && (
|
||||
<div className="bill-pager">
|
||||
<span className="total">// 共 {ledgerCount} 条 · 第 {safeBillPage} / {billTotalPages} 页</span>
|
||||
<div className="pages">
|
||||
<button type="button" disabled={safeBillPage <= 1} onClick={() => setBillPage(safeBillPage - 1)}>上一页</button>
|
||||
{pageWindow(safeBillPage, billTotalPages).map((p, i) => (
|
||||
p === "ellipsis"
|
||||
? <span key={`e${i}`} className="ellipsis">…</span>
|
||||
: <button key={p} className={p === safeBillPage ? "active" : ""} type="button" onClick={() => setBillPage(p)}>{p}</button>
|
||||
))}
|
||||
<button type="button" disabled={safeBillPage >= billTotalPages} onClick={() => setBillPage(safeBillPage + 1)}>下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`tab-panel ${tab === "by-project" ? "active" : ""}`}>
|
||||
@@ -270,9 +333,12 @@ export function AccountPage({ billing, ledgers, trend, projects, teamMembers, on
|
||||
<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>
|
||||
))}
|
||||
{teamMembers.map((m) => {
|
||||
const monthly = Number(m.monthly_credit_limit || 0);
|
||||
return (
|
||||
<tr key={m.id}><td className="who"><span className="av">{m.user.username.slice(0, 1).toUpperCase()}</span>{m.user.username}</td><td>{ROLE_LABEL[m.role] || m.role}</td><td className="quota"><span className="used">{money(m.month_charged || 0)}</span> <span className="lim">/ {monthly > 0 ? money(monthly) : "不限"}</span></td><td>{STATUS_LABEL[m.status] || m.status}</td></tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user