Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
client
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
e8278a8e
authored
Jun 18, 2026
by
renyizhao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
分档计费与大屏
parent
f739f0e6
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
346 additions
and
29 deletions
+346
-29
src/views/computingResource/resourceList.vue
+284
-1
src/views/console/components/model-card.vue
+62
-28
No files found.
src/views/computingResource/resourceList.vue
View file @
e8278a8e
...
@@ -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
>
src/views/console/components/model-card.vue
View file @
e8278a8e
<
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.
6
5
;
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
:
60
px
;
min-height
:
39
px
;
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
>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment