|
|
@@ -56,7 +56,7 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="action-bar__right">
|
|
|
- <el-button type="primary" @click="openCreateModel">
|
|
|
+ <el-button v-permission="'add'" type="primary" @click="openCreateModel">
|
|
|
<el-icon><Plus /></el-icon>
|
|
|
新建模型
|
|
|
</el-button>
|
|
|
@@ -96,8 +96,22 @@
|
|
|
<template #dropdown>
|
|
|
<el-dropdown-menu>
|
|
|
<el-dropdown-item @click="openDetailModel(row.id)">详情</el-dropdown-item>
|
|
|
- <el-dropdown-item @click="openEditModel(row.id)">编辑</el-dropdown-item>
|
|
|
- <el-dropdown-item @click="deleteModelConfirm(row.id)" divided>
|
|
|
+ <el-dropdown-item v-permission="'edit'" @click="openEditModel(row.id)">编辑</el-dropdown-item>
|
|
|
+ <el-dropdown-item
|
|
|
+ v-if="row.source === 'remote'"
|
|
|
+ v-permission="'edit'"
|
|
|
+ @click="updateModelCredentials(row.id)"
|
|
|
+ >
|
|
|
+ 更新凭证
|
|
|
+ </el-dropdown-item>
|
|
|
+ <el-dropdown-item
|
|
|
+ v-if="row.source === 'remote'"
|
|
|
+ v-permission="'edit'"
|
|
|
+ @click="deleteModelCredentials(row.id)"
|
|
|
+ >
|
|
|
+ <span class="danger-text">删除凭证</span>
|
|
|
+ </el-dropdown-item>
|
|
|
+ <el-dropdown-item v-permission="'del'" @click="deleteModelConfirm(row.id)" divided>
|
|
|
<span class="danger-text">删除</span>
|
|
|
</el-dropdown-item>
|
|
|
</el-dropdown-menu>
|
|
|
@@ -141,270 +155,37 @@
|
|
|
</el-card>
|
|
|
</div>
|
|
|
|
|
|
- <el-dialog v-model="showDetailDialog" title="模型详情" width="700px">
|
|
|
- <el-descriptions :column="1" border v-if="currentDetailModel">
|
|
|
- <el-descriptions-item label="模型标识">{{ currentDetailModel.name }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="显示名称">{{ currentDetailModel.title }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="模型类型">{{
|
|
|
- getModelTypeName(currentDetailModel.type)
|
|
|
- }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="模型来源">{{
|
|
|
- currentDetailModel.source === 'local' ? '本地Ollama' : '远程API'
|
|
|
- }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="服务商">{{
|
|
|
- currentDetailModel.provider
|
|
|
- }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="描述">{{
|
|
|
- currentDetailModel.description || '无'
|
|
|
- }}</el-descriptions-item>
|
|
|
- <el-descriptions-item v-if="currentDetailModel.source === 'remote'" label="API地址">
|
|
|
- {{ currentDetailModel.parameters.base_url || '无' }}
|
|
|
- </el-descriptions-item>
|
|
|
- <el-descriptions-item v-if="currentDetailModel.source === 'remote'" label="API Key">
|
|
|
- {{ maskApiKey(currentDetailModel.parameters.api_key) }}
|
|
|
- </el-descriptions-item>
|
|
|
- <el-descriptions-item v-if="currentDetailModel.source === 'remote'" label="连接测试">
|
|
|
- <div class="model-check-box">
|
|
|
- <el-button
|
|
|
- type="primary"
|
|
|
- plain
|
|
|
- :loading="detailCheckLoading"
|
|
|
- @click="checkCurrentDetailModel"
|
|
|
- >
|
|
|
- 测试连接
|
|
|
- </el-button>
|
|
|
- <el-alert
|
|
|
- v-if="detailCheckResult.message"
|
|
|
- :type="detailCheckResult.success ? 'success' : 'error'"
|
|
|
- :title="detailCheckResult.message"
|
|
|
- :closable="false"
|
|
|
- show-icon
|
|
|
- />
|
|
|
- </div>
|
|
|
- </el-descriptions-item>
|
|
|
- <el-descriptions-item label="创建时间">{{
|
|
|
- currentDetailModel.creationTime
|
|
|
- }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="更新时间">{{
|
|
|
- currentDetailModel.updateTime
|
|
|
- }}</el-descriptions-item>
|
|
|
- </el-descriptions>
|
|
|
- <template #footer>
|
|
|
- <el-button @click="showDetailDialog = false">关闭</el-button>
|
|
|
- </template>
|
|
|
- </el-dialog>
|
|
|
-
|
|
|
- <el-drawer
|
|
|
+ <ModelDetailDialog
|
|
|
+ v-model="showDetailDialog"
|
|
|
+ :model="currentDetailModel"
|
|
|
+ :check-loading="detailCheckLoading"
|
|
|
+ :check-result="detailCheckResult"
|
|
|
+ :get-model-type-name="getModelTypeName"
|
|
|
+ @check="checkCurrentDetailModel"
|
|
|
+ />
|
|
|
+
|
|
|
+ <ModelEditDrawer
|
|
|
+ ref="modelFormRef"
|
|
|
v-model="showModelDialog"
|
|
|
- direction="rtl"
|
|
|
- :title="currentModelId ? '编辑模型' : '新建模型'"
|
|
|
- width="700px"
|
|
|
- >
|
|
|
- <el-form
|
|
|
- ref="modelFormRef"
|
|
|
- :model="modelForm"
|
|
|
- :rules="modelRules"
|
|
|
- label-width="120px"
|
|
|
- label-position="top"
|
|
|
- >
|
|
|
- <el-form-item label="模型来源" prop="source">
|
|
|
- <el-radio-group v-model="modelForm.source" @change="handleSourceChange">
|
|
|
- <el-radio label="local">本地Ollama</el-radio>
|
|
|
- <el-radio label="remote">服务商</el-radio>
|
|
|
- </el-radio-group>
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item v-if="modelForm.source === 'remote'" label="模型类型" prop="type">
|
|
|
- <el-select
|
|
|
- v-model="modelForm.type"
|
|
|
- placeholder="请选择模型类型"
|
|
|
- @change="handleTypeChange"
|
|
|
- >
|
|
|
- <el-option label="对话模型" value="KnowledgeQA" />
|
|
|
- <el-option label="Embedding模型" value="Embedding" />
|
|
|
- <el-option label="Rerank模型" value="Rerank" />
|
|
|
- <el-option label="视觉模型" value="VLLM" />
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item v-if="modelForm.source === 'local'" label="本地模型" prop="name">
|
|
|
- <el-select v-model="modelForm.name" placeholder="请选择" style="width: 100%">
|
|
|
- <el-option
|
|
|
- v-for="model in availableLocalModels"
|
|
|
- :key="model.name"
|
|
|
- :label="model.name"
|
|
|
- :value="model.name"
|
|
|
- />
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item v-if="modelForm.source === 'remote'" label="服务商" prop="provider">
|
|
|
- <el-select
|
|
|
- v-model="modelForm.provider"
|
|
|
- placeholder="请选择服务商"
|
|
|
- @change="handleProviderChange"
|
|
|
- style="width: 100%"
|
|
|
- >
|
|
|
- <el-option v-for="p in providers" :key="p.value" :label="p.label" :value="p.value" />
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item label="模型名称" prop="name">
|
|
|
- <el-input
|
|
|
- v-model="modelForm.name"
|
|
|
- :readonly="modelForm.source === 'local'"
|
|
|
- placeholder="如:llama3.1"
|
|
|
- />
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item label="显示名称" prop="title">
|
|
|
- <el-input v-model="modelForm.title" placeholder="请输入显示名称" />
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item label="描述" prop="description">
|
|
|
- <el-input
|
|
|
- v-model="modelForm.description"
|
|
|
- type="textarea"
|
|
|
- rows="2"
|
|
|
- placeholder="模型描述..."
|
|
|
- />
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item v-if="modelForm.source === 'remote'" label="API地址" prop="base_url">
|
|
|
- <el-autocomplete
|
|
|
- v-model="modelForm.base_url"
|
|
|
- :fetch-suggestions="queryBaseUrlSuggestions"
|
|
|
- clearable
|
|
|
- placeholder="可从默认地址选择,或手动输入"
|
|
|
- style="width: 100%"
|
|
|
- />
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item v-if="modelForm.source === 'remote'" label="API Key" prop="api_key">
|
|
|
- <el-input v-model="modelForm.api_key" type="password" placeholder="输入你的API密钥" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item
|
|
|
- v-if="modelForm.source === 'remote' && modelForm.type === 'Embedding'"
|
|
|
- label="维度"
|
|
|
- prop="dimension"
|
|
|
- >
|
|
|
- <el-input-number
|
|
|
- v-model="modelForm.dimension"
|
|
|
- :min="1"
|
|
|
- :step="1"
|
|
|
- controls-position="right"
|
|
|
- style="width: 100%"
|
|
|
- placeholder="请输入向量维度"
|
|
|
- />
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item
|
|
|
- v-if="modelForm.source === 'remote' && modelForm.type === 'Embedding'"
|
|
|
- label="截断Tokens"
|
|
|
- prop="truncate_prompt_tokens"
|
|
|
- >
|
|
|
- <el-input-number
|
|
|
- v-model="modelForm.truncate_prompt_tokens"
|
|
|
- :min="-1"
|
|
|
- :step="1"
|
|
|
- controls-position="right"
|
|
|
- style="width: 100%"
|
|
|
- placeholder="请输入截断 token 数"
|
|
|
- />
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item
|
|
|
- v-if="modelForm.source === 'remote' && modelForm.type === 'KnowledgeQA'"
|
|
|
- label="支持视觉"
|
|
|
- prop="supports_vision"
|
|
|
- >
|
|
|
- <el-switch v-model="modelForm.supports_vision" />
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item
|
|
|
- v-if="modelForm.source === 'remote' && modelForm.type === 'KnowledgeQA'"
|
|
|
- label="思考模式参数格式"
|
|
|
- prop="thinking_control"
|
|
|
- >
|
|
|
- <el-select
|
|
|
- v-model="modelForm.thinking_control"
|
|
|
- placeholder="请选择思考模式参数格式"
|
|
|
- popper-class="thinking-control-select"
|
|
|
- style="width: 100%"
|
|
|
- >
|
|
|
- <el-option
|
|
|
- v-for="option in thinkingControlOptions"
|
|
|
- :key="option.value"
|
|
|
- :label="option.title"
|
|
|
- :value="option.value"
|
|
|
- >
|
|
|
- <div class="thinking-option">
|
|
|
- <div class="thinking-option__title">{{ option.title }}</div>
|
|
|
- <div class="thinking-option__desc">{{ option.description }}</div>
|
|
|
- </div>
|
|
|
- </el-option>
|
|
|
- </el-select>
|
|
|
- <div class="field-tip">
|
|
|
- 决定智能体「思考模式」开/关时如何写入 API。已尝试按厂商/模型预选,若与实际情况不符请按 API 文档手动修改;选「不写入」时,智能体「思考模式」开关不生效。
|
|
|
- </div>
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item prop="custom_headers">
|
|
|
- <div class="header-config">
|
|
|
- <div class="header-config__top">
|
|
|
- <span>自定义请求头(可选)</span>
|
|
|
- <el-button type="primary" link @click="addCustomHeader">
|
|
|
- <el-icon> <Plus /> </el-icon>添加请求头
|
|
|
- </el-button>
|
|
|
- </div>
|
|
|
- <div class="header-config__desc">
|
|
|
- 调用远程模型API时附加的HTTP请求头,常用于鉴权、链路追踪等场景
|
|
|
- </div>
|
|
|
- <div class="header-config__rows">
|
|
|
- <div
|
|
|
- v-for="(item, index) in customHeaderList"
|
|
|
- :key="index"
|
|
|
- class="header-config__row"
|
|
|
- >
|
|
|
- <el-input v-model="item.key" placeholder="Header名称" />
|
|
|
- <el-input v-model="item.value" placeholder="Header值" />
|
|
|
- <el-button
|
|
|
- link
|
|
|
- type="danger"
|
|
|
- :disabled="customHeaderList.length === 1"
|
|
|
- @click="removeCustomHeader(index)"
|
|
|
- >
|
|
|
- 删除
|
|
|
- </el-button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item>
|
|
|
- <div v-if="modelForm.source === 'remote'" class="model-check-box">
|
|
|
- <el-button
|
|
|
- type="primary"
|
|
|
- plain
|
|
|
- :loading="formCheckLoading"
|
|
|
- @click="checkModelFormConnection"
|
|
|
- >
|
|
|
- 测试连接
|
|
|
- </el-button>
|
|
|
- <el-alert
|
|
|
- v-if="formCheckResult.message"
|
|
|
- :type="formCheckResult.success ? 'success' : 'error'"
|
|
|
- :title="formCheckResult.message"
|
|
|
- :closable="false"
|
|
|
- show-icon
|
|
|
- />
|
|
|
- </div>
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
- <template #footer>
|
|
|
- <el-button @click="showModelDialog = false">取消</el-button>
|
|
|
- <el-button type="primary" @click="submitModelForm" :loading="submitLoading">提交</el-button>
|
|
|
- </template>
|
|
|
- </el-drawer>
|
|
|
+ :current-model-id="currentModelId"
|
|
|
+ :model-form="modelForm"
|
|
|
+ :model-rules="modelRules"
|
|
|
+ :available-local-models="availableLocalModels"
|
|
|
+ :providers="providers"
|
|
|
+ :custom-header-list="customHeaderList"
|
|
|
+ :thinking-control-options="thinkingControlOptions"
|
|
|
+ :form-check-loading="formCheckLoading"
|
|
|
+ :form-check-result="formCheckResult"
|
|
|
+ :submit-loading="submitLoading"
|
|
|
+ :query-base-url-suggestions="queryBaseUrlSuggestions"
|
|
|
+ @source-change="handleSourceChange"
|
|
|
+ @type-change="handleTypeChange"
|
|
|
+ @provider-change="handleProviderChange"
|
|
|
+ @add-custom-header="addCustomHeader"
|
|
|
+ @remove-custom-header="removeCustomHeader"
|
|
|
+ @check="checkModelFormConnection"
|
|
|
+ @submit="submitModelForm"
|
|
|
+ />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -413,6 +194,8 @@ import { computed, ref, reactive, onMounted, watch } from 'vue'
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
import { Plus, Refresh, MoreFilled, Search, RefreshRight } from '@element-plus/icons-vue'
|
|
|
import { aiModel, ollama } from '@repo/api-service'
|
|
|
+import ModelDetailDialog from './components/ModelDetailDialog.vue'
|
|
|
+import ModelEditDrawer from './components/ModelEditDrawer.vue'
|
|
|
import type {
|
|
|
ModelItem,
|
|
|
ModelProvider,
|
|
|
@@ -465,7 +248,8 @@ const thinkingControlOptions: Array<{
|
|
|
},
|
|
|
{
|
|
|
title: 'thinking.type',
|
|
|
- description: '火山引擎 Ark;腾讯云 LKEAP(DeepSeek V3 等,选 LKEAP 时默认此项;R1 请改「不写入」)',
|
|
|
+ description:
|
|
|
+ '火山引擎 Ark;腾讯云 LKEAP(DeepSeek V3 等,选 LKEAP 时默认此项;R1 请改「不写入」)',
|
|
|
value: 'thinking_type'
|
|
|
}
|
|
|
]
|
|
|
@@ -568,11 +352,6 @@ const typeLabelMap: Record<string, string> = {
|
|
|
// return map[type]
|
|
|
// }
|
|
|
|
|
|
-function maskApiKey(key: string) {
|
|
|
- if (!key) return '无'
|
|
|
- if (key.length <= 8) return '****'
|
|
|
- return `${key.slice(0, 4)}${'*'.repeat(key.length - 8)}${key.slice(-4)}`
|
|
|
-}
|
|
|
function getModelTypeName(type?: string) {
|
|
|
if (!type) return '-'
|
|
|
return typeLabelMap[type] || type
|
|
|
@@ -834,7 +613,11 @@ function getDefaultThinkingControl(provider?: string): ThinkingControlType {
|
|
|
) {
|
|
|
return 'thinking_type'
|
|
|
}
|
|
|
- if (providerValue.includes('nvidia') || providerValue.includes('nim') || providerValue.includes('vllm')) {
|
|
|
+ if (
|
|
|
+ providerValue.includes('nvidia') ||
|
|
|
+ providerValue.includes('nim') ||
|
|
|
+ providerValue.includes('vllm')
|
|
|
+ ) {
|
|
|
return 'chat_template_kwargs'
|
|
|
}
|
|
|
return 'none'
|
|
|
@@ -884,11 +667,7 @@ async function checkCurrentDetailModel() {
|
|
|
try {
|
|
|
const detail = currentDetailModel.value
|
|
|
const payload: Record<string, any> = {
|
|
|
- name: detail.name,
|
|
|
- type: detail.type,
|
|
|
- source: detail.source,
|
|
|
- provider: detail.provider,
|
|
|
- api_key: detail.parameters?.api_key
|
|
|
+ id: detail.id
|
|
|
}
|
|
|
if (detail.type === 'Embedding') {
|
|
|
payload.truncate_prompt_tokens =
|
|
|
@@ -1001,6 +780,40 @@ async function deleteModelConfirm(id: string) {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+async function updateModelCredentials(id: string) {
|
|
|
+ try {
|
|
|
+ const { value } = await ElMessageBox.prompt('请输入新的 API Key', '更新模型凭证', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ inputType: 'password',
|
|
|
+ inputPlaceholder: '请输入 API Key',
|
|
|
+ inputValidator: (value) => !!value?.trim() || '请输入 API Key'
|
|
|
+ })
|
|
|
+ await aiModel.postModelCredentials({ id, api_key: value.trim() })
|
|
|
+ ElMessage.success('凭证更新成功')
|
|
|
+ getAllModelList()
|
|
|
+ } catch (error) {
|
|
|
+ if (error !== 'cancel' && error !== 'close') {
|
|
|
+ ElMessage.error('凭证更新失败')
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function deleteModelCredentials(id: string) {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm('确定要删除该模型凭证吗?删除后该模型可能无法调用。', '提示', {
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+ await aiModel.postModelDeleteCredentials({ id })
|
|
|
+ ElMessage.success('凭证删除成功')
|
|
|
+ getAllModelList()
|
|
|
+ } catch (error) {
|
|
|
+ if (error !== 'cancel' && error !== 'close') {
|
|
|
+ ElMessage.error('凭证删除失败')
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
watch(
|
|
|
() => modelForm.type,
|
|
|
() => {
|
|
|
@@ -1052,42 +865,6 @@ onMounted(() => {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
-.field-tip {
|
|
|
- margin-top: 8px;
|
|
|
- color: var(--text-tertiary);
|
|
|
- font-size: 12px;
|
|
|
- line-height: 1.6;
|
|
|
-}
|
|
|
-
|
|
|
-.thinking-option {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- justify-content: center;
|
|
|
- min-height: 54px;
|
|
|
- padding: 6px 0;
|
|
|
- line-height: 1.35;
|
|
|
-}
|
|
|
-
|
|
|
-.thinking-option__title {
|
|
|
- color: var(--text-primary);
|
|
|
- font-size: 13px;
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-
|
|
|
-.thinking-option__desc {
|
|
|
- margin-top: 3px;
|
|
|
- color: var(--text-tertiary);
|
|
|
- font-size: 12px;
|
|
|
- white-space: normal;
|
|
|
-}
|
|
|
-
|
|
|
-:global(.thinking-control-select .el-select-dropdown__item) {
|
|
|
- height: auto;
|
|
|
- min-height: 62px;
|
|
|
- padding-top: 4px;
|
|
|
- padding-bottom: 4px;
|
|
|
-}
|
|
|
-
|
|
|
.action-bar {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
@@ -1123,23 +900,6 @@ onMounted(() => {
|
|
|
width: 240px;
|
|
|
}
|
|
|
|
|
|
-.toolbar-meta {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 10px;
|
|
|
- flex-wrap: wrap;
|
|
|
- margin-bottom: 16px;
|
|
|
-}
|
|
|
-
|
|
|
-.pill {
|
|
|
- padding: 8px 12px;
|
|
|
- border-radius: 999px;
|
|
|
- border: 1px solid var(--border-light);
|
|
|
- background: var(--bg-container);
|
|
|
- font-size: 12px;
|
|
|
- color: var(--text-secondary);
|
|
|
-}
|
|
|
-
|
|
|
.pagination-wrap {
|
|
|
display: flex;
|
|
|
justify-content: flex-end;
|
|
|
@@ -1258,41 +1018,6 @@ onMounted(() => {
|
|
|
gap: 8px;
|
|
|
}
|
|
|
|
|
|
-.meta-list {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
- gap: 10px;
|
|
|
-}
|
|
|
-
|
|
|
-.meta-item {
|
|
|
- padding: 10px 12px;
|
|
|
- border-radius: 14px;
|
|
|
- background: var(--bg-container);
|
|
|
- border: 1px solid var(--border-light);
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 4px;
|
|
|
- min-width: 0;
|
|
|
-}
|
|
|
-
|
|
|
-.meta-label {
|
|
|
- font-size: 12px;
|
|
|
- color: var(--text-tertiary);
|
|
|
-}
|
|
|
-
|
|
|
-.meta-value {
|
|
|
- font-size: 13px;
|
|
|
- color: var(--text-primary);
|
|
|
- word-break: break-word;
|
|
|
-}
|
|
|
-
|
|
|
-.card-footer {
|
|
|
- display: flex;
|
|
|
- justify-content: flex-end;
|
|
|
- gap: 6px;
|
|
|
- margin-top: auto;
|
|
|
-}
|
|
|
-
|
|
|
.actions {
|
|
|
display: flex;
|
|
|
justify-content: flex-end;
|
|
|
@@ -1329,132 +1054,9 @@ onMounted(() => {
|
|
|
grid-column: span 4;
|
|
|
}
|
|
|
|
|
|
-.card-header {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-
|
|
|
-pre {
|
|
|
- margin: 0;
|
|
|
- padding: 8px;
|
|
|
- background: var(--bg-container);
|
|
|
- border-radius: 4px;
|
|
|
- font-size: 12px;
|
|
|
-}
|
|
|
-
|
|
|
-.provider-grid {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
|
- gap: 12px;
|
|
|
-}
|
|
|
-
|
|
|
-.provider-card {
|
|
|
- position: relative;
|
|
|
- padding: 16px;
|
|
|
- border-radius: 16px;
|
|
|
- border: 1px solid var(--border-light);
|
|
|
- transition: all 0.2s ease;
|
|
|
-}
|
|
|
-
|
|
|
-.provider-card:hover {
|
|
|
- transform: translateY(-2px);
|
|
|
- box-shadow: var(--shadow-md);
|
|
|
-}
|
|
|
-
|
|
|
-.provider-card__top {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- gap: 10px;
|
|
|
- margin-bottom: 8px;
|
|
|
-}
|
|
|
-
|
|
|
-.provider-card__label {
|
|
|
- font-size: 18px;
|
|
|
- font-weight: 700;
|
|
|
- line-height: 1.15;
|
|
|
- color: var(--text-primary);
|
|
|
-}
|
|
|
-
|
|
|
-.provider-card__desc {
|
|
|
- font-size: 18px;
|
|
|
- line-height: 1.5;
|
|
|
- color: var(--text-secondary);
|
|
|
- min-height: 72px;
|
|
|
- margin-bottom: 12px;
|
|
|
-}
|
|
|
-
|
|
|
-.provider-card__tags {
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 8px;
|
|
|
-}
|
|
|
-
|
|
|
-.provider-card__action {
|
|
|
- opacity: 0;
|
|
|
- pointer-events: none;
|
|
|
- transition: opacity 0.2s ease;
|
|
|
-}
|
|
|
-
|
|
|
-.provider-card:hover .provider-card__action {
|
|
|
- opacity: 1;
|
|
|
- pointer-events: auto;
|
|
|
-}
|
|
|
-
|
|
|
-.provider-card--tone-1,
|
|
|
-.provider-card--tone-2,
|
|
|
-.provider-card--tone-3,
|
|
|
-.provider-card--tone-4,
|
|
|
-.provider-card--tone-5 {
|
|
|
- background: var(--bg-container);
|
|
|
-}
|
|
|
-
|
|
|
-.header-config {
|
|
|
- width: 100%;
|
|
|
-}
|
|
|
-
|
|
|
-.header-config__top {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
-}
|
|
|
-
|
|
|
-.header-config__desc {
|
|
|
- color: var(--text-tertiary);
|
|
|
- font-size: 12px;
|
|
|
- margin: 4px 0 10px;
|
|
|
-}
|
|
|
-
|
|
|
-.header-config__rows {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 8px;
|
|
|
-}
|
|
|
-
|
|
|
-.header-config__row {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: 1fr 1fr auto;
|
|
|
- gap: 8px;
|
|
|
- align-items: center;
|
|
|
-}
|
|
|
-
|
|
|
-.model-check-box {
|
|
|
- width: 100%;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 8px;
|
|
|
-}
|
|
|
-
|
|
|
@media (max-width: 768px) {
|
|
|
.search-input {
|
|
|
width: 100%;
|
|
|
}
|
|
|
-
|
|
|
- .meta-list,
|
|
|
- .header-config__row {
|
|
|
- grid-template-columns: 1fr;
|
|
|
- }
|
|
|
}
|
|
|
</style>
|