添加账户中心
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
from datetime import timedelta
|
import calendar
|
||||||
|
from datetime import date, timedelta
|
||||||
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
@@ -34,6 +35,7 @@ _STAGE_BUCKET = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
def summary(request):
|
def summary(request):
|
||||||
@@ -229,17 +231,41 @@ def trend(request):
|
|||||||
peak = max((Decimal(s["amount"]) for s in series), default=Decimal("0"))
|
peak = max((Decimal(s["amount"]) for s in series), default=Decimal("0"))
|
||||||
avg = (total_14d / len(series)).quantize(Decimal("0.0001")) if series else Decimal("0")
|
avg = (total_14d / len(series)).quantize(Decimal("0.0001")) if series else Decimal("0")
|
||||||
|
|
||||||
# 本月按阶段分布(task.task_type → 4 桶)
|
# 按任务类型分布(与后台「任务监控」类型列同源);?month=YYYY-MM 指定月份,默认当月
|
||||||
month_start = today.replace(day=1)
|
# 有扣费的类型全部返回,前端按金额降序全部展示,不截断
|
||||||
# 只用 task.task_type/project,defer 掉 task 的大 blob(单条可达 3MB+ base64 图),别把几十 MB 拉回
|
month_param = (request.query_params.get("month") or "").strip()
|
||||||
month_charges = charges.filter(created_at__date__gte=month_start).select_related("task").defer("task__request_payload", "task__response_payload")
|
dist_year, dist_month = today.year, today.month
|
||||||
|
if len(month_param) == 7 and month_param[4] == "-":
|
||||||
|
try:
|
||||||
|
dist_year = int(month_param[:4])
|
||||||
|
dist_month = int(month_param[5:7])
|
||||||
|
if not (1 <= dist_month <= 12):
|
||||||
|
raise ValueError("bad month")
|
||||||
|
except ValueError:
|
||||||
|
dist_year, dist_month = today.year, today.month
|
||||||
|
month_param = f"{dist_year:04d}-{dist_month:02d}"
|
||||||
|
else:
|
||||||
|
month_param = f"{dist_year:04d}-{dist_month:02d}"
|
||||||
|
month_start = date(dist_year, dist_month, 1)
|
||||||
|
last_day = calendar.monthrange(dist_year, dist_month)[1]
|
||||||
|
month_end = date(dist_year, dist_month, last_day)
|
||||||
|
month_charges = (
|
||||||
|
charges.filter(created_at__date__gte=month_start, created_at__date__lte=month_end)
|
||||||
|
.select_related("task")
|
||||||
|
.defer("task__request_payload", "task__response_payload")
|
||||||
|
)
|
||||||
by_stage = {"script": Decimal("0"), "base": Decimal("0"), "storyboard": Decimal("0"), "video": Decimal("0")}
|
by_stage = {"script": Decimal("0"), "base": Decimal("0"), "storyboard": Decimal("0"), "video": Decimal("0")}
|
||||||
|
by_task_type: dict[str, Decimal] = {}
|
||||||
project_amounts: dict[str, Decimal] = {}
|
project_amounts: dict[str, Decimal] = {}
|
||||||
|
month_charged = Decimal("0")
|
||||||
for row in month_charges:
|
for row in month_charges:
|
||||||
|
month_charged += row.amount
|
||||||
task = row.task
|
task = row.task
|
||||||
bucket = _STAGE_BUCKET.get(task.task_type) if task else None
|
bucket = _STAGE_BUCKET.get(task.task_type) if task else None
|
||||||
if bucket:
|
if bucket:
|
||||||
by_stage[bucket] += row.amount
|
by_stage[bucket] += row.amount
|
||||||
|
if task and task.task_type:
|
||||||
|
by_task_type[task.task_type] = by_task_type.get(task.task_type, Decimal("0")) + row.amount
|
||||||
pid = str(row.project_id) if row.project_id else None
|
pid = str(row.project_id) if row.project_id else None
|
||||||
if pid:
|
if pid:
|
||||||
project_amounts[pid] = project_amounts.get(pid, Decimal("0")) + row.amount
|
project_amounts[pid] = project_amounts.get(pid, Decimal("0")) + row.amount
|
||||||
@@ -250,7 +276,10 @@ def trend(request):
|
|||||||
"total_14d": str(total_14d),
|
"total_14d": str(total_14d),
|
||||||
"avg": str(avg),
|
"avg": str(avg),
|
||||||
"peak": str(peak),
|
"peak": str(peak),
|
||||||
|
"month": month_param,
|
||||||
|
"month_charged": str(month_charged),
|
||||||
"by_stage": {k: str(v) for k, v in by_stage.items()},
|
"by_stage": {k: str(v) for k, v in by_stage.items()},
|
||||||
|
"by_task_type": {k: str(v) for k, v in by_task_type.items()},
|
||||||
"by_project": {k: str(v) for k, v in project_amounts.items()},
|
"by_project": {k: str(v) for k, v in project_amounts.items()},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1364,3 +1364,6 @@ class QuickCreateCoordinatorTests(TestCase):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+791
-396
File diff suppressed because it is too large
Load Diff
@@ -943,8 +943,12 @@ export const api = {
|
|||||||
if (filters?.user) q.set("user", filters.user);
|
if (filters?.user) q.set("user", filters.user);
|
||||||
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") {
|
billingTrend(range?: "day" | "week" | "month", month?: string) {
|
||||||
return request<BillingTrend>(`/api/billing/trend/${range ? `?range=${range}` : ""}`);
|
const q = new URLSearchParams();
|
||||||
|
if (range) q.set("range", range);
|
||||||
|
if (month) q.set("month", month);
|
||||||
|
const qs = q.toString();
|
||||||
|
return request<BillingTrend>(`/api/billing/trend/${qs ? `?${qs}` : ""}`);
|
||||||
},
|
},
|
||||||
modelConfigs() {
|
modelConfigs() {
|
||||||
// 创作页下拉依赖完整 active 目录;加大 page_size 避免默认分页截断
|
// 创作页下拉依赖完整 active 目录;加大 page_size 避免默认分页截断
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { AlertCircle, Boxes, Check, ChevronDown, ChevronRight, Info, LogOut, Settings, UsersRound } from "lucide-react";
|
import { AlertCircle, Boxes, Check, ChevronDown, ChevronRight, Info, LogOut, Settings, WalletCards } from "lucide-react";
|
||||||
import { IconKitSvg } from "./IconKitSvg";
|
import { IconKitSvg } from "./IconKitSvg";
|
||||||
import { ConfirmModal, useBodyScrollLock, useOverlayTransition } from "./overlays";
|
import { ConfirmModal, useBodyScrollLock, useOverlayTransition } from "./overlays";
|
||||||
import type { Product, Project, Team, User } from "../types";
|
import type { Product, Project, Team, User } from "../types";
|
||||||
@@ -22,7 +22,7 @@ const SHELL_COMMANDS: Command[] = [
|
|||||||
{ id: "video-replace", group: "导航", label: "视频复刻", sub: "保留参考视频的人物、场景与节奏,复刻出自己的带货内容", page: "videoReplace", icon: "replace", key: "E" },
|
{ id: "video-replace", group: "导航", label: "视频复刻", sub: "保留参考视频的人物、场景与节奏,复刻出自己的带货内容", page: "videoReplace", icon: "replace", key: "E" },
|
||||||
{ id: "library", group: "导航", label: "成品库", sub: "素材、人物、场景、成片统一管理", page: "library", icon: "folder", key: "A" },
|
{ id: "library", group: "导航", label: "成品库", sub: "素材、人物、场景、成片统一管理", page: "library", icon: "folder", key: "A" },
|
||||||
{ id: "team", group: "导航", label: "团队", sub: "成员、权限、额度、协作记录", page: "team", icon: "users" },
|
{ id: "team", group: "导航", label: "团队", sub: "成员、权限、额度、协作记录", page: "team", icon: "users" },
|
||||||
{ id: "account", group: "导航", label: "账单库", sub: "余额、充值、账单流水", page: "account", icon: "creditCard" },
|
{ id: "account", group: "导航", label: "账户中心", sub: "余额、充值、账单流水", page: "account", icon: "creditCard" },
|
||||||
{ id: "settings", group: "导航", label: "系统设置", sub: "个人信息、通知、安全、偏好", page: "settings", icon: "settings" },
|
{ id: "settings", group: "导航", label: "系统设置", sub: "个人信息、通知、安全、偏好", page: "settings", icon: "settings" },
|
||||||
{ id: "messages", group: "常用动作", label: "消息中心", sub: "任务提醒、协作评论、系统通知", page: "messages", icon: "bell", key: "M" },
|
{ id: "messages", group: "常用动作", label: "消息中心", sub: "任务提醒、协作评论、系统通知", page: "messages", icon: "bell", key: "M" },
|
||||||
{ id: "new-product", group: "常用动作", label: "新建商品", sub: "从商品信息开始生成素材与视频", page: "productCreateUpload", icon: "productPlus" },
|
{ id: "new-product", group: "常用动作", label: "新建商品", sub: "从商品信息开始生成素材与视频", page: "productCreateUpload", icon: "productPlus" },
|
||||||
@@ -114,7 +114,7 @@ const ACCOUNT_ITEMS: { act: Page; icon: string; label: string }[] = [
|
|||||||
{ act: "settings", icon: "settings", label: "个人设置" },
|
{ act: "settings", icon: "settings", label: "个人设置" },
|
||||||
{ act: "messages", icon: "bell", label: "消息中心" },
|
{ act: "messages", icon: "bell", label: "消息中心" },
|
||||||
{ act: "team", icon: "users", label: "团队管理" },
|
{ act: "team", icon: "users", label: "团队管理" },
|
||||||
{ act: "account", icon: "creditCard", label: "账单库" }
|
{ act: "account", icon: "creditCard", label: "账户中心" }
|
||||||
];
|
];
|
||||||
|
|
||||||
export function AccountMenu({ open, anchorRect, onClose, navigate, logout, user, team, canManageBilling = true }: {
|
export function AccountMenu({ open, anchorRect, onClose, navigate, logout, user, team, canManageBilling = true }: {
|
||||||
@@ -231,7 +231,7 @@ const NAV: NavDef[] = [
|
|||||||
{ id: "free-create", page: "freeCreate", label: "自由创作", icon: "film" },
|
{ id: "free-create", page: "freeCreate", label: "自由创作", icon: "film" },
|
||||||
{ id: "library", page: "library", label: "成品库", icon: "library" },
|
{ id: "library", page: "library", label: "成品库", icon: "library" },
|
||||||
{ id: "team", page: "team", label: "团队", icon: "users" },
|
{ id: "team", page: "team", label: "团队", icon: "users" },
|
||||||
{ id: "account", page: "account", label: "账单库", icon: "creditCard" },
|
{ id: "account", page: "account", label: "账户中心", icon: "creditCard" },
|
||||||
{ id: "trash", page: "trash", label: "垃圾桶", icon: "trash" },
|
{ id: "trash", page: "trash", label: "垃圾桶", icon: "trash" },
|
||||||
{ id: "settings", page: "settings", label: "系统设置", icon: "settings" }
|
{ id: "settings", page: "settings", label: "系统设置", icon: "settings" }
|
||||||
];
|
];
|
||||||
@@ -239,20 +239,19 @@ const RESOURCE_NAV: Array<{ id: string; page: Page; label: string }> = [
|
|||||||
{ id: "products", page: "products", label: "商品库" },
|
{ id: "products", page: "products", label: "商品库" },
|
||||||
{ id: "models", page: "models", label: "模特库" },
|
{ id: "models", page: "models", label: "模特库" },
|
||||||
{ id: "library", page: "library", label: "成品库" },
|
{ id: "library", page: "library", label: "成品库" },
|
||||||
{ id: "account", page: "account", label: "账单库" },
|
|
||||||
{ id: "trash", page: "trash", label: "垃圾桶" },
|
{ id: "trash", page: "trash", label: "垃圾桶" },
|
||||||
];
|
];
|
||||||
|
|
||||||
type MajorNav = "resource" | "team" | "settings";
|
type MajorNav = "resource" | "account" | "settings";
|
||||||
const MAJOR_META: Record<MajorNav, { label: string; icon: "boxes" | "users-round" | "settings" }> = {
|
const MAJOR_META: Record<MajorNav, { label: string; icon: "boxes" | "wallet-cards" | "settings" }> = {
|
||||||
resource: { label: "资源中心", icon: "boxes" },
|
resource: { label: "资源中心", icon: "boxes" },
|
||||||
team: { label: "团队中心", icon: "users-round" },
|
account: { label: "账户中心", icon: "wallet-cards" },
|
||||||
settings: { label: "系统设置", icon: "settings" },
|
settings: { label: "系统设置", icon: "settings" },
|
||||||
};
|
};
|
||||||
|
|
||||||
function LiquidIcon({ name }: { name: "boxes" | "users-round" | "settings" }) {
|
function LiquidIcon({ name }: { name: "boxes" | "wallet-cards" | "settings" }) {
|
||||||
if (name === "boxes") return <Boxes size={20} strokeWidth={1.9} />;
|
if (name === "boxes") return <Boxes size={20} strokeWidth={1.9} />;
|
||||||
if (name === "users-round") return <UsersRound size={20} strokeWidth={1.9} />;
|
if (name === "wallet-cards") return <WalletCards size={20} strokeWidth={1.9} />;
|
||||||
return <Settings size={20} strokeWidth={1.9} />;
|
return <Settings size={20} strokeWidth={1.9} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +440,7 @@ export function Sidebar({ page, navigate, user, team, canManageBilling = true, p
|
|||||||
const resourceNavItems = RESOURCE_NAV.filter((item) => navItems.some((nav) => nav.page === item.page));
|
const resourceNavItems = RESOURCE_NAV.filter((item) => navItems.some((nav) => nav.page === item.page));
|
||||||
const topModule = topModuleForPage(page);
|
const topModule = topModuleForPage(page);
|
||||||
const activeMajor: MajorNav | null =
|
const activeMajor: MajorNav | null =
|
||||||
activeNav === "team" ? "team"
|
activeNav === "account" ? "account"
|
||||||
: activeNav === "settings" ? "settings"
|
: activeNav === "settings" ? "settings"
|
||||||
: topModule ? null
|
: topModule ? null
|
||||||
: resourceNavItems.some((item) => item.page === activeNav) ? "resource"
|
: resourceNavItems.some((item) => item.page === activeNav) ? "resource"
|
||||||
@@ -477,7 +476,7 @@ export function Sidebar({ page, navigate, user, team, canManageBilling = true, p
|
|||||||
const resourceOpen = activeMajor === "resource" && resourcePinned;
|
const resourceOpen = activeMajor === "resource" && resourcePinned;
|
||||||
|
|
||||||
const resourceItemRef = useRef<HTMLButtonElement>(null);
|
const resourceItemRef = useRef<HTMLButtonElement>(null);
|
||||||
const teamItemRef = useRef<HTMLButtonElement>(null);
|
const accountItemRef = useRef<HTMLButtonElement>(null);
|
||||||
const settingsItemRef = useRef<HTMLButtonElement>(null);
|
const settingsItemRef = useRef<HTMLButtonElement>(null);
|
||||||
const submenuRef = useRef<HTMLDivElement>(null);
|
const submenuRef = useRef<HTMLDivElement>(null);
|
||||||
const prevMajorRef = useRef<MajorNav | null>(activeMajor);
|
const prevMajorRef = useRef<MajorNav | null>(activeMajor);
|
||||||
@@ -486,7 +485,7 @@ export function Sidebar({ page, navigate, user, team, canManageBilling = true, p
|
|||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const item = activeMajor === "resource" ? resourceItemRef.current
|
const item = activeMajor === "resource" ? resourceItemRef.current
|
||||||
: activeMajor === "team" ? teamItemRef.current
|
: activeMajor === "account" ? accountItemRef.current
|
||||||
: activeMajor === "settings" ? settingsItemRef.current
|
: activeMajor === "settings" ? settingsItemRef.current
|
||||||
: null;
|
: null;
|
||||||
const leavingResource = prevMajorRef.current === "resource" && activeMajor !== "resource";
|
const leavingResource = prevMajorRef.current === "resource" && activeMajor !== "resource";
|
||||||
@@ -520,7 +519,7 @@ export function Sidebar({ page, navigate, user, team, canManageBilling = true, p
|
|||||||
|
|
||||||
const onResize = () => {
|
const onResize = () => {
|
||||||
const current = activeMajor === "resource" ? resourceItemRef.current
|
const current = activeMajor === "resource" ? resourceItemRef.current
|
||||||
: activeMajor === "team" ? teamItemRef.current
|
: activeMajor === "account" ? accountItemRef.current
|
||||||
: activeMajor === "settings" ? settingsItemRef.current
|
: activeMajor === "settings" ? settingsItemRef.current
|
||||||
: null;
|
: null;
|
||||||
if (!current) return;
|
if (!current) return;
|
||||||
@@ -638,14 +637,14 @@ export function Sidebar({ page, navigate, user, team, canManageBilling = true, p
|
|||||||
</div>
|
</div>
|
||||||
{canManageBilling && (
|
{canManageBilling && (
|
||||||
<button
|
<button
|
||||||
ref={teamItemRef}
|
ref={accountItemRef}
|
||||||
className={`nav-item${activeMajor === "team" ? " active" : ""}`}
|
className={`nav-item${activeMajor === "account" ? " active" : ""}`}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setMobileNavOpen(false); navigate("team"); }}
|
onClick={() => { setMobileNavOpen(false); navigate("account"); }}
|
||||||
>
|
>
|
||||||
<span className="nav-inner">
|
<span className="nav-inner">
|
||||||
<UsersRound size={23} strokeWidth={1.8} />
|
<WalletCards size={23} strokeWidth={1.8} />
|
||||||
<span className="nav-label">团队中心</span>
|
<span className="nav-label">账户中心</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2571,7 +2571,7 @@ export function ModelPhotoDemoPage({ variant, products, onBack, navigate }: { va
|
|||||||
<div className="dm-form-cta">
|
<div className="dm-form-cta">
|
||||||
<div className="dm-cost">
|
<div className="dm-cost">
|
||||||
<span>预估扣费 <span className="v">≈ 20 积分/张</span></span>
|
<span>预估扣费 <span className="v">≈ 20 积分/张</span></span>
|
||||||
<span>余额以账单库为准</span>
|
<span>余额以账户中心为准</span>
|
||||||
</div>
|
</div>
|
||||||
<button className="dm-gen" type="button" onClick={toRealTool}>
|
<button className="dm-gen" type="button" onClick={toRealTool}>
|
||||||
<Sparkles size={15} />
|
<Sparkles size={15} />
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export const mainNav: NavItem[] = [
|
|||||||
{ page: "projects", label: "视频创作", icon: FolderKanban },
|
{ page: "projects", label: "视频创作", icon: FolderKanban },
|
||||||
{ page: "pipeline", label: "生产管线", icon: WandSparkles },
|
{ page: "pipeline", label: "生产管线", icon: WandSparkles },
|
||||||
{ page: "library", label: "成品库", icon: Library },
|
{ page: "library", label: "成品库", icon: Library },
|
||||||
{ page: "account", label: "账单库", icon: Wallet }
|
{ page: "account", label: "账户中心", icon: Wallet }
|
||||||
];
|
];
|
||||||
|
|
||||||
export const supplementNav: NavItem[] = [
|
export const supplementNav: NavItem[] = [
|
||||||
@@ -116,7 +116,7 @@ export const routeLabels: Record<Page, string> = {
|
|||||||
projectWizard: "新建视频项目",
|
projectWizard: "新建视频项目",
|
||||||
pipeline: "生产管线",
|
pipeline: "生产管线",
|
||||||
library: "成品库",
|
library: "成品库",
|
||||||
account: "账单库",
|
account: "账户中心",
|
||||||
team: "团队",
|
team: "团队",
|
||||||
messages: "消息",
|
messages: "消息",
|
||||||
assetFactory: "图片工具",
|
assetFactory: "图片工具",
|
||||||
|
|||||||
@@ -579,6 +579,10 @@ export type BillingTrend = {
|
|||||||
avg: string;
|
avg: string;
|
||||||
peak: string;
|
peak: string;
|
||||||
by_stage: { script: string; base: string; storyboard: string; video: string };
|
by_stage: { script: string; base: string; storyboard: string; video: string };
|
||||||
|
/** 与后台任务监控「类型」列同源: AITask.task_type → 查询月消耗积分(有数据的类型都会返回) */
|
||||||
|
by_task_type?: Record<string, string>;
|
||||||
|
month?: string;
|
||||||
|
month_charged?: string;
|
||||||
by_project: Record<string, string>;
|
by_project: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user