diff --git a/core/backend/apps/billing/serializers.py b/core/backend/apps/billing/serializers.py index d26f56e..bc869a9 100644 --- a/core/backend/apps/billing/serializers.py +++ b/core/backend/apps/billing/serializers.py @@ -13,6 +13,9 @@ class CreditAccountSerializer(serializers.ModelSerializer): class CreditLedgerSerializer(serializers.ModelSerializer): # 成员展示名:优先真实姓名 → 用户名 → 邮箱;系统流水(无 user)留空 user_label = serializers.SerializerMethodField() + # 扣费项目:与任务监控类型列同源,供账户中心流水展示 + task_type = serializers.SerializerMethodField() + project_title = serializers.SerializerMethodField() class Meta: model = CreditLedger @@ -21,7 +24,9 @@ class CreditLedgerSerializer(serializers.ModelSerializer): "user", "user_label", "project", + "project_title", "task", + "task_type", "ledger_type", "amount", "balance_after", @@ -37,6 +42,16 @@ class CreditLedgerSerializer(serializers.ModelSerializer): return "" return user.first_name or user.username or user.email or "" + def get_task_type(self, obj): + task = getattr(obj, "task", None) + return getattr(task, "task_type", "") or "" + + def get_project_title(self, obj): + project = getattr(obj, "project", None) + if project is None: + return "" + return getattr(project, "title", None) or getattr(project, "name", None) or "" + class CreditReservationSerializer(serializers.ModelSerializer): class Meta: diff --git a/core/backend/apps/billing/views.py b/core/backend/apps/billing/views.py index 0264107..bc884ec 100644 --- a/core/backend/apps/billing/views.py +++ b/core/backend/apps/billing/views.py @@ -74,6 +74,18 @@ def ledgers(request): queryset = queryset.filter(user_id=user_id) if ledger_type and ledger_type in CreditLedger.Type.values: queryset = queryset.filter(ledger_type=ledger_type) + # 按自然月过滤(账户页流水月份下拉);count/分页随月份变化 + month_param = (request.query_params.get("month") or "").strip() + if len(month_param) == 7 and month_param[4] == "-": + try: + y = int(month_param[:4]) + m = int(month_param[5:7]) + if 1 <= m <= 12: + month_start = date(y, m, 1) + month_end = date(y, m, calendar.monthrange(y, m)[1]) + queryset = queryset.filter(created_at__date__gte=month_start, created_at__date__lte=month_end) + except ValueError: + pass # 服务端分页:总数随流水增长(原先写死 [:100] 导致永远 100 条) try: page = max(1, int(request.query_params.get("page", 1))) diff --git a/core/frontend/src/account-page.css b/core/frontend/src/account-page.css index e867cbc..3ba22e0 100644 --- a/core/frontend/src/account-page.css +++ b/core/frontend/src/account-page.css @@ -228,12 +228,12 @@ flex: 0 0 auto; } - .billing-month-picker .custom-select-shell, - .billing-month-picker .custom-select-trigger { + .billing-month-picker .rs-select, + .billing-month-picker .rs-select-btn { width: 100%; } - .billing-month-picker .custom-select-trigger { + .billing-month-picker .rs-select-btn { height: 36px; padding: 0 10px 0 12px; border-color: rgba(255, 255, 255, 0.2) !important; @@ -244,8 +244,8 @@ font-weight: 600; } - .billing-month-picker .custom-select-trigger:hover, - .billing-month-picker .custom-select-shell.open .custom-select-trigger { + .billing-month-picker .rs-select-btn:hover, + .billing-month-picker .rs-select.open .rs-select-btn { border-color: rgba(138, 171, 255, 0.72) !important; color: #fff !important; background: #323743 !important; @@ -545,12 +545,12 @@ } .billing-ledger-toolbar .billing-ledger-filter, - .billing-ledger-toolbar .custom-select-shell { + .billing-ledger-toolbar .rs-select { width: 136px; flex: 0 0 136px; } - .billing-ledger-toolbar .custom-select-trigger { + .billing-ledger-toolbar .rs-select-btn { height: 38px; padding: 0 11px; border-radius: 9px; @@ -869,3 +869,19 @@ color: #161a22; font-weight: 650; } + +/* 月份下拉选中态:门户菜单在 body,用全局覆盖热区里误显成黑色的选中项 */ +body .rs-select-menu-portal .rs-select-option.selected { + background: rgba(0, 47, 167, 0.08) !important; + color: #002fa7 !important; + font-weight: 600 !important; +} +body .rs-select-menu-portal .rs-select-option.selected .rs-select-option-label { + color: #002fa7 !important; +} +.billing-month-picker .rs-select-btn .rs-select-label { + color: #fff !important; +} +.billing-month-picker .rs-select-btn svg { + color: rgba(255, 255, 255, 0.72); +} diff --git a/core/frontend/src/api.ts b/core/frontend/src/api.ts index 92eade8..df12bf2 100644 --- a/core/frontend/src/api.ts +++ b/core/frontend/src/api.ts @@ -936,11 +936,12 @@ export const api = { billingSummary() { return request("/api/billing/summary/"); }, - ledgers(page = 1, pageSize = 10, filters?: { ledger_type?: string; user?: string }) { - // 类型/成员筛选传参给后端做全量过滤(count/总页数随筛选变化 · R70) + ledgers(page = 1, pageSize = 10, filters?: { ledger_type?: string; user?: string; month?: string }) { + // 类型/成员/月份筛选传参给后端做全量过滤(count/总页数随筛选变化) const q = new URLSearchParams({ page: String(page), page_size: String(pageSize) }); if (filters?.ledger_type) q.set("ledger_type", filters.ledger_type); if (filters?.user) q.set("user", filters.user); + if (filters?.month) q.set("month", filters.month); return request<{ count: number; page: number; page_size: number; results: Ledger[] }>(`/api/billing/ledgers/?${q.toString()}`); }, billingTrend(range?: "day" | "week" | "month", month?: string) { diff --git a/core/frontend/src/routes/account.tsx b/core/frontend/src/routes/account.tsx index 0a569c7..213bb83 100644 --- a/core/frontend/src/routes/account.tsx +++ b/core/frontend/src/routes/account.tsx @@ -60,6 +60,11 @@ function FeatureIcon({ name }: { name: string }) { } function featureIconForLedger(ledger: Ledger): string { + const tt = (ledger.task_type || "").toLowerCase(); + if (tt.includes("image") || tt.includes("triview") || tt.includes("product_image") || tt.includes("person_image") || tt.includes("scene_image")) return "image"; + if (tt.includes("digest") || tt.includes("video_digest")) return "scan"; + if (tt.includes("free_video") || tt.includes("film") || tt.includes("video_segment") || tt.includes("voiceover") || tt.includes("export")) return "film"; + if (tt.includes("storyboard")) return "clapperboard"; const text = `${ledger.ledger_type} ${ledger.reason}`.toLowerCase(); if (text.includes("image") || text.includes("图片")) return "image"; if (text.includes("digest") || text.includes("提示词") || text.includes("提炼")) return "scan"; @@ -69,6 +74,17 @@ function featureIconForLedger(ledger: Ledger): string { return "clapperboard"; } +function ledgerFeatureLabel(ledger: Ledger): string { + if (ledger.task_type) return taskTypeLabel(ledger.task_type); + return ledgerTypeLabel(ledger.ledger_type); +} + +function ledgerLocationLabel(ledger: Ledger): string { + if (ledger.project_title) return ledger.project_title; + if (ledger.ledger_type === "recharge") return "账户充值"; + return ledgerReasonLabel(ledger.reason); +} + function formatMonthLabel(ym: string) { const [y, m] = ym.split("-"); return `${y}年${Number(m)}月`; @@ -183,13 +199,14 @@ export function AccountPage({ billing, projects: _projects, team, onRecharge: _o setLedgersLoading(true); api.ledgers(billPage, BILLS_PER_PAGE, { ledger_type: billType === "all" ? undefined : billType, + month: ledgerMonth || undefined, }).then((data) => { if (!alive) return; setLedgerRows(data.results); setLedgerCount(data.count); }).catch(() => {}).finally(() => { if (alive) setLedgersLoading(false); }); return () => { alive = false; }; - }, [billPage, billType, reloadFlag]); + }, [billPage, billType, ledgerMonth, reloadFlag]); const [billJump, setBillJump] = useState(""); @@ -253,10 +270,8 @@ export function AccountPage({ billing, projects: _projects, team, onRecharge: _o setBillPage(1); }, [featureFilter]); - const visibleLedgers = useMemo(() => { - if (!ledgerMonth) return ledgerRows; - return ledgerRows.filter((l) => (l.created_at || "").startsWith(ledgerMonth)); - }, [ledgerRows, ledgerMonth]); + // 月份/类型已在服务端过滤,当前页结果直接展示 + const visibleLedgers = ledgerRows; const billTotalPages = Math.max(1, Math.ceil(ledgerCount / BILLS_PER_PAGE)); const safeBillPage = Math.min(billPage, billTotalPages); @@ -266,20 +281,40 @@ export function AccountPage({ billing, projects: _projects, team, onRecharge: _o setBillJump(""); } - function exportLedgers() { - if (visibleLedgers.length === 0) { + async function exportLedgers() { + const pageSize = 100; + const all: Ledger[] = []; + let page = 1; + let total = Infinity; + try { + while (all.length < total) { + const data = await api.ledgers(page, pageSize, { + ledger_type: billType === "all" ? undefined : billType, + month: ledgerMonth || undefined, + }); + total = data.count; + all.push(...data.results); + if (data.results.length === 0) break; + page += 1; + if (page > 500) break; // 安全上限 + } + } catch { + onNotify("error", "导出失败,请稍后重试"); + return; + } + if (all.length === 0) { onNotify("info", "当前没有可导出的流水"); return; } const header = ["功能", "消耗位置", "成员", "时间", "消耗积分"]; - const lines = visibleLedgers.map((l) => { + const lines = all.map((l) => { const amt = Number(l.amount); const signed = (amt > 0 && l.ledger_type === "recharge") || l.ledger_type === "release" || l.ledger_type === "refund" ? `+${pts(Math.abs(amt))}` : `-${pts(Math.abs(amt))}`; return [ - ledgerTypeLabel(l.ledger_type), - ledgerReasonLabel(l.reason).replace(/,/g, ","), + ledgerFeatureLabel(l), + ledgerLocationLabel(l).replace(/,/g, ","), l.user_label || "系统", formatLedgerTime(l.created_at), signed, @@ -292,7 +327,7 @@ export function AccountPage({ billing, projects: _projects, team, onRecharge: _o a.download = `积分消耗流水-${ledgerMonth || "all"}.csv`; a.click(); URL.revokeObjectURL(url); - onNotify("success", "已导出积分消耗流水"); + onNotify("success", `已导出 ${all.length} 条积分消耗流水`); } const monthUsageDisplay = trend?.month_charged != null ? Number(trend.month_charged) : (selectedMonth === months[0]?.value ? used : null); @@ -456,7 +491,7 @@ export function AccountPage({ billing, projects: _projects, team, onRecharge: _o onChange={(v) => { setLedgerMonth(v); setBillPage(1); }} options={months.map((m) => ({ value: m.value, label: m.label }))} /> - @@ -491,11 +526,11 @@ export function AccountPage({ billing, projects: _projects, team, onRecharge: _o
- {ledgerTypeLabel(l.ledger_type)} + {ledgerFeatureLabel(l)}
- {ledgerReasonLabel(l.reason)} - {l.ledger_type === "recharge" ? "账户充值" : "积分变动 · 可追溯流水"} + {ledgerLocationLabel(l)} + {ledgerTypeLabel(l.ledger_type)}{l.reason ? ` · ${ledgerReasonLabel(l.reason)}` : ""}
{l.user_label || "系统"} diff --git a/core/frontend/src/types.ts b/core/frontend/src/types.ts index 781305e..52f205e 100644 --- a/core/frontend/src/types.ts +++ b/core/frontend/src/types.ts @@ -554,6 +554,10 @@ export type Ledger = { reason: string; created_at: string; user_label?: string; + task?: string | null; + task_type?: string; + project?: string | null; + project_title?: string; }; export type UserPreference = {