2026-05-25 18:04:10 +08:00

543 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="model-detail-page">
<header class="top-nav">
<button class="back-btn" type="button" @click="goBack">
<i class="el-icon-arrow-left"></i>
返回
</button>
<span class="nav-divider"></span>
<span class="token-market-link" @click="goTokenMarket">Token市集</span>
<div class="nav-actions">
<span>控制台</span>
<span>用户后台</span>
</div>
</header>
<main class="detail-container">
<section class="model-hero-card">
<div class="model-logo">
<i class="el-icon-data-analysis"></i>
</div>
<div class="model-summary">
<div class="model-title-row">
<h1>{{ modelInfo.name }}</h1>
<el-tag size="mini" type="success">{{ modelInfo.tag }}</el-tag>
</div>
<div class="model-meta">
<span v-for="item in modelInfo.metaList" :key="item">{{ item }}</span>
</div>
<p>{{ modelInfo.description }}</p>
</div>
</section>
<section class="version-card">
<div>
<h3>版本介绍</h3>
<p>
模型ID{{ modelInfo.modelId }}
<i class="el-icon-copy-document"></i>
</p>
<p>{{ modelInfo.versionDescription }}</p>
</div>
<div class="version-actions">
<el-button size="small" @click="goApiDocument">API文档</el-button>
<el-button
size="small"
:type="canExperience ? 'primary' : 'info'"
:disabled="!canExperience"
@click="goExperience"
>
体验
</el-button>
</div>
</section>
<section class="two-column">
<div class="info-card">
<h3>模型能力</h3>
<div v-for="item in capabilityList" :key="item.label" class="info-row">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
</div>
</div>
<div class="info-card">
<h3>模型限制</h3>
<div v-for="item in limitList" :key="item.label" class="info-row">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
</div>
</div>
</section>
<section class="price-card">
<h3>服务价格</h3>
<div class="price-list">
<div class="price-item input">
<span>模型输入</span>
<strong>{{ modelInfo.inputPrice }}</strong>
<em>{{ modelInfo.priceUnit }}</em>
</div>
<div class="price-item output">
<span>模型输出</span>
<strong>{{ modelInfo.outputPrice }}</strong>
<em>{{ modelInfo.priceUnit }}</em>
</div>
<div v-if="hasValue(modelInfo.cacheHitInputPrice)" class="price-item cache">
<span>缓存命中输入</span>
<strong>{{ modelInfo.cacheHitInputPrice }}</strong>
<em>{{ modelInfo.priceUnit }}</em>
</div>
</div>
</section>
<section class="article-card">
<h3>1. 模型介绍</h3>
<p>{{ modelInfo.longDescription }}</p>
<h3>2. 模型亮点</h3>
<div
v-for="(item, index) in featureList"
:key="item.label"
class="feature-block"
:class="index % 2 === 0 ? 'blue' : 'purple'"
>
<i :class="index % 2 === 0 ? 'el-icon-cpu' : 'el-icon-magic-stick'"></i>
<div>
<strong>{{ item.label }}</strong>
<p>{{ item.value }}</p>
</div>
</div>
</section>
</main>
<footer class="page-footer">© 2026 开元云科技 · 模型公共服务平台</footer>
</div>
</template>
<script>
export default {
name: 'ModelDetail',
data() {
return {
modelInfo: {
name: 'MiniMax-M2.5',
modelId: 'abab7c72c278cfba',
description: 'MiniMax-M2.5 是面向复杂任务处理的通用语言模型,适用于知识问答、文案创作、工具调用和办公生产力场景。',
longDescription: 'MiniMax-M2.5 是 MiniMax 推出的新一代旗舰语言模型,致力于提升真实世界复杂任务中的表现。在推理、工具使用和搜索、办公生产力场景中均具备较好的任务完成能力。',
tag: '对话模型',
metaList: ['MiniMax', '对话模型', '192K上下文'],
versionDescription: '当前版本能力稳定,适合内容生成、知识问答、工具调用和复杂任务规划。',
inputPrice: '0.0021',
outputPrice: '0.0084',
cacheHitInputPrice: '',
priceUnit: '元/千Tokens',
experience: 1
},
capabilityList: [
{ label: '接口类型', value: '/v2/chat/completions' },
{ label: '接入ID', value: 'minimax-m2.5' },
{ label: '输入类型', value: '文本 / 图像 / 音频' },
{ label: '输出类型', value: '文本 / 结构化输出' },
{ label: 'Function Call', value: '支持' },
{ label: '联网搜索', value: '支持' },
{ label: '私有化部署', value: '不支持' }
],
limitList: [
{ label: '输入长度', value: '192K' },
{ label: '单轮输出', value: '32K' },
{ label: '输出长度', value: '128K' },
{ label: '服务速度限制', value: '60 RPM / 250000 TPM' }
],
featureList: [
{ label: '推理', value: '擅长处理数学、代码、逻辑分析和复杂任务拆解,适合作为业务助手和智能问答底座。' },
{ label: '模型调优', value: '提供稳定的上下文理解能力,便于后续结合业务数据进行知识增强和场景优化。' }
]
}
},
computed: {
canExperience() {
return Number(this.modelInfo.experience) === 1
}
},
created() {
this.loadModelDetail()
},
watch: {
'$route.query.id'() {
this.loadModelDetail()
}
},
methods: {
// TOKEN 市集卡片跳转时会缓存完整模型对象,详情页优先使用这份数据渲染。
loadModelDetail() {
const model = this.getCachedTokenMarketModel()
if (!model) return
const name = this.displayValue(model.display_name || model.model_name)
const modelId = this.displayValue(model.llmid || model.model_name || model.id)
const modelType = this.displayValue(model.model_type)
const provider = this.displayValue(model.provider)
const contextLength = this.displayValue(model.context_length)
const billingMethod = this.displayValue(model.billing_method)
const description = this.normalizeText(model.description)
this.modelInfo = {
name,
modelId,
description,
longDescription: description,
tag: modelType,
metaList: [provider, modelType, contextLength !== '-' ? `${contextLength}上下文` : '', billingMethod].filter(Boolean),
versionDescription: `${name} 当前版本由 ${provider} 提供,适合在 ${modelType} 场景中使用。`,
inputPrice: this.formatPrice(model.input_token_price),
outputPrice: this.formatPrice(model.output_token_price),
cacheHitInputPrice: this.formatPrice(model.cache_hit_input_price),
priceUnit: this.getPriceUnit(model.billing_unit),
experience: this.normalizeExperience(model.experience)
}
this.capabilityList = this.parseInfoList(model.capabilities, [
{ label: '接口类型', value: '-' },
{ label: '接入ID', value: modelId },
{ label: '模型类型', value: modelType },
{ label: '供应商', value: provider }
])
this.limitList = this.parseInfoList(model.limitations, [
{ label: '上下文长度', value: contextLength },
{ label: '计费方式', value: billingMethod },
{ label: '计费单位', value: this.displayValue(model.billing_unit) }
])
this.featureList = this.parseInfoList(model.highlights, [
{ label: '模型说明', value: description },
{ label: '适用场景', value: `${modelType}、内容生成、智能问答` }
])
},
getCachedTokenMarketModel() {
const cache = sessionStorage.getItem('tokenMarketSelectedModel')
if (!cache) return null
try {
const model = JSON.parse(cache)
const queryId = String(this.$route.query.id || this.$route.query.model_id || '')
if (!queryId || String(model.id) === queryId || String(model.llmid) === queryId || String(model.model_name) === queryId) {
return model
}
} catch (error) {
console.warn('[模型详情] 解析 TOKEN 市集模型缓存失败', error)
}
return null
},
parseInfoList(value, fallback = []) {
const parsed = this.safeParse(value)
let list = []
if (Array.isArray(parsed)) {
list = parsed.map((item, index) => ({
label: this.displayValue(item.name || item.label || item.key || `字段${index + 1}`),
value: this.displayValue(this.pickValue(item.value, item.text, item.content))
}))
} else if (parsed && typeof parsed === 'object') {
list = Object.keys(parsed).map(key => ({
label: key,
value: this.displayValue(parsed[key])
}))
} else if (typeof parsed === 'string' && parsed) {
list = [{ label: '说明', value: parsed }]
}
return list.length ? list : fallback
},
normalizeText(value) {
const parsed = this.safeParse(value)
if (Array.isArray(parsed)) {
return parsed.map(item => `${this.displayValue(item.name || item.label)}${this.displayValue(item.value)}`).join('')
}
if (parsed && typeof parsed === 'object') {
return Object.keys(parsed).map(key => `${key}${this.displayValue(parsed[key])}`).join('')
}
return this.displayValue(parsed)
},
safeParse(value) {
if (value === undefined || value === null || value === '') return ''
if (typeof value !== 'string') return value
try {
return JSON.parse(value)
} catch (error) {
return value
}
},
pickValue(...values) {
return values.find(value => value !== undefined && value !== null && value !== '')
},
formatPrice(value) {
if (!this.hasValue(value)) return '-'
const num = Number(value)
if (Number.isNaN(num)) return value
return num.toFixed(6).replace(/\.?0+$/, '')
},
getPriceUnit(unit) {
return unit ? `元/${unit}` : '元/千Tokens'
},
displayValue(value) {
if (value === undefined || value === null || value === '') return '-'
return value
},
hasValue(value) {
return value !== undefined && value !== null && value !== '' && value !== '-'
},
normalizeExperience(value) {
return Number(value) === 1 ? 1 : 0
},
goTokenMarket() {
if (this.$route.query.single === '1') {
this.$router.push({ path: '/tokenMarket', query: { category: 'TOKEN市集', single: '1' } })
return
}
this.$router.push({ path: '/product', query: { category: 'TOKEN市集' } })
},
goBack() {
this.$router.back()
},
buildModelQuery() {
return {
id: this.$route.query.id,
model_id: this.$route.query.model_id || this.$route.query.id,
from: 'tokenMarket',
category: 'TOKEN市集',
single: this.$route.query.single
}
},
goApiDocument() {
this.$router.push({ name: 'modelApiDocument', query: this.buildModelQuery() })
},
goExperience() {
if (!this.canExperience) return
this.$router.push({ name: 'modelExperience', query: this.buildModelQuery() })
}
}
}
</script>
<style lang="less" scoped>
.model-detail-page {
height: 100vh;
color: #1f2d3d;
background: #f6f8fb;
overflow-y: auto;
overflow-x: hidden;
}
.top-nav {
display: flex;
align-items: center;
height: 48px;
padding: 0 28px;
color: #667085;
background: #ffffff;
border-bottom: 1px solid #edf1f7;
}
.back-btn {
padding: 0;
color: #667085;
cursor: pointer;
background: transparent;
border: none;
}
.nav-divider {
width: 1px;
height: 14px;
margin: 0 14px;
background: #d8dee9;
}
.nav-actions {
display: flex;
gap: 24px;
margin-left: auto;
font-size: 13px;
}
.token-market-link {
cursor: pointer;
&:hover {
color: #2f6bff;
}
}
.detail-container {
width: 1180px;
max-width: calc(100vw - 48px);
margin: 28px auto 0;
}
.model-hero-card,
.version-card,
.info-card,
.price-card,
.article-card {
background: #ffffff;
border: 1px solid #edf1f7;
border-radius: 12px;
}
.model-hero-card {
display: flex;
padding: 28px;
}
.model-logo {
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 58px;
width: 58px;
height: 58px;
margin-right: 18px;
color: #ffffff;
font-size: 28px;
background: linear-gradient(135deg, #ff6b6b, #7c6cff);
border-radius: 12px;
}
.model-title-row {
display: flex;
align-items: center;
gap: 10px;
h1 {
margin: 0;
font-size: 26px;
}
}
.model-meta {
display: flex;
gap: 12px;
margin: 12px 0;
color: #667085;
font-size: 13px;
}
.model-summary p,
.version-card p,
.article-card p {
color: #667085;
line-height: 1.8;
}
.version-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 22px 28px;
margin-top: 18px;
}
.two-column {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
margin-top: 18px;
}
.info-card {
padding: 24px;
h3 {
margin: 0 0 18px;
padding-left: 10px;
border-left: 3px solid #2f6bff;
}
}
.info-row {
display: flex;
justify-content: space-between;
padding: 13px 0;
border-bottom: 1px solid #f0f2f5;
span {
color: #667085;
}
}
.price-card,
.article-card {
padding: 24px;
margin-top: 18px;
}
.price-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 18px;
}
.price-item {
padding: 20px;
border-radius: 12px;
span,
em {
display: block;
color: #667085;
font-style: normal;
}
strong {
display: block;
margin: 8px 0 4px;
color: #5356d8;
font-size: 28px;
}
&.input {
background: #eef4ff;
}
&.output {
background: #fff0fa;
}
&.cache {
background: #f0fdf4;
}
}
.feature-block {
display: flex;
gap: 14px;
padding: 18px;
margin-top: 14px;
border-radius: 12px;
i {
color: #2f6bff;
font-size: 20px;
}
strong {
display: block;
margin-bottom: 6px;
}
&.blue {
background: #eef4ff;
}
&.purple {
background: #fff0fa;
}
}
.page-footer {
padding: 32px 0;
color: #98a2b3;
text-align: center;
font-size: 12px;
}
</style>