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
60a32cda
authored
Jun 08, 2026
by
renyizhao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
模型列表icon问题
parent
9ba2dabd
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
362 additions
and
16 deletions
+362
-16
src/components/LobeIcon/index.vue
+173
-0
src/views/computingResource/resourceList.vue
+160
-3
src/views/console/components/model-card.vue
+18
-11
vite.config.js
+11
-2
No files found.
src/components/LobeIcon/index.vue
0 → 100644
View file @
60a32cda
<
template
>
<svg
v-if=
"paths"
:width=
"size"
:height=
"size"
viewBox=
"0 0 24 24"
xmlns=
"http://www.w3.org/2000/svg"
:class=
"['lobe-icon',
{ 'lobe-icon-rounded': rounded }]"
>
<!-- 部分厂商(Qwen / ChatGLM / Wenxin / Stability 等)用 linearGradient 做渐变填充 -->
<defs
v-if=
"gradient"
>
<linearGradient
:id=
"gradientId"
:x1=
"gradient.x1"
:x2=
"gradient.x2"
:y1=
"gradient.y1"
:y2=
"gradient.y2"
>
<stop
v-for=
"(s, idx) in gradient.stops"
:key=
"idx"
:offset=
"s.offset"
:stop-color=
"s.stopColor"
:stop-opacity=
"s.stopOpacity"
/>
</linearGradient>
</defs>
<path
v-for=
"(p, idx) in paths"
:key=
"idx"
:d=
"p.d"
:fill=
"resolvePathFill(p.fill)"
/>
</svg>
<span
v-else
:class=
"['lobe-icon-fallback',
{ 'lobe-icon-rounded': rounded }]"
:style="fallbackStyle"
>
{{
fallbackLetter
}}
</span>
</
template
>
<
script
setup
>
import
{
computed
,
getCurrentInstance
}
from
'vue'
const
instance
=
getCurrentInstance
()
const
uid
=
instance
?.
uid
??
Math
.
floor
(
Math
.
random
()
*
1
e9
)
const
props
=
defineProps
({
name
:
{
type
:
String
,
default
:
''
},
variant
:
{
type
:
String
,
default
:
'Color'
},
size
:
{
type
:
[
Number
,
String
],
default
:
24
},
color
:
{
type
:
String
,
default
:
''
},
rounded
:
{
type
:
Boolean
,
default
:
true
}
})
// 加载 lobehub 包 Color + Mono 组件源文件
const
sources
=
import
.
meta
.
glob
(
[
'@lobehub/icons-es/*/components/Color.js'
,
'@lobehub/icons-es/*/components/Mono.js'
],
{
eager
:
true
,
query
:
'?raw'
,
import
:
'default'
}
)
// 提 path:d: "x" + fill: "x" 或 fill: 变量
function
extractPaths
(
raw
)
{
// 三种 fill 写法都要匹配:字面字符串 / 变量名(fill / fill0 / 其他)
const
tokenRe
=
/d:
\s
*"
([^
"
]
+
)
"|fill:
\s
*"
([^
"
]
+
)
"|fill:
\s
*
([
A-Za-z_$
][\w
$
]
*
)
/g
const
tokens
=
[]
let
m
while
((
m
=
tokenRe
.
exec
(
raw
))
!==
null
)
{
if
(
m
[
1
]
!=
null
)
tokens
.
push
({
type
:
'd'
,
value
:
m
[
1
]
})
else
if
(
m
[
2
]
!=
null
)
tokens
.
push
({
type
:
'fill'
,
value
:
m
[
2
],
isVar
:
false
})
else
if
(
m
[
3
]
!=
null
)
tokens
.
push
({
type
:
'fill'
,
value
:
m
[
3
],
isVar
:
true
})
}
const
paths
=
[]
let
pending
=
null
for
(
const
t
of
tokens
)
{
if
(
t
.
type
===
'd'
)
{
if
(
pending
)
paths
.
push
(
pending
)
pending
=
{
d
:
t
.
value
,
fill
:
t
.
isVar
?
'useFillId'
:
t
.
value
,
fillIsVar
:
t
.
isVar
}
}
else
if
(
pending
)
{
pending
.
fill
=
t
.
isVar
?
'useFillId'
:
t
.
value
pending
.
fillIsVar
=
t
.
isVar
}
}
if
(
pending
)
paths
.
push
(
pending
)
return
paths
}
// 提 linearGradient(lobehub 用 useFillId 生成 id,raw 里 id 是变量)
function
extractGradient
(
raw
)
{
if
(
!
/linearGradient/
.
test
(
raw
))
return
null
const
xy
=
raw
.
match
(
/x1:
\s
*"
([^
"
]
*
)
"
[\s\S]
*
?
x2:
\s
*"
([^
"
]
*
)
"
[\s\S]
*
?
y1:
\s
*"
([^
"
]
*
)
"
[\s\S]
*
?
y2:
\s
*"
([^
"
]
*
)
"/
)
if
(
!
xy
)
return
null
const
stops
=
[]
const
stopRe
=
/offset:
\s
*"
([^
"
]
*
)
"
[\s\S]
*
?
stopColor:
\s
*"
([^
"
]
+
)
"
[\s\S]
*
?
stopOpacity:
\s
*"
?([\d
.
]
+
)
"
?
/g
let
m
while
((
m
=
stopRe
.
exec
(
raw
))
!==
null
)
{
stops
.
push
({
offset
:
m
[
1
],
stopColor
:
m
[
2
],
stopOpacity
:
m
[
3
]
})
}
if
(
!
stops
.
length
)
return
null
return
{
x1
:
xy
[
1
],
x2
:
xy
[
2
],
y1
:
xy
[
3
],
y2
:
xy
[
4
],
stops
}
}
const
pathMap
=
{}
for
(
const
[
filePath
,
raw
]
of
Object
.
entries
(
sources
))
{
const
m
=
filePath
.
match
(
/
\/([^/]
+
)\/
components
\/(
Mono|Color
)\.
js$/
)
if
(
!
m
)
continue
const
[,
vendor
,
variant
]
=
m
const
paths
=
extractPaths
(
raw
)
if
(
!
paths
.
length
)
continue
const
gradient
=
extractGradient
(
raw
)
if
(
!
pathMap
[
vendor
])
pathMap
[
vendor
]
=
{}
pathMap
[
vendor
][
variant
]
=
{
paths
,
gradient
}
}
const
iconData
=
computed
(()
=>
{
if
(
!
props
.
name
)
return
null
return
pathMap
[
props
.
name
]?.[
props
.
variant
]
||
null
})
const
paths
=
computed
(()
=>
iconData
.
value
?.
paths
||
null
)
const
gradient
=
computed
(()
=>
iconData
.
value
?.
gradient
||
null
)
const
gradientId
=
computed
(()
=>
`lobe-grad-
${
props
.
name
}
-
${
uid
}
`
)
// 解析 path 的 fill:
// - 传 color prop:所有 path 全部用 color 覆盖
// - 变量 fill(lobehub useFillId 生成的 url(#xxx)):用 url(#我们的 id) 引用 defs
// - 字面颜色:原样
function
resolvePathFill
(
rawFill
)
{
if
(
props
.
color
)
return
props
.
color
||
'currentColor'
if
(
rawFill
===
'useFillId'
)
return
`url(#
${
gradientId
.
value
}
)`
return
rawFill
||
'currentColor'
}
const
fallbackLetter
=
computed
(()
=>
(
props
.
name
||
'AI'
).
charAt
(
0
).
toUpperCase
())
const
sizePx
=
computed
(()
=>
typeof
props
.
size
===
'number'
?
props
.
size
+
'px'
:
props
.
size
)
const
fallbackStyle
=
computed
(()
=>
({
width
:
sizePx
.
value
,
height
:
sizePx
.
value
,
fontSize
:
`calc(
${
sizePx
.
value
}
* 0.5)`
,
lineHeight
:
sizePx
.
value
}))
</
script
>
<
style
scoped
lang=
"scss"
>
.lobe-icon
{
display
:
inline-block
;
vertical-align
:
middle
;
flex-shrink
:
0
;
overflow
:
hidden
;
}
.lobe-icon-rounded
{
border-radius
:
20%
;
}
.lobe-icon-fallback
{
display
:
inline-flex
;
align-items
:
center
;
justify-content
:
center
;
background
:
linear-gradient
(
135deg
,
#4e6ef2
,
#6b8aff
);
color
:
#fff
;
font-weight
:
600
;
flex-shrink
:
0
;
}
</
style
>
src/views/computingResource/resourceList.vue
View file @
60a32cda
...
@@ -277,11 +277,30 @@
...
@@ -277,11 +277,30 @@
</div>
</div>
</el-dialog>
</el-dialog>
<!-- 底部固定:未登录或未获取令牌时显示 -->
<transition
name=
"el-fade-in"
v-if=
"tabActive === 999"
>
<div
v-if=
"!hasToken"
class=
"bottom-cta"
>
<div
class=
"cta-card"
>
<div
class=
"cta-text"
>
<div
class=
"cta-title"
>
{{ ctaTitle }}
</div>
<div
class=
"cta-subtitle"
>
{{ ctaSubtitle }}
</div>
</div>
<el-button
type=
"primary"
size=
"large"
class=
"cta-btn"
:loading=
"ctaLoading"
@
click=
"handleGetTokenCta"
>
{{ ctaButtonText }}
</el-button>
</div>
</div>
</transition>
</div>
</div>
</template>
</template>
<
script
name=
"ResourceList"
setup
>
<
script
name=
"ResourceList"
setup
>
import
{
ref
,
watch
,
nextTick
}
from
'vue'
import
{
ref
,
watch
,
nextTick
,
computed
}
from
'vue'
import
{
ElMessage
,
ElMessageBox
}
from
'element-plus'
import
{
ElMessage
,
ElMessageBox
}
from
'element-plus'
import
SvgIcon
from
'@/components/SvgIcon/index.vue'
import
SvgIcon
from
'@/components/SvgIcon/index.vue'
import
{
import
{
...
@@ -293,6 +312,8 @@ import {
...
@@ -293,6 +312,8 @@ import {
createPay
,
createPay
,
modelsWithPricing
modelsWithPricing
}
from
'@/api/computingResource.js'
}
from
'@/api/computingResource.js'
import
{
getTokenInfo
,
getToken
}
from
'@/api/console.js'
import
useUserStore
from
'@/store/modules/user'
import
{
useRoute
,
useRouter
}
from
'vue-router'
import
{
useRoute
,
useRouter
}
from
'vue-router'
import
QRCode
from
'qrcode'
import
QRCode
from
'qrcode'
import
request
from
'@/utils/request'
import
request
from
'@/utils/request'
...
@@ -303,6 +324,7 @@ import ModelCard from '@/views/console/components/model-card.vue'
...
@@ -303,6 +324,7 @@ import ModelCard from '@/views/console/components/model-card.vue'
const
route
=
useRoute
()
const
route
=
useRoute
()
const
router
=
useRouter
()
const
router
=
useRouter
()
const
userStore
=
useUserStore
()
const
showVersion
=
ref
(
2
)
const
showVersion
=
ref
(
2
)
const
tabActive
=
ref
()
const
tabActive
=
ref
()
...
@@ -641,8 +663,66 @@ function getModelList() {
...
@@ -641,8 +663,66 @@ function getModelList() {
})
})
}
}
onMounted
(()
=>
{
// 底部"获取令牌"按钮相关
const
hasToken
=
ref
(
true
)
// 默认认为有 token,未登录时保持 true 不显示
const
ctaLoading
=
ref
(
false
)
const
ctaTitle
=
computed
(()
=>
userStore
.
token
?
'尚未获取 API 令牌'
:
'解锁全部 AI 模型'
)
const
ctaSubtitle
=
computed
(()
=>
userStore
.
token
?
'获取令牌后即可调用 OpenAI、DeepSeek 等数百款模型'
:
'登录后即可一键获取 API 令牌,立即体验'
)
const
ctaButtonText
=
computed
(()
=>
userStore
.
token
?
'立即获取'
:
'登录并获取'
)
function
checkTokenStatus
()
{
if
(
!
userStore
.
token
)
{
// 未登录:显示按钮
hasToken
.
value
=
false
return
}
// 已登录:调接口查 token 状态
getTokenInfo
().
then
(
res
=>
{
if
(
res
&&
res
.
hasToken
)
{
hasToken
.
value
=
true
}
else
{
hasToken
.
value
=
false
}
}).
catch
(()
=>
{
// 接口失败:保守显示按钮
hasToken
.
value
=
false
})
}
function
handleGetTokenCta
()
{
if
(
!
userStore
.
token
)
{
// 未登录:跳转到登录页,回跳到本页
const
currentPath
=
route
.
fullPath
router
.
push
({
path
:
'/login'
,
query
:
{
redirect
:
currentPath
}
})
return
}
// 已登录:调 getToken 创建 New API 账号 + 获取 apiKey
ctaLoading
.
value
=
true
getToken
().
then
(
res
=>
{
ctaLoading
.
value
=
false
if
(
res
&&
res
.
success
&&
res
.
apiKey
)
{
ElMessage
.
success
(
'令牌获取成功,正在跳转到个人中心...'
)
hasToken
.
value
=
true
router
.
push
(
'/console/overview'
)
}
else
{
ElMessage
.
error
(
res
?.
message
||
'获取令牌失败'
)
}
}).
catch
(
err
=>
{
ctaLoading
.
value
=
false
ElMessage
.
error
(
'获取令牌失败: '
+
(
err
.
message
||
'未知错误'
))
})
}
onMounted
(()
=>
{
getModelList
()
getModelList
()
checkTokenStatus
()
})
})
</
script
>
</
script
>
...
@@ -949,6 +1029,83 @@ onMounted(() => {
...
@@ -949,6 +1029,83 @@ onMounted(() => {
.model-list
{
.model-list
{
display
:
grid
;
display
:
grid
;
grid-template-columns
:
repeat
(
auto-fill
,
minmax
(
380px
,
1
fr
));
grid-template-columns
:
repeat
(
auto-fill
,
minmax
(
380px
,
1
fr
));
gap
:
16px
;
gap
:
16px
;
padding-bottom
:
45px
;
}
/* 底部固定"获取令牌"卡片 */
.bottom-cta
{
position
:
fixed
;
left
:
0
;
right
:
0
;
bottom
:
0
;
z-index
:
1000
;
padding
:
0
24px
20px
;
display
:
flex
;
justify-content
:
center
;
pointer-events
:
none
;
}
.cta-card
{
pointer-events
:
auto
;
width
:
min
(
720px
,
100%
);
display
:
flex
;
align-items
:
center
;
gap
:
20px
;
padding
:
16px
20px
16px
24px
;
background
:
rgba
(
255
,
255
,
255
,
0.78
);
-webkit-backdrop-filter
:
saturate
(
180%
)
blur
(
20px
);
backdrop-filter
:
saturate
(
180%
)
blur
(
20px
);
border
:
1px
solid
rgba
(
255
,
255
,
255
,
0.6
);
border-radius
:
20px
;
box-shadow
:
0
-4px
24px
rgba
(
0
,
0
,
0
,
0.04
),
0
12px
32px
rgba
(
46
,
119
,
227
,
0.12
),
0
0
0
1px
rgba
(
46
,
119
,
227
,
0.05
);
}
.cta-text
{
flex
:
1
;
min-width
:
0
;
text-align
:
left
;
}
.cta-title
{
font-size
:
15px
;
font-weight
:
600
;
color
:
#1f2329
;
line-height
:
1.4
;
margin-bottom
:
4px
;
}
.cta-subtitle
{
font-size
:
13px
;
font-weight
:
400
;
color
:
#6b7280
;
line-height
:
1.5
;
}
.cta-btn
{
flex-shrink
:
0
;
height
:
44px
;
min-width
:
140px
;
padding
:
0
24px
;
font-size
:
15px
;
font-weight
:
600
;
letter-spacing
:
1px
;
border-radius
:
12px
;
background
:
linear-gradient
(
135deg
,
#2E77E3
0%
,
#4e8ff7
100%
);
border
:
none
;
box-shadow
:
0
4px
12px
rgba
(
46
,
119
,
227
,
0.28
);
transition
:
transform
0.2s
ease
,
box-shadow
0.2s
ease
;
&:hover
{
transform
:
translateY
(
-1px
);
box-shadow
:
0
6px
16px
rgba
(
46
,
119
,
227
,
0.38
);
}
&
:active
{
transform
:
translateY
(
0
);
}
}
}
</
style
>
</
style
>
src/views/console/components/model-card.vue
View file @
60a32cda
...
@@ -6,7 +6,11 @@
...
@@ -6,7 +6,11 @@
<!-- 头部:图标 + 模型名 -->
<!-- 头部:图标 + 模型名 -->
<div
class=
"card-header"
>
<div
class=
"card-header"
>
<div
class=
"model-icon"
>
<div
class=
"model-icon"
>
<span>
{{
iconLetter
}}
</span>
<LobeIcon
:name=
"iconInfo.name"
:variant=
"iconInfo.variant"
:size=
"30"
/>
</div>
</div>
<div
class=
"model-name"
:title=
"model.modelName"
>
{{
model
.
modelName
}}
</div>
<div
class=
"model-name"
:title=
"model.modelName"
>
{{
model
.
modelName
}}
</div>
</div>
</div>
...
@@ -32,6 +36,7 @@
...
@@ -32,6 +36,7 @@
<
script
setup
>
<
script
setup
>
import
{
computed
}
from
'vue'
import
{
computed
}
from
'vue'
import
LobeIcon
from
'@/components/LobeIcon/index.vue'
const
props
=
defineProps
({
const
props
=
defineProps
({
model
:
{
model
:
{
...
@@ -40,9 +45,16 @@ const props = defineProps({
...
@@ -40,9 +45,16 @@ const props = defineProps({
}
}
})
})
const
iconLetter
=
computed
(()
=>
{
// 从 model.icon 字符串中拆出 vendor 名 + variant
const
name
=
props
.
model
?.
modelName
||
'AI'
// "DeepSeek.Color" -> { name: "DeepSeek", variant: "Color" }
return
name
.
charAt
(
0
).
toUpperCase
()
// "DeepSeek.Mono" -> { name: "DeepSeek", variant: "Mono" }
// 后端不主动推断,没配就返回空,由 LobeIcon fallback 到首字母
const
iconInfo
=
computed
(()
=>
{
if
(
props
.
model
?.
icon
)
{
const
[
name
,
variant
]
=
props
.
model
.
icon
.
split
(
'.'
)
return
{
name
,
variant
:
variant
||
'Color'
}
}
return
{
name
:
''
,
variant
:
'Color'
}
})
})
function
formatPrice
(
price
)
{
function
formatPrice
(
price
)
{
...
@@ -96,17 +108,12 @@ function formatPrice(price) {
...
@@ -96,17 +108,12 @@ function formatPrice(price) {
width
:
44px
;
width
:
44px
;
height
:
44px
;
height
:
44px
;
border-radius
:
10px
;
border-radius
:
10px
;
background
:
linear-gradient
(
135deg
,
#4e6ef2
,
#6b8aff
)
;
background
:
#f5f6f8
;
display
:
flex
;
display
:
flex
;
align-items
:
center
;
align-items
:
center
;
justify-content
:
center
;
justify-content
:
center
;
flex-shrink
:
0
;
flex-shrink
:
0
;
overflow
:
hidden
;
span
{
color
:
#fff
;
font-size
:
20px
;
font-weight
:
600
;
}
}
}
.model-name
{
.model-name
{
...
...
vite.config.js
View file @
60a32cda
...
@@ -18,16 +18,25 @@ export default defineConfig(({ mode, command }) => {
...
@@ -18,16 +18,25 @@ export default defineConfig(({ mode, command }) => {
// 设置路径
// 设置路径
'~'
:
path
.
resolve
(
__dirname
,
'./'
),
'~'
:
path
.
resolve
(
__dirname
,
'./'
),
// 设置别名
// 设置别名
'@'
:
path
.
resolve
(
__dirname
,
'./src'
)
'@'
:
path
.
resolve
(
__dirname
,
'./src'
),
// lobehub icons 包别名(供 import.meta.glob 使用)
'@lobehub/icons-es'
:
path
.
resolve
(
__dirname
,
'node_modules/@lobehub/icons/es'
)
},
},
// https://cn.vitejs.dev/config/#resolve-extensions
// https://cn.vitejs.dev/config/#resolve-extensions
extensions
:
[
'.mjs'
,
'.js'
,
'.ts'
,
'.jsx'
,
'.tsx'
,
'.json'
,
'.vue'
]
extensions
:
[
'.mjs'
,
'.js'
,
'.ts'
,
'.jsx'
,
'.tsx'
,
'.json'
,
'.vue'
]
},
},
//
vite 相关配置
//
允许 Vite 从 node_modules/@lobehub 读取 .js 源文件(供 ?raw 提取 SVG path)
server
:
{
server
:
{
//port: 18081,
//port: 18081,
host
:
true
,
host
:
true
,
open
:
true
,
open
:
true
,
fs
:
{
// Vite 5 设为白名单制:必须显式包含项目根,否则默认拒绝
allow
:
[
path
.
resolve
(
__dirname
),
path
.
resolve
(
__dirname
,
'node_modules/@lobehub'
)
]
},
proxy
:
{
proxy
:
{
// https://cn.vitejs.dev/config/#server-proxy
// https://cn.vitejs.dev/config/#server-proxy
'/dev-api'
:
{
'/dev-api'
:
{
...
...
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