feat(canvas): 在画布用户状态栏新增算力点余额展示

- 在画布详情页右上角用户头像左侧新增当前算力点余额展示
- 未登录或无用户信息时显示 0
- 使用 Coins 图标配合数字显示算力点余额
- 通过 Tooltip 提供当前算力点余额提示信息
- 算力点数值采用千分位分隔格式化显示
- 仅在画布模式下显示算力点余额信息
This commit is contained in:
HouYunFei
2026-05-25 14:25:57 +08:00
parent 2181b3b885
commit 823fb2523f
3 changed files with 12 additions and 1 deletions
+1
View File
@@ -59,6 +59,7 @@
- 优先使用 `canvasThemes``useThemeStore` 或 Ant Design `ConfigProvider` token。 - 优先使用 `canvasThemes``useThemeStore` 或 Ant Design `ConfigProvider` token。
- 不要硬编码黑白、stone、slate 等颜色导致浅色/深色主题不一致。 - 不要硬编码黑白、stone、slate 等颜色导致浅色/深色主题不一致。
- 新增画布按钮、弹窗、浮层时,尽量复用已有工具栏、节点面板、Modal 的视觉风格。 - 新增画布按钮、弹窗、浮层时,尽量复用已有工具栏、节点面板、Modal 的视觉风格。
- 画布顶部工具栏和状态信息优先采用极简扁平风格:无边框、无阴影、无胶囊背景,融入整体背景,弱化按钮感,仅保留轻微 hover 反馈,保持简洁现代、低视觉重量。
- 图片节点尺寸逻辑要尊重原始比例,除非功能明确要求自由变形。 - 图片节点尺寸逻辑要尊重原始比例,除非功能明确要求自由变形。
- 批量生成、多图展示、助手面板等画布交互要尽量简洁,不要占用过多画布空间。 - 批量生成、多图展示、助手面板等画布交互要尽量简洁,不要占用过多画布空间。
+1
View File
@@ -2,6 +2,7 @@
- 用户模块新增 `users` 完整字段和 `credit_logs` 表;账号密码注册/登录已开放,后端 `/api/v1` 模型渠道接口需要登录后调用;Linux.do OAuth 登录参数移到后台系统设置,登录页按开关展示 Linux.do 按钮并使用 `public/icons/linuxdo.svg` 图标,Linux.do 原始资料写入 `users.extra.linuxDo`;后台 `/admin/users` 可新增、编辑、删除用户并在编辑弹窗内调整算力点,编辑用户不修改头像和算力点,算力点变化会写入 `credit_logs`;后台新增 `/admin/credit-logs` 算力点日志表格和增删改接口,需要确认注册、登录、Linux.do 配置保存和回调、后台筛选分页、用户保存删除、算力点调整、日志增删改查、登录态下生图/对话/视频和算力点流水都正常。 - 用户模块新增 `users` 完整字段和 `credit_logs` 表;账号密码注册/登录已开放,后端 `/api/v1` 模型渠道接口需要登录后调用;Linux.do OAuth 登录参数移到后台系统设置,登录页按开关展示 Linux.do 按钮并使用 `public/icons/linuxdo.svg` 图标,Linux.do 原始资料写入 `users.extra.linuxDo`;后台 `/admin/users` 可新增、编辑、删除用户并在编辑弹窗内调整算力点,编辑用户不修改头像和算力点,算力点变化会写入 `credit_logs`;后台新增 `/admin/credit-logs` 算力点日志表格和增删改接口,需要确认注册、登录、Linux.do 配置保存和回调、后台筛选分页、用户保存删除、算力点调整、日志增删改查、登录态下生图/对话/视频和算力点流水都正常。
- 管理后台 `/admin/settings` 公开配置新增模型算力点配置;后端 `/api/v1` 生图、图生图、对话和视频创建接口会在上游调用成功后按模型扣除用户算力点并写入 `credit_logs``ai_consume` 记录,余额不足会拒绝调用,前端远程调用成功后会刷新当前用户信息;系统可用模型临时删除时会保留 JSON 里的历史模型算力点配置,渠道里临时移除模型时公开配置下拉仍保留已见过的模型候选,需要确认不同模型扣费、余额不足拦截、流水记录、顶部余额更新、算力点输入不回跳、临时删除模型后配置保留和可重新添加正常。 - 管理后台 `/admin/settings` 公开配置新增模型算力点配置;后端 `/api/v1` 生图、图生图、对话和视频创建接口会在上游调用成功后按模型扣除用户算力点并写入 `credit_logs``ai_consume` 记录,余额不足会拒绝调用,前端远程调用成功后会刷新当前用户信息;系统可用模型临时删除时会保留 JSON 里的历史模型算力点配置,渠道里临时移除模型时公开配置下拉仍保留已见过的模型候选,需要确认不同模型扣费、余额不足拦截、流水记录、顶部余额更新、算力点输入不回跳、临时删除模型后配置保留和可重新添加正常。
- 画布详情页右上角用户头像左侧新增当前算力点余额展示,未登录或无用户信息时显示 0,需要确认登录、未登录和调用后刷新余额显示正常。
- 配置弹窗和后台公开配置新增默认视频模型;视频节点通过设置弹层单独配置视频质量、尺寸和秒数,连接图片节点时会作为 `input_reference` 参考图生成视频,并按 OpenAI 视频接口格式提交 `multipart/form-data``size``seconds``vquality` 和参考图参数,需要确认本地直连和云端渠道下默认视频模型选择、节点视频参数配置、参考图生成、保存和重试都正常。 - 配置弹窗和后台公开配置新增默认视频模型;视频节点通过设置弹层单独配置视频质量、尺寸和秒数,连接图片节点时会作为 `input_reference` 参考图生成视频,并按 OpenAI 视频接口格式提交 `multipart/form-data``size``seconds``vquality` 和参考图参数,需要确认本地直连和云端渠道下默认视频模型选择、节点视频参数配置、参考图生成、保存和重试都正常。
- 画布新增视频节点:工具栏可新建视频节点,上传/拖拽本地视频后可在节点内播放、下载、保存到我的素材;节点下方对话框和生成配置节点支持视频生成,前端会调用 OpenAI 兼容 `videos` 接口并把生成结果保存为本地视频 Blob;后端远程渠道新增 `/api/v1/videos``/api/v1/videos/:id``/api/v1/videos/:id/content` 代理,需要确认本地渠道和远程渠道都能生成、轮询和下载视频。 - 画布新增视频节点:工具栏可新建视频节点,上传/拖拽本地视频后可在节点内播放、下载、保存到我的素材;节点下方对话框和生成配置节点支持视频生成,前端会调用 OpenAI 兼容 `videos` 接口并把生成结果保存为本地视频 Blob;后端远程渠道新增 `/api/v1/videos``/api/v1/videos/:id``/api/v1/videos/:id/content` 代理,需要确认本地渠道和远程渠道都能生成、轮询和下载视频。
- 图片和视频节点会按素材或生成结果的真实宽高比调整节点尺寸;空图片/视频节点在尺寸设置切换到 16:9、9:16 等比例时也会同步调整节点外框,需要确认上传、生成、重试、素材插入和空节点尺寸切换后的黑边情况。 - 图片和视频节点会按素材或生成结果的真实宽高比调整节点尺寸;空图片/视频节点在尺寸设置切换到 16:9、9:16 等比例时也会同步调整节点外框,需要确认上传、生成、重试、素材插入和空节点尺寸切换后的黑边情况。
@@ -1,7 +1,7 @@
"use client"; "use client";
import type { CSSProperties, RefObject } from "react"; import type { CSSProperties, RefObject } from "react";
import { Avatar, Dropdown } from "antd"; import { Avatar, Dropdown, Tooltip } from "antd";
import { Keyboard, LogOut, Settings2, Shield } from "lucide-react"; import { Keyboard, LogOut, Settings2, Shield } from "lucide-react";
import type { ItemType } from "antd/es/menu/interface"; import type { ItemType } from "antd/es/menu/interface";
import Link from "next/link"; import Link from "next/link";
@@ -33,6 +33,7 @@ export function UserStatusActions({ showConfig = true, variant = "default", onOp
const openConfigDialog = useConfigStore((state) => state.openConfigDialog); const openConfigDialog = useConfigStore((state) => state.openConfigDialog);
const canvasTheme = canvasThemes[theme]; const canvasTheme = canvasThemes[theme];
const userName = user?.displayName || user?.username || "用户"; const userName = user?.displayName || user?.username || "用户";
const credits = user?.credits ?? 0;
const avatarUrl = user?.avatarUrl?.trim(); const avatarUrl = user?.avatarUrl?.trim();
const avatarText = (userName.trim()[0] || "U").toUpperCase(); const avatarText = (userName.trim()[0] || "U").toUpperCase();
const naturalIconClass = "inline-flex size-8 shrink-0 items-center justify-center text-stone-600 transition hover:text-stone-950 dark:text-stone-300 dark:hover:text-white [&_svg]:size-4"; const naturalIconClass = "inline-flex size-8 shrink-0 items-center justify-center text-stone-600 transition hover:text-stone-950 dark:text-stone-300 dark:hover:text-white [&_svg]:size-4";
@@ -59,6 +60,14 @@ export function UserStatusActions({ showConfig = true, variant = "default", onOp
<AnimatedThemeToggler theme={theme} onThemeChange={setTheme} className={naturalIconClass} style={iconStyle} aria-label={theme === "dark" ? "切换到浅色主题" : "切换到深色主题"} title={theme === "dark" ? "切换到浅色主题" : "切换到深色主题"} /> <AnimatedThemeToggler theme={theme} onThemeChange={setTheme} className={naturalIconClass} style={iconStyle} aria-label={theme === "dark" ? "切换到浅色主题" : "切换到深色主题"} title={theme === "dark" ? "切换到浅色主题" : "切换到深色主题"} />
<VersionReleaseModal style={versionStyle} /> <VersionReleaseModal style={versionStyle} />
<GitHubLink className={cn("bg-transparent hover:bg-transparent dark:hover:bg-transparent", gitHubClassName)} style={gitHubStyle} /> <GitHubLink className={cn("bg-transparent hover:bg-transparent dark:hover:bg-transparent", gitHubClassName)} style={gitHubStyle} />
{variant === "canvas" ? (
<Tooltip title="当前算力点余额" placement="bottom">
<div className="flex h-8 shrink-0 items-center gap-1.5 px-1.5 text-xs font-medium tabular-nums opacity-75 transition hover:opacity-100" style={{ color: canvasTheme.node.text }}>
<span className="text-sm leading-none"></span>
<span>{credits.toLocaleString()}</span>
</div>
</Tooltip>
) : null}
<div ref={accountRef}> <div ref={accountRef}>
<Dropdown open={accountOpen} onOpenChange={onAccountOpenChange} trigger={["click"]} placement="bottomRight" getPopupContainer={getPopupContainer} styles={{ root: { minWidth: 150 } }} menu={{ items: menuItems }}> <Dropdown open={accountOpen} onOpenChange={onAccountOpenChange} trigger={["click"]} placement="bottomRight" getPopupContainer={getPopupContainer} styles={{ root: { minWidth: 150 } }} menu={{ items: menuItems }}>
<button type="button" className="flex size-8 shrink-0 items-center justify-center rounded-full bg-transparent p-0 text-[0] leading-[0] transition" aria-label="账户菜单"> <button type="button" className="flex size-8 shrink-0 items-center justify-center rounded-full bg-transparent p-0 text-[0] leading-[0] transition" aria-label="账户菜单">