Commit 60a32cda by renyizhao

模型列表icon问题

parent 9ba2dabd
<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() * 1e9)
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>
......@@ -277,11 +277,30 @@
</div>
</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>
</template>
<script name="ResourceList" setup>
import {ref, watch, nextTick} from 'vue'
import {ref, watch, nextTick, computed} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import SvgIcon from '@/components/SvgIcon/index.vue'
import {
......@@ -293,6 +312,8 @@ import {
createPay,
modelsWithPricing
} from '@/api/computingResource.js'
import { getTokenInfo, getToken } from '@/api/console.js'
import useUserStore from '@/store/modules/user'
import {useRoute, useRouter} from 'vue-router'
import QRCode from 'qrcode'
import request from '@/utils/request'
......@@ -303,6 +324,7 @@ import ModelCard from '@/views/console/components/model-card.vue'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const showVersion = ref(2)
const tabActive = ref()
......@@ -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()
checkTokenStatus()
})
</script>
......@@ -949,6 +1029,83 @@ onMounted(() => {
.model-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
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>
......@@ -6,7 +6,11 @@
<!-- 头部:图标 + 模型名 -->
<div class="card-header">
<div class="model-icon">
<span>{{ iconLetter }}</span>
<LobeIcon
:name="iconInfo.name"
:variant="iconInfo.variant"
:size="30"
/>
</div>
<div class="model-name" :title="model.modelName">{{ model.modelName }}</div>
</div>
......@@ -32,6 +36,7 @@
<script setup>
import { computed } from 'vue'
import LobeIcon from '@/components/LobeIcon/index.vue'
const props = defineProps({
model: {
......@@ -40,9 +45,16 @@ const props = defineProps({
}
})
const iconLetter = computed(() => {
const name = props.model?.modelName || 'AI'
return name.charAt(0).toUpperCase()
// 从 model.icon 字符串中拆出 vendor 名 + variant
// "DeepSeek.Color" -> { name: "DeepSeek", variant: "Color" }
// "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) {
......@@ -96,17 +108,12 @@ function formatPrice(price) {
width: 44px;
height: 44px;
border-radius: 10px;
background: linear-gradient(135deg, #4e6ef2, #6b8aff);
background: #f5f6f8;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
span {
color: #fff;
font-size: 20px;
font-weight: 600;
}
overflow: hidden;
}
.model-name {
......
......@@ -18,16 +18,25 @@ export default defineConfig(({ mode, command }) => {
// 设置路径
'~': 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
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
// vite 相关配置
// 允许 Vite 从 node_modules/@lobehub 读取 .js 源文件(供 ?raw 提取 SVG path)
server: {
//port: 18081,
host: true,
open: true,
fs: {
// Vite 5 设为白名单制:必须显式包含项目根,否则默认拒绝
allow: [
path.resolve(__dirname),
path.resolve(__dirname, 'node_modules/@lobehub')
]
},
proxy: {
// https://cn.vitejs.dev/config/#server-proxy
'/dev-api': {
......
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