2026-09-02 11:30:14 +08:00

1122 lines
26 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="token-market-page">
<!-- 左侧筛选栏 -->
<aside class="menu-sidebar">
<div class="sidebar-header">
<h3 class="sidebar-title">
<i class="el-icon-s-operation"></i>
<span>筛选模型</span>
</h3>
<button
v-if="hasActiveFilter"
class="reset-btn"
@click="resetFilter"
>
<i class="el-icon-refresh-right"></i>
重置
</button>
</div>
<el-input
v-model="searchKeyword"
class="product-search"
clearable
prefix-icon="el-icon-search"
placeholder="输入关键词 搜索模型"
></el-input>
<div class="filter-scroll">
<section class="token-filter-section">
<header class="filter-header">
<h4>模型类型</h4>
</header>
<div class="token-filter-grid">
<button
class="filter-chip"
:class="{ active: !tokenActiveModelType }"
@click="tokenActiveModelType = ''"
>
全部
</button>
<button
v-for="item in tokenModelTypeList"
:key="item"
class="filter-chip"
:class="{ active: tokenActiveModelType === item }"
@click="toggleTokenModelType(item)"
>
{{ item }}
</button>
</div>
</section>
<section class="token-filter-section">
<header class="filter-header">
<h4>提供方</h4>
</header>
<div class="token-filter-grid">
<button
class="filter-chip"
:class="{ active: !tokenActiveProvider }"
@click="tokenActiveProvider = ''"
>
全部
</button>
<button
v-for="item in tokenProviderList"
:key="item"
class="filter-chip"
:class="{ active: tokenActiveProvider === item }"
@click="toggleTokenProvider(item)"
>
<el-tooltip
:content="item"
placement="top"
:disabled="!isProviderNameOverflow(item)"
>
<span class="chip-label">{{ item }}</span>
</el-tooltip>
</button>
</div>
</section>
</div>
</aside>
<!-- 主内容区 -->
<main class="main-content">
<div class="token-market-grid">
<article
v-for="product in displayedTokenProducts"
:key="product.id"
class="token-market-card"
>
<div class="token-card-top">
<span class="token-provider-avatar">
<img
v-if="getModelLogoUrl(product.model_logo) && !product.logoLoadFailed"
:src="getModelLogoUrl(product.model_logo)"
:alt="product.display_name || product.model_name || '模型logo'"
@error="handleModelLogoError(product)"
>
<template v-else>{{ getProviderInitial(product.provider || product.display_name) }}</template>
</span>
<div class="token-title-group">
<h3>{{ product.display_name || product.model_name }}</h3>
<p>
<i class="el-icon-office-building"></i>
{{ product.provider || product.model_name || '-' }}
</p>
</div>
</div>
<div class="token-price-grid">
<div
v-for="(price, priceIndex) in parseBillingMethod(product.billing_method)"
:key="`${product.id}-billing-${priceIndex}`"
class="token-price-item"
>
<span class="price-name">{{ price.name }}</span>
<strong class="price-value">{{ price.price }}</strong>
<em class="price-desc">{{ price.desc }}</em>
</div>
</div>
<div class="token-card-footer">
<div class="token-feature-tags">
<span
v-for="(tag, tagIndex) in getTokenFeatureTags(product)"
:key="`${tag.type}-${tag.text}-${tagIndex}`"
:class="tag.type"
>
<i :class="tag.icon"></i>
{{ tag.text }}
</span>
</div>
</div>
<div class="token-hover-actions">
<button
class="primary-action"
:class="{ disabled: !isModelExperienceEnabled(product) }"
:disabled="!isModelExperienceEnabled(product)"
@click.stop="goModelExperience(product)"
>
<i class="el-icon-video-play"></i>
快速体验
</button>
<button @click.stop="goModelApiDocument(product)">
<i class="el-icon-document"></i>
API参考
</button>
</div>
</article>
</div>
<!-- 空状态 -->
<div v-if="!displayedTokenProducts.length" class="empty-state">
<div class="empty-icon">
<i class="el-icon-box"></i>
</div>
<p class="empty-text">{{ searchKeyword ? '没有匹配的模型' : '暂无模型数据' }}</p>
<span class="empty-hint">
{{ hasActiveFilter ? '可以调整搜索关键词或筛选条件后再试' : '请稍后再来,或联系管理员补充模型' }}
</span>
</div>
</main>
</div>
</template>
<script>
import { reqNavList } from "@/api/newHome";
const getImageUrlPrefix = () => {
const origin = window.location.origin
if (origin.includes('localhost') || origin.includes('dev.opencomputing.cn')) {
return 'https://dev.opencomputing.cn/idfile?path='
}
if (origin.includes('www.opencomputing.cn')) {
return 'https://www.opencomputing.cn/idfile?path='
}
if (origin.includes('www.ncmatch.cn')) {
return 'https://www.ncmatch.cn/idfile?path='
}
return `${origin}/idfile?path=`
}
export default {
name: "TokenMarket",
data() {
return {
searchKeyword: '',
tokenList: [],
tokenModelTypeList: [],
tokenProviderList: [],
tokenActiveModelType: '',
tokenActiveProvider: "",
IMG_URL: getImageUrlPrefix(),
};
},
computed: {
displayedTokenProducts() {
const keyword = this.searchKeyword.trim().toLowerCase();
return this.tokenList.filter(item => {
const matchKeyword = !keyword || [
item.display_name,
item.model_name,
item.model_type,
item.provider,
item.description
].join(' ').toLowerCase().includes(keyword);
const matchType = !this.tokenActiveModelType || item.model_type === this.tokenActiveModelType;
const matchProvider = !this.tokenActiveProvider || item.provider === this.tokenActiveProvider;
return matchKeyword && matchType && matchProvider;
});
},
hasActiveFilter() {
return Boolean(
this.searchKeyword.trim() ||
this.tokenActiveModelType ||
this.tokenActiveProvider
);
},
typeCountMap() {
const map = {};
this.tokenList.forEach(item => {
if (item.model_type) {
map[item.model_type] = (map[item.model_type] || 0) + 1;
}
});
return map;
},
providerCountMap() {
const map = {};
this.tokenList.forEach(item => {
if (item.provider) {
map[item.provider] = (map[item.provider] || 0) + 1;
}
});
return map;
},
loginState() {
const userId = sessionStorage.getItem('userId');
return userId !== null && userId !== 'null' && userId !== '';
},
},
async mounted() {
await this.loadNavData();
},
methods: {
isTokenMarketCategory(title) {
return ['TOKEN市集', 'Token市集', 'token市集', 'Token Market'].includes(title);
},
setTokenMarketData(category) {
const marketData = category && category.token_market && category.token_market.data
? category.token_market.data
: {};
this.tokenList = Array.isArray(marketData.model_list) ? marketData.model_list : [];
this.tokenModelTypeList = Array.isArray(marketData.model_type_list) ? marketData.model_type_list : [];
this.tokenProviderList = Array.isArray(marketData.provider_list) ? marketData.provider_list : [];
},
toggleTokenModelType(type) {
this.tokenActiveModelType = this.tokenActiveModelType === type ? '' : type;
},
toggleTokenProvider(provider) {
this.tokenActiveProvider = this.tokenActiveProvider === provider ? '' : provider;
},
resetFilter() {
this.searchKeyword = '';
this.tokenActiveModelType = '';
this.tokenActiveProvider = '';
},
getTypeCount(type) {
return this.typeCountMap[type] || 0;
},
getProviderCount(provider) {
return this.providerCountMap[provider] || 0;
},
// 提供方名称长度判定:> 8 个字符时大概率会换行超过 2 行,启用 tooltip
isProviderNameOverflow(name) {
if (!name) return false;
// 中文字符按 2 个英文字符宽度计,整体大于约 16 个字符视为可能溢出
let width = 0;
for (const ch of String(name)) {
// CJK 范围
if (/[ -鿿]/.test(ch)) width += 2;
else width += 1;
}
return width > 14;
},
parseBillingMethod(value) {
const text = String(value || '').trim();
if (!text) {
return [{ name: '计费方式', price: '按量计费', desc: '详情以后端配置为准' }];
}
return text
.split(/\n+/)
.map(line => line.replace(/^\s*[-–—]\s*/, '').trim())
.filter(Boolean)
.map(line => {
const match = line.match(/^([^:]+)[:]\s*([^\[]+)(?:\[(.+)\])?$/);
if (!match) {
return { name: '计费说明', price: line, desc: '' };
}
return {
name: match[1].trim(),
price: match[2].trim(),
desc: (match[3] || '').trim()
};
});
},
getProviderInitial(provider) {
return provider ? provider.slice(0, 1) : 'M';
},
getModelLogoUrl(modelLogo) {
if (!modelLogo) return '';
if (/^https?:\/\//.test(modelLogo) || modelLogo.startsWith('data:') || modelLogo.startsWith('blob:')) {
return modelLogo;
}
return `${this.IMG_URL}${modelLogo}`;
},
handleModelLogoError(product) {
this.$set(product, 'logoLoadFailed', true);
},
normalizeTokenConfigList(value) {
if (!value) return [];
let parsedValue = value;
if (typeof value === 'string') {
try {
parsedValue = JSON.parse(value);
} catch (error) {
return [];
}
}
if (Array.isArray(parsedValue)) {
return parsedValue.map(item => ({
name: item.name || item.label || item.key || '',
value: item.value || item.text || item.content || ''
})).filter(item => item.name || item.value);
}
if (parsedValue && typeof parsedValue === 'object') {
return Object.keys(parsedValue).map(key => ({ name: key, value: parsedValue[key] }));
}
return [];
},
getTokenFeatureTags(model) {
const capabilities = this.normalizeTokenConfigList(model.capabilities);
const highlights = this.normalizeTokenConfigList(model.highlights);
const inputType = capabilities.find(item => item.name === '输入类型');
const outputType = capabilities.find(item => item.name === '输出类型');
const highlight = highlights[0];
return [
{
text: inputType && inputType.value ? inputType.value : (model.model_type || '模型能力'),
icon: 'el-icon-view',
type: 'green'
},
{
text: outputType && outputType.value ? outputType.value : (highlight && (highlight.value || highlight.name)) || '智能生成',
icon: 'el-icon-magic-stick',
type: 'purple'
},
].filter(item => item.text);
},
isModelExperienceEnabled(model) {
return Number(model && model.experience) === 1;
},
goModelApiDocument(model) {
this.cacheTokenMarketModel(model);
this.$router.push({
name: 'modelApiDocument',
query: {
id: model.id,
model_id: model.id,
from: 'tokenMarket',
category: 'TOKEN市集'
}
});
},
goModelExperience(model) {
if (!this.isModelExperienceEnabled(model)) return;
if (!this.loginState) {
this.$message.warning('请先登录再进行体验哦')
return;
}
this.cacheTokenMarketModel(model);
this.$router.push({
name: 'modelExperience',
query: {
id: model.id,
model_id: model.id,
from: 'tokenMarket',
category: 'TOKEN市集'
}
});
},
cacheTokenMarketModel(model) {
if (!model) return;
sessionStorage.setItem('tokenMarketSelectedModel', JSON.stringify(model));
},
async loadNavData() {
try {
const baseUrl = window.location.href.split('#')[0];
const hostname = window.location.hostname
const homePath = hostname.includes('ncmatch.cn') || hostname.includes('zgcopc.opencomputing.cn')
? '/ncmatchHome/index'
: '/homePage/index';
const response = await reqNavList({ url_link: `${baseUrl}#${homePath}` });
if (response.status && response.data.product_service) {
response.data.product_service.forEach(category => {
if (this.isTokenMarketCategory(category.firTitle)) {
this.setTokenMarketData(category);
}
});
}
} catch (error) {
console.error("加载模型数据失败:", error);
}
},
}
};
</script>
<style lang="less" scoped>
// ====== 配色变量 ======
@primary: #1e6fff;
@primary-light: #eef5ff;
@primary-lighter: #f0f7ff;
@primary-deep: #155eef;
@text-dark: #1f2937;
@text-mid: #475467;
@text-soft: #98a2b3;
@border: #edf1f7;
@border-soft: #f1f5f9;
@bg-card: #ffffff;
@shadow-card: 0 2px 12px rgba(31, 45, 61, 0.06);
@shadow-hover: 0 0 0 3px rgba(30, 111, 255, 0.12), 0 16px 32px rgba(30, 111, 255, 0.16);
.token-market-page {
display: flex;
gap: 24px;
padding: 24px 32px 40px;
min-height: calc(100vh - 80px);
background:
radial-gradient(800px circle at 8% 0%, rgba(30, 111, 255, 0.06) 0%, transparent 40%),
radial-gradient(700px circle at 95% 8%, rgba(125, 105, 255, 0.05) 0%, transparent 42%),
linear-gradient(180deg, #f5f8ff 0%, #ffffff 60%);
box-sizing: border-box;
}
/* ==================== 侧栏 ==================== */
.menu-sidebar {
width: 320px;
flex-shrink: 0;
position: sticky;
top: 80px;
height: calc(100vh - 80px - 48px); // 撑满整屏高度48px = 上下内边距总和
display: flex;
flex-direction: column;
padding: 20px 18px;
background: rgba(255, 255, 255, 0.86);
border: 1px solid rgba(237, 241, 247, 0.8);
border-radius: 18px;
box-shadow: 0 8px 24px rgba(31, 45, 61, 0.05);
backdrop-filter: blur(10px);
box-sizing: border-box;
transition: box-shadow 0.3s;
overflow: hidden;
&:hover {
box-shadow: 0 12px 32px rgba(31, 45, 61, 0.08);
}
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
margin-bottom: 16px;
}
.sidebar-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
color: @text-dark;
font-size: 15px;
font-weight: 700;
letter-spacing: 0.3px;
i {
color: @primary;
font-size: 16px;
}
}
.reset-btn {
display: inline-flex;
align-items: center;
gap: 4px;
height: 26px;
padding: 0 10px;
color: @primary;
background: @primary-lighter;
border: 1px solid #c7defb;
border-radius: 999px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
&:hover {
background: @primary-light;
border-color: @primary;
transform: scale(1.04);
}
i {
font-size: 12px;
}
}
.product-search {
margin-bottom: 18px;
:deep(.el-input__inner) {
height: 40px;
line-height: 40px;
color: @text-dark;
font-size: 13px;
background: rgba(248, 250, 252, 0.7);
border: 1px solid @border;
border-radius: 12px;
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
&:focus {
background: #fff;
border-color: @primary;
box-shadow: 0 0 0 4px rgba(30, 111, 255, 0.12);
}
&::placeholder {
color: @text-soft;
}
}
:deep(.el-input__prefix) {
left: 12px;
i {
color: @text-soft;
font-size: 14px;
line-height: 40px;
}
}
:deep(.el-input__suffix) {
right: 10px;
}
}
// 滚动区:撑满侧栏剩余高度,内容超出时出现滚动条
.filter-scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
padding-right: 4px;
scrollbar-width: thin;
scrollbar-color: #d0d7e2 transparent;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: #d0d7e2;
border-radius: 4px;
}
}
.product-search {
flex-shrink: 0;
}
.token-filter-section {
margin-top: 20px;
}
.filter-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
padding-left: 2px;
h4 {
margin: 0;
color: @text-soft;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
}
}
.filter-count {
padding: 1px 7px;
color: @primary;
font-size: 11px;
font-weight: 700;
background: @primary-light;
border-radius: 999px;
}
.token-filter-grid {
display: flex;
flex-wrap: wrap;
gap: 6px 6px;
}
.filter-chip {
display: inline-flex;
align-items: center;
justify-content: center;
max-width: 100%;
height: 28px;
padding: 0 12px;
color: @text-mid;
font-size: 13px;
font-weight: 500;
line-height: 1;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 999px;
cursor: pointer;
transition: color 0.2s, background 0.2s, border-color 0.2s, box-shadow 0.2s, transform 0.15s;
// tooltip 内的 label
:deep(.el-tooltip) {
max-width: 100%;
display: inline-flex;
}
.chip-label {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&:hover {
color: @primary;
background: @primary-lighter;
border-color: #bcd6fb;
transform: translateY(-1px);
}
&.active {
color: #fff;
font-weight: 600;
background: linear-gradient(135deg, @primary, #4d86ff);
border-color: transparent;
box-shadow: 0 4px 10px rgba(30, 111, 255, 0.28);
&:hover {
color: #fff;
background: linear-gradient(135deg, @primary-deep, #4170ee);
border-color: transparent;
transform: translateY(-1px);
}
}
}
/* ==================== 主内容区 ==================== */
.main-content {
flex: 1;
min-width: 0;
}
.summary-text {
color: @text-mid;
font-size: 14px;
strong {
margin: 0 2px;
color: @primary;
font-size: 18px;
font-weight: 700;
}
}
.active-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.active-tag {
display: inline-flex;
align-items: center;
gap: 5px;
height: 26px;
padding: 0 6px 0 10px;
color: @primary;
font-size: 12px;
font-weight: 600;
background: @primary-light;
border: 1px solid #c7defb;
border-radius: 999px;
i:first-child {
font-size: 12px;
}
.close-icon {
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
margin-left: 2px;
color: @primary;
background: #fff;
border-radius: 50%;
cursor: pointer;
transition: background 0.2s, color 0.2s;
&:hover {
color: #fff;
background: @primary;
}
}
}
.token-market-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 18px;
align-items: stretch;
}
/* ==================== 卡片 ==================== */
.token-market-card {
position: relative;
display: flex;
flex-direction: column;
overflow: hidden; // 裁剪 hover-actions 底部圆角
min-height: 240px;
padding: 16px;
background: @bg-card;
border: 1px solid rgba(228, 234, 243, 0.9);
border-radius: 14px;
box-shadow: @shadow-card;
cursor: pointer;
transition: border-color 0.25s, transform 0.25s, box-shadow 0.25s;
&::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 140px;
height: 140px;
background: radial-gradient(circle at 100% 0%, rgba(30, 111, 255, 0.08) 0%, transparent 60%);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
&:hover {
border-color: #8bbcff;
transform: translateY(-4px);
box-shadow: @shadow-hover;
&::before {
opacity: 1;
}
.token-hover-actions {
opacity: 1;
transform: translateY(0);
}
.token-card-top h3 {
color: @primary;
}
}
}
/* 卡片顶部 */
.token-card-top {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 14px;
}
.token-provider-avatar {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 42px;
height: 42px;
color: #fff;
font-size: 16px;
font-weight: 700;
background: linear-gradient(135deg, #60a5fa, #3b82f6, #2563eb);
border-radius: 10px;
box-shadow: 0 4px 10px rgba(59, 130, 246, 0.28);
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: contain;
background: #fff;
}
}
.token-title-group {
flex: 1;
min-width: 0;
h3 {
margin: 0;
color: @text-dark;
font-size: 16px;
font-weight: 700;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.25s;
}
p {
display: flex;
align-items: center;
gap: 4px;
margin: 3px 0 0;
color: @text-soft;
font-size: 12px;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
i {
font-size: 11px;
}
}
}
/* 计费区 */
.token-price-grid {
flex: 1;
max-height: 178px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 12px;
padding-right: 2px;
scrollbar-width: thin;
scrollbar-color: #d0d7e2 transparent;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: #d0d7e2;
border-radius: 4px;
}
}
.token-price-item {
padding: 8px 12px;
background: linear-gradient(180deg, #f8fafc 0%, #f3f7fb 100%);
border: 1px solid @border-soft;
border-radius: 10px;
transition: background 0.2s, border-color 0.2s;
&:hover {
background: linear-gradient(180deg, #ffffff 0%, #f5f9ff 100%);
border-color: #d6e4fb;
}
}
.price-name {
display: block;
margin-bottom: 3px;
color: @text-soft;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.4px;
text-transform: uppercase;
}
.price-value {
display: block;
color: @text-dark;
font-size: 14px;
font-weight: 800;
line-height: 1.3;
}
.price-desc {
display: block;
margin-top: 3px;
color: @text-soft;
font-size: 11px;
font-style: normal;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 底部标签 */
.token-card-footer {
display: flex;
align-items: center;
margin-top: auto;
}
.token-feature-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
width: 100%;
span {
display: inline-flex;
align-items: center;
gap: 3px;
max-width: 120px;
height: 22px;
padding: 0 8px;
font-size: 11px;
font-weight: 600;
border-radius: 999px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: transform 0.2s;
&:hover {
transform: translateY(-1px);
}
i {
font-size: 11px;
}
&.green {
color: #047857;
background: #d1fae5;
}
&.purple {
color: #5b21b6;
background: #ede9fe;
}
&.blue {
color: #1e40af;
background: #dbeafe;
}
}
}
/* hover 操作栏 */
.token-hover-actions {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
display: flex;
gap: 6px;
padding: 8px 10px;
opacity: 0;
background: rgba(255, 255, 255, 0.97);
border-top: 1px solid @border;
box-shadow: 0 -6px 16px rgba(31, 45, 61, 0.04);
backdrop-filter: blur(10px);
transform: translateY(100%);
transition: opacity 0.22s, transform 0.22s;
button {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
height: 32px;
padding: 0 10px;
color: @text-mid;
font-size: 13px;
font-weight: 600;
background: #fff;
border: 1px solid #d8e0ee;
border-radius: 9px;
cursor: pointer;
transition: color 0.18s, border-color 0.18s, background 0.18s, transform 0.18s;
&:hover {
color: @primary;
border-color: #9ec5ff;
background: @primary-lighter;
transform: translateY(-1px);
}
}
.primary-action {
color: #fff;
background: linear-gradient(135deg, @primary, #5d8dff);
border-color: transparent;
box-shadow: 0 2px 6px rgba(30, 111, 255, 0.25);
&:hover {
color: #fff;
background: linear-gradient(135deg, @primary-deep, #4a7aef);
box-shadow: 0 4px 10px rgba(30, 111, 255, 0.35);
}
&.disabled,
&:disabled {
color: #fff;
background: linear-gradient(135deg, #cbd5e1, #b8c2cf);
box-shadow: none;
cursor: not-allowed;
transform: none;
}
}
}
/* 空状态 */
.empty-state {
grid-column: 1 / -1;
display: flex;
flex-direction: column;
align-items: center;
padding: 80px 20px;
background: #fff;
border: 2px dashed #d6e0ee;
border-radius: 18px;
}
.empty-icon {
display: flex;
align-items: center;
justify-content: center;
width: 72px;
height: 72px;
margin-bottom: 16px;
color: @text-soft;
font-size: 32px;
background: linear-gradient(135deg, @primary-lighter, #f1f5f9);
border-radius: 20px;
}
.empty-text {
margin: 0 0 8px;
color: @text-dark;
font-size: 16px;
font-weight: 600;
}
.empty-hint {
color: @text-soft;
font-size: 13px;
}
/* ==================== 响应式 ==================== */
@media (max-width: 1280px) {
.token-market-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 900px) {
.token-market-page {
flex-direction: column;
padding: 16px;
}
.menu-sidebar {
width: 100%;
position: static;
height: auto;
max-height: 60vh;
}
.token-market-grid {
grid-template-columns: 1fr;
}
}
</style>