解决账户中心的问题
This commit is contained in:
@@ -13,6 +13,9 @@ class CreditAccountSerializer(serializers.ModelSerializer):
|
|||||||
class CreditLedgerSerializer(serializers.ModelSerializer):
|
class CreditLedgerSerializer(serializers.ModelSerializer):
|
||||||
# 成员展示名:优先真实姓名 → 用户名 → 邮箱;系统流水(无 user)留空
|
# 成员展示名:优先真实姓名 → 用户名 → 邮箱;系统流水(无 user)留空
|
||||||
user_label = serializers.SerializerMethodField()
|
user_label = serializers.SerializerMethodField()
|
||||||
|
# 扣费项目:与任务监控类型列同源,供账户中心流水展示
|
||||||
|
task_type = serializers.SerializerMethodField()
|
||||||
|
project_title = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CreditLedger
|
model = CreditLedger
|
||||||
@@ -21,7 +24,9 @@ class CreditLedgerSerializer(serializers.ModelSerializer):
|
|||||||
"user",
|
"user",
|
||||||
"user_label",
|
"user_label",
|
||||||
"project",
|
"project",
|
||||||
|
"project_title",
|
||||||
"task",
|
"task",
|
||||||
|
"task_type",
|
||||||
"ledger_type",
|
"ledger_type",
|
||||||
"amount",
|
"amount",
|
||||||
"balance_after",
|
"balance_after",
|
||||||
@@ -37,6 +42,16 @@ class CreditLedgerSerializer(serializers.ModelSerializer):
|
|||||||
return ""
|
return ""
|
||||||
return user.first_name or user.username or user.email or ""
|
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 CreditReservationSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -74,6 +74,18 @@ def ledgers(request):
|
|||||||
queryset = queryset.filter(user_id=user_id)
|
queryset = queryset.filter(user_id=user_id)
|
||||||
if ledger_type and ledger_type in CreditLedger.Type.values:
|
if ledger_type and ledger_type in CreditLedger.Type.values:
|
||||||
queryset = queryset.filter(ledger_type=ledger_type)
|
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 条)
|
# 服务端分页:总数随流水增长(原先写死 [:100] 导致永远 100 条)
|
||||||
try:
|
try:
|
||||||
page = max(1, int(request.query_params.get("page", 1)))
|
page = max(1, int(request.query_params.get("page", 1)))
|
||||||
|
|||||||
@@ -228,12 +228,12 @@
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.billing-month-picker .custom-select-shell,
|
.billing-month-picker .rs-select,
|
||||||
.billing-month-picker .custom-select-trigger {
|
.billing-month-picker .rs-select-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.billing-month-picker .custom-select-trigger {
|
.billing-month-picker .rs-select-btn {
|
||||||
height: 36px;
|
height: 36px;
|
||||||
padding: 0 10px 0 12px;
|
padding: 0 10px 0 12px;
|
||||||
border-color: rgba(255, 255, 255, 0.2) !important;
|
border-color: rgba(255, 255, 255, 0.2) !important;
|
||||||
@@ -244,8 +244,8 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.billing-month-picker .custom-select-trigger:hover,
|
.billing-month-picker .rs-select-btn:hover,
|
||||||
.billing-month-picker .custom-select-shell.open .custom-select-trigger {
|
.billing-month-picker .rs-select.open .rs-select-btn {
|
||||||
border-color: rgba(138, 171, 255, 0.72) !important;
|
border-color: rgba(138, 171, 255, 0.72) !important;
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
background: #323743 !important;
|
background: #323743 !important;
|
||||||
@@ -545,12 +545,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.billing-ledger-toolbar .billing-ledger-filter,
|
.billing-ledger-toolbar .billing-ledger-filter,
|
||||||
.billing-ledger-toolbar .custom-select-shell {
|
.billing-ledger-toolbar .rs-select {
|
||||||
width: 136px;
|
width: 136px;
|
||||||
flex: 0 0 136px;
|
flex: 0 0 136px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.billing-ledger-toolbar .custom-select-trigger {
|
.billing-ledger-toolbar .rs-select-btn {
|
||||||
height: 38px;
|
height: 38px;
|
||||||
padding: 0 11px;
|
padding: 0 11px;
|
||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
@@ -869,3 +869,19 @@
|
|||||||
color: #161a22;
|
color: #161a22;
|
||||||
font-weight: 650;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -936,11 +936,12 @@ export const api = {
|
|||||||
billingSummary() {
|
billingSummary() {
|
||||||
return request<BillingSummary>("/api/billing/summary/");
|
return request<BillingSummary>("/api/billing/summary/");
|
||||||
},
|
},
|
||||||
ledgers(page = 1, pageSize = 10, filters?: { ledger_type?: string; user?: string }) {
|
ledgers(page = 1, pageSize = 10, filters?: { ledger_type?: string; user?: string; month?: string }) {
|
||||||
// 类型/成员筛选传参给后端做全量过滤(count/总页数随筛选变化 · R70)
|
// 类型/成员/月份筛选传参给后端做全量过滤(count/总页数随筛选变化)
|
||||||
const q = new URLSearchParams({ page: String(page), page_size: String(pageSize) });
|
const q = new URLSearchParams({ page: String(page), page_size: String(pageSize) });
|
||||||
if (filters?.ledger_type) q.set("ledger_type", filters.ledger_type);
|
if (filters?.ledger_type) q.set("ledger_type", filters.ledger_type);
|
||||||
if (filters?.user) q.set("user", filters.user);
|
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()}`);
|
return request<{ count: number; page: number; page_size: number; results: Ledger[] }>(`/api/billing/ledgers/?${q.toString()}`);
|
||||||
},
|
},
|
||||||
billingTrend(range?: "day" | "week" | "month", month?: string) {
|
billingTrend(range?: "day" | "week" | "month", month?: string) {
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ function FeatureIcon({ name }: { name: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function featureIconForLedger(ledger: Ledger): 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();
|
const text = `${ledger.ledger_type} ${ledger.reason}`.toLowerCase();
|
||||||
if (text.includes("image") || text.includes("图片")) return "image";
|
if (text.includes("image") || text.includes("图片")) return "image";
|
||||||
if (text.includes("digest") || text.includes("提示词") || text.includes("提炼")) return "scan";
|
if (text.includes("digest") || text.includes("提示词") || text.includes("提炼")) return "scan";
|
||||||
@@ -69,6 +74,17 @@ function featureIconForLedger(ledger: Ledger): string {
|
|||||||
return "clapperboard";
|
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) {
|
function formatMonthLabel(ym: string) {
|
||||||
const [y, m] = ym.split("-");
|
const [y, m] = ym.split("-");
|
||||||
return `${y}年${Number(m)}月`;
|
return `${y}年${Number(m)}月`;
|
||||||
@@ -183,13 +199,14 @@ export function AccountPage({ billing, projects: _projects, team, onRecharge: _o
|
|||||||
setLedgersLoading(true);
|
setLedgersLoading(true);
|
||||||
api.ledgers(billPage, BILLS_PER_PAGE, {
|
api.ledgers(billPage, BILLS_PER_PAGE, {
|
||||||
ledger_type: billType === "all" ? undefined : billType,
|
ledger_type: billType === "all" ? undefined : billType,
|
||||||
|
month: ledgerMonth || undefined,
|
||||||
}).then((data) => {
|
}).then((data) => {
|
||||||
if (!alive) return;
|
if (!alive) return;
|
||||||
setLedgerRows(data.results);
|
setLedgerRows(data.results);
|
||||||
setLedgerCount(data.count);
|
setLedgerCount(data.count);
|
||||||
}).catch(() => {}).finally(() => { if (alive) setLedgersLoading(false); });
|
}).catch(() => {}).finally(() => { if (alive) setLedgersLoading(false); });
|
||||||
return () => { alive = false; };
|
return () => { alive = false; };
|
||||||
}, [billPage, billType, reloadFlag]);
|
}, [billPage, billType, ledgerMonth, reloadFlag]);
|
||||||
|
|
||||||
const [billJump, setBillJump] = useState("");
|
const [billJump, setBillJump] = useState("");
|
||||||
|
|
||||||
@@ -253,10 +270,8 @@ export function AccountPage({ billing, projects: _projects, team, onRecharge: _o
|
|||||||
setBillPage(1);
|
setBillPage(1);
|
||||||
}, [featureFilter]);
|
}, [featureFilter]);
|
||||||
|
|
||||||
const visibleLedgers = useMemo(() => {
|
// 月份/类型已在服务端过滤,当前页结果直接展示
|
||||||
if (!ledgerMonth) return ledgerRows;
|
const visibleLedgers = ledgerRows;
|
||||||
return ledgerRows.filter((l) => (l.created_at || "").startsWith(ledgerMonth));
|
|
||||||
}, [ledgerRows, ledgerMonth]);
|
|
||||||
|
|
||||||
const billTotalPages = Math.max(1, Math.ceil(ledgerCount / BILLS_PER_PAGE));
|
const billTotalPages = Math.max(1, Math.ceil(ledgerCount / BILLS_PER_PAGE));
|
||||||
const safeBillPage = Math.min(billPage, billTotalPages);
|
const safeBillPage = Math.min(billPage, billTotalPages);
|
||||||
@@ -266,20 +281,40 @@ export function AccountPage({ billing, projects: _projects, team, onRecharge: _o
|
|||||||
setBillJump("");
|
setBillJump("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportLedgers() {
|
async function exportLedgers() {
|
||||||
if (visibleLedgers.length === 0) {
|
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", "当前没有可导出的流水");
|
onNotify("info", "当前没有可导出的流水");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const header = ["功能", "消耗位置", "成员", "时间", "消耗积分"];
|
const header = ["功能", "消耗位置", "成员", "时间", "消耗积分"];
|
||||||
const lines = visibleLedgers.map((l) => {
|
const lines = all.map((l) => {
|
||||||
const amt = Number(l.amount);
|
const amt = Number(l.amount);
|
||||||
const signed = (amt > 0 && l.ledger_type === "recharge") || l.ledger_type === "release" || l.ledger_type === "refund"
|
const signed = (amt > 0 && l.ledger_type === "recharge") || l.ledger_type === "release" || l.ledger_type === "refund"
|
||||||
? `+${pts(Math.abs(amt))}`
|
? `+${pts(Math.abs(amt))}`
|
||||||
: `-${pts(Math.abs(amt))}`;
|
: `-${pts(Math.abs(amt))}`;
|
||||||
return [
|
return [
|
||||||
ledgerTypeLabel(l.ledger_type),
|
ledgerFeatureLabel(l),
|
||||||
ledgerReasonLabel(l.reason).replace(/,/g, ","),
|
ledgerLocationLabel(l).replace(/,/g, ","),
|
||||||
l.user_label || "系统",
|
l.user_label || "系统",
|
||||||
formatLedgerTime(l.created_at),
|
formatLedgerTime(l.created_at),
|
||||||
signed,
|
signed,
|
||||||
@@ -292,7 +327,7 @@ export function AccountPage({ billing, projects: _projects, team, onRecharge: _o
|
|||||||
a.download = `积分消耗流水-${ledgerMonth || "all"}.csv`;
|
a.download = `积分消耗流水-${ledgerMonth || "all"}.csv`;
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
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);
|
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); }}
|
onChange={(v) => { setLedgerMonth(v); setBillPage(1); }}
|
||||||
options={months.map((m) => ({ value: m.value, label: m.label }))}
|
options={months.map((m) => ({ value: m.value, label: m.label }))}
|
||||||
/>
|
/>
|
||||||
<button className="compact-action billing-ledger-export" type="button" onClick={exportLedgers}>
|
<button className="compact-action billing-ledger-export" type="button" onClick={() => void exportLedgers()}>
|
||||||
<Download size={16} strokeWidth={2} />
|
<Download size={16} strokeWidth={2} />
|
||||||
导出流水
|
导出流水
|
||||||
</button>
|
</button>
|
||||||
@@ -491,11 +526,11 @@ export function AccountPage({ billing, projects: _projects, team, onRecharge: _o
|
|||||||
<div className="billing-ledger-row" key={l.id} data-ledger-month={(l.created_at || "").slice(0, 7)} data-ledger-feature={l.ledger_type}>
|
<div className="billing-ledger-row" key={l.id} data-ledger-month={(l.created_at || "").slice(0, 7)} data-ledger-feature={l.ledger_type}>
|
||||||
<div className="billing-ledger-feature">
|
<div className="billing-ledger-feature">
|
||||||
<span className="billing-ledger-icon"><FeatureIcon name={featureIconForLedger(l)} /></span>
|
<span className="billing-ledger-icon"><FeatureIcon name={featureIconForLedger(l)} /></span>
|
||||||
<strong>{ledgerTypeLabel(l.ledger_type)}</strong>
|
<strong title={ledgerFeatureLabel(l)}>{ledgerFeatureLabel(l)}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="billing-ledger-copy">
|
<div className="billing-ledger-copy">
|
||||||
<strong title={ledgerReasonLabel(l.reason)}>{ledgerReasonLabel(l.reason)}</strong>
|
<strong title={ledgerLocationLabel(l)}>{ledgerLocationLabel(l)}</strong>
|
||||||
<span>{l.ledger_type === "recharge" ? "账户充值" : "积分变动 · 可追溯流水"}</span>
|
<span>{ledgerTypeLabel(l.ledger_type)}{l.reason ? ` · ${ledgerReasonLabel(l.reason)}` : ""}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="billing-ledger-meta">
|
<div className="billing-ledger-meta">
|
||||||
<strong>{l.user_label || "系统"}</strong>
|
<strong>{l.user_label || "系统"}</strong>
|
||||||
|
|||||||
@@ -554,6 +554,10 @@ export type Ledger = {
|
|||||||
reason: string;
|
reason: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
user_label?: string;
|
user_label?: string;
|
||||||
|
task?: string | null;
|
||||||
|
task_type?: string;
|
||||||
|
project?: string | null;
|
||||||
|
project_title?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserPreference = {
|
export type UserPreference = {
|
||||||
|
|||||||
Reference in New Issue
Block a user