feat: 前后端联调 — 任务创建 + 账户信息 + 预估花费 (0.0.18)

- task: device_id 改为必填参数,API 层 Omit 封装 + getDeviceId() 自动注入
- pricing.ts: Python 后端 calculate_cost 完整移植,六种定价模式 + 6 个 Bug 修复
  (浮点冗余/维度匹配不一致/null 守卫/matrix 数据提取/unit_scale 等)
- ModelInputForm: 三层兜底预估消费展示 + React Hooks 顺序修复
- AccountInfo: VIP 等级列表匹配,套餐详情展示(说明/周期/席位/到期)
- 模型列表加密缓存 (safe-storage, stale-while-revalidate)
- VIP API 模块 (激活码/等级/订单/模拟支付)
- device_id 统一使用 UUID v4 持久化 (与 login/register 对齐)
This commit is contained in:
2026-06-10 19:38:26 +08:00
parent 848fdeca3d
commit 95019316b2
19 changed files with 1681 additions and 292 deletions

View File

@@ -33,6 +33,12 @@ import { emit, EVENTS } from '@/utils/event-bus';
import { OutputTypeMap } from '@/services/modules/task';
import type { OutputTypeString, TaskInfoItemResponseBody } from '@/services/modules/task';
import { useModelUI, isValueEmpty, type UIFieldConfig } from './useModelUI';
import {
calculateCost,
formatYuan,
type ModelDefinition,
type ModelPricingConfig,
} from '@/utils/pricing';
const { Text, Title } = Typography;
@@ -108,6 +114,19 @@ const EMPTY_ARRAY: never[] = [];
/** 稳定空对象引用,避免每次渲染创建新 {} 导致 useMemo 失效 */
const EMPTY_OBJ: Record<string, unknown> = {};
/**
* 从自由格式 JSON 中安全读取数字(兼容 int / float / string
*/
function safeNum(v: unknown): number | null {
if (v === null || v === undefined) return null;
if (typeof v === 'number' && !Number.isNaN(v)) return v;
if (typeof v === 'string') {
const n = Number(v.trim());
return Number.isNaN(n) ? null : n;
}
return null;
}
// ---------- 主组件 ----------
export function ModelInputForm() {
@@ -117,6 +136,9 @@ export function ModelInputForm() {
const [form] = Form.useForm();
const [taskTag, setTaskTag] = useState('');
// 跟踪表单当前值(用于动态计价,每次字段变化时更新)
const [currentValues, setCurrentValues] = useState<Record<string, unknown>>({});
// 提交突变 Hook
const { execute: submitTask, loading: submitting, errorMessage } = useSubmitTask({
onSuccess: useCallback((_data: TaskInfoItemResponseBody) => {
@@ -151,6 +173,10 @@ export function ModelInputForm() {
if (selectedModel && Object.keys(initialValues).length > 0) {
form.setFieldsValue(initialValues);
setTaskTag('');
// 同步价格计算:切换模型时立即用默认值更新预估
setCurrentValues(initialValues);
} else if (!selectedModel) {
setCurrentValues({});
}
}, [selectedModel?.id, initialValues, selectedModel, form]);
@@ -159,6 +185,7 @@ export function ModelInputForm() {
form.resetFields();
form.setFieldsValue(initialValues);
setTaskTag('');
setCurrentValues(initialValues);
}, [form, initialValues]);
// ======== 提交 ========
@@ -212,6 +239,57 @@ export function ModelInputForm() {
}
};
// ======== 动态预估消费金额(必须在所有提前返回之前) ========
const estimatedCostDisplay: string = useMemo(() => {
const rawCfg = selectedModel?.pricing_config;
if (!rawCfg) return '消费';
const cfg = rawCfg as unknown as ModelPricingConfig;
// ═══ 第一层:完整动态计价(覆盖全部 6 种模式) ═══
const modelDef: ModelDefinition = {
id: selectedModel.id,
name: selectedModel.name,
pricing_config: cfg,
};
const cost = calculateCost(modelDef, currentValues);
if (cost !== null && cost !== undefined) {
if (cost === 0) return '消费 ¥0免费';
return `消费 ¥${formatYuan(cost)}`;
}
// ═══ 第二层兜底提取基础价格calculateCost 失败时) ═══
const rawObj = cfg.raw as Record<string, unknown> | undefined;
const fallbackPrice =
safeNum(cfg.price) ??
safeNum(cfg.price_cent != null ? cfg.price_cent / 100 : null) ??
safeNum(rawObj?.price) ??
safeNum(rawObj?.base_price);
const unitName =
(rawObj?.unit_name as string) ||
(rawObj?.unit as string) ||
undefined;
if (fallbackPrice !== null) {
const formatted = formatYuan(fallbackPrice);
if (unitName) return `消费 ¥${formatted}/${unitName}`;
if (fallbackPrice > 0) return `消费约 ¥${formatted}`;
return `消费 ¥${formatted}`;
}
if (cfg.price_per_k != null) return `消费 ¥${formatYuan(cfg.price_per_k)}/千字符`;
if (cfg.price_per_k_cent != null) return `消费 ¥${formatYuan(cfg.price_per_k_cent / 100)}/千字符`;
const modeLabel = cfg.mode || cfg.price_type || cfg.billing_mode;
if (modeLabel) return `消费(${modeLabel}`;
return '消费';
}, [selectedModel, currentValues]);
// ======== 未选择模型 ========
if (!selectedModel) {
@@ -233,7 +311,6 @@ export function ModelInputForm() {
const metaConfig = (selectedModel.meta_config || {}) as Record<string, unknown>;
const uiConfig = (selectedModel.ui_config || {}) as Record<string, unknown>;
const pricingConfig = (selectedModel.pricing_config || {}) as Record<string, unknown>;
const outputType = metaConfig.output_type as OutputTypeString | undefined;
const estimatedDuration = metaConfig.estimated_duration_seconds as number | undefined;
@@ -241,11 +318,6 @@ export function ModelInputForm() {
const group = uiConfig.group as string | undefined;
const helpUrl = metaConfig.help_url as string | undefined;
const priceUnit = (pricingConfig.unit as Record<string, unknown>)?.name as string | undefined;
const price = pricingConfig.price as number | undefined;
const priceType = pricingConfig.price_type as string | undefined;
// ======== 正常渲染 ========
return (
<div
@@ -322,6 +394,7 @@ export function ModelInputForm() {
<Form
form={form}
layout="vertical"
onValuesChange={(_, allValues) => setCurrentValues(allValues)}
>
{fields.map((field) => (
<Form.Item
@@ -401,9 +474,7 @@ export function ModelInputForm() {
flexShrink: 0,
}}
>
{priceType === 'SIMPLE' && price !== undefined
? `消费 ¥${price}/${priceUnit || '次'}`
: '消费'}
{estimatedCostDisplay}
</Button>
</div>
</div>