价格管理:label 中文化、金额保留5位小数、通用配置置顶重排、取消模型类型筛选、去除列表计费方式标签

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-01 17:15:55 +08:00
co-authored by Claude Code
parent aa7c8856fb
commit 490abd60e9
6 changed files with 223 additions and 246 deletions
@@ -1,5 +1,32 @@
<template>
<div class="editor-body">
<div class="editor-col">
<!-- 通用配置 -->
<div class="card">
<div class="card-head">
<span class="card-title">通用配置</span>
</div>
<div class="card-body">
<div class="cfg-row">
<div class="cfg-item">
<div class="cfg-label">门禁金额</div>
<el-input-number v-model="minBalance" :min="0" :precision="5" :step="0.01" :controls="false" class="cfg-min-balance" />
</div>
<div class="cfg-item">
<div class="cfg-label">币种</div>
<el-select v-model="currency" class="cfg-currency">
<el-option label="人民币" value="CNY" />
<el-option label="美元" value="USD" />
</el-select>
</div>
<div class="cfg-item">
<div class="cfg-label">启用</div>
<el-switch v-model="enabled" />
</div>
</div>
</div>
</div>
</div>
<div class="editor-col">
<!-- 周期订阅 -->
<div class="card">
@@ -9,58 +36,27 @@
</div>
<div class="card-body">
<el-form label-position="top" size="default">
<el-form-item label="周期 period">
<el-form-item label="周期">
<el-select v-model="period" class="ctrl-select">
<el-option label="year按年" value="year" />
<el-option label="按年" value="year" />
</el-select>
</el-form-item>
<el-form-item label="每周期价格(元)">
<el-input-number
v-model="price"
:min="0"
:precision="2"
:precision="5"
:step="0.01"
:controls="false"
class="ctrl-input"
/>
</el-form-item>
<div class="form-hint">本期支持按年订阅period=year订阅购买 / 续费流程接入另期</div>
<div class="form-hint">本期支持按年订阅订阅购买 / 续费流程接入另期</div>
</el-form>
</div>
</div>
</div>
<div class="editor-col">
<!-- 通用配置 -->
<div class="card">
<div class="card-head">
<span class="card-title">通用配置</span>
</div>
<div class="card-body">
<el-form label-position="top" size="default">
<el-form-item label="门禁 min_balance(元)">
<el-input-number
v-model="minBalance"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
class="ctrl-input"
/>
</el-form-item>
<el-form-item label="币种">
<el-select v-model="currency" class="ctrl-select">
<el-option label="人民币 (CNY)" value="CNY" />
<el-option label="美元 (USD)" value="USD" />
</el-select>
</el-form-item>
<el-form-item label="启用">
<el-switch v-model="enabled" />
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
</template>
@@ -134,14 +130,12 @@ defineExpose({ save });
<style scoped lang="scss">
.editor-body {
display: flex;
gap: 20px;
flex-wrap: wrap;
display: block;
}
.editor-col {
flex: 1;
min-width: 380px;
width: 100%;
min-width: 0;
}
.card {
@@ -182,6 +176,33 @@ defineExpose({ save });
line-height: 1.7;
}
.cfg-row {
display: flex;
align-items: flex-end;
gap: 40px;
flex-wrap: wrap;
}
.cfg-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.cfg-label {
font-size: 13px;
color: #606266;
}
.cfg-min-balance {
width: 200px;
}
.cfg-currency {
width: 200px;
}
.ctrl-input {
width: 220px;
}
@@ -1,5 +1,32 @@
<template>
<div class="editor-body">
<div class="editor-col">
<!-- 通用配置 -->
<div class="card">
<div class="card-head">
<span class="card-title">通用配置</span>
</div>
<div class="card-body">
<div class="cfg-row">
<div class="cfg-item">
<div class="cfg-label">门禁金额</div>
<el-input-number v-model="minBalance" :min="0" :precision="5" :step="0.01" :controls="false" class="cfg-min-balance" />
</div>
<div class="cfg-item">
<div class="cfg-label">币种</div>
<el-select v-model="currency" class="cfg-currency">
<el-option label="人民币" value="CNY" />
<el-option label="美元" value="USD" />
</el-select>
</div>
<div class="cfg-item">
<div class="cfg-label">启用</div>
<el-switch v-model="enabled" />
</div>
</div>
</div>
</div>
</div>
<div class="editor-col">
<!-- 基础计费 -->
<div class="card">
@@ -8,12 +35,12 @@
</div>
<div class="card-body">
<el-form label-position="top" size="default">
<el-form-item label="计费单位 unit">
<el-form-item label="计费单位">
<el-select v-model="unit" class="ctrl-select" @change="onUnitChange">
<el-option v-for="o in UNIT_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
</el-select>
</el-form-item>
<el-form-item label="阶梯分档 tiered">
<el-form-item label="阶梯分档">
<el-switch v-model="tiered" :disabled="!isTokenUnit" />
</el-form-item>
<div class="form-hint">阶梯=按输入 token 分档<b>命中哪档整单按哪档价不累加</b>档位区间相邻不重叠下档 max+1 = 上档 min token 单位per_1K / per_1M可用</div>
@@ -40,7 +67,7 @@
<el-button size="small" type="danger" text @click="removeRule(i)">删除</el-button>
</div>
<div class="rc-sec match">命中条件 match</div>
<div class="rc-sec match">命中条件</div>
<el-form label-position="top" size="small">
<div class="rc-grid">
<template v-if="subject.modelType === 'reason'">
@@ -78,23 +105,23 @@
</div>
</el-form>
<div class="rc-sec price">计价项 price</div>
<div class="rc-sec price">计价项</div>
<el-form label-position="top" size="small">
<div class="rc-grid">
<template v-if="isTokenUnit">
<el-form-item label="输入单价 input">
<el-input-number v-model="r.input" :min="0" :precision="6" :step="0.0001" :controls="false" class="rc-control" />
<el-form-item label="输入单价">
<el-input-number v-model="r.input" :min="0" :precision="5" :step="0.0001" :controls="false" class="rc-control" />
</el-form-item>
<el-form-item label="输出单价 output">
<el-input-number v-model="r.output" :min="0" :precision="6" :step="0.0001" :controls="false" class="rc-control" />
<el-form-item label="输出单价">
<el-input-number v-model="r.output" :min="0" :precision="5" :step="0.0001" :controls="false" class="rc-control" />
</el-form-item>
<el-form-item label="缓存命中 cacheHit">
<el-input-number v-model="r.cacheHit" :min="0" :precision="6" :step="0.0001" :controls="false" class="rc-control" />
<el-form-item label="缓存命中">
<el-input-number v-model="r.cacheHit" :min="0" :precision="5" :step="0.0001" :controls="false" class="rc-control" />
</el-form-item>
</template>
<template v-else>
<el-form-item label="每单位单价 unitPrice">
<el-input-number v-model="r.unitPrice" :min="0" :precision="6" :step="0.0001" :controls="false" class="rc-control" />
<el-form-item label="每单位单价">
<el-input-number v-model="r.unitPrice" :min="0" :precision="5" :step="0.0001" :controls="false" class="rc-control" />
</el-form-item>
</template>
</div>
@@ -102,14 +129,14 @@
<!-- 媒体价格模板 token 单位媒体命中条件已移除改为对特定媒体类型设独立覆盖价 mediaPrices -->
<div v-if="isTokenUnit" class="media-price-section">
<div class="rc-sec price">媒体价格模板 mediaPrices可选</div>
<div class="rc-sec price">媒体价格模板可选</div>
<div v-for="(mp, mIdx) in r.mediaPrices" :key="mIdx" class="media-price-row">
<el-select v-model="mp.mediaType" class="mp-media" placeholder="媒体类型">
<el-option v-for="o in getAvailableMediaOptions(r, mIdx)" :key="o.value" :label="o.label" :value="o.value" />
</el-select>
<el-input-number v-model="mp.input" :min="0" :precision="6" :step="0.0001" :controls="false" class="mp-num" placeholder="input" />
<el-input-number v-model="mp.output" :min="0" :precision="6" :step="0.0001" :controls="false" class="mp-num" placeholder="output" />
<el-input-number v-model="mp.cacheHit" :min="0" :precision="6" :step="0.0001" :controls="false" class="mp-num" placeholder="cacheHit" />
<el-input-number v-model="mp.input" :min="0" :precision="5" :step="0.0001" :controls="false" class="mp-num" placeholder="输入价" />
<el-input-number v-model="mp.output" :min="0" :precision="5" :step="0.0001" :controls="false" class="mp-num" placeholder="输出价" />
<el-input-number v-model="mp.cacheHit" :min="0" :precision="5" :step="0.0001" :controls="false" class="mp-num" placeholder="缓存命中价" />
<el-button type="danger" text size="small" @click="removeMediaPrice(r, mIdx)">删除</el-button>
</div>
<el-button size="small" class="media-add-btn" @click="addMediaPrice(r)">+ 添加媒体价格</el-button>
@@ -125,37 +152,6 @@
</div>
</div>
<div class="editor-col">
<!-- 通用配置 -->
<div class="card">
<div class="card-head">
<span class="card-title">通用配置</span>
</div>
<div class="card-body">
<el-form label-position="top" size="default">
<el-form-item label="门禁 min_balance(元)">
<el-input-number
v-model="minBalance"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
class="ctrl-input"
/>
</el-form-item>
<el-form-item label="币种">
<el-select v-model="currency" class="ctrl-select">
<el-option label="人民币 (CNY)" value="CNY" />
<el-option label="美元 (USD)" value="USD" />
</el-select>
</el-form-item>
<el-form-item label="启用">
<el-switch v-model="enabled" />
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
</template>
@@ -405,14 +401,12 @@ defineExpose({ save });
<style scoped lang="scss">
.editor-body {
display: flex;
gap: 20px;
flex-wrap: wrap;
display: block;
}
.editor-col {
flex: 1;
min-width: 380px;
width: 100%;
min-width: 0;
}
.card {
@@ -461,13 +455,6 @@ defineExpose({ save });
margin-top: 10px;
}
.ctrl-input {
width: 220px;
}
.ctrl-select {
width: 220px;
}
.rule-card {
border: 1px solid #e4e7ed;
@@ -575,4 +562,34 @@ defineExpose({ save });
font-size: 12.5px;
line-height: 1.7;
}
.cfg-row {
display: flex;
align-items: flex-end;
gap: 40px;
flex-wrap: wrap;
}
.cfg-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.cfg-label {
font-size: 13px;
color: #606266;
}
.cfg-min-balance {
width: 200px;
}
.cfg-currency {
width: 200px;
}
.ctrl-select {
width: 220px;
}
</style>
@@ -2,19 +2,6 @@
<div class="subject-list">
<div v-if="loading" class="subject-loading">加载计价对象</div>
<template v-else>
<!-- 模型类型筛选 -->
<div v-if="hasModel" class="filter-bar">
<button
v-for="f in FILTER_OPTIONS"
:key="f.value"
class="filter-chip"
:class="{ active: modelFilter === f.value }"
@click="modelFilter = f.value"
>
{{ f.label }}
</button>
</div>
<!-- 工作流 -->
<div class="grp-head">
<span class="grp-name">工作流</span>
@@ -29,7 +16,6 @@
>
<span class="type-badge t-workflow">工作流</span>
<span class="subj-name" :title="s.name">{{ s.name }}</span>
<span v-for="m in s.modes" :key="m" class="mode-chip">{{ m }}</span>
<span
class="status-dot"
:class="s.hasConfig ? 'on' : 'off'"
@@ -37,13 +23,13 @@
/>
</div>
<!-- 模型支持类型筛选 -->
<!-- 模型 -->
<div class="grp-head">
<span class="grp-name">模型</span>
<span class="grp-count">{{ filteredModels.length }}</span>
<span class="grp-count">{{ modelSubjects.length }}</span>
</div>
<div
v-for="s in filteredModels"
v-for="s in modelSubjects"
:key="`${s.type}:${s.id}`"
class="subj-item"
:class="{ active: `${s.type}:${s.id}` === currentKey }"
@@ -57,7 +43,7 @@
:title="s.hasConfig ? '已配置' : '未配置'"
/>
</div>
<div v-if="!filteredModels.length" class="empty-tip">该类型暂无模型</div>
<div v-if="!modelSubjects.length" class="empty-tip">暂无模型</div>
<!-- 业务模块 -->
<div class="grp-head">
@@ -73,7 +59,6 @@
>
<span class="type-badge t-business">业务</span>
<span class="subj-name" :title="s.name">{{ s.name }}</span>
<span v-for="m in s.modes" :key="m" class="mode-chip">{{ m }}</span>
<span
class="status-dot"
:class="s.hasConfig ? 'on' : 'off'"
@@ -85,12 +70,12 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import type { ModelType, Subject } from '../mock';
import { computed } from 'vue';
import type { Subject } from '../mock';
/**
* 左侧计价对象枚举列表。
* 分三组渲染(工作流 / 模型 / 业务模块),模型组支持类型筛选
* 分三组渲染(工作流 / 模型 / 业务模块);
* 配置状态以实心(已配置)/ 空心(未配置)圆点呈现。
*/
const props = defineProps<{
@@ -105,57 +90,10 @@ const workflowSubjects = computed(() => props.subjects.filter((s) => s.type ===
const modelSubjects = computed(() => props.subjects.filter((s) => s.type === 'model'));
const businessSubjects = computed(() => props.subjects.filter((s) => s.type === 'business'));
const hasModel = computed(() => modelSubjects.value.length > 0);
/** 模型类型筛选 */
const FILTER_OPTIONS: { value: ModelType | 'all'; label: string }[] = [
{ value: 'all', label: '全部' },
{ value: 'reason', label: '推理' },
{ value: 'video', label: '视频' },
{ value: 'audio', label: '音频' },
{ value: 'image', label: '图片' },
];
const modelFilter = ref<ModelType | 'all'>('all');
const filteredModels = computed(() =>
modelFilter.value === 'all'
? modelSubjects.value
: modelSubjects.value.filter((s) => s.modelType === modelFilter.value),
);
</script>
<style scoped lang="scss">
.subject-list {
.filter-bar {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 10px 16px 2px;
.filter-chip {
border: 1px solid #dcdfe6;
background: #fff;
color: #606266;
border-radius: 14px;
padding: 3px 12px;
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
line-height: 1.4;
&:hover {
color: #409eff;
border-color: #409eff;
}
&.active {
background: #ecf5ff;
border-color: #409eff;
color: #409eff;
font-weight: 500;
}
}
}
.grp-head {
display: flex;
align-items: center;
@@ -251,14 +189,6 @@ const filteredModels = computed(() =>
}
}
.mode-chip {
font-size: 11px;
background: #f4f4f5;
color: #606266;
border-radius: 3px;
padding: 1px 5px;
flex-shrink: 0;
}
.status-dot {
width: 8px;
@@ -1,5 +1,32 @@
<template>
<div class="editor-body">
<div class="editor-col">
<!-- 通用配置 -->
<div class="card">
<div class="card-head">
<span class="card-title">通用配置</span>
</div>
<div class="card-body">
<div class="cfg-row">
<div class="cfg-item">
<div class="cfg-label">门禁金额</div>
<el-input-number v-model="minBalance" :min="0" :precision="5" :step="0.01" :controls="false" class="cfg-min-balance" />
</div>
<div class="cfg-item">
<div class="cfg-label">币种</div>
<el-select v-model="currency" class="cfg-currency">
<el-option label="人民币" value="CNY" />
<el-option label="美元" value="USD" />
</el-select>
</div>
<div class="cfg-item">
<div class="cfg-label">启用</div>
<el-switch v-model="enabled" />
</div>
</div>
</div>
</div>
</div>
<div class="editor-col">
<!-- 计费模式 -->
<div class="card">
@@ -11,12 +38,12 @@
<!-- 按条计费 per_item -->
<div class="mode-item">
<div class="mode-item-head">
<span class="mode-name">按条计费 <span class="mode-code">per_item</span></span>
<span class="mode-name">按条计费</span>
<el-switch v-model="modes.per_item" />
</div>
<div v-if="modes.per_item" class="mode-item-body">
<el-form label-position="top" size="default">
<el-form-item label="档位列表(上不封顶,命中首个 maxSec ≥ 时长 的档)">
<el-form-item label="档位列表(上不封顶,命中首个档位上限 ≥ 时长的档)">
<div class="tier-list">
<div v-for="(tier, i) in tiers" :key="i" class="tier-row">
<el-input-number
@@ -24,13 +51,13 @@
:min="0"
:precision="0"
:controls="false"
placeholder="maxSec"
placeholder="档位上限(秒)"
class="tier-input"
/>
<el-input-number
v-model="tier.price"
:min="0"
:precision="2"
:precision="5"
:controls="false"
placeholder="价格(元)"
class="tier-input"
@@ -44,7 +71,7 @@
<el-input-number
v-model="overflowUnitPrice"
:min="0"
:precision="2"
:precision="5"
:step="0.01"
:controls="false"
class="ctrl-input"
@@ -57,7 +84,7 @@
<!-- 按秒计费 per_second -->
<div class="mode-item">
<div class="mode-item-head">
<span class="mode-name">按秒计费 <span class="mode-code">per_second</span></span>
<span class="mode-name">按秒计费</span>
<el-switch v-model="modes.per_second" />
</div>
<div v-if="modes.per_second" class="mode-item-body">
@@ -66,7 +93,7 @@
<el-input-number
v-model="perSecondUnitPrice"
:min="0"
:precision="2"
:precision="5"
:step="0.01"
:controls="false"
class="ctrl-input"
@@ -79,7 +106,7 @@
<!-- token 计费 per_token -->
<div class="mode-item">
<div class="mode-item-head">
<span class="mode-name"> Token 计费 <span class="mode-code">per_token</span></span>
<span class="mode-name"> Token 计费</span>
<el-switch v-model="modes.per_token" />
</div>
<div v-if="modes.per_token" class="mode-item-body">
@@ -93,37 +120,6 @@
</div>
</div>
<div class="editor-col">
<!-- 通用配置 -->
<div class="card">
<div class="card-head">
<span class="card-title">通用配置</span>
</div>
<div class="card-body">
<el-form label-position="top" size="default">
<el-form-item label="门禁 min_balance(元)">
<el-input-number
v-model="minBalance"
:min="0"
:precision="2"
:step="0.01"
:controls="false"
class="ctrl-input"
/>
</el-form-item>
<el-form-item label="币种">
<el-select v-model="currency" class="ctrl-select">
<el-option label="人民币 (CNY)" value="CNY" />
<el-option label="美元 (USD)" value="USD" />
</el-select>
</el-form-item>
<el-form-item label="启用">
<el-switch v-model="enabled" />
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
</template>
@@ -224,14 +220,12 @@ defineExpose({ save });
<style scoped lang="scss">
.editor-body {
display: flex;
gap: 20px;
flex-wrap: wrap;
display: block;
}
.editor-col {
flex: 1;
min-width: 380px;
width: 100%;
min-width: 0;
}
.card {
@@ -286,19 +280,12 @@ defineExpose({ save });
.mode-name {
font-size: 13px;
font-weight: 600;
.mode-code {
font-size: 12px;
font-weight: 400;
color: #909399;
margin-left: 6px;
}
}
}
.mode-item-body {
padding: 12px;
}
.mode-item-body {
padding: 12px;
}
}
.tier-list {
@@ -328,7 +315,29 @@ defineExpose({ save });
width: 220px;
}
.ctrl-select {
width: 220px;
.cfg-row {
display: flex;
align-items: flex-end;
gap: 40px;
flex-wrap: wrap;
}
.cfg-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.cfg-label {
font-size: 13px;
color: #606266;
}
.cfg-min-balance {
width: 200px;
}
.cfg-currency {
width: 200px;
}
</style>
+3 -3
View File
@@ -141,9 +141,9 @@ const current = computed<Subject | undefined>(() =>
const headerSub = computed(() => {
const s = current.value;
if (!s) return '加载计价对象…';
if (s.type === 'workflow') return `subjectType=workflow · subjectId=${s.id}`;
if (s.type === 'model') return `subjectType=model · subjectId=${s.id}${s.typeLabel}modelType=${s.modelType}`;
return `subjectType=business · subjectId=${s.id}`;
if (s.type === 'workflow') return `工作流对象 · 标识:${s.id}`;
if (s.type === 'model') return `模型对象${s.typeLabel})· 标识:${s.id}`;
return `业务模块对象 · 标识:${s.id}`;
});
/** 详情请求序号,防止快速切换时旧响应覆盖当前对象 */
+11 -11
View File
@@ -240,21 +240,21 @@ export const MODEL_TYPE_CODES: Record<ModelType, number> = {
};
export const UNIT_OPTIONS: { value: Unit; label: string }[] = [
{ value: 'per_1M', label: 'per_1M每百万token' },
{ value: 'per_1K', label: 'per_1K(每千token' },
{ value: 'per_1', label: 'per_1(每个/每张图' },
{ value: 'per_second', label: 'per_second每秒' },
{ value: 'per_minute', label: 'per_minute每分钟' },
{ value: 'per_hour', label: 'per_hour每小时' },
{ value: 'per_char', label: 'per_char每字' },
{ value: 'per_1M', label: '每百万 Token' },
{ value: 'per_1K', label: '每千 Token' },
{ value: 'per_1', label: '每个 / 每张图' },
{ value: 'per_second', label: '每秒' },
{ value: 'per_minute', label: '每分钟' },
{ value: 'per_hour', label: '每小时' },
{ value: 'per_char', label: '每字' },
];
/** 媒体价格模板可选媒体类型(媒体命中条件已移除,媒体改按模板定价) */
export const MEDIA_PRICE_OPTIONS: { value: 'text' | 'audio' | 'video' | 'image'; label: string }[] = [
{ value: 'text', label: '文本 text' },
{ value: 'audio', label: '音频 audio' },
{ value: 'video', label: '视频 video' },
{ value: 'image', label: '图片 image' },
{ value: 'text', label: '文本' },
{ value: 'audio', label: '音频' },
{ value: 'video', label: '视频' },
{ value: 'image', label: '图片' },
];
/** outputAudio 命中条件(video):给用户展示中文(有声/无声) */