feat: 构建系统重构 + VCHSM 架构迁移 + 右键菜单修复 + 尺寸参数统一

## 构建、打包与分发系统重构 (build/)

- 构建系统收敛至 build/ 目录:build.mjs + electron-builder.js + 6 个脚本

- electron-builder 配置从 package.json 提取为 build/electron-builder.js

- 新增 --edition (personal/enterprise) 和 --channel (stable/beta/alpha) 参数

- OSS 路径按渠道隔离

- 新增 pre-build-check.mjs:Git/CHANGELOG/版本/环境变量校验

- 新增 bump-version.mjs:版本号管理 + CHANGELOG 自动生成

- updater.ts 发送 channel/edition/deviceFingerprint

- 新增 src/shared/utils/rollout.ts 灰度测试工具

- 新增 10 条 NPM 脚本

## VCHSM 五层架构迁移

- 7 个业务模块 + ~500 处导入路径同步

## 修复

- MediaCardContextMenu 改用共享 ContextMenu 组件

- ComboBox 尺寸参数显示统一 (size-format.ts)

## 文档

- UpdateA.md / build/README.md / README.md / .env.example 更新

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-29 18:41:07 +08:00
parent e0d91335c7
commit 337d6c1205
187 changed files with 5252 additions and 1702 deletions

View File

@@ -1,73 +0,0 @@
// ============================================================
// 公告模块 — 类型定义 + API 请求
// GET /api/v1/announcements
// ============================================================
import { get } from '../request';
// ============================================================
// AnnouncementUrlObj — 公告模块路由常量
// ============================================================
const AnnouncementUrlObj = {
list: '/api/v1/announcements',
} as const;
// ---------- 类型 ----------
// ============================================================
// 公告类型常量
// ============================================================
export const AnnouncementTypeEnum = {
System: "system",
Feature: "feature",
Activity: "activity",
Maintenance: "maintenance",
} as const;
export type AnnouncementTypeValue = typeof AnnouncementTypeEnum[keyof typeof AnnouncementTypeEnum];
export const AnnouncementTypeLabelMap: Record<AnnouncementTypeValue, string> = {
[AnnouncementTypeEnum.System]: "系统公告",
[AnnouncementTypeEnum.Feature]: "功能更新",
[AnnouncementTypeEnum.Activity]: "活动通知",
[AnnouncementTypeEnum.Maintenance]: "维护公告",
};
export const AnnouncementTypeColorMap: Record<AnnouncementTypeValue, string> = {
[AnnouncementTypeEnum.System]: "blue",
[AnnouncementTypeEnum.Feature]: "purple",
[AnnouncementTypeEnum.Activity]: "orange",
[AnnouncementTypeEnum.Maintenance]: "red",
};
/** 公告类型(向后兼容:包含已知枚举 + string 宽类型) */
export type AnnouncementType = AnnouncementTypeValue | string;
/** 单条公告 */
export interface Announcement {
id: number;
title: string;
content: string;
type: AnnouncementType;
is_active: boolean;
pinned: boolean;
expires_at: string;
created_by: string;
created_at: string;
updated_at: string;
}
/** 公告列表响应 */
export type AnnouncementListResponse = Announcement[];
// ---------- API ----------
/**
* 获取公告列表
* GET /api/v1/announcements
*/
export function fetchAnnouncements(): Promise<AnnouncementListResponse> {
return get<AnnouncementListResponse>(AnnouncementUrlObj.list);
}

View File

