469 lines
28 KiB
HTML
469 lines
28 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>计价配置管理 · 原型预览</title>
|
||
<style>
|
||
:root{
|
||
--primary:#409eff; --primary-light:#ecf5ff; --danger:#f56c6c; --success:#67c23a;
|
||
--text:#303133; --text2:#606266; --text3:#909399; --border:#dcdfe6; --bg:#f5f7fa; --card:#fff;
|
||
}
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{font-family:"PingFang SC","Microsoft YaHei",system-ui,sans-serif;background:var(--bg);color:var(--text);font-size:14px}
|
||
.topbar{background:#fff;border-bottom:1px solid var(--border);padding:14px 24px;display:flex;align-items:center;justify-content:space-between}
|
||
.topbar h1{font-size:18px;font-weight:600}
|
||
.topbar .tag{font-size:12px;color:var(--text3);margin-left:12px}
|
||
.topbar .badge-proto{background:var(--primary-light);color:var(--primary);border-radius:4px;padding:3px 8px;font-size:12px}
|
||
.layout{display:flex;gap:16px;padding:16px 24px;align-items:flex-start}
|
||
.panel{background:var(--card);border:1px solid var(--border);border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.04)}
|
||
/* 左侧枚举列表 */
|
||
.sidebar{width:300px;flex-shrink:0;overflow:hidden}
|
||
.sidebar .p-head{padding:12px 16px;border-bottom:1px solid var(--border);font-weight:600;display:flex;justify-content:space-between;align-items:center}
|
||
.sidebar .p-head .hint{font-size:12px;color:var(--text3);font-weight:400}
|
||
.grp-title{padding:10px 16px 4px;font-size:12px;color:var(--text3)}
|
||
.subj{display:flex;align-items:center;gap:8px;padding:10px 16px;cursor:pointer;border-left:3px solid transparent;transition:.15s}
|
||
.subj:hover{background:var(--primary-light)}
|
||
.subj.active{background:var(--primary-light);border-left-color:var(--primary)}
|
||
.subj .name{flex:1;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.subj .type-badge{font-size:11px;border-radius:3px;padding:1px 6px;color:#fff;flex-shrink:0}
|
||
.subj .conf-badge{font-size:11px;border-radius:3px;padding:1px 6px;flex-shrink:0}
|
||
.conf-yes{background:#f0f9eb;color:var(--success)} .conf-no{background:#fef0f0;color:var(--danger)}
|
||
.t-reason{background:#e6a23c}.t-chat{background:#409eff}.t-audio{background:#67c23a}.t-video{background:#909399}.t-workflow{background:#8e44ad}.t-business{background:#e74c3c}
|
||
.mode-chip{font-size:11px;background:#f4f4f5;color:var(--text2);border-radius:3px;padding:1px 5px}
|
||
/* 右侧编辑区 */
|
||
.editor{flex:1;min-width:0;overflow:hidden}
|
||
.e-head{padding:14px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
||
.e-head .st{color:var(--text3);font-size:12px}
|
||
.e-head .modes{margin-top:4px}
|
||
.e-body{padding:20px;display:flex;gap:20px;flex-wrap:wrap}
|
||
.col{flex:1;min-width:380px}
|
||
.card{border:1px solid var(--border);border-radius:6px;margin-bottom:16px;overflow:hidden}
|
||
.card .c-head{background:#fafafa;padding:10px 14px;font-weight:600;display:flex;align-items:center;justify-content:space-between;font-size:14px}
|
||
.card .c-head .st{font-size:12px;color:var(--text3);font-weight:400}
|
||
.card .c-body{padding:14px}
|
||
.f-row{display:flex;gap:12px;margin-bottom:10px;align-items:center;flex-wrap:wrap}
|
||
.f{display:flex;flex-direction:column;gap:4px}
|
||
.f label{font-size:12px;color:var(--text2)}
|
||
.f input,.f select{height:30px;border:1px solid var(--border);border-radius:4px;padding:0 8px;font-size:13px;outline:none;background:#fff}
|
||
.f input:focus,.f select:focus{border-color:var(--primary)}
|
||
.f input.wide{width:220px}
|
||
.f input.num{width:110px}
|
||
.tbl{width:100%;border-collapse:collapse}
|
||
.tbl th,.tbl td{border:1px solid var(--border);padding:5px 8px;font-size:13px;text-align:left}
|
||
.tbl th{background:#fafafa;font-weight:500;color:var(--text2)}
|
||
.tbl input{width:100%;border:none;outline:none;font-size:13px;padding:2px}
|
||
.tbl input:focus{background:var(--primary-light)}
|
||
.btn{height:30px;padding:0 14px;border-radius:4px;border:1px solid var(--border);background:#fff;color:var(--text);cursor:pointer;font-size:13px}
|
||
.btn:hover{color:var(--primary);border-color:var(--primary)}
|
||
.btn.primary{background:var(--primary);border-color:var(--primary);color:#fff}
|
||
.btn.primary:hover{background:#66b1ff}
|
||
.btn.mini{height:24px;padding:0 8px;font-size:12px}
|
||
.btn.danger:hover{color:var(--danger);border-color:var(--danger)}
|
||
.hint{font-size:12px;color:var(--text3)}
|
||
.switch{position:relative;display:inline-block;width:40px;height:20px}
|
||
.switch input{opacity:0;width:0;height:0}
|
||
.slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:#ccc;transition:.2s;border-radius:10px}
|
||
.slider:before{position:absolute;content:"";height:16px;width:16px;left:2px;bottom:2px;background:#fff;transition:.2s;border-radius:50%}
|
||
input:checked+.slider{background:var(--primary)}
|
||
input:checked+.slider:before{transform:translateX(20px)}
|
||
.json{width:100%;min-height:320px;font-family:Consolas,Menlo,monospace;font-size:12.5px;border:1px solid var(--border);border-radius:6px;padding:12px;background:#1e1e1e;color:#d4d4d4;white-space:pre;overflow:auto;line-height:1.55}
|
||
.json .p-key{color:#9cdcfe}.json .p-str{color:#ce9178}.json .p-num{color:#b5cea8}.json .p-bool{color:#569cd6}.json .p-null{color:#6a9955}
|
||
.note{border:1px solid #e6a23c;background:#fdf6ec;color:#b88230;border-radius:6px;padding:10px 14px;font-size:12.5px;line-height:1.7;margin-bottom:16px}
|
||
.toast{position:fixed;top:20px;left:50%;transform:translateX(-50%);background:var(--success);color:#fff;padding:10px 20px;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.15);opacity:0;transition:.3s;z-index:99;font-size:14px}
|
||
.toast.show{opacity:1}
|
||
.section-title{font-size:13px;color:var(--text2);margin:18px 0 10px;display:flex;align-items:center;gap:6px}
|
||
.section-title::before{content:"";width:3px;height:14px;background:var(--primary);border-radius:2px}
|
||
.api-box{margin-top:16px}
|
||
.rule-card{border:1px solid var(--border);border-radius:6px;margin-bottom:10px;padding:12px}
|
||
.rule-card .rc-head{display:flex;align-items:center;gap:8px;margin-bottom:10px}
|
||
.rule-card .rc-head input{flex:1}
|
||
.rc-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px}
|
||
.money-note{font-size:12px;color:var(--text3);margin-bottom:8px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="topbar">
|
||
<h1>计价配置管理 <span class="tag">/pricing · 原型预览</span></h1>
|
||
<span class="badge-proto">原型预览 · 静态无后端</span>
|
||
</div>
|
||
|
||
<div class="layout">
|
||
|
||
<!-- ============ 左侧:计价对象枚举 ============ -->
|
||
<div class="panel sidebar">
|
||
<div class="p-head">计价对象 <span class="hint">GET /pricing/subjects</span></div>
|
||
<div class="grp-title">工作流</div>
|
||
<div id="subj-workflow"></div>
|
||
<div class="grp-title">模型(实时拉取 model-gateway 系统模型)</div>
|
||
<div id="subj-models"></div>
|
||
<div class="grp-title">业务模块</div>
|
||
<div id="subj-business"></div>
|
||
</div>
|
||
|
||
<!-- ============ 右侧:配置编辑 ============ -->
|
||
<div class="panel editor">
|
||
<div class="e-head">
|
||
<div>
|
||
<div style="font-size:15px;font-weight:600" id="ed-title">工作流</div>
|
||
<div class="st" id="ed-sub">subjectType=workflow · subjectId=workflow</div>
|
||
</div>
|
||
<div>
|
||
<button class="btn" onclick="loadApiExample()">枚举响应示例</button>
|
||
<button class="btn primary" onclick="saveMock()">保存配置</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 工作流编辑 -->
|
||
<div class="e-body" id="ed-workflow" style="display:none">
|
||
<div class="col">
|
||
<div class="note">工作流可同时配置 <b>按条 / 按秒 / 按token</b> 三套价格。创建/使用工作流时,从这些配置中<b>选择本次执行的扣费方式</b>。只配置的模式才会出现在 rules 里。</div>
|
||
|
||
<!-- per_item -->
|
||
<div class="card">
|
||
<div class="c-head">按条计费 per_item
|
||
<span class="st"><label class="switch"><input type="checkbox" id="w-item-on" onchange="renderWorkflow()"><span class="slider"></span></label></span>
|
||
</div>
|
||
<div class="c-body">
|
||
<div class="hint">档位开放列表,上不封顶。命中首个 <code>maxSec ≥ 时长</code> 的档;超出最大档按 overflowUnitPrice 线性叠加。</div>
|
||
<table class="tbl" style="margin:10px 0">
|
||
<tr><th>档位最大秒数 maxSec</th><th>价格(元)</th><th></th></tr>
|
||
<tbody id="w-item-tiers"></tbody>
|
||
</table>
|
||
<button class="btn mini" onclick="addTier()">+ 加档</button>
|
||
<div class="f-row" style="margin-top:10px">
|
||
<div class="f"><label>超出最大档单价(元/秒)</label><input class="num" id="w-item-overflow" type="number" step="0.01" value="0.9" oninput="renderWorkflow()"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- per_second -->
|
||
<div class="card">
|
||
<div class="c-head">按秒计费 per_second
|
||
<span class="st"><label class="switch"><input type="checkbox" id="w-sec-on" onchange="renderWorkflow()"><span class="slider"></span></label></span>
|
||
</div>
|
||
<div class="c-body">
|
||
<div class="hint">按秒向上取整(不满 1 秒按 1 秒),再乘单价。</div>
|
||
<div class="f-row" style="margin-top:10px">
|
||
<div class="f"><label>单价(元/秒)</label><input class="num" id="w-sec-unit" type="number" step="0.01" value="0.9" oninput="renderWorkflow()"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- per_token -->
|
||
<div class="card">
|
||
<div class="c-head">按token计费 per_token
|
||
<span class="st"><label class="switch"><input type="checkbox" id="w-tok-on" onchange="renderWorkflow()"><span class="slider"></span></label></span>
|
||
</div>
|
||
<div class="c-body">
|
||
<div class="note" style="margin-bottom:0">价格<b>取自各模型自己的计价配置</b>(模型列表里配的那份),不在此重复配置。未配价的模型按 0 计。⚠️ per_token 费率不冻结在快照,结算时按模型实时价。</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col">
|
||
<div class="section-title">公共配置</div>
|
||
<div class="card"><div class="c-body">
|
||
<div class="f-row">
|
||
<div class="f"><label>门禁 min_balance(元)</label><input class="num" id="w-minbalance" type="number" step="0.01" value="0" oninput="renderWorkflow()"></div>
|
||
<div class="f"><label>币种</label><select id="w-currency" onchange="renderWorkflow()"><option value="CNY" selected>CNY</option><option value="USD">USD</option></select></div>
|
||
<div class="f"><label>启用</label><label class="switch"><input type="checkbox" id="w-enabled" checked onchange="renderWorkflow()"><span class="slider"></span></label></div>
|
||
</div>
|
||
</div></div>
|
||
<div class="section-title">rules JSON(实时预览)</div>
|
||
<textarea class="json" id="wf-json" readonly spellcheck="false"></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 模型编辑 -->
|
||
<div class="e-body" id="ed-model" style="display:none">
|
||
<div class="col">
|
||
<div class="card">
|
||
<div class="c-head">PriceConfig 配置
|
||
<span class="st"><select id="m-example" onchange="loadExample()">
|
||
<option value="">选择示例填充</option>
|
||
<option value="audio">音频模型示例</option>
|
||
<option value="video">视频模型示例</option>
|
||
<option value="reason">推理模型示例</option>
|
||
</select></span>
|
||
</div>
|
||
<div class="c-body">
|
||
<div class="f-row">
|
||
<div class="f"><label>计价基准 unit</label><select id="m-unit" onchange="renderModel()"><option value="per_1M" selected>per_1M(百万token)</option><option value="per_1K">per_1K(千token)</option><option value="per_1">per_1(单条)</option></select></div>
|
||
<div class="f"><label>币种</label><select id="m-currency" onchange="renderModel()"><option value="CNY" selected>CNY</option><option value="USD">USD</option></select></div>
|
||
<div class="f"><label>启用</label><label class="switch"><input type="checkbox" id="m-enabled" checked onchange="renderModel()"><span class="slider"></span></label></div>
|
||
</div>
|
||
<div class="f-row"><div class="f"><label>模型级折扣 rate(如 0.4=4折,空=无)</label><input class="num" id="m-discount" type="number" step="0.01" oninput="renderModel()"></div></div>
|
||
<div class="money-note">价格单位:元 / 基准单位(per_1M 即每百万 token)。不同模型类型配置内容不同——音频/视频/推理见示例。</div>
|
||
<div id="m-rules"></div>
|
||
<button class="btn mini" onclick="addModelRule()">+ 加规则</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col">
|
||
<div class="section-title">公共配置</div>
|
||
<div class="card"><div class="c-body">
|
||
<div class="f-row">
|
||
<div class="f"><label>门禁 min_balance(元)</label><input class="num" id="m-minbalance" type="number" step="0.01" value="0" oninput="renderModel()"></div>
|
||
</div>
|
||
</div></div>
|
||
<div class="section-title">rules JSON(实时预览)</div>
|
||
<textarea class="json" id="m-json" readonly spellcheck="false"></textarea>
|
||
<div class="note" style="margin-top:12px">结算入参(ChargeUsage 扩展,本期备用):<code>{promptTokens, completionTokens, cachedTokens, mediaType}</code>。mediaType = audio / no_video / has_video 由请求体参考媒体字段推导。</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 业务模块编辑 -->
|
||
<div class="e-body" id="ed-business" style="display:none">
|
||
<div class="col">
|
||
<div class="card">
|
||
<div class="c-head">周期订阅 per_period
|
||
<span class="st">通用周期建模,后续可扩 month/quarter</span>
|
||
</div>
|
||
<div class="c-body">
|
||
<div class="hint" style="margin-bottom:10px">按周期一次性收取,与用量无关。本期支持按年(period=year)。</div>
|
||
<div class="f-row">
|
||
<div class="f"><label>周期 period</label>
|
||
<select id="b-period" onchange="renderBusiness()"><option value="year" selected>year(按年)</option></select>
|
||
</div>
|
||
<div class="f"><label>每周期价格(元)</label>
|
||
<input class="num" id="b-price" type="number" step="0.01" value="1999" oninput="renderBusiness()">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col">
|
||
<div class="section-title">公共配置</div>
|
||
<div class="card"><div class="c-body">
|
||
<div class="f-row">
|
||
<div class="f"><label>门禁 min_balance(元)</label><input class="num" id="b-minbalance" type="number" step="0.01" value="0" oninput="renderBusiness()"></div>
|
||
<div class="f"><label>币种</label><select id="b-currency" onchange="renderBusiness()"><option value="CNY" selected>CNY</option><option value="USD">USD</option></select></div>
|
||
<div class="f"><label>启用</label><label class="switch"><input type="checkbox" id="b-enabled" checked onchange="renderBusiness()"><span class="slider"></span></label></div>
|
||
</div>
|
||
</div></div>
|
||
<div class="section-title">rules JSON(实时预览)</div>
|
||
<textarea class="json" id="b-json" readonly spellcheck="false"></textarea>
|
||
<div class="hint" style="margin-top:8px">订阅购买/续费流程接入另期;本期提供价格标准配置。</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 枚举响应示例 -->
|
||
<div class="e-body" id="ed-api" style="display:none">
|
||
<div class="col" style="flex:1;min-width:0">
|
||
<div class="section-title">GET /pricing/subjects · 枚举响应</div>
|
||
<textarea class="json" id="api-json" readonly spellcheck="false"></textarea>
|
||
<div class="hint" style="margin-top:8px">模型部分实时调 model-gateway <code>/listModelManage</code>(system_model=true);model-gateway 不可用时接口报错。</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toast" id="toast"></div>
|
||
|
||
<script>
|
||
/* ================= 主体数据(模拟枚举) ================= */
|
||
const SUBJECTS = [
|
||
{type:'workflow', id:'workflow', name:'工作流', modes:['per_item','per_second','per_token'], hasConfig:true, enabled:1},
|
||
{type:'model', id:'101', name:'deepseek-v3', modelType:'reason', typeLabel:'推理', conf:true},
|
||
{type:'model', id:'102', name:'gpt-4o', modelType:'chat', typeLabel:'对话', conf:true},
|
||
{type:'model', id:'103', name:'语音合成·统一', modelType:'audio', typeLabel:'音频', conf:false},
|
||
{type:'model', id:'104', name:'视频理解·统一', modelType:'video', typeLabel:'视频', conf:false},
|
||
{type:'business', id:'ai_customer_service', name:'AI客服', typeLabel:'业务', conf:false},
|
||
];
|
||
|
||
/* ================= 模型示例(对应 design §4.2) ================= */
|
||
const EXAMPLES = {
|
||
audio:{unit:'per_1M',currency:'CNY',enabled:1,minBalance:0,
|
||
rules:[{name:'音频-统一价格(万字符5元)', input:500, output:500, cacheHit:0, inputAudio:500, cacheHitAudio:0, cacheStorageHour:0, match:''}]},
|
||
video:{unit:'per_1M',currency:'CNY',enabled:1,minBalance:0,
|
||
rules:[
|
||
{name:'输入不含视频', input:23, output:23, cacheHit:0, inputAudio:0, cacheHitAudio:0, cacheStorageHour:0, match:'{"mediaType":"no_video"}'},
|
||
{name:'输入包含视频', input:14, output:14, cacheHit:0, inputAudio:0, cacheHitAudio:0, cacheStorageHour:0, match:'{"mediaType":"has_video"}'},
|
||
]},
|
||
reason:{unit:'per_1M',currency:'CNY',enabled:1,minBalance:0,
|
||
rules:[
|
||
{name:'输入[0,32000]', input:0.6, output:3.6, cacheHit:0.12, inputAudio:9, cacheHitAudio:1.8, cacheStorageHour:0.017, match:'{"inputLengthMax":32000}'},
|
||
{name:'输入(32000,128000]', input:0.9, output:5.4, cacheHit:0.18, inputAudio:13.5, cacheHitAudio:2.7, cacheStorageHour:0.017, match:'{"inputLengthMin":32001,"inputLengthMax":128000}'},
|
||
{name:'输入(128000,256000]', input:1.8, output:10.8, cacheHit:0.36, inputAudio:27, cacheHitAudio:5.4, cacheStorageHour:0.017, match:'{"inputLengthMin":128001,"inputLengthMax":256000}'},
|
||
]},
|
||
};
|
||
|
||
/* ================= 状态 ================= */
|
||
let current = 'workflow';
|
||
let modelRules = [];
|
||
|
||
/* ================= 渲染左侧枚举 ================= */
|
||
function renderSubjects(){
|
||
const wf = SUBJECTS.find(s=>s.type==='workflow');
|
||
document.getElementById('subj-workflow').innerHTML = subjectHTML(wf);
|
||
const models = SUBJECTS.filter(s=>s.type==='model');
|
||
document.getElementById('subj-models').innerHTML = models.map(subjectHTML).join('');
|
||
const biz = SUBJECTS.filter(s=>s.type==='business');
|
||
document.getElementById('subj-business').innerHTML = biz.map(subjectHTML).join('');
|
||
}
|
||
function subjectHTML(s){
|
||
const conf = s.type==='workflow' ? s.hasConfig : s.conf;
|
||
const typeLabel = s.type==='workflow' ? '工作流' : (s.type==='business' ? '业务' : s.typeLabel);
|
||
const tclass = s.type==='workflow' ? 't-workflow' : (s.type==='business' ? 't-business' : 't-'+s.modelType);
|
||
let extra='';
|
||
if(s.type==='workflow'){ extra = s.modes.map(m=>`<span class="mode-chip">${m}</span>`).join(' '); }
|
||
if(s.type==='business'){ extra = `<span class="mode-chip">per_period</span>`; }
|
||
return `<div class="subj ${current===s.type+':'+s.id?'active':''}" onclick="selectSubject('${s.type}','${s.id}')">
|
||
<span class="type-badge ${tclass}">${typeLabel}</span>
|
||
<span class="name">${s.name}</span>
|
||
${extra}
|
||
<span class="conf-badge ${conf?'conf-yes':'conf-no'}">${conf?'已配置':'未配置'}</span>
|
||
</div>`;
|
||
}
|
||
|
||
/* ================= 切换主体 ================= */
|
||
function selectSubject(type, id){
|
||
current = type+':'+id;
|
||
renderSubjects();
|
||
const panels = ['ed-workflow','ed-model','ed-business','ed-api'];
|
||
panels.forEach(p=>document.getElementById(p).style.display='none');
|
||
if(type==='workflow'){
|
||
document.getElementById('ed-workflow').style.display='flex';
|
||
document.getElementById('ed-title').textContent='工作流';
|
||
document.getElementById('ed-sub').textContent='subjectType=workflow · subjectId=workflow';
|
||
loadWorkflowState();
|
||
} else if(type==='model'){
|
||
document.getElementById('ed-model').style.display='flex';
|
||
const s = SUBJECTS.find(x=>x.type==='model'&&x.id===id);
|
||
document.getElementById('ed-title').textContent=s.name;
|
||
document.getElementById('ed-sub').textContent=`subjectType=model · subjectId=${s.id}(${s.typeLabel})`;
|
||
const ex = EXAMPLES[s.modelType];
|
||
if(ex && !document.getElementById('m-example').value){ loadState(ex); }
|
||
else if(!modelRules.length){ modelRules=[emptyRule()]; renderModel(); }
|
||
} else if(type==='business'){
|
||
document.getElementById('ed-business').style.display='flex';
|
||
const s = SUBJECTS.find(x=>x.type==='business'&&x.id===id);
|
||
document.getElementById('ed-title').textContent=s.name;
|
||
document.getElementById('ed-sub').textContent=`subjectType=business · subjectId=${s.id}`;
|
||
renderBusiness();
|
||
}
|
||
}
|
||
function renderBusiness(){
|
||
const cfg = { subjectType:'business', subjectId:current.split(':')[1],
|
||
rules:{ period:document.getElementById('b-period').value, price:+document.getElementById('b-price').value },
|
||
minBalance:+document.getElementById('b-minbalance').value,
|
||
currency:document.getElementById('b-currency').value,
|
||
enabled:document.getElementById('b-enabled').checked?1:0 };
|
||
document.getElementById('b-json').value = JSON.stringify(cfg,null,2);
|
||
}
|
||
function selectSubjectRaw(){}
|
||
function loadApiExample(){
|
||
['ed-workflow','ed-model','ed-api'].forEach(p=>document.getElementById(p).style.display='none');
|
||
document.getElementById('ed-api').style.display='flex';
|
||
document.getElementById('ed-title').textContent='枚举响应示例';
|
||
document.getElementById('ed-sub').textContent='GET /pricing/subjects';
|
||
const wf = SUBJECTS.find(s=>s.type==='workflow');
|
||
const api = {
|
||
subjects:[
|
||
{subjectType:'workflow', subjectId:'workflow', name:'工作流', chargeModes:['per_item','per_second','per_token'], hasConfig:true, enabled:1},
|
||
...SUBJECTS.filter(s=>s.type==='model').map(s=>({subjectType:'model', subjectId:s.id, name:s.name, modelType:s.modelType, hasConfig:s.conf, enabled:1})),
|
||
...SUBJECTS.filter(s=>s.type==='business').map(s=>({subjectType:'business', subjectId:s.id, name:s.name, chargeModes:['per_period'], hasConfig:s.conf, enabled:1}))
|
||
]
|
||
};
|
||
document.getElementById('api-json').value = JSON.stringify(api,null,2);
|
||
}
|
||
|
||
/* ================= 工作流编辑 ================= */
|
||
let tiers = [{maxSec:15,price:29},{maxSec:30,price:49},{maxSec:60,price:89}];
|
||
function renderWorkflowTiers(){
|
||
const tb=document.getElementById('w-item-tiers');
|
||
tb.innerHTML = tiers.map((t,i)=>`<tr>
|
||
<td><input type="number" value="${t.maxSec}" oninput="tiers[${i}].maxSec=+this.value;renderWorkflow()"></td>
|
||
<td><input type="number" step="0.01" value="${t.price}" oninput="tiers[${i}].price=+this.value;renderWorkflow()"></td>
|
||
<td><button class="btn mini danger" onclick="tiers.splice(${i},1);renderWorkflowTiers();renderWorkflow()">删</button></td></tr>`).join('');
|
||
}
|
||
function addTier(){ tiers.push({maxSec:60,price:89}); renderWorkflowTiers(); renderWorkflow(); }
|
||
function loadWorkflowState(){ renderWorkflowTiers(); renderWorkflow(); }
|
||
function renderWorkflow(){
|
||
const rules = {};
|
||
if(document.getElementById('w-item-on').checked){
|
||
rules.per_item = {tiers:tiers.filter(t=>t&&t.maxSec>0), overflowUnitPrice:+document.getElementById('w-item-overflow').value};
|
||
}
|
||
if(document.getElementById('w-sec-on').checked){
|
||
rules.per_second = {unitPrice:+document.getElementById('w-sec-unit').value};
|
||
}
|
||
if(document.getElementById('w-tok-on').checked){
|
||
rules.per_token = {};
|
||
}
|
||
const cfg = { subjectType:'workflow', subjectId:'workflow', rules,
|
||
minBalance:+document.getElementById('w-minbalance').value,
|
||
currency:document.getElementById('w-currency').value,
|
||
enabled:document.getElementById('w-enabled').checked?1:0 };
|
||
document.getElementById('wf-json').value = JSON.stringify(cfg,null,2);
|
||
}
|
||
|
||
/* ================= 模型编辑 ================= */
|
||
function emptyRule(){ return {name:'',input:0,output:0,cacheHit:0,inputAudio:0,cacheHitAudio:0,cacheStorageHour:0,match:''}; }
|
||
function addModelRule(){ modelRules.push(emptyRule()); renderModel(); }
|
||
function loadExample(){
|
||
const v=document.getElementById('m-example').value;
|
||
if(!v) return;
|
||
loadState(JSON.parse(JSON.stringify(EXAMPLES[v])));
|
||
}
|
||
function loadState(ex){
|
||
document.getElementById('m-unit').value=ex.unit||'per_1M';
|
||
document.getElementById('m-currency').value=ex.currency||'CNY';
|
||
document.getElementById('m-enabled').checked = ex.enabled!==0;
|
||
document.getElementById('m-minbalance').value = ex.minBalance||0;
|
||
document.getElementById('m-discount').value = ex.discount ? ex.discount.rate : '';
|
||
modelRules = ex.rules && ex.rules.length ? JSON.parse(JSON.stringify(ex.rules)) : [emptyRule()];
|
||
renderModel();
|
||
}
|
||
function renderModel(){
|
||
const box=document.getElementById('m-rules');
|
||
box.innerHTML = modelRules.map((r,i)=>`<div class="rule-card">
|
||
<div class="rc-head">
|
||
<span style="color:var(--text3);font-size:12px">规则 #${i+1}</span>
|
||
<input value="${esc(r.name)}" placeholder="规则名(如:输入[0,32000])" oninput="modelRules[${i}].name=this.value;renderModel()">
|
||
<button class="btn mini danger" onclick="modelRules.splice(${i},1);renderModel()">删</button>
|
||
</div>
|
||
<div class="rc-grid">
|
||
${num('input','输入单价 input',i,'元/基准')}
|
||
${num('output','输出单价 output',i,'元/基准')}
|
||
${num('cacheHit','缓存命中 cacheHit',i,'元/基准')}
|
||
${num('inputAudio','音频输入 inputAudio',i,'元/基准')}
|
||
${num('cacheHitAudio','音频缓存 cacheHitAudio',i,'元/基准')}
|
||
${num('cacheStorageHour','缓存存储 cacheStorageHour',i,'元/小时')}
|
||
</div>
|
||
<div class="f" style="margin-top:8px"><label>命中条件 match(JSON,空=无条件命中)</label>
|
||
<input value="${esc(r.match)}" placeholder='{"inputLengthMax":32000} 或 {"mediaType":"has_video"}' oninput="modelRules[${i}].match=this.value;renderModel()"></div>
|
||
</div>`).join('');
|
||
const rules = modelRules.filter(r=>r.name||r.input||r.output||r.cacheHit||r.inputAudio||r.cacheHitAudio||r.cacheStorageHour||r.match)
|
||
.map(r=>({name:r.name, input:+r.input, output:+r.output, cacheHit:+r.cacheHit,
|
||
inputAudio:+r.inputAudio, cacheHitAudio:+r.cacheHitAudio, cacheStorageHour:+r.cacheStorageHour,
|
||
...(r.match?{match:JSON.parse(r.match||'{}')}:{})}));
|
||
const unit = document.getElementById('m-unit').value;
|
||
const dis = document.getElementById('m-discount').value;
|
||
const cfg = { unit, rules, currency:document.getElementById('m-currency').value,
|
||
...(dis!==''?{discount:{rate:+dis}}:{discount:null}) };
|
||
document.getElementById('m-json').value = JSON.stringify({subjectType:'model', subjectId:current.split(':')[1], rules:cfg, minBalance:+document.getElementById('m-minbalance').value, enabled:document.getElementById('m-enabled').checked?1:0},null,2);
|
||
}
|
||
function num(key,label,i,unit){
|
||
return `<div class="f"><label>${label}</label><input type="number" step="0.01" value="${modelRules[i][key]}" oninput="modelRules[${i}].${key}=+this.value;renderModel()"></div>`;
|
||
}
|
||
function esc(s){ return (s||'').replace(/"/g,'"').replace(/</g,'<'); }
|
||
|
||
/* ================= 保存(mock) ================= */
|
||
function saveMock(){
|
||
const t=document.getElementById('toast');
|
||
t.textContent='已保存(模拟)· rules JSON 已在上方预览';
|
||
t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),1800);
|
||
}
|
||
|
||
/* ================= 初始化 ================= */
|
||
renderSubjects();
|
||
selectSubject('workflow','workflow');
|
||
document.getElementById('w-item-on').checked=true;
|
||
document.getElementById('w-sec-on').checked=true;
|
||
document.getElementById('w-tok-on').checked=true;
|
||
renderWorkflow();
|
||
</script>
|
||
</body>
|
||
</html> |