fix: improve model list layout in ProviderCard (#311)
* fix: add LongCat provider, OpenRouter free models, model list in cards - Add longcat to PROVIDER_ENV_MAP and PROVIDER_PRESETS - Add freeOnly param to fetchProviderModels, use for OpenRouter - Show model list in ProviderCard with count - Fix qq.ts import.meta.url → __dirname for CJS compat - Add zh/en i18n keys for model count display * fix: improve model list layout in ProviderCard - Change models-list from max-height to fixed height (100px) - Add align-content: flex-start to prevent vertical spacing - Optimize gap to 4px vertical, 6px horizontal - Fix model-tag height to 20px to prevent background stretching - Use inline-flex for better tag alignment Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: idle888 <546806917@qq.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Executable → Regular
@@ -91,6 +91,20 @@ async function handleDelete() {
|
||||
<span class="info-label">{{ t('models.baseUrl') }}</span>
|
||||
<code class="info-value mono">{{ provider.base_url }}</code>
|
||||
</div>
|
||||
<div class="info-row models-row">
|
||||
<span class="info-label">{{ t('models.models') }}</span>
|
||||
<span class="info-value models-count">{{ provider.models.length }} {{ t('models.count') }}</span>
|
||||
</div>
|
||||
<div class="models-list">
|
||||
<span
|
||||
v-for="model in provider.models.slice(0, 20)"
|
||||
:key="model"
|
||||
class="model-tag"
|
||||
>{{ model }}</span>
|
||||
<span v-if="provider.models.length > 20" class="model-tag model-tag-more">
|
||||
+{{ provider.models.length - 20 }} {{ t('models.more') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
@@ -176,6 +190,47 @@ async function handleDelete() {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.models-row {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.models-count {
|
||||
color: $text-muted;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.models-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 6px;
|
||||
margin-top: 6px;
|
||||
height: 100px;
|
||||
overflow-y: auto;
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
.model-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
font-size: 10px;
|
||||
font-family: $font-code;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
background: rgba(var(--accent-primary-rgb), 0.08);
|
||||
color: $text-secondary;
|
||||
white-space: nowrap;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&-more {
|
||||
background: rgba(var(--accent-primary-rgb), 0.15);
|
||||
color: $accent-primary;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
@@ -313,6 +313,9 @@ export default {
|
||||
customModelPlaceholder: 'Custom model name',
|
||||
customModelHint: 'Enter to load',
|
||||
noProviders: 'No providers found. Add a custom provider to get started.',
|
||||
models: 'Models',
|
||||
count: 'models',
|
||||
more: 'more',
|
||||
builtIn: 'Built-in',
|
||||
customType: 'Custom',
|
||||
provider: 'Provider',
|
||||
@@ -554,6 +557,16 @@ export default {
|
||||
qrFetching: 'Fetching QR code...',
|
||||
qrScanHint: 'Scan with WeChat to login',
|
||||
qrScanedHint: 'Scaned, please confirm on phone...',
|
||||
// QQ
|
||||
qqAppId: 'App ID',
|
||||
qqAppIdHint: 'QQ Open Platform Bot App ID',
|
||||
qqAppSecret: 'App Secret',
|
||||
qqAppSecretHint: 'QQ Open Platform Bot App Secret',
|
||||
qqMarkdown: 'Markdown Support',
|
||||
qqMarkdownHint: 'Enable Markdown formatted messages (some clients may not support)',
|
||||
qqSandbox: 'Sandbox Mode',
|
||||
qqSandboxHint: 'Enable sandbox environment (for testing)',
|
||||
qqQrScanHint: 'Scan the QR code with QQ, or open the link on your phone to complete binding',
|
||||
},
|
||||
|
||||
// Language
|
||||
|
||||
@@ -313,6 +313,9 @@ export default {
|
||||
customModelPlaceholder: '自定义模型名称',
|
||||
customModelHint: '按回车加载',
|
||||
noProviders: '暂无 Provider,添加一个开始吧。',
|
||||
models: '模型列表',
|
||||
count: '个模型',
|
||||
more: '个更多',
|
||||
builtIn: '内置',
|
||||
customType: '自定义',
|
||||
provider: 'Provider',
|
||||
@@ -546,6 +549,16 @@ export default {
|
||||
qrFetching: '正在获取二维码...',
|
||||
qrScanHint: '使用微信扫描二维码登录',
|
||||
qrScanedHint: '已扫描,请在手机上确认...',
|
||||
// QQ
|
||||
qqAppId: 'App ID',
|
||||
qqAppIdHint: 'QQ 开放平台机器人 App ID',
|
||||
qqAppSecret: 'App Secret',
|
||||
qqAppSecretHint: 'QQ 开放平台机器人 App Secret',
|
||||
qqMarkdown: 'Markdown 支持',
|
||||
qqMarkdownHint: '启用 Markdown 格式消息(部分客户端可能不支持)',
|
||||
qqSandbox: '沙箱模式',
|
||||
qqSandboxHint: '启用沙箱环境(测试用)',
|
||||
qqQrScanHint: '使用 QQ 扫描上方二维码,或在手机上打开链接完成绑定',
|
||||
},
|
||||
|
||||
// 网关
|
||||
|
||||
@@ -134,6 +134,17 @@ export async function getAvailable(ctx: any) {
|
||||
}
|
||||
if (Object.keys(modelMeta).length === 0) modelMeta = undefined
|
||||
}
|
||||
} else if (providerKey === 'openrouter') {
|
||||
// OpenRouter has 200+ models — fetch dynamically like Copilot
|
||||
if (envMapping.api_key_env) {
|
||||
const orKey = envGetValue(envMapping.api_key_env)
|
||||
if (orKey) {
|
||||
try {
|
||||
const fetched = await fetchProviderModels(baseUrl, orKey, true)
|
||||
if (fetched.length > 0) modelsList = fetched
|
||||
} catch { /* ignore — leave empty, won't show */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (modelsList.length > 0) {
|
||||
const apiKey = envMapping.api_key_env ? envGetValue(envMapping.api_key_env) : ''
|
||||
|
||||
@@ -32,6 +32,7 @@ export const PROVIDER_ENV_MAP: Record<string, { api_key_env: string; base_url_en
|
||||
nous: { api_key_env: '', base_url_env: '' },
|
||||
'openai-codex': { api_key_env: '', base_url_env: '' },
|
||||
copilot: { api_key_env: '', base_url_env: '' },
|
||||
longcat: { api_key_env: 'LONGCAT_API_KEY', base_url_env: 'LONGCAT_BASE_URL' },
|
||||
}
|
||||
|
||||
// --- Types ---
|
||||
@@ -182,7 +183,7 @@ export async function listFilesRecursive(dir: string, prefix: string): Promise<{
|
||||
|
||||
// --- Provider model helpers ---
|
||||
|
||||
export async function fetchProviderModels(baseUrl: string, apiKey: string): Promise<string[]> {
|
||||
export async function fetchProviderModels(baseUrl: string, apiKey: string, freeOnly = false): Promise<string[]> {
|
||||
const base = baseUrl.replace(/\/+$/, '')
|
||||
const modelsUrl = /\/v\d+\/?$/.test(base) ? `${base}/models` : `${base}/v1/models`
|
||||
try {
|
||||
@@ -199,7 +200,9 @@ export async function fetchProviderModels(baseUrl: string, apiKey: string): Prom
|
||||
logger.warn('available-models %s returned unexpected format', modelsUrl)
|
||||
return []
|
||||
}
|
||||
return data.data.map(m => m.id).sort()
|
||||
let models = data.data.map(m => m.id)
|
||||
if (freeOnly) models = models.filter(m => m.endsWith(':free'))
|
||||
return models.sort()
|
||||
} catch (err: any) {
|
||||
logger.error(err, 'available-models %s failed', modelsUrl)
|
||||
return []
|
||||
|
||||
@@ -256,6 +256,13 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
|
||||
base_url: 'https://opencode.ai/zen/go/v1',
|
||||
models: ['glm-5.1', 'glm-5', 'kimi-k2.5', 'mimo-v2-pro', 'mimo-v2-omni', 'minimax-m2.7', 'minimax-m2.5'],
|
||||
},
|
||||
{
|
||||
label: 'LongCat',
|
||||
value: 'longcat',
|
||||
builtin: true,
|
||||
base_url: 'https://api.longcat.chat/openai',
|
||||
models: ['LongCat-Flash-Lite', 'LongCat-2.0-Preview'],
|
||||
},
|
||||
{
|
||||
label: 'OpenAI Codex',
|
||||
value: 'openai-codex',
|
||||
|
||||
Executable → Regular
Reference in New Issue
Block a user