@@ -1,218 +0,0 @@
import {post, get} from '../request';
// ============================================================
// AuthUrlObj — 认证模块路由常量
// 集中管理,后续接口升级 v2 等只需改此处
// ============================================================
const AuthUrlObj = {
login: '/api/v1/auth/login',
register: '/api/v1/auth/register',
sendSms: '/api/v1/auth/send-sms',
refreshToken: '/api/v1/auth/refresh',
userInfo: '/api/v1/auth/me',
changePassword: '/api/v1/auth/change-password',
resetPassword: '/api/v1/auth/reset-password',
logout: '/api/v1/auth/logout',
} as const;
// ============================================================
// 认证相关 DTO 传输模型
// ============================================================
export const UserRoleTypeEnum = {
User: "user",
Admin: "admin",
SuperAdmin: "superadmin",
} as const;
export type UserRoleTypeValue = typeof UserRoleTypeEnum[keyof typeof UserRoleTypeEnum];
export const UserRoleTypeLabelMap: Record<UserRoleTypeValue, string> = {
[UserRoleTypeEnum.User]: "用户账户",
[UserRoleTypeEnum.Admin]: "管理账户",
[UserRoleTypeEnum.SuperAdmin]: "超管账户"
};
export const UserAccountTypeEnum = {
Individual: 'individual',
Company: 'company',
Employee: 'employee',
} as const;
export type UserAccountTypeValue = typeof UserAccountTypeEnum[keyof typeof UserAccountTypeEnum];
export const UserAccountTypeLabelMap: Record<UserAccountTypeValue, string> = {
[UserAccountTypeEnum.Individual]: "个人用户",
[UserAccountTypeEnum.Company]: "企业用户",
[UserAccountTypeEnum.Employee]: "员工账号"
};
/** 短信验证码请求体 */
export interface SendSMSRequestBody {
phone: string;
}
/** 注册请求体 */
export interface RegisterRequestBody {
// 用户名3~64 位,仅允许字母、数字、-、_
username: string;
// 密码6~128 位
password: string;
// 手机号5~32 位
phone: string;
// 短信验证码4~8 位
sms_code: string;
// 设备标识(网卡 MAC8~128 位
device_id: string;
// 设备标签/备注(可选)
device_label: string;
// 个人昵称(可选)
display_name: string;
// 邀请码(可选),填写后触发邀请佣金逻辑
referral_code: string;
}
export interface RegisterResponseBody {
access_token: string;
refresh_token: string;
token_type: string;
expires_in: 0;
refresh_expires_in: 0;
user_id: string;
username: string;
role: UserRoleTypeValue;
email: string;
balance_cent: 0;
account_type: UserAccountTypeValue;
display_name: string;
company_id: string;
real_name: string;
phone: string;
license_code: string;
admin_permissions: string []
}
/** 登录请求体 */
export interface LoginRequestBody {
username: string;
password: string;
device_id: string;
user_ip: string;
}
export interface LoginResponseBody {
access_token: string;
refresh_token: string;
token_type: string;
expires_in: number;
refresh_expires_in: number;
user_id: string;
username: string;
role: UserRoleTypeValue;
email: string;
balance_cent: number;
account_type: UserAccountTypeValue;
display_name: string;
company_id: string;
real_name: string;
phone: string;
license_code: string;
admin_permissions: string[];
}
export interface RefreshTokenRequestBody {
refresh_token: string;
}
export interface RefreshTokenResponseBody {
access_token: string;
refresh_token: string;
token_type: string;
expires_in: number;
refresh_expires_in: number;
}
export interface UserInfoBody {
id: string;
username: string;
email: string | null;
balance_cent: number;
gift_balance_cent: number;
role: UserRoleTypeValue;
status: string;
account_type: UserAccountTypeValue;
display_name: string | null;
real_name: string | null;
phone: string;
company_id: string | null;
discount_rate: number | null;
subscription_plan: string;
subscription_expires_at: Date;
seat_limit: number | null;
last_login_at: Date;
created_at: Date;
model_package_id: number;
vip_level_id: number;
software_expires_at: Date;
referral_code: string | null;
admin_permissions: string[] | null;
}
export interface ChangePasswordRequestBody {
current_password: string;
new_password: string;
}
export interface UsePhoneResetPasswordRequestBody {
phone: string;
sms_code: string;
new_password: string;
}
// ============================================================
// AuthAPI — 认证相关 API 静态方法类
// 调用方式AuthAPI.login() / AuthAPI.register() 等
// ============================================================
export class AuthAPI {
/** 登录 */
static async login(data: LoginRequestBody): Promise<LoginResponseBody> {
return await post<LoginResponseBody>(AuthUrlObj.login, data);
}
/** 注册 */
static async register(data: RegisterRequestBody): Promise<RegisterResponseBody> {
return await post<RegisterResponseBody>(AuthUrlObj.register, data);
}
/** 发送短信验证码 */
static async sendSms(data: SendSMSRequestBody): Promise<void> {
return await post<void>(AuthUrlObj.sendSms, data);
}
/** 刷新 Token */
static async refreshToken(data: RefreshTokenRequestBody): Promise<RefreshTokenResponseBody> {
return await post<RefreshTokenResponseBody>(AuthUrlObj.refreshToken, data);
}
/** 获取当前用户信息 */
static async getUserInfo(signal?: AbortSignal): Promise<UserInfoBody> {
return await get<UserInfoBody>(AuthUrlObj.userInfo, undefined, { signal });
}
/** 修改密码(已登录) */
static async changePassword(data: ChangePasswordRequestBody): Promise<void> {
return await post<void>(AuthUrlObj.changePassword, data);
}
/** 手机验证码重置密码 */
static async resetPassword(data: UsePhoneResetPasswordRequestBody): Promise<void> {
return await post<void>(AuthUrlObj.resetPassword, data);
}
/** 退出登录 */
static async logout(): Promise<void> {
return await post<void>(AuthUrlObj.logout);
}
}

View File

@@ -1,59 +0,0 @@
// ============================================================
// Banner 模块 — 首页轮播图 API
// GET /api/v1/banners
// ============================================================
import {get} from '../request';
// ---------- 路由常量 ----------
const BannerUrlObj = {
list: '/api/v1/creatives',
} as const;
// ---------- 类型 ----------
// ============================================================
// Banner 分类常量
// ============================================================
export const BannerCategoryEnum = {
Promotion: "promotion",
Banner: "banner",
} as const;
export type BannerCategoryValue = typeof BannerCategoryEnum[keyof typeof BannerCategoryEnum];
export const BannerCategoryLabelMap: Record<BannerCategoryValue, string> = {
[BannerCategoryEnum.Promotion]: "推广",
[BannerCategoryEnum.Banner]: "轮播",
};
/** 单条 Banner */
export interface Banner {
id: number;
title: string;
content: "真人素材提示";
image_url: string;
link_url?: string;
category: BannerCategoryValue;
vip_level_id?: number;
sort_order: number;
is_active: boolean;
created_by: string;
created_at: Date;
updated_at: Date;
}
/** Banner 列表响应 */
export type BannerListResponse = Banner[];
// ---------- API ----------
/**
* 获取 Banner 轮播图列表
* GET /api/v1/banners
*/
export function fetchBanners(signal?: AbortSignal): Promise<BannerListResponse> {
return get<BannerListResponse>(BannerUrlObj.list, undefined, { signal });
}

View File

@@ -1,91 +0,0 @@
import {type Nullable} from "@/utils/display";
import {get} from "@/services/request.ts";
const BillingUrlObj = {
getBalance: '/api/v1/billing/balance',
getLedger: '/api/v1/billing/ledger',
ExportLedger: "/api/v1/billing/ledger/export"
} as const;
export interface BalanceResponseBody {
user_id: string,
balance_cent: Nullable<number>,
gift_balance_cent: Nullable<number>,
currency: "CNY",
today_spent_cent: number,
today_calls: number
}
export interface LedgerReqeustParam {
month: Nullable<Date>,
page: number,
page_size: number
}
export interface LedgerItemResponseBody {
id: string,
entry_type: string,
balance_cent: string,
amount_cent: number,
currency: string,
balance_after_cent: number,
task_id: string,
description: "string",
accounting_month: "string",
accounting_day: "string",
created_at: Date
}
export interface ExportLedgerRequstParam {
start_time: Date,
end_time: Date,
entry_type: Nullable<string>
debug?: boolean
}
export class BillingAPI {
static getBalance(signal?: AbortSignal): Promise<BalanceResponseBody> {
return get<BalanceResponseBody>(BillingUrlObj.getBalance, undefined, {signal});
}
static getLedger(param: LedgerReqeustParam, signal?: AbortSignal): Promise<LedgerItemResponseBody[]> {
return get<LedgerItemResponseBody[]>(BillingUrlObj.getLedger, param as unknown as Record<string, unknown>, {signal});
}
static exportUrl = BillingUrlObj.ExportLedger;
}
// ============================================================
// 账单条目类型常量
// ============================================================
export const EntryTypeEnum = {
TaskHold: "task_hold",
Recharge: "recharge",
Gift: "gift",
HoldRelease: "hold_release",
HoldSettle: "hold_settle",
} as const;
export type EntryTypeValue = typeof EntryTypeEnum[keyof typeof EntryTypeEnum];
export const EntryTypeLabelMap = {
[EntryTypeEnum.TaskHold]: "任务消耗",
[EntryTypeEnum.Recharge]: "充值",
[EntryTypeEnum.Gift]: "赠送",
[EntryTypeEnum.HoldRelease]: "退款",
[EntryTypeEnum.HoldSettle]: "结算",
} as const;
export const EntryTypeColorMap = {
[EntryTypeEnum.TaskHold]: "orange",
[EntryTypeEnum.Recharge]: "green",
[EntryTypeEnum.Gift]: "green",
[EntryTypeEnum.HoldRelease]: "blue",
[EntryTypeEnum.HoldSettle]: "orange",
} as const;
export type EntryUITypeValue = typeof EntryTypeLabelMap[keyof typeof EntryTypeLabelMap];
export type ColorValue = typeof EntryTypeColorMap[keyof typeof EntryTypeColorMap];

