Commit e8278a8e by renyizhao

分档计费与大屏

parent f739f0e6
...@@ -169,7 +169,7 @@ ...@@ -169,7 +169,7 @@
</el-button> </el-button>
</div> </div>
<div class="model-list"> <div class="model-list">
<model-card v-for="m in filteredModels" :key="m.modelName" :model="m" /> <model-card v-for="m in filteredModels" :key="m.modelName" :model="m" @click="openModelDetail(m)" />
</div> </div>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
...@@ -326,6 +326,98 @@ ...@@ -326,6 +326,98 @@
</div> </div>
</transition> </transition>
<!-- 模型详情侧边栏 -->
<el-drawer v-model="showModelDrawer" direction="rtl" :size="500" title="模型详情">
<template #default>
<div v-if="selectedModel" class="model-detail">
<!-- 图标和模型名 -->
<div class="model-detail-header">
<div class="model-detail-icon">
<LobeIcon
:name="selectedModelIcon.name"
:variant="selectedModelIcon.variant"
:size="36"
/>
</div>
<div class="model-detail-name">{{ selectedModel.modelName }}</div>
</div>
<!-- 基本信息 -->
<div class="model-detail-section">
<div class="section-title">基本信息</div>
<div class="section-desc">模型的详细描述和基本特性</div>
</div>
<!-- 描述 -->
<div class="model-detail-desc">
{{ selectedModel.description || '暂无描述' }}
</div>
<!-- 标签 -->
<div v-if="selectedModelTags.length > 0" class="model-detail-tags">
<el-tag
v-for="(tag, index) in selectedModelTags"
:key="tag"
size="small"
:type="tagColors[index % tagColors.length]"
effect="plain"
>{{ tag }}</el-tag>
</div>
<!-- 价格信息 -->
<div class="model-detail-section">
<div class="section-title">动态计费</div>
<div class="section-desc">价格根据用量档位和请求条件动态调整</div>
</div>
<!-- 按次计费 -->
<div v-if="selectedModelIsPerCall" class="price-item">
<span class="price-label">单次价格</span>
<span class="price-value">¥{{ formatModelPrice(selectedModel.inputPrice) }}</span>
</div>
<!-- 分档计费表格 -->
<template v-else-if="selectedModelBillingMode === 'tiered_expr' && selectedModel.tiers">
<div class="tier-table">
<div class="tier-header">
<div class="tier-cell">档位</div>
<div class="tier-cell">输入 (¥/1M tokens)</div>
<div class="tier-cell">补全 (¥/1M tokens)</div>
</div>
<div v-for="(tier, index) in selectedModel.tiers" :key="index" class="tier-row">
<div class="tier-cell">
<div class="tier-name">{{ tier.name }}</div>
<div v-if="tier.conditionDesc" class="tier-condition">{{ tier.conditionDesc }}</div>
</div>
<div class="tier-cell price-cell">{{ formatModelPrice(tier.inputRatio) }}</div>
<div class="tier-cell price-cell">{{ formatModelPrice(tier.outputRatio) }}</div>
</div>
</div>
</template>
<!-- 按量计费 -->
<template v-else>
<div class="price-item">
<span class="price-label">输入价格</span>
<span class="price-value">¥{{ formatModelPrice(selectedModel.inputPrice) }} / 1M tokens</span>
</div>
<div class="price-item">
<span class="price-label">输出价格</span>
<span class="price-value">¥{{ formatModelPrice(selectedModel.outputPrice) }} / 1M tokens</span>
</div>
<div v-if="selectedModelCacheReadPrice > 0" class="price-item">
<span class="price-label">缓存读取</span>
<span class="price-value">¥{{ formatModelPrice(selectedModelCacheReadPrice) }} / 1M tokens</span>
</div>
<div v-if="selectedModelCacheCreatePrice > 0" class="price-item">
<span class="price-label">缓存创建</span>
<span class="price-value">¥{{ formatModelPrice(selectedModelCacheCreatePrice) }} / 1M tokens</span>
</div>
</template>
</div>
</template>
</el-drawer>
</div> </div>
</template> </template>
...@@ -351,6 +443,7 @@ import {PayOrderStatusEnum} from "@/utils/constants.js"; ...@@ -351,6 +443,7 @@ import {PayOrderStatusEnum} from "@/utils/constants.js";
import PDFObject from 'pdfobject'; import PDFObject from 'pdfobject';
import axios from "axios"; import axios from "axios";
import ModelCard from '@/views/console/components/model-card.vue' import ModelCard from '@/views/console/components/model-card.vue'
import LobeIcon from '@/components/LobeIcon/index.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
...@@ -376,6 +469,44 @@ const qrCode = ref({ ...@@ -376,6 +469,44 @@ const qrCode = ref({
visible: false visible: false
}) })
// 模型详情侧边栏
const showModelDrawer = ref(false)
const selectedModel = ref(null)
function openModelDetail(model) {
selectedModel.value = model
showModelDrawer.value = true
}
const selectedModelIcon = computed(() => {
if (selectedModel.value?.icon) {
const [name, variant] = selectedModel.value.icon.split('.')
return { name, variant: variant || 'Color' }
}
return { name: '', variant: 'Color' }
})
const selectedModelIsPerCall = computed(() => Number(selectedModel.value?.quotaType) === 1)
const selectedModelBillingMode = computed(() => selectedModel.value?.billingMode || '')
const selectedModelTags = computed(() => {
if (!selectedModel.value?.tags) return []
return selectedModel.value.tags.split(',').map(t => t.trim()).filter(t => t)
})
const selectedModelCacheReadPrice = computed(() => selectedModel.value?.cacheReadPrice || 0)
const selectedModelCacheCreatePrice = computed(() => selectedModel.value?.cacheCreatePrice || 0)
function formatModelPrice(price) {
if (price === null || price === undefined) return '-'
const num = Number(price)
if (Number.isNaN(num)) return '-'
return num.toFixed(4)
}
const tagColors = ['primary', 'success', 'warning', 'danger', '']
function getTypeData() { function getTypeData() {
categoryMenuNew().then(res => { categoryMenuNew().then(res => {
...@@ -1217,4 +1348,156 @@ onMounted(() => { ...@@ -1217,4 +1348,156 @@ onMounted(() => {
transform: translateY(0); transform: translateY(0);
} }
} }
/* 模型详情侧边栏 */
.model-detail {
padding: 0 4px;
}
.model-detail-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 0 16px;
border-bottom: 1px solid #eee;
margin-bottom: 16px;
}
.model-detail-icon {
width: 48px;
height: 48px;
border-radius: 10px;
background: #f5f6f8;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.model-detail-name {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
word-break: break-word;
}
.model-detail-section {
margin-bottom: 12px;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #303233;
margin-bottom: 4px;
}
.section-desc {
font-size: 13px;
color: #949899;
}
.model-detail-desc {
font-size: 14px;
color: #606266;
line-height: 1.6;
margin-bottom: 16px;
padding: 12px;
background: #f5f7fa;
border-radius: 8px;
white-space: pre-wrap;
word-break: break-word;
}
.model-detail-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 20px;
.el-tag {
margin: 0;
}
}
.price-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
.price-label {
font-size: 14px;
color: #606266;
}
.price-value {
font-size: 14px;
color: #ee5a52;
font-weight: 600;
}
/* 分档计费表格 */
.tier-table {
border: 1px solid #ebeef5;
border-radius: 8px;
overflow: hidden;
margin-bottom: 16px;
}
.tier-header {
display: flex;
background: #f5f7fa;
font-weight: 600;
font-size: 13px;
color: #606266;
}
.tier-row {
display: flex;
border-top: 1px solid #ebeef5;
}
.tier-cell {
flex: 1;
padding: 10px 12px;
font-size: 13px;
color: #303233;
}
.tier-cell:first-child {
flex: 1.3;
}
.price-cell {
text-align: center;
color: #303233;
font-weight: 500;
}
.tier-header .tier-cell {
text-align: center;
}
.tier-name {
display: inline-block;
padding: 2px 8px;
background: #e6f0ff;
color: #2e77e3;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.tier-condition {
font-size: 11px;
color: #909399;
margin-top: 4px;
}
</style> </style>
<template> <template>
<div class="model-card"> <div class="model-card" @click="$emit('click', model)">
<!-- 右上角:按量/按次收费 --> <!-- 右上角:按量/按次收费 -->
<div class="tag-top">{{ billingTagText }}</div> <div class="tag-top">{{ billingTagText }}</div>
...@@ -32,23 +32,38 @@ ...@@ -32,23 +32,38 @@
{{ model.description || '暂无描述' }} {{ model.description || '暂无描述' }}
</div> </div>
<!-- 底部:收费价格 tag + 价格(按量/按次不同展示) --> <!-- 底部:价格展示 -->
<div class="card-footer"> <div class="card-footer">
<div class="tag-price">{{ priceTagText }}</div>
<div class="price-text"> <div class="price-text">
<!-- 按次计费:单次价格 --> <!-- 按次计费:单次价格 -->
<template v-if="isPerCall"> <template v-if="isPerCall">
<span>每次调用<b>¥{{ formatPrice(model.inputPrice) }}</b></span> <span>每次调用<b>¥{{ formatPrice(model.inputPrice) }}</b></span>
</template> </template>
<!-- 分档计费:显示第一档 -->
<template v-else-if="hasTieredPrice">
<span>输入价格:<b>¥{{ formatPrice(model.tiers[0].inputRatio) }}</b></span>
<span class="unit"> / 1M</span>
<span class="divider"></span>
<span>输出价格:<b>¥{{ formatPrice(model.tiers[0].outputRatio) }}</b></span>
<span class="unit"> / 1M</span>
</template>
<!-- 按量计费:输入/输出 --> <!-- 按量计费:输入/输出 -->
<template v-else> <template v-else>
<span>输入:<b>¥{{ formatPrice(model.inputPrice) }}</b></span> <span>输入价格:<b>¥{{ formatPrice(model.inputPrice) }}</b></span>
<span class="unit"> / 1M tokens</span> <span class="unit"> / 1M</span>
<span class="divider"></span> <span class="divider"></span>
<span>输出:<b>¥{{ formatPrice(model.outputPrice) }}</b></span> <span>输出价格:<b>¥{{ formatPrice(model.outputPrice) }}</b></span>
<span class="unit"> / 1M tokens</span> <span class="unit"> / 1M</span>
</template> </template>
</div> </div>
<!-- 缓存价格另起一行 -->
<div v-if="hasCachePrice" class="price-text cache-price-row">
<span>缓存读取价格:<b>¥{{ formatPrice(model.cacheReadPrice) }}</b></span>
<span class="unit"> / 1M</span>
<span class="divider"></span>
<span>缓存写入价格:<b>¥{{ formatPrice(model.cacheCreatePrice) }}</b></span>
<span class="unit"> / 1M</span>
</div>
</div> </div>
</div> </div>
</template> </template>
...@@ -64,6 +79,8 @@ const props = defineProps({ ...@@ -64,6 +79,8 @@ const props = defineProps({
} }
}) })
defineEmits(['click'])
// 从 model.icon 字符串中拆出 vendor 名 + variant // 从 model.icon 字符串中拆出 vendor 名 + variant
// "DeepSeek.Color" -> { name: "DeepSeek", variant: "Color" } // "DeepSeek.Color" -> { name: "DeepSeek", variant: "Color" }
// "DeepSeek.Mono" -> { name: "DeepSeek", variant: "Mono" } // "DeepSeek.Mono" -> { name: "DeepSeek", variant: "Mono" }
...@@ -80,11 +97,28 @@ const iconInfo = computed(() => { ...@@ -80,11 +97,28 @@ const iconInfo = computed(() => {
const isPerCall = computed(() => Number(props.model?.quotaType) === 1) const isPerCall = computed(() => Number(props.model?.quotaType) === 1)
// 右上角 tag 文案 // 右上角 tag 文案
const billingTagText = computed(() => (isPerCall.value ? '按次收费' : '按量收费')) const billingTagText = computed(() => {
if (isPerCall.value) return '按次收费'
if (isTiered.value) return '动态计费'
return '按量收费'
})
// 是否分档计费
const isTiered = computed(() => props.model?.billingMode === 'tiered_expr')
// 底部 tag 文案 // 底部 tag 文案
const priceTagText = computed(() => (isPerCall.value ? '单次价格' : '收费价格')) const priceTagText = computed(() => (isPerCall.value ? '单次价格' : '收费价格'))
// 是否有缓存价格(缓存读取价格和缓存创建价格都大于0才显示)
const hasCachePrice = computed(() => {
return props.model?.cacheReadPrice > 0 || props.model?.cacheCreatePrice > 0
})
// 是否有分档价格
const hasTieredPrice = computed(() => {
return isTiered.value && props.model?.tiers && props.model.tiers.length > 0
})
// 模型标签列表 // 模型标签列表
const modelTags = computed(() => { const modelTags = computed(() => {
if (!props.model?.tags) return [] if (!props.model?.tags) return []
...@@ -112,6 +146,9 @@ function formatPrice(price) { ...@@ -112,6 +146,9 @@ function formatPrice(price) {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
transition: box-shadow 0.2s ease, transform 0.2s ease; transition: box-shadow 0.2s ease, transform 0.2s ease;
min-height: 180px; min-height: 180px;
display: flex;
flex-direction: column;
cursor: pointer;
&:hover { &:hover {
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
...@@ -167,13 +204,13 @@ function formatPrice(price) { ...@@ -167,13 +204,13 @@ function formatPrice(price) {
.card-description { .card-description {
font-size: 13px; font-size: 13px;
color: #666; color: #666;
line-height: 1.65; line-height: 1.5;
margin-bottom: 12px; margin-bottom: 12px;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 3; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
min-height: 60px; min-height: 39px;
word-break: break-word; word-break: break-word;
} }
...@@ -191,31 +228,19 @@ function formatPrice(price) { ...@@ -191,31 +228,19 @@ function formatPrice(price) {
.card-footer { .card-footer {
display: flex; display: flex;
align-items: center; flex-direction: column;
flex-wrap: wrap; gap: 4px;
gap: 12px; margin-top: auto;
}
.tag-price {
background: #f0f5ff;
color: #4e6ef2;
font-size: 12px;
font-weight: 500;
padding: 4px 12px;
border-radius: 6px;
line-height: 1.5;
} }
.price-text { .price-text {
font-size: 13px; font-size: 13px;
color: #666; color: #666;
display: flex; display: flex;
height: 26px; flex-wrap: wrap;
align-items: center; align-items: center;
// flex-wrap: wrap; gap: 4px;
& span { & span {
display: block;
height: 26px;
display: flex; display: flex;
align-items: center; align-items: center;
} }
...@@ -233,5 +258,14 @@ function formatPrice(price) { ...@@ -233,5 +258,14 @@ function formatPrice(price) {
color: #999; color: #999;
font-size: 12px; font-size: 12px;
} }
.cache-price {
color: #909399;
}
}
.cache-price-row {
font-size: 13px;
color: #666;
} }
</style> </style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment