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:
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
// 设备标识(网卡 MAC),8~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);
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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];
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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_at(ISO 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>);
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user