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:
@@ -1,210 +1,235 @@
|
||||
// ============================================================
|
||||
// AccountInfo — 账户信息展示
|
||||
// 数据来源:AuthAPI.getUserInfo() → GET /api/v1/auth/me
|
||||
// ============================================================
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import {useEffect, useState, useCallback} from 'react';
|
||||
import {
|
||||
Card,
|
||||
Descriptions,
|
||||
Button,
|
||||
Typography,
|
||||
Skeleton,
|
||||
Result,
|
||||
Space,
|
||||
Tag,
|
||||
Divider,
|
||||
theme as antTheme,
|
||||
Card,
|
||||
Descriptions,
|
||||
Button,
|
||||
Typography,
|
||||
Skeleton,
|
||||
Result,
|
||||
Space,
|
||||
Tag,
|
||||
Divider,
|
||||
theme as antTheme,
|
||||
} from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
WalletOutlined,
|
||||
CrownOutlined,
|
||||
BarChartOutlined,
|
||||
RightOutlined,
|
||||
UserOutlined,
|
||||
WalletOutlined,
|
||||
CrownOutlined,
|
||||
BarChartOutlined,
|
||||
RightOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { AuthAPI, type UserInfoBody } from '@/services/modules/auth';
|
||||
import {AuthAPI, type UserInfoBody, UserAccountTypeLabelMap} from '@/services/modules/auth';
|
||||
import {VipAPI, VipLevelsItem} from "@/services/modules/vip-api.ts";
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
const {Text, Title} = Typography;
|
||||
|
||||
/** 将分转为元,保留两位小数 */
|
||||
function centToYuan(cent: number): string {
|
||||
return (cent / 100).toFixed(2);
|
||||
return (cent / 100).toFixed(2);
|
||||
}
|
||||
|
||||
/** 格式化日期 */
|
||||
function formatDate(date: Date | string | undefined): string {
|
||||
if (!date) return '—';
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
return d.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
||||
if (!date) return '—';
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
return d.toLocaleDateString('zh-CN', {year: 'numeric', month: '2-digit', day: '2-digit'});
|
||||
}
|
||||
|
||||
export function AccountInfo() {
|
||||
const { token } = antTheme.useToken();
|
||||
const [userInfo, setUserInfo] = useState<UserInfoBody | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const {token} = antTheme.useToken();
|
||||
const [userInfo, setUserInfo] = useState<UserInfoBody | null>(null);
|
||||
const [vipsInfo, setVipsInfo] = useState<VipLevelsItem[] | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const info = await AuthAPI.getUserInfo();
|
||||
setUserInfo(info);
|
||||
} catch (err) {
|
||||
setError((err as Error).message || '加载账户信息失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const info: UserInfoBody = await AuthAPI.getUserInfo();
|
||||
const vips: VipLevelsItem[] = await VipAPI.getVipLevels();
|
||||
setVipsInfo(vips);
|
||||
setUserInfo(info);
|
||||
} catch (err) {
|
||||
setError((err as Error).message || '加载账户信息失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
// ======== 加载态 ========
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Skeleton active paragraph={{ rows: 6 }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ======== 错误态 ========
|
||||
if (error || !userInfo) {
|
||||
return (
|
||||
<Result
|
||||
status="error"
|
||||
title="加载失败"
|
||||
subTitle={error || '未能获取账户信息'}
|
||||
extra={
|
||||
<Button type="primary" onClick={load}>
|
||||
重试
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ======== 数据展示 ========
|
||||
const totalBalance = userInfo.balance_cent + userInfo.gift_balance_cent;
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24, maxWidth: 640 }}>
|
||||
{/* ======== 标题:账户 ======== */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<Space align="center" size={12}>
|
||||
<UserOutlined style={{ fontSize: 22, color: token.colorPrimary }} />
|
||||
<div>
|
||||
<Title level={4} style={{ margin: 0 }}>
|
||||
{userInfo.display_name || userInfo.username}
|
||||
</Title>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
ID: {userInfo.id}
|
||||
</Text>
|
||||
</div>
|
||||
{userInfo.role !== 'user' && (
|
||||
<Tag color="gold" style={{ lineHeight: '18px', fontSize: 11 }}>
|
||||
{userInfo.role === 'admin' ? '管理员' : '超级管理员'}
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
<Button type="primary" ghost size="small" icon={<WalletOutlined />}>
|
||||
充值
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* ======== 积分栏 ======== */}
|
||||
<Card
|
||||
size="small"
|
||||
styles={{
|
||||
body: { padding: '16px 20px' },
|
||||
}}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Text type="secondary" style={{ fontSize: 12, marginBottom: 8, display: 'block' }}>
|
||||
账户总积分
|
||||
</Text>
|
||||
<Title level={3} style={{ margin: '0 0 12px 0' }}>
|
||||
{centToYuan(totalBalance)}
|
||||
<Text type="secondary" style={{ fontSize: 14, marginLeft: 4 }}>积分</Text>
|
||||
</Title>
|
||||
<Space size={32}>
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>主积分</Text>
|
||||
<br />
|
||||
<Text strong>{centToYuan(userInfo.balance_cent)}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>赠送积分</Text>
|
||||
<br />
|
||||
<Text strong>{centToYuan(userInfo.gift_balance_cent)}</Text>
|
||||
</div>
|
||||
{userInfo.discount_rate > 0 && userInfo.discount_rate < 1 && (
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>折扣率</Text>
|
||||
<br />
|
||||
<Tag color="green">{Math.round(userInfo.discount_rate * 100)}%</Tag>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* ======== 套餐信息 ======== */}
|
||||
<Card
|
||||
size="small"
|
||||
styles={{
|
||||
body: { padding: '16px 20px' },
|
||||
}}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
|
||||
<Space size={8}>
|
||||
<CrownOutlined style={{ color: token.colorWarning }} />
|
||||
<Text strong>当前套餐</Text>
|
||||
</Space>
|
||||
<Button type="link" size="small" icon={<RightOutlined />} iconPlacement="end">
|
||||
续费 / 升级
|
||||
</Button>
|
||||
// ======== 加载态 ========
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{padding: 24}}>
|
||||
<Skeleton active paragraph={{rows: 6}} />
|
||||
</div>
|
||||
<Descriptions column={1} size="small" colon={false}>
|
||||
<Descriptions.Item label="套餐名称">
|
||||
<Text strong>{userInfo.subscription_plan || '免费版'}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="到期时间">
|
||||
<Text type={new Date(userInfo.subscription_expires_at) < new Date() ? 'danger' : undefined}>
|
||||
{formatDate(userInfo.subscription_expires_at)}
|
||||
</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="席位上限">
|
||||
{userInfo.seat_limit || '—'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
{/* ======== 今日统计 ======== */}
|
||||
<Card
|
||||
size="small"
|
||||
styles={{
|
||||
body: { padding: '16px 20px' },
|
||||
}}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Space size={8} style={{ marginBottom: 12 }}>
|
||||
<BarChartOutlined style={{ color: token.colorSuccess }} />
|
||||
<Text strong>今日使用</Text>
|
||||
</Space>
|
||||
<Text type="secondary" style={{ display: 'block', textAlign: 'center', padding: '12px 0', fontSize: 13 }}>
|
||||
统计数据将在次日更新
|
||||
</Text>
|
||||
</Card>
|
||||
// ======== 错误态 ========
|
||||
if (error || !userInfo) {
|
||||
return (
|
||||
<Result
|
||||
status="error"
|
||||
title="加载失败"
|
||||
subTitle={error || '未能获取账户信息'}
|
||||
extra={
|
||||
<Button type="primary" onClick={load}>
|
||||
重试
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
{/* ======== 查看更多权益 ======== */}
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
<Button type="link" block icon={<RightOutlined />} iconPlacement="end" style={{ color: token.colorPrimary }}>
|
||||
查看更多权益
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
// ======== 数据展示 ========
|
||||
const totalBalance: number = userInfo.balance_cent + userInfo.gift_balance_cent;
|
||||
|
||||
// 匹配当前 VIP 等级(vip_level_id → VipLevelsItem)
|
||||
const matchedVip: VipLevelsItem | null =
|
||||
userInfo.vip_level_id != null && vipsInfo
|
||||
? (vipsInfo.find((v) => v.id === userInfo.vip_level_id) ?? null)
|
||||
: null;
|
||||
|
||||
const isExpired = new Date(userInfo.software_expires_at) < new Date();
|
||||
return (
|
||||
<div style={{padding: 24, maxWidth: 640}}>
|
||||
{/* ======== 标题:账户 ======== */}
|
||||
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16}}>
|
||||
<Space align="center" size={12}>
|
||||
<UserOutlined style={{fontSize: 22, color: token.colorPrimary}} />
|
||||
<div>
|
||||
<Title level={4} style={{margin: 0}}>
|
||||
{userInfo.display_name || userInfo.real_name || userInfo.username || userInfo.id}
|
||||
{userInfo.account_type && (
|
||||
<Tag color="gold" style={{lineHeight: '18px', fontSize: 11}}>
|
||||
{UserAccountTypeLabelMap[userInfo.account_type]}
|
||||
</Tag>
|
||||
)}
|
||||
</Title>
|
||||
<Text type="secondary" style={{fontSize: 12}}>
|
||||
ID: {userInfo.id}
|
||||
</Text>
|
||||
</div>
|
||||
</Space>
|
||||
<Button type="primary" ghost size="small" icon={<WalletOutlined />}>
|
||||
充值
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* ======== 积分栏 ======== */}
|
||||
<Card
|
||||
size="small"
|
||||
styles={{
|
||||
body: {padding: '16px 20px'},
|
||||
}}
|
||||
style={{marginBottom: 16}}
|
||||
>
|
||||
<Text type="secondary" style={{fontSize: 12, marginBottom: 8, display: 'block'}}>
|
||||
账户总积分
|
||||
</Text>
|
||||
<Title level={3} style={{margin: '0 0 12px 0'}}>
|
||||
{centToYuan(totalBalance)}
|
||||
<Text type="secondary" style={{fontSize: 14, marginLeft: 4}}>积分</Text>
|
||||
</Title>
|
||||
<Space size={32}>
|
||||
<div>
|
||||
<Text type="secondary" style={{fontSize: 12}}>主积分</Text>
|
||||
<br />
|
||||
<Text strong>{centToYuan(userInfo.balance_cent)}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text type="secondary" style={{fontSize: 12}}>赠送积分</Text>
|
||||
<br />
|
||||
<Text strong>{centToYuan(userInfo.gift_balance_cent)}</Text>
|
||||
</div>
|
||||
{userInfo.discount_rate && userInfo.discount_rate > 0 && userInfo.discount_rate < 1 && (
|
||||
<div>
|
||||
<Text type="secondary" style={{fontSize: 12}}>折扣率</Text>
|
||||
<br />
|
||||
<Tag color="green">{Math.round(userInfo.discount_rate * 100)}%</Tag>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* ======== 套餐信息 ======== */}
|
||||
<Card
|
||||
size="small"
|
||||
styles={{
|
||||
body: {padding: '16px 20px'},
|
||||
}}
|
||||
style={{marginBottom: 16}}
|
||||
>
|
||||
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12}}>
|
||||
<Space size={8}>
|
||||
<CrownOutlined style={{color: token.colorWarning}} />
|
||||
<Text strong>当前套餐</Text>
|
||||
</Space>
|
||||
<Button type="link" size="small" icon={<RightOutlined />} iconPlacement="end">
|
||||
续费 / 升级
|
||||
</Button>
|
||||
</div>
|
||||
<Descriptions column={1} size="small" colon={false}>
|
||||
<Descriptions.Item label="套餐名称">
|
||||
<Space size={8}>
|
||||
<Text strong>
|
||||
{matchedVip?.name || userInfo.subscription_plan || '免费版'}
|
||||
</Text>
|
||||
{matchedVip?.level != null && (
|
||||
<Tag color="gold" style={{fontSize: 11, lineHeight: '18px'}}>
|
||||
Lv.{matchedVip.level}
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
{matchedVip?.description && (
|
||||
<Descriptions.Item label="套餐说明">
|
||||
<Text type="secondary" style={{fontSize: 12}}>
|
||||
{matchedVip.description}
|
||||
</Text>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
<Descriptions.Item label="套餐周期">
|
||||
{matchedVip?.duration_days ? `${matchedVip.duration_days} 天` : '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="到期时间">
|
||||
<Text type={isExpired ? 'danger' : undefined}>
|
||||
{formatDate(userInfo.software_expires_at)}
|
||||
</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="席位上限">
|
||||
{matchedVip?.seat_limit ?? userInfo.seat_limit ?? '—'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
{/* ======== 今日统计 ======== */}
|
||||
<Card
|
||||
size="small"
|
||||
styles={{
|
||||
body: {padding: '16px 20px'},
|
||||
}}
|
||||
style={{marginBottom: 16}}
|
||||
>
|
||||
<Space size={8} style={{marginBottom: 12}}>
|
||||
<BarChartOutlined style={{color: token.colorSuccess}} />
|
||||
<Text strong>今日使用</Text>
|
||||
</Space>
|
||||
<Text type="secondary" style={{display: 'block', textAlign: 'center', padding: '12px 0', fontSize: 13}}>
|
||||
统计数据将在次日更新
|
||||
</Text>
|
||||
</Card>
|
||||
|
||||
{/* ======== 查看更多权益 ======== */}
|
||||
<Divider style={{margin: '8px 0'}} />
|
||||
<Button type="link" block icon={<RightOutlined />} iconPlacement="end" style={{color: token.colorPrimary}}>
|
||||
查看更多权益
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user