View File

@@ -1,159 +0,0 @@
import {get} from '../request';
// 注意:后端 /api/v1/models 的 category 查询参数实际不可用(传参返回空列表),
// 因此客户端采用"全量拉取 + 按 category_name 字段分组"的策略。
// 后端分类 slug 格式为 img2img / txt2img 等,前端通过 CATEGORY_SLUG_MAP 映射。
// ---------- 路由常量 ----------
const ModelsUrlObj = {
modelList: '/api/v1/models',
getModelInfo: '/api/v1/models/{model_id}',
estimateCost: '/api/v1/models/{model_id}/estimate'
} as const;
// ---------- 类型 ----------
/** 模型类别常量数组(单一数据源,类型由此推导) */
export const MODEL_CATEGORIES = [
'image_to_image',
'text_to_image',
'image_to_video',
'text_to_audio',
] as const;
export type ModelCategoryStringEnum = typeof MODEL_CATEGORIES[number];
/**
* 模型参数 Schema 项
*
* 后端 /api/v1/models 返回的 param_schema 为对象数组,
* 每项描述一个模型入参的元信息标识、字段映射、规格约束、UI 配置)。
*/
export interface ParamSchemaItem {
id: string;
/** 映射到提交参数中的字段名 */
map_to: string;
/** 规格约束(类型、范围、默认值等) */
spec: Record<string, unknown>;
/** UI 控件配置 */
ui: Record<string, unknown>;
}
/** 模型类别中文映射 */
export const MODEL_CATEGORY_LABELS: Record<ModelCategoryStringEnum, string> = {
image_to_image: '图生图',
text_to_image: '文生图',
image_to_video: '图生视频',
text_to_audio: '文生音频',
};
/**
* 前端 ModelCategory → 后端分类 slug 映射
*
* 后端分类 API 使用简写 slug如 img2img而非前端的长标识符。
* 查询模型列表时需将前端类别转为后端 slug 作为 category 参数。
*/
export const CATEGORY_SLUG_MAP: Record<ModelCategoryStringEnum, string> = {
image_to_image: 'img2img',
text_to_image: 'txt2img',
image_to_video: 'img2video',
text_to_audio: 'txt2audio',
};
/** 后端 slug → 前端 ModelCategory 反向映射(由 CATEGORY_SLUG_MAP 自动推导) */
const SLUG_TO_CATEGORY: Record<string, ModelCategoryStringEnum> = Object.fromEntries(
Object.entries(CATEGORY_SLUG_MAP).map(([cat, slug]) => [slug, cat as ModelCategoryStringEnum]),
) as Record<string, ModelCategoryStringEnum>;
/**
* 根据任意 category_name 值解析出最佳中文标签
*
* 解析优先级:
* 1. 匹配 MODEL_CATEGORY_LABELS前端 ModelCategory 字面量)
* 2. 匹配 SLUG_TO_CATEGORY 反向映射(后端 slug → 中英文标签)
* 3. 兜底返回原始字符串
*/
export function resolveCategoryLabel(categoryName: string): string {
// 优先按 ModelCategory 字面量查找
if (categoryName in MODEL_CATEGORY_LABELS) {
return MODEL_CATEGORY_LABELS[categoryName as ModelCategoryStringEnum];
}
// 按后端 slug 反向查找
const cat = SLUG_TO_CATEGORY[categoryName];
if (cat) {
return MODEL_CATEGORY_LABELS[cat];
}
// 兜底返回原始值
return categoryName;
}
/**
* 将前端 ModelCategory 转为后端 API 用的分类 slug
*/
export function toCategorySlug(category: ModelCategoryStringEnum): string {
return CATEGORY_SLUG_MAP[category];
}
/**
* 查询可用模型的请求参数
*/
export interface ModelsListReqeustParams {
provider_name?: string;
category?: ModelCategoryStringEnum;
page: number;
page_size: number;
}
/**
* 查询可用模型的响应体
*/
export interface ModelsListResponseBody {
id: string;
name: string;
provider_name: string;
model_type: ModelCategoryStringEnum;
category_name: ModelCategoryStringEnum | null;
status: string;
priority: number;
response_mode: string;
pricing_config: Record<string, unknown> | null;
param_schema: ParamSchemaItem[];
quota_config: Record<string, unknown> | null;
ui_config: Record<string, unknown>;
meta_config: Record<string, unknown>;
constraints_config: Record<string, unknown>;
expires_at: string;
created_at: string;
updated_at: string;
}
/**
* 模型信息请求参数
*/
export interface ModelInfoRequestParams {
model_id: string;
}
/** 模型详情响应体(与列表项结构一致) */
export type ModelInfoResponseBody = ModelsListResponseBody;
// ---------- API ----------
export class ModelAPI {
/**
* 获取可用模型列表
* GET /api/v1/models
*/
static fetchModels(params: ModelsListReqeustParams, signal?: AbortSignal): Promise<ModelsListResponseBody[]> {
return get<ModelsListResponseBody[]>(ModelsUrlObj.modelList, params as unknown as Record<string, unknown>, { signal });
}
/**
* 获取单个模型详情
* GET /api/v1/models/:id
*/
static fetchModelInfo(modelId: string): Promise<ModelInfoResponseBody> {
const url = ModelsUrlObj.getModelInfo.replace('{model_id}', modelId);
return get<ModelInfoResponseBody>(url);
}
}

