# 扩展可视化倍率设置设计文档 > **创建日期**:2026-06-04 > **状态**:待实施 > **相关组件**:ModelSettingsVisualEditor.jsx ## 需求概述 在现有的 `ModelSettingsVisualEditor` 组件中添加高级倍率字段支持,允许用户为每个模型单独设置缓存、图片、音频相关的倍率。 ### 当前限制 现有的可视化倍率设置只能设置: - 模型倍率 - 补全倍率 ### 目标 新增以下5个高级倍率字段: - 缓存读取倍率 - 缓存创建倍率 - 图片倍率 - 音频输入倍率 - 音频输出倍率 ## 设计方案 ### 界面设计 #### 主表格(保持不变) 表格列结构保持现有的简洁布局: | 列名 | 说明 | |------|------| | 模型名称 | 显示模型名称,如有矛盾显示红色标签 | | 模型固定价格 | 按次计费的价格输入框 | | 模型倍率 | 按量计费的模型倍率 | | 补全倍率 | 按量计费的补全倍率 | | 操作 | 编辑、删除按钮 | #### 编辑弹窗(新增"高级比例"区域) 在现有的表单结构中,在"定价模式"和"价格设置方式"区域之后,增加一个新的表单区域: ```jsx {pricingMode === 'per-token' && ( <> {/* 现有的价格设置方式区域 */} ... {/* 新增:高级比例区域 */} )} ``` **设计决策**: - 只在"按量计费"模式下显示高级比例字段 - 按次计费模式不需要这些倍率 - 使用 `Form.InputNumber` 确保输入的是有效数字 - `placeholder` 显示默认值提示 - `step={0.01}` 允许小数点后两位精度 ### 数据结构 #### 内部状态扩展 在 `models` 状态数组中,每个模型对象增加5个字段: ```javascript { name: 'gpt-4', price: '', ratio: '', completionRatio: '', cacheRatio: '', // 新增 cacheCreationRatio: '', // 新增 imageRatio: '', // 新增 audioRatio: '', // 新增 audioCompletionRatio: '', // 新增 hasConflict: false } ``` #### 数据初始化 ```javascript useEffect(() => { try { const modelPrice = JSON.parse(props.options.ModelPrice || '{}'); const modelRatio = JSON.parse(props.options.ModelRatio || '{}'); const completionRatio = JSON.parse(props.options.CompletionRatio || '{}'); // 新增:解析高级倍率字段 const cacheRatio = JSON.parse(props.options.CacheRatio || '{}'); const createCacheRatio = JSON.parse(props.options.CreateCacheRatio || '{}'); const imageRatio = JSON.parse(props.options.ImageRatio || '{}'); const audioRatio = JSON.parse(props.options.AudioRatio || '{}'); const audioCompletionRatio = JSON.parse(props.options.AudioCompletionRatio || '{}'); // 合并所有模型名称 const modelNames = new Set([ ...Object.keys(modelPrice), ...Object.keys(modelRatio), ...Object.keys(completionRatio), ...Object.keys(cacheRatio), ...Object.keys(createCacheRatio), ...Object.keys(imageRatio), ...Object.keys(audioRatio), ...Object.keys(audioCompletionRatio), ]); const modelData = Array.from(modelNames).map((name) => { return { name, price: modelPrice[name] === undefined ? '' : modelPrice[name], ratio: modelRatio[name] === undefined ? '' : modelRatio[name], completionRatio: completionRatio[name] === undefined ? '' : completionRatio[name], cacheRatio: cacheRatio[name] === undefined ? '' : cacheRatio[name], cacheCreationRatio: createCacheRatio[name] === undefined ? '' : createCacheRatio[name], imageRatio: imageRatio[name] === undefined ? '' : imageRatio[name], audioRatio: audioRatio[name] === undefined ? '' : audioRatio[name], audioCompletionRatio: audioCompletionRatio[name] === undefined ? '' : audioCompletionRatio[name], hasConflict: modelPrice[name] !== '' && (modelRatio[name] !== '' || completionRatio[name] !== ''), }; }); setModels(modelData); } catch (error) { console.error('JSON解析错误:', error); } }, [props.options]); ``` #### API 请求结构 保存时将5个新增字段序列化到对应的 JSON 字段: ```javascript const SubmitData = async () => { setLoading(true); const output = { ModelPrice: {}, ModelRatio: {}, CompletionRatio: {}, CacheRatio: {}, // 新增 CreateCacheRatio: {}, // 新增 ImageRatio: {}, // 新增 AudioRatio: {}, // 新增 AudioCompletionRatio: {} // 新增 }; try { // 数据转换 models.forEach((model) => { if (model.price !== '') { output.ModelPrice[model.name] = parseFloat(model.price); } else { if (model.ratio !== '') output.ModelRatio[model.name] = parseFloat(model.ratio); if (model.completionRatio !== '') output.CompletionRatio[model.name] = parseFloat(model.completionRatio); } // 新增:处理高级倍率字段 if (model.cacheRatio !== '') output.CacheRatio[model.name] = parseFloat(model.cacheRatio); if (model.cacheCreationRatio !== '') output.CreateCacheRatio[model.name] = parseFloat(model.cacheCreationRatio); if (model.imageRatio !== '') output.ImageRatio[model.name] = parseFloat(model.imageRatio); if (model.audioRatio !== '') output.AudioRatio[model.name] = parseFloat(model.audioRatio); if (model.audioCompletionRatio !== '') output.AudioCompletionRatio[model.name] = parseFloat(model.audioCompletionRatio); }); // 准备API请求数组 const finalOutput = { ModelPrice: JSON.stringify(output.ModelPrice, null, 2), ModelRatio: JSON.stringify(output.ModelRatio, null, 2), CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2), CacheRatio: JSON.stringify(output.CacheRatio, null, 2), CreateCacheRatio: JSON.stringify(output.CreateCacheRatio, null, 2), ImageRatio: JSON.stringify(output.ImageRatio, null, 2), AudioRatio: JSON.stringify(output.AudioRatio, null, 2), AudioCompletionRatio: JSON.stringify(output.AudioCompletionRatio, null, 2), }; const requestQueue = Object.entries(finalOutput).map(([key, value]) => { return API.put('/api/option/', { key, value }); }); // 批量处理请求 const results = await Promise.all(requestQueue); // 验证结果(保持现有逻辑) // ... } catch (error) { // 错误处理 } finally { setLoading(false); } }; ``` ### 交互逻辑 #### 1. 编辑模型 编辑现有模型时,需要在弹窗中正确回显高级倍率字段的值: ```javascript const editModel = (record) => { setIsEditMode(true); // 现有的定价模式设置逻辑 // ... // 创建模型副本 const modelCopy = { ...record }; // 如果有倍率数据,转换为 token price(现有逻辑) if (record.ratio) { modelCopy.tokenPrice = calculateTokenPriceFromRatio(parseFloat(record.ratio)).toString(); if (record.completionRatio) { modelCopy.completionTokenPrice = ( parseFloat(modelCopy.tokenPrice) * parseFloat(record.completionRatio) ).toString(); } } // 设置当前模型 setCurrentModel(modelCopy); setVisible(true); // 设置表单值 setTimeout(() => { if (formRef.current) { formRef.current.setValues({ name: modelCopy.name, // 现有字段 ratioInput: modelCopy.ratio, completionRatioInput: modelCopy.completionRatio, modelTokenPrice: modelCopy.tokenPrice, completionTokenPrice: modelCopy.completionTokenPrice, // 新增:高级倍率字段 cacheRatio: modelCopy.cacheRatio, cacheCreationRatio: modelCopy.cacheCreationRatio, imageRatio: modelCopy.imageRatio, audioRatio: modelCopy.audioRatio, audioCompletionRatio: modelCopy.audioCompletionRatio, }); } }, 0); }; ``` #### 2. 添加/更新模型 在 `addOrUpdateModel` 函数中处理高级倍率字段: ```javascript const addOrUpdateModel = (values) => { const existingModelIndex = models.findIndex( (model) => model.name === values.name, ); const updatedModel = { name: values.name, price: values.price || '', ratio: values.ratio || '', completionRatio: values.completionRatio || '', // 新增:高级倍率字段 cacheRatio: values.cacheRatio || '', cacheCreationRatio: values.cacheCreationRatio || '', imageRatio: values.imageRatio || '', audioRatio: values.audioRatio || '', audioCompletionRatio: values.audioCompletionRatio || '', }; updatedModel.hasConflict = updatedModel.price !== '' && (updatedModel.ratio !== '' || updatedModel.completionRatio !== ''); if (existingModelIndex >= 0) { // 更新现有模型 setModels((prev) => prev.map((model, index) => index !== existingModelIndex ? model : updatedModel ) ); setVisible(false); showSuccess(t('更新成功')); } else { // 添加新模型 if (models.some((model) => model.name === values.name)) { showError(t('模型名称已存在')); return; } setModels((prev) => [updatedModel, ...prev]); setVisible(false); showSuccess(t('添加成功')); } }; ``` #### 3. 输入验证 在 `updateModel` 函数中增加对高级倍率字段的验证: ```javascript const updateModel = (name, field, value) => { if (isNaN(value) && value !== '') { // 允许空字符串 showError(t('请输入数字')); return; } setModels((prev) => prev.map((model) => { if (model.name !== name) return model; const updated = { ...model, [field]: value }; updated.hasConflict = updated.price !== '' && (updated.ratio !== '' || updated.completionRatio !== ''); return updated; }), ); }; ``` ### 默认值处理策略 **前端处理默认值**:用户输入框留空时,保存为空字符串,后端使用全局默认值。 **默认值映射**: - 缓存读取倍率:1.0 - 缓存创建倍率:1.25 - 图片倍率:1.0 - 音频输入倍率:1.0 - 音频输出倍率:1.0 **实现方式**: - 输入框使用 `placeholder` 提示默认值 - 提交时空字符串保持为空,不转换 - 后端处理空值时返回对应的默认值 ### 模型能力处理 **策略**:全部显示,用户自行判断 - 所有5个高级倍率字段始终显示在编辑弹窗中 - 用户根据自己使用的模型判断是否填写相应字段 - 不在前端进行模型能力验证或字段隐藏 **设计理由**: - 简化实现,避免维护复杂的模型能力映射 - 给用户更多控制权,允许为任意模型设置任意倍率 - 与渠道价格设置的实现方式保持一致 ## 实现要点 ### 1. 字段映射 确保前端字段名与后端 `options` 表中的 key 一致: | 前端字段名 | 后端 Option Key | 默认值 | |-----------|----------------|--------| | cacheRatio | CacheRatio | 1.0 | | cacheCreationRatio | CreateCacheRatio | 1.25 | | imageRatio | ImageRatio | 1.0 | | audioRatio | AudioRatio | 1.0 | | audioCompletionRatio | AudioCompletionRatio | 1.0 | ### 2. 数值验证 - 输入时验证为有效数字:`if (isNaN(value) && value !== '')` - 范围验证:`min={0}` 确保非负数 - 小数精度:`step={0.01}` 允许两位小数 ### 3. 空值处理 - 空字符串视为未设置,保存时保持为空字符串 - 后端处理时如果值为空,返回对应的全局默认值 - 前端解析 JSON 时,如果字段不存在,默认为空字符串:`cacheRatio[name] === undefined ? '' : cacheRatio[name]` ### 4. 样式统一 - 使用与现有字段一致的 `Form.InputNumber` 组件 - 保持相同的 `label`、`placeholder` 样式 - 表单区域使用 `Form.Section` 包裹,标题为"高级比例(留空使用默认值)" ### 5. 国际化支持 需要为新增的文本添加翻译: **中文 (zh-CN.json)**: ```json { "高级比例(留空使用默认值)": "高级比例(留空使用默认值)", "缓存读取倍率": "缓存读取倍率", "缓存创建倍率(5分钟)": "缓存创建倍率(5分钟)", "图片倍率": "图片倍率", "音频输入倍率": "音频输入倍率", "音频输出倍率": "音频输出倍率", "默认值:1.0": "默认值:1.0", "默认值:1.25": "默认值:1.25" } ``` **英文 (en.json)**: ```json { "高级比例(留空使用默认值)": "Advanced Ratios (Leave empty for defaults)", "缓存读取倍率": "Cache Read Ratio", "缓存创建倍率(5分钟)": "Cache Creation Ratio (5min)", "图片倍率": "Image Ratio", "音频输入倍率": "Audio Input Ratio", "音频输出倍率": "Audio Output Ratio", "默认值:1.0": "Default: 1.0", "默认值:1.25": "Default: 1.25" } ``` ## 测试要点 ### 1. 数据初始化测试 - 测试从空 options 初始化,确保所有字段为空字符串 - 测试从已有数据初始化,确保正确解析所有倍率值 ### 2. 编辑功能测试 - 测试编辑现有模型,确保高级倍率字段正确回显 - 测试添加新模型,确保所有字段可以正常输入 - 测试删除模型,确保相关数据正确清理 ### 3. 保存功能测试 - 测试只填写部分高级倍率字段,确保正确保存 - 测试所有高级倍率字段都留空,确保保存为空字符串 - 测试填写数值超出范围,确保验证生效 ### 4. 默认值测试 - 测试留空字段,确保后端使用默认值 - 测试输入 0,确保保存为数值 0 而不是默认值 ### 5. 边界情况测试 - 测试输入负数,确保被拒绝(min={0}) - 测试输入非数字字符,确保显示错误提示 - 测试输入非常多小数位,确保 step={0.01} 生效 ## 参考实现 本设计参考了 `feat/channel-pricing-extended` 分支中的 `ChannelPricingView.jsx` 实现,特别是: 1. **表单结构**:使用 `Form.Section` 包裹高级比例字段 2. **字段命名**:使用与后端 Option Key 一致的字段名 3. **默认值处理**:使用 placeholder 提示默认值,空字符串表示使用默认值 4. **输入验证**:使用 `Form.InputNumber` 配合 `min` 和 `step` 属性 ## 实施检查清单 - [ ] 修改 `ModelSettingsVisualEditor.jsx`,增加高级比例表单区域 - [ ] 更新数据初始化逻辑,解析新增的5个 JSON 字段 - [ ] 更新保存逻辑,序列化并提交新增的5个字段 - [ ] 更新编辑模型逻辑,正确回显高级倍率字段 - [ ] 更新添加/更新模型逻辑,处理高级倍率字段 - [ ] 添加输入验证,确保输入有效数字 - [ ] 添加国际化文本(中英文) - [ ] 测试数据初始化、编辑、保存功能 - [ ] 测试边界情况和默认值处理