View File

@@ -1,99 +0,0 @@
import { get, post } from '@/services/request';
export const PAYMENT_METHODS = {
Alipay: 'alipay',
WeChat: 'wechat',
Mock: "mock"
} as const;
export type PaymentMethod = typeof PAYMENT_METHODS[keyof typeof PAYMENT_METHODS];
export type AvailablePaymentMethod = typeof PAYMENT_METHODS.Alipay;
export interface CreateOrderRequestBody {
amount_cent: number,
payment_channel: AvailablePaymentMethod
}
export const PAYMENT_STATUS = {
Pending: 'pending', // 待支付
Paid: 'paid', // 已支付
Expired: 'expired', // 已过期
Failed: 'failed', // 支付失败
} as const;
export type PaymentStatus = typeof PAYMENT_STATUS[keyof typeof PAYMENT_STATUS];
// 中文映射表
export const PAYMENT_STATUS_TEXT_MAP: Record<PaymentStatus, string> = {
[PAYMENT_STATUS.Pending]: '待支付',
[PAYMENT_STATUS.Paid]: '已支付',
[PAYMENT_STATUS.Expired]: '已过期',
[PAYMENT_STATUS.Failed]: '支付失败',
};
// 辅助函数:根据状态获取中文
export function getPaymentStatusText(status: PaymentStatus): string {
return PAYMENT_STATUS_TEXT_MAP[status];
}
export interface CreateOrderResponseBody {
id: string,
user_id: string,
amount_cent: number,
status: PaymentStatus,
payment_channel: AvailablePaymentMethod,
out_trade_no: string,
transaction_id: string,
pay_url: string,
expires_at: Date,
paid_at: Date,
created_at: Date,
}
export interface OrderListRequestParam {
status?: PaymentStatus,
page: number,
page_size: number,
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface OrderListResponseBody extends CreateOrderResponseBody {
}
export interface OrderInfoRequestParam {
order_id: string,
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface OrderInfoResponseBody extends CreateOrderResponseBody {
}
const PaymentUrlObj = {
createOrder: "/api/v1/payments/orders",
getOrderList: "/api/v1/payments/orders",
getOrderInfo: "/api/v1/payments/orders/{order_id}",
};
// ============================================================
// 调用方式PaymentAPI.createOrder() / PaymentAPI.getOrdersListInfo() 等
// ============================================================
export class PaymentAPI {
/** 创建支付订单 */
static async createOrder(body: CreateOrderRequestBody): Promise<CreateOrderResponseBody> {
return await post<CreateOrderResponseBody>(PaymentUrlObj.createOrder, body);
}
/** 获取订单列表(支持按状态筛选 + 分页) */
static async getOrdersListInfo(param: OrderListRequestParam, signal?: AbortSignal): Promise<OrderListResponseBody[]> {
return await get<OrderListResponseBody[]>(PaymentUrlObj.getOrderList, param as unknown as Record<string, unknown>, { signal });
}
/** 获取单个订单详情 */
static async getOrderInfo(param: OrderInfoRequestParam): Promise<OrderInfoResponseBody> {
const url = PaymentUrlObj.getOrderInfo.replace('{order_id}', param.order_id);
return await get<OrderInfoResponseBody>(url);
}
}

View File

@@ -1,271 +0,0 @@
// ============================================================
// 任务模块 — 任务记录 CRUD API
// GET /api/v1/tasks → 任务列表(分页/游标)
// POST /api/v1/tasks → 提交任务
// GET /api/v1/tasks/:id → 任务详情
// GET /api/v1/tasks/:id/status → 轻量轮询状态
// GET /api/v1/tasks/export → 导出 CSV
//
// TaskAPI 的静态方法直接返回 get/post 的 Promise<T>
// 不做额外的 async/await 包装,由调用方决定是否 await。
// ============================================================
import {get, post} from '../request';
import {getDeviceId} from '@/utils/env';
import {Nullable} from "@/utils/display";
// ============================================================
// 基础枚举 / 字面量类型
// ============================================================
/** 输出类型映射 */
export enum OutputTypeMap {
image = '图片',
video = '视频',
audio = '音频',
}
export type OutputTypeString = keyof typeof OutputTypeMap;
/** 任务状态 */
export enum TaskStatusMap {
pending = '待处理',
submitted = '已提交',
processing = '处理中',
success = '成功',
failed = '失败',
}
export type TaskStatusString = keyof typeof TaskStatusMap;
/** 需要持续轮询的非终态状态 */
export const POLLING_STATUSES: TaskStatusString[] = ['pending', 'submitted', 'processing'];
// ============================================================
// 请求体 / 参数类型
// ============================================================
/** 任务列表请求参数 */
export interface TaskListRequestParams {
status?: TaskStatusString;
user_ids?: string[];
model_ids?: string[];
/** 游标分页:上次最后一条的 created_atISO 8601 */
cursor?: string;
/** 页码OFFSET 分页模式default: 1 */
page?: number;
/** 每页条数default: 20 */
page_size?: number;
}
/** 创建任务请求体 */
export interface CreatTaskRequestBody {
model_id: string;
params: Record<string, string>;
device_id: string;
tags?: string[];
}
/**
* 批量任务轮询查询参数
*/
export interface BatchTaskStatusRequestParam {
task_ids: string[];
}
/**
* 批量任务状态轮询响应头
*/
export interface BatchTaskStatusResponseBody {
items: Nullable<TaskStatusResponseBody>[],
balance_cent: number,
next_poll_ms: number;
}
/** 导出 CSV 参数 */
export interface ExportTaskCSVParams {
start_date: Date;
end_date: Date;
status?: TaskStatusString;
user_ids?: string[];
model_ids?: string[];
}
// ============================================================
// 内容项 — 带判别标签的联合类型
// ============================================================
interface BaseContentItem {
type: string;
}
interface TextContent extends BaseContentItem {
type: 'text';
text: string;
}
interface ImageUrlContent extends BaseContentItem {
type: 'image_url';
role: 'reference_image';
image_url: { url: string };
}
interface VideoUrlContent extends BaseContentItem {
type: 'video_url';
role: 'reference_video';
video_url: { url: string };
}
interface AudioContent extends BaseContentItem {
type: 'audio_url';
role: 'reference_audio';
audio_url: { url: string };
}
export type ContentItem =
| TextContent
| ImageUrlContent
| VideoUrlContent
| AudioContent;
// ============================================================
// 各输出类型的 UI 参数
// ============================================================
export interface ImageUiParams {
imageUrls?: string[];
prompt: string;
aspectRatio: string;
resolution?: string;
}
export interface VideoUiParams {
resolution?: string;
ratio?: string;
model?: string;
generate_audio: boolean;
duration: number;
content: ContentItem[];
}
export interface AudioUiParams {
format?: string;
sampleRate?: number;
bitrate?: number;
}
// ============================================================
// 响应体 — 用判别联合按 output_type 分发
// ============================================================
/** 任务条目基类(不含 output_type 特有字段) */
interface BaseTaskItemResponseBody {
id: string;
user_id: string;
model_id: string;
model_name: string;
provider_name: string;
status: TaskStatusString;
external_task_id: string;
cost_cent: number;
estimated_cost_cent: number;
error_code: string;
poll_attempts: number;
is_favorite: boolean;
submitted_at: Date;
finished_at: Date;
created_at: Date;
updated_at: Date;
prompt?: string;
output_assets?: Array<{ url: string }>;
tags: Array<string | null>;
error_message: string;
user_display_name: string | null;
}
/** 判别联合:根据 output_type 收窄 ui_params */
export type TaskInfoItemResponseBody =
| (BaseTaskItemResponseBody & { output_type: 'image'; ui_params: ImageUiParams })
| (BaseTaskItemResponseBody & { output_type: 'video'; ui_params: VideoUiParams })
| (BaseTaskItemResponseBody & { output_type: 'audio'; ui_params: AudioUiParams });
/** 任务列表分页响应 */
export interface TaskInfoDataResponseBody {
has_more: boolean;
next_cursor: string;
page: number;
page_size: number;
total: number;
items: TaskInfoItemResponseBody[];
}
/** 轻量轮询状态响应 */
export interface TaskStatusResponseBody {
task_id: string;
status: TaskStatusString;
progress?: number;
output_assets?: string[];
error_code?: string;
error_message?: string;
cost_cent?: number;
queue_position: number,
submitted_at: Date,
finished_at: Date,
external_task_id: string,
refund_cent: number,
refunded_at: Date,
refund_reason: string,
next_poll_ms: number
}
// ============================================================
// 路由常量
// ============================================================
const TaskUrlObj = {
task: '/api/v1/tasks',
taskInfo: (task_id: string) => `/api/v1/tasks/${task_id}`,
taskStatus: (task_id: string) => `/api/v1/tasks/${task_id}/status`,
tasksCSV: '/api/v1/tasks/export',
batchTaskStatus: "/api/v1/tasks/status/batch"
} as const;
// ============================================================
// TaskAPI — 静态方法直接返回 Promise<T>,不做二次 async 包装
// ============================================================
export class TaskAPI {
/** 获取任务列表(分页/游标) */
static getTaskList(params: TaskListRequestParams, signal?: AbortSignal): Promise<TaskInfoDataResponseBody> {
return get<TaskInfoDataResponseBody>(TaskUrlObj.task, params as Record<string, unknown>, {signal});
}
/** 提交新任务device_id 由 API 层自动注入 getDeviceId(),调用方无需传入) */
static submitTask(body: Omit<CreatTaskRequestBody, 'device_id'>): Promise<TaskInfoItemResponseBody> {
return post<TaskInfoItemResponseBody>(TaskUrlObj.task, {
...body,
device_id: getDeviceId(),
});
}
/** 获取任务详情 */
static getTaskInfo(task_id: string, signal?: AbortSignal): Promise<TaskInfoItemResponseBody> {
return get<TaskInfoItemResponseBody>(TaskUrlObj.taskInfo(task_id), undefined, {signal});
}
/** 轻量轮询任务状态 */
static getTaskStatus(task_id: string): Promise<TaskStatusResponseBody> {
return get<TaskStatusResponseBody>(TaskUrlObj.taskStatus(task_id));
}
/** 批量查询任务状态 — GET + 逗号拼接 task_ids避免 axios 默认 bracket 序列化 task_ids[]=... */
static batchTaskStatus(body: BatchTaskStatusRequestParam): Promise<BatchTaskStatusResponseBody> {
return post<BatchTaskStatusResponseBody>(TaskUrlObj.batchTaskStatus, body);
}
/** 导出任务 CSV */
static exportTaskCSV(params: ExportTaskCSVParams): Promise<File> {
return get<File>(TaskUrlObj.tasksCSV, params as unknown as Record<string, unknown>);
}
}

View File

@@ -1,160 +0,0 @@
import {post, get} from '../request';
import {type UserInfoBody} from "@/services/modules/auth";
import {type Nullable} from "@/utils/display";
// ============================================================
// VipApiUrl — VIP 模块路由常量
// ============================================================
const VipApiUrl = {
ActivateVipCode: "/api/v1/vip/activate",
GetVipLevelsList: "/api/v1/vip/levels",
VipOrders: "/api/v1/vip/orders",
MockPay: "/api/v1/vip/mock-pay",
} as const;
// ============================================================
// 激活码相关
// ============================================================
export interface ActivateVipCodeRequestParams {
code: string;
}
export type ActivateVipCodeResponseBody = UserInfoBody;
// ============================================================
// 账户类型常量
// ============================================================
export const TargetAccountType = {
Individual: "individual",
Company: "company"
} as const;
export type TargetAccountTypeValue = typeof TargetAccountType[keyof typeof TargetAccountType];
export const TargetAccountTypeLabelMap: Record<TargetAccountTypeValue, string> = {
[TargetAccountType.Individual]: "个人套餐",
[TargetAccountType.Company]: "企业套餐"
};
// ============================================================
// VIP 等级
// ============================================================
export interface VipLevelsItem {
id: number;
name: string;
level: number;
price_cent: number;
duration_days: number;
gift_balance_cent: number;
model_package_id: number;
description: Nullable<string>;
image: Nullable<string>;
is_active: boolean;
sort_order: number;
target_account_type: TargetAccountTypeValue;
seat_limit: Nullable<number>;
created_at: Date;
updated_at: Date;
}
// ============================================================
// VIP 订单
// ============================================================
/** VIP 订单状态常量 */
export const VipOrderStatus = {
Pending: "pending",
Paid: "paid",
Expired: "expired",
Cancelled: "cancelled",
} as const;
export type VipOrderStatusValue = typeof VipOrderStatus[keyof typeof VipOrderStatus];
export const VipOrderStatusLabelMap: Record<VipOrderStatusValue, string> = {
[VipOrderStatus.Pending]: "待支付",
[VipOrderStatus.Paid]: "已支付",
[VipOrderStatus.Expired]: "已过期",
[VipOrderStatus.Cancelled]: "已取消",
};
/** POST /api/v1/vip/orders — 创建购买订单 */
export interface CreateVipOrderRequest {
/** 购买的 VIP 等级 ID */
vip_level_id: number;
/** 支付渠道alipay | mock默认 alipay */
payment_channel?: string;
}
/** 订单列表查询参数 */
export interface VipOrderListParams {
/** 按状态筛选,可选 */
status?: VipOrderStatusValue;
/** 页码,默认 1 */
page?: number;
/** 每页条数,默认 20 */
page_size?: number;
}
/** VIP 订单读取 Schema */
export interface VipOrderRead {
id: string;
user_id: string;
vip_level_id: number;
amount_cent: number;
status: VipOrderStatusValue;
out_trade_no: string;
/** 支付宝/微信交易号 */
transaction_id: Nullable<string>;
/** 支付链接(扫码支付用) */
pay_url: Nullable<string>;
/** 订单过期时间 */
expires_at: Nullable<Date>;
/** 支付完成时间 */
paid_at: Nullable<Date>;
created_at: Date;
}
// ============================================================
// VipAPI — VIP 模块 API 静态方法类
// 调用方式VipAPI.activateVipCode() / VipAPI.getVipLevels() 等
// ============================================================
// TODO(human): 请确认 VipOrderStatus 的状态值和中文标签是否正确,根据实际后端逻辑补充/修改
export class VipAPI {
/** 使用激活码激活 VIP 权益 */
static async activateVipCode(data: ActivateVipCodeRequestParams): Promise<ActivateVipCodeResponseBody> {
return await post<ActivateVipCodeResponseBody>(VipApiUrl.ActivateVipCode, data);
}
/** 获取 VIP 等级列表(已登录时按账户类型过滤) */
static async getVipLevels(signal?: AbortSignal): Promise<VipLevelsItem[]> {
return await get<VipLevelsItem[]>(VipApiUrl.GetVipLevelsList, undefined, {signal});
}
/** 创建 VIP 套餐购买订单 */
static async createVipOrder(data: CreateVipOrderRequest): Promise<VipOrderRead> {
return await post<VipOrderRead>(VipApiUrl.VipOrders, data);
}
/** 查询当前用户的 VIP 订单列表 */
static async getVipOrders(params?: VipOrderListParams): Promise<VipOrderRead[]> {
return await get<VipOrderRead[]>(VipApiUrl.VipOrders, params as Record<string, unknown>);
}
/** 获取单个 VIP 订单详情 */
static async getVipOrder(orderId: string): Promise<VipOrderRead> {
const url = `${VipApiUrl.VipOrders}/${orderId}`;
return await get<VipOrderRead>(url);
}
/** 模拟支付成功(仅开发测试用) */
static async mockPay(orderId: string): Promise<VipOrderRead> {
return await get<VipOrderRead>(`${VipApiUrl.MockPay}/${orderId}`);
}
}