ForcePilot/web/src/components/ModelProvidersComponent.vue
Zhao Minhao 55f2e020b5
Add dark/light theme toggle feature (#343)
* Add dark/light theme toggle feature

* update: refine dark/light theme and related components

* 调整语义化主题变量,优化暗/亮模式切换效果

* Revert "调整语义化主题变量,优化暗/亮模式切换效果"

This reverts commit 85d9373297686fdbacb252a880b2847d20a39641.

* 深色模式适配:减少 !important,迁移 CSS 变量,优化布局

* style: 替换硬编码颜色值为CSS变量以支持主题切换

将多处硬编码的颜色值(如#fff、#f0f0f0等)替换为CSS变量(如--gray-0、--gray-150等),统一管理颜色样式,便于主题切换和样式维护。主要修改包括背景色、边框色、文字色等视觉元素,同时移除不再需要的深色模式适配代码。

* style: 使用CSS变量替换硬编码颜色值

* refactor(theme): 重构主题系统,统一使用CSS变量并优化暗色模式实现

- 移除冗余的theme.js配置文件,将主题配置集中到theme store
- 使用ant-design-vue的darkAlgorithm实现暗色模式
- 在多个组件中替换硬编码颜色值为CSS变量
- 优化用户信息组件,整合文档中心和主题切换功能
- 更新基础CSS样式,完善颜色系统和滚动条样式

* reset: 恢复被覆盖的修改(96d257a7ace38c94e2b113d56472d211d1141087)

* feat(theme): 实现深色主题支持并重构样式系统

重构颜色变量系统,添加深色主题支持
移除硬编码颜色值,统一使用CSS变量
优化图表组件以响应主题切换
清理无用样式代码,提升可维护性

* docs: 更新开发规范文档

* style: 调整边框和背景颜色样式

---------

Co-authored-by: Wenjie Zhang <xerrors@163.com>
2025-11-23 01:39:44 +08:00

1442 lines
37 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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>
<!-- 自定义供应商管理区域 -->
<div class="custom-providers-section">
<div class="section-header">
<h3>自定义供应商</h3>
<a-button type="primary" @click="openAddCustomProviderModal">
<template #icon>
<PlusOutlined />
</template>
添加自定义供应商
</a-button>
</div>
<p class="section-description">
添加自定义的LLM供应商支持OpenAI兼容的API格式API密钥支持直接配置或使用环境变量名
</p>
<!-- 自定义供应商列表 -->
<div class="custom-provider-card" v-for="(provider, providerId) in customProviders" :key="providerId">
<div class="card-header">
<div class="provider-info">
<h4>{{ provider.name }}</h4>
<span class="provider-id">{{ providerId }}</span>
</div>
<div class="provider-actions">
<a-button type="text" size="small" @click="testCustomProvider(providerId, provider.default)">
<template #icon>
<ApiOutlined />
</template>
测试连接
</a-button>
<a-button type="text" size="small" @click="openEditCustomProviderModal(providerId, provider)">
<template #icon>
<EditOutlined />
</template>
编辑
</a-button>
<a-popconfirm
title="确定要删除这个自定义供应商吗?"
@confirm="deleteCustomProvider(providerId)"
ok-text="确定"
cancel-text="取消"
>
<a-button type="text" size="small" danger>
<template #icon>
<DeleteOutlined />
</template>
删除
</a-button>
</a-popconfirm>
</div>
</div>
<div class="card-content">
<div class="provider-details">
<div class="detail-item">
<span class="label">API地址:</span>
<span class="value">{{ provider.base_url }}</span>
</div>
<div class="detail-item">
<span class="label">默认模型:</span>
<span class="value">{{ provider.default }}</span>
</div>
<div class="detail-item">
<span class="label">可用模型:</span>
<span class="value">{{ provider.models?.join(', ') || '无' }}</span>
</div>
</div>
</div>
</div>
<!-- 无自定义供应商时的提示 -->
<div v-if="Object.keys(customProviders).length === 0" class="empty-state">
<a-empty description="暂无自定义供应商">
<a-button type="primary" @click="openAddCustomProviderModal">添加自定义供应商</a-button>
</a-empty>
</div>
</div>
<a-divider />
<!-- 系统内置供应商 -->
<div class="builtin-providers-section">
<div class="section-header">
<h3>系统内置供应商</h3>
<div class="providers-stats">
<span class="stats-item available">
{{ modelKeys.length }} 可用
</span>
<span class="stats-item unavailable">
{{ notModelKeys.length }} 未配置
</span>
</div>
</div>
<!-- 已配置的供应商 -->
<div class="model-provider-card configured-provider" v-for="(item, key) in modelKeys" :key="key">
<div class="card-header" @click="toggleExpand(item)">
<div :class="{'model-icon': true, 'available': modelStatus[item]}">
<img :src="modelIcons[item] || modelIcons.default" alt="模型图标">
</div>
<div class="model-title-container">
<h3>{{ modelNames[item].name }}</h3>
<div class="provider-meta">
<span class="model-count">{{ modelNames[item].models?.length || 0 }} 个模型</span>
<span class="provider-id">{{ item }}</span>
</div>
</div>
<div class="provider-actions">
<a-button
type="text"
size="small"
class="expand-button"
@click.stop="openProviderConfig(item)"
title="配置模型"
>
<SettingOutlined />
</a-button>
<a-button
type="text"
size="small"
class="expand-button"
@click.stop="toggleExpand(item)"
>
<span class="icon-wrapper" :class="{'rotated': expandedModels[item]}">
<DownCircleOutlined />
</span>
</a-button>
</div>
</div>
<div class="card-body-wrapper" :class="{'expanded': expandedModels[item]}">
<div class="card-body" v-if="modelStatus[item]">
<div class="card-models" v-for="(model, idx) in modelNames[item].models" :key="idx">
<div class="model_name">{{ model }}</div>
</div>
</div>
</div>
</div>
<!-- 未配置的供应商 -->
<div class="model-provider-card unconfigured-provider" v-for="(item, key) in notModelKeys" :key="key">
<div class="card-header">
<div class="model-icon">
<img :src="modelIcons[item] || modelIcons.default" alt="模型图标">
</div>
<div class="model-title-container">
<h3>{{ modelNames[item].name }}</h3>
<a :href="modelNames[item].url" target="_blank" class="model-url">
查看信息 <InfoCircleOutlined />
</a>
</div>
<div class="missing-keys">
需配置<span>{{ modelNames[item].env }}</span>
</div>
</div>
</div>
</div>
<!-- 模型提供商配置弹窗 -->
<a-modal
class="provider-config-modal"
v-model:open="providerConfig.visible"
:title="`配置${providerConfig.providerName}模型`"
@ok="saveProviderConfig"
@cancel="cancelProviderConfig"
:okText="'保存配置'"
:cancelText="'取消'"
:ok-type="'primary'"
:width="800"
>
<div v-if="providerConfig.loading" class="modal-loading-container">
<a-spin :indicator="h(LoadingOutlined, { style: { fontSize: '32px', color: 'var(--main-color)' }})" />
<div class="loading-text">正在获取模型列表...</div>
</div>
<div v-else class="modal-config-content">
<div class="modal-config-header">
<h3>选择 {{ providerConfig.providerName }} 的模型</h3>
<p class="description">勾选您希望在系统中启用的模型,请注意,列表中可能包含非对话模型,请仔细甄别。</p>
</div>
<div class="modal-models-section">
<div class="model-search">
<a-input
v-model:value="providerConfig.searchQuery"
placeholder="搜索模型..."
allow-clear
>
<template #prefix>
<SearchOutlined />
</template>
</a-input>
</div>
<!-- 显示选中统计信息 -->
<div class="selection-summary">
<span>已选择 {{ providerConfig.selectedModels.length }} 个模型</span>
<span v-if="providerConfig.searchQuery" class="filter-info">
(当前筛选显示 {{ filteredModels.length }} 个)
</span>
</div>
<div class="modal-checkbox-list">
<div v-for="(model, index) in filteredModels" :key="index" class="modal-checkbox-item">
<a-checkbox
:checked="providerConfig.selectedModels.includes(model.id)"
@change="(e) => handleModelSelect(model.id, e.target.checked)"
>
{{ model.id }}
</a-checkbox>
</div>
</div>
<div v-if="providerConfig.allModels.length === 0" class="modal-no-models">
<a-alert v-if="!modelStatus[providerConfig.provider]" type="warning" message="请在 .env 中配置对应的 APIKEY并重新启动服务" />
<div v-else>
<a-alert type="warning" message="该提供商暂未适配获取模型列表的方法,如需添加模型,请编辑 src/config/static/models.py 。">
<template #description>
<a href="https://xerrors.github.io/Yuxi-Know/latest/intro/model-config.html" target="_blank">模型配置</a>
</template>
</a-alert>
</div>
</div>
</div>
</div>
</a-modal>
<!-- 自定义供应商配置弹窗 -->
<a-modal
v-model:open="customProviderModal.visible"
:title="customProviderModal.isEdit ? '编辑自定义供应商' : '添加自定义供应商'"
@ok="saveCustomProvider"
@cancel="cancelCustomProvider"
:okText="'保存'"
:cancelText="'取消'"
:ok-type="'primary'"
:width="600"
:confirmLoading="customProviderModal.loading"
>
<a-form
ref="customProviderForm"
:model="customProviderModal.data"
:rules="customProviderRules"
layout="vertical"
>
<a-form-item label="供应商ID" name="providerId" v-if="!customProviderModal.isEdit">
<a-input
v-model:value="customProviderModal.data.providerId"
placeholder="请输入唯一的供应商标识符my-provider"
:disabled="customProviderModal.isEdit"
/>
</a-form-item>
<a-form-item label="供应商名称" name="name">
<a-input
v-model:value="customProviderModal.data.name"
placeholder="请输入供应商显示名称"
/>
</a-form-item>
<a-form-item label="API地址" name="base_url">
<a-input
v-model:value="customProviderModal.data.base_url"
placeholder="请输入API基础地址https://api.example.com/v1"
/>
</a-form-item>
<a-form-item label="默认模型" name="default">
<a-input
v-model:value="customProviderModal.data.default"
placeholder="请输入默认模型名称"
/>
</a-form-item>
<a-form-item label="API密钥" name="env">
<a-input
v-model:value="customProviderModal.data.env"
placeholder="请输入API密钥或环境变量名MY_API_KEY"
/>
<div class="form-help-text">
支持直接输入API密钥或使用环境变量名MY_API_KEY
</div>
</a-form-item>
<a-form-item label="支持的模型" name="models">
<a-textarea
v-model:value="customProviderModal.data.modelsText"
placeholder="请输入支持的模型列表,每行一个模型"
:rows="4"
/>
<div class="form-help-text">
每行输入一个模型名称例如gpt-3.5-turbo
</div>
</a-form-item>
<a-form-item label="文档地址" name="url">
<a-input
v-model:value="customProviderModal.data.url"
placeholder="请输入供应商文档地址(可选)"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { computed, reactive, watch, h, ref } from 'vue'
import { message } from 'ant-design-vue';
import {
InfoCircleOutlined,
SettingOutlined,
DownCircleOutlined,
LoadingOutlined,
SearchOutlined,
PlusOutlined,
EditOutlined,
DeleteOutlined,
ApiOutlined
} from '@ant-design/icons-vue';
import { useConfigStore } from '@/stores/config';
import { modelIcons } from '@/utils/modelIcon';
import { agentApi } from '@/apis/agent_api';
import { customProviderApi } from '@/apis/system_api';
const configStore = useConfigStore();
// 计算属性
const modelNames = computed(() => configStore.config?.model_names);
const modelStatus = computed(() => configStore.config?.model_provider_status);
// 自定义供应商计算属性
const customProviders = computed(() => {
const providers = configStore.config?.model_names || {};
return Object.fromEntries(
Object.entries(providers).filter(([key, value]) => value.custom === true)
);
});
// 提供商配置相关状态
const providerConfig = reactive({
visible: false,
provider: '',
providerName: '',
models: [],
allModels: [], // 所有可用的模型
selectedModels: [], // 用户选择的模型
loading: false,
searchQuery: '',
});
// 筛选 modelStatus 中为真的key
const modelKeys = computed(() => {
return Object.keys(modelStatus.value || {}).filter(key => modelStatus.value[key] && !customProviders.value[key]);
});
// 筛选 modelStatus 中为假的key
const notModelKeys = computed(() => {
return Object.keys(modelStatus.value || {}).filter(key => !modelStatus.value[key]);
});
// 模型展开状态管理
const expandedModels = reactive({});
// 监听 modelKeys 变化,确保新添加的模型也是默认展开状态
watch(modelKeys, (newKeys) => {
newKeys.forEach(key => {
if (expandedModels[key] === undefined) {
expandedModels[key] = true;
}
});
}, { immediate: true });
// 切换展开状态
const toggleExpand = (item) => {
expandedModels[item] = !expandedModels[item];
};
// 处理模型选择
const handleModelSelect = (modelId, checked) => {
const selectedModels = providerConfig.selectedModels;
const index = selectedModels.indexOf(modelId);
if (checked && index === -1) {
selectedModels.push(modelId);
} else if (!checked && index > -1) {
selectedModels.splice(index, 1);
}
};
// 打开提供商配置
const openProviderConfig = (provider) => {
providerConfig.provider = provider;
providerConfig.providerName = modelNames.value[provider].name;
providerConfig.allModels = [];
providerConfig.visible = true;
providerConfig.loading = true;
providerConfig.searchQuery = ''; // 重置搜索关键词
// 获取当前选择的模型作为初始选中值
const currentModels = modelNames.value[provider]?.models || [];
providerConfig.selectedModels = [...currentModels];
// 获取所有可用模型
fetchProviderModels(provider);
};
// 获取模型提供商的模型列表
const fetchProviderModels = (provider) => {
providerConfig.loading = true;
agentApi.getProviderModels(provider)
.then(data => {
console.log(`${provider} 模型列表:`, data);
// 处理各种可能的API返回格式
let modelsList = [];
// 情况1: { data: [...] }
if (data.data && Array.isArray(data.data)) {
modelsList = data.data;
}
// 情况2: { models: [...] } (字符串数组)
else if (data.models && Array.isArray(data.models)) {
modelsList = data.models.map(model => typeof model === 'string' ? { id: model } : model);
}
// 情况3: { models: { data: [...] } }
else if (data.models && data.models.data && Array.isArray(data.models.data)) {
modelsList = data.models.data;
}
console.log("处理后的模型列表:", modelsList);
providerConfig.allModels = modelsList;
providerConfig.loading = false;
})
.catch(error => {
console.error(`获取${provider}模型列表失败:`, error);
message.error({ content: `获取${modelNames.value[provider].name}模型列表失败`, duration: 2 });
providerConfig.loading = false;
});
};
// 保存提供商配置
const saveProviderConfig = async () => {
if (!modelStatus.value[providerConfig.provider]) {
message.error('请在 .env 中配置对应的 APIKEY并重新启动服务');
return;
}
message.loading({ content: '保存配置中...', key: 'save-config', duration: 0 });
try {
// 发送选择的模型列表到后端
const data = await agentApi.updateProviderModels(providerConfig.provider, providerConfig.selectedModels);
console.log('更新后的模型列表:', data.models);
message.success({ content: '模型配置已保存!', key: 'save-config', duration: 2 });
// 关闭弹窗
providerConfig.visible = false;
// 刷新配置
configStore.refreshConfig();
} catch (error) {
console.error('保存配置失败:', error);
message.error({ content: '保存配置失败: ' + error.message, key: 'save-config', duration: 2 });
}
};
// 取消提供商配置
const cancelProviderConfig = () => {
providerConfig.visible = false;
};
// 计算筛选后的模型列表
const filteredModels = computed(() => {
const allModels = providerConfig.allModels || [];
const searchQuery = providerConfig.searchQuery.toLowerCase();
return allModels.filter(model => model.id.toLowerCase().includes(searchQuery));
});
// 自定义供应商管理
const customProviderForm = ref();
const customProviderModal = reactive({
visible: false,
isEdit: false,
loading: false,
data: {
providerId: '',
name: '',
base_url: '',
default: '',
env: '',
modelsText: '',
models: [],
url: ''
}
});
// 自定义供应商表单验证规则
const customProviderRules = {
providerId: [
{ required: true, message: '请输入供应商ID', trigger: 'blur' },
{ pattern: /^[a-zA-Z0-9_-]+$/, message: '供应商ID只能包含字母、数字、下划线和横线', trigger: 'blur' },
{
validator: (rule, value) => {
if (!value) return Promise.resolve();
// 检查是否与现有供应商ID重复
if (modelNames.value && modelNames.value[value]) {
return Promise.reject('供应商ID已存在请使用其他ID');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
name: [
{ required: true, message: '请输入供应商名称', trigger: 'blur' }
],
base_url: [
{ required: true, message: '请输入API地址', trigger: 'blur' },
{ type: 'url', message: '请输入有效的URL地址', trigger: 'blur' }
],
default: [
{ required: true, message: '请输入默认模型', trigger: 'blur' }
],
env: [
{ required: true, message: '请输入API密钥或环境变量', trigger: 'blur' }
]
};
// API密钥掩码显示
const maskApiKey = (apiKey) => {
if (!apiKey) return '未配置';
// 如果是环境变量格式,直接显示
if (apiKey.startsWith('${') && apiKey.endsWith('}')) {
return apiKey;
}
// 如果是直接的API密钥进行掩码处理
if (apiKey.length > 8) {
return apiKey.substring(0, 4) + '***' + apiKey.substring(apiKey.length - 4);
}
return '***';
};
// 打开添加自定义供应商弹窗
const openAddCustomProviderModal = () => {
customProviderModal.visible = true;
customProviderModal.isEdit = false;
resetCustomProviderForm();
};
// 打开编辑自定义供应商弹窗
const openEditCustomProviderModal = (providerId, provider) => {
customProviderModal.visible = true;
customProviderModal.isEdit = true;
// 填充表单数据
customProviderModal.data.providerId = providerId;
customProviderModal.data.name = provider.name;
customProviderModal.data.base_url = provider.base_url;
customProviderModal.data.default = provider.default;
customProviderModal.data.env = provider.env;
customProviderModal.data.models = provider.models || [];
customProviderModal.data.modelsText = (provider.models || []).join('\n');
customProviderModal.data.url = provider.url || '';
};
// 重置自定义供应商表单
const resetCustomProviderForm = () => {
customProviderModal.data = {
providerId: '',
name: '',
base_url: '',
default: '',
env: '',
modelsText: '',
models: [],
url: ''
};
customProviderForm.value?.resetFields();
};
// 保存自定义供应商
const saveCustomProvider = async () => {
try {
await customProviderForm.value.validate();
customProviderModal.loading = true;
// 处理模型列表
const models = customProviderModal.data.modelsText
.split('\n')
.map(model => model.trim())
.filter(model => model.length > 0);
const providerData = {
name: customProviderModal.data.name,
base_url: customProviderModal.data.base_url,
default: customProviderModal.data.default,
env: customProviderModal.data.env,
models: models,
url: customProviderModal.data.url,
custom: true
};
let result;
if (customProviderModal.isEdit) {
result = await customProviderApi.updateCustomProvider(
customProviderModal.data.providerId,
providerData
);
message.success('自定义供应商更新成功');
} else {
result = await customProviderApi.addCustomProvider(
customProviderModal.data.providerId,
providerData
);
message.success(`自定义供应商 ${customProviderModal.data.providerId} 添加成功`);
}
// 关闭弹窗并刷新配置
customProviderModal.visible = false;
await configStore.refreshConfig();
} catch (error) {
if (error.errorFields) {
// 表单验证错误
return;
}
// 处理API错误响应
let errorMessage = '未知错误';
if (error.response?.data?.detail) {
errorMessage = error.response.data.detail;
} else if (error.message) {
errorMessage = error.message;
} else if (typeof error === 'string') {
errorMessage = error;
}
message.error(`操作失败: ${errorMessage}`);
} finally {
customProviderModal.loading = false;
}
};
// 取消自定义供应商操作
const cancelCustomProvider = () => {
customProviderModal.visible = false;
resetCustomProviderForm();
};
// 删除自定义供应商
const deleteCustomProvider = async (providerId) => {
try {
const result = await customProviderApi.deleteCustomProvider(providerId);
message.success('自定义供应商删除成功');
await configStore.refreshConfig();
} catch (error) {
message.error(`删除失败: ${error.message || error.response?.data?.detail || '未知错误'}`);
}
};
// 测试自定义供应商连接
const testCustomProvider = async (providerId, modelName) => {
try {
message.loading({ content: '正在测试连接...', key: 'test-connection', duration: 0 });
const result = await customProviderApi.testCustomProvider(providerId, modelName);
if (result.status?.status === 'available') {
message.success({ content: '连接测试成功', key: 'test-connection', duration: 2 });
} else {
message.error({
content: `连接测试失败: ${result.status?.message || '未知错误'}`,
key: 'test-connection',
duration: 3
});
}
} catch (error) {
message.error({
content: `测试失败: ${error.message || error.response?.data?.detail || '未知错误'}`,
key: 'test-connection',
duration: 3
});
}
};
</script>
<style lang="less" scoped>
// 自定义供应商区域样式
.custom-providers-section {
margin-bottom: 24px;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--gray-900);
}
}
.section-description {
margin: 0 0 16px 0;
color: var(--gray-600);
font-size: 14px;
line-height: 1.5;
}
.custom-provider-card {
border: 1px solid var(--gray-200);
background: var(--gray-0);
border-radius: 8px;
margin-bottom: 12px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
&:hover {
border-color: var(--gray-300);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: var(--gray-25);
border-bottom: 1px solid var(--gray-200);
.provider-info {
display: flex;
align-items: center;
gap: 12px;
h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--gray-900);
}
.provider-id {
background: var(--main-color);
color: var(--gray-0);
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
}
.provider-actions {
display: flex;
gap: 8px;
.ant-btn {
display: flex;
align-items: center;
gap: 4px;
}
}
}
.card-content {
padding: 16px 20px;
.provider-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 12px;
.detail-item {
display: flex;
flex-direction: column;
gap: 4px;
.label {
font-size: 12px;
color: var(--gray-600);
font-weight: 500;
}
.value {
font-size: 14px;
color: var(--gray-900);
word-break: break-all;
}
}
}
}
}
.empty-state {
text-align: center;
padding: 40px 20px;
background: var(--gray-25);
border-radius: 8px;
border: 1px dashed var(--gray-300);
}
}
.builtin-providers-section {
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--gray-900);
}
.providers-stats {
display: flex;
gap: 12px;
font-size: 13px;
.stats-item {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
&.available {
background: rgba(34, 197, 94, 0.1);
color: #16a34a;
}
&.unavailable {
background: rgba(249, 115, 22, 0.1);
color: #ea580c;
}
}
}
}
}
// 表单帮助文本样式
.form-help-text {
font-size: 12px;
color: var(--gray-600);
margin-top: 4px;
line-height: 1.4;
}
.model-provider-card {
border: 1px solid var(--gray-150);
background-color: var(--gray-0);
border-radius: 8px;
margin-bottom: 16px;
padding: 0;
transition: all 0.3s ease;
overflow: hidden;
&:hover {
border-color: var(--gray-200);
}
// 自定义模型容器特殊样式
&.custom-models-card {
.card-header {
h3 {
color: var(--main-color);
font-weight: 600;
}
}
}
// 已配置provider的样式
&.configured-provider {
.model-icon {
&.available {
filter: grayscale(0%);
}
}
.provider-meta {
.model-count {
color: var(--gray-600);
font-weight: 500;
}
}
}
// 未配置provider的样式
&.unconfigured-provider {
.card-header {
background: var(--gray-25);
h3 {
color: var(--gray-700);
font-weight: 500;
}
.model-icon {
filter: grayscale(100%);
}
}
.missing-keys {
color: var(--gray-700);
font-size: 12px;
font-weight: 500;
& > span {
margin-left: 6px;
user-select: all;
background-color: rgba(251, 146, 60, 0.15);
color: #d97706;
padding: 3px 8px;
border-radius: 6px;
font-weight: 600;
font-size: 11px;
border: 1px solid rgba(251, 146, 60, 0.2);
}
}
}
.card-header {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
padding: 8px 16px;
background: var(--gray-0);
transition: all 0.3s ease;
&:hover {
background: var(--gray-50);
}
.model-title-container {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
h3 {
margin: 0;
font-size: 15px;
font-weight: 600;
color: var(--gray-900);
}
.provider-meta {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
.model-count {
color: var(--gray-600);
font-weight: 500;
}
.provider-id {
background: var(--gray-100);
color: var(--gray-600);
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
font-family: 'Courier New', monospace;
}
.provider-link {
color: var(--gray-500);
transition: color 0.2s ease;
&:hover {
color: var(--main-color);
}
}
}
}
.model-url {
font-size: 12px;
width: fit-content;
color: var(--gray-500);
transition: color 0.2s ease;
&:hover {
color: var(--main-color);
}
}
.model-icon {
width: 28px;
height: 28px;
border-radius: 6px;
overflow: hidden;
filter: grayscale(100%);
transition: filter 0.2s ease;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
border: 1px solid var(--gray-100);
border-radius: 6px;
}
&.available {
filter: grayscale(0%);
}
}
.expand-button, .config-button {
height: 32px;
width: 32px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
cursor: pointer;
color: var(--gray-500);
border-radius: 6px;
transition: all 0.2s ease;
&:hover {
background-color: var(--gray-50);
color: var(--gray-700);
}
.icon-wrapper {
display: inline-flex;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&.rotated {
transform: rotate(180deg);
}
}
}
a {
text-decoration: none;
color: var(--gray-500);
font-size: 12px;
transition: all 0.2s ease;
&:hover {
color: var(--main-color);
}
}
.details, .missing-keys {
margin-left: auto;
}
.missing-keys {
color: var(--gray-700);
font-size: 12px;
font-weight: 500;
& > span {
margin-left: 6px;
user-select: all;
background-color: rgba(251, 146, 60, 0.15);
color: #d97706;
padding: 3px 8px;
border-radius: 6px;
font-weight: 600;
font-size: 11px;
border: 1px solid rgba(251, 146, 60, 0.2);
}
}
.expand-button, .config-button {
height: 32px;
width: 32px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
cursor: pointer;
color: var(--gray-500);
border-radius: 6px;
transition: all 0.2s ease;
&:hover {
background-color: var(--gray-50);
color: var(--gray-700);
}
.icon-wrapper {
display: inline-flex;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&.rotated {
transform: rotate(180deg);
}
}
}
}
.card-body-wrapper {
max-height: 0;
overflow: hidden;
transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1);
background: var(--gray-0);
&.expanded {
max-height: 800px;
}
}
.card-body {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 12px;
padding: 16px;
background: var(--gray-10);
// 普通模型卡片样式
.card-models {
width: 100%;
border-radius: 6px;
border: 1px solid var(--gray-150);
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
background: var(--gray-0);
transition: all 0.3s ease;
min-height: 48px;
&:hover {
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.03);
border-color: var(--gray-200);
}
.model_name {
font-size: 14px;
font-weight: 500;
color: var(--gray-900);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.4;
}
.select-btn {
width: 16px;
height: 16px;
flex: 0 0 16px;
border-radius: 50%;
border: 2px solid var(--gray-300);
background: var(--gray-0);
transition: all 0.2s ease;
&:hover {
border-color: var(--main-color);
}
}
}
// 自定义模型卡片样式 - 统一设计
.custom-model {
display: flex;
flex-direction: column;
align-items: stretch;
padding: 12px 16px;
gap: 8px;
cursor: pointer;
min-height: 72px;
background: var(--gray-0);
border-radius: 6px;
border: 1px solid var(--gray-150);
transition: all 0.3s ease;
&:hover {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
border-color: var(--gray-200);
.card-models__header .action {
opacity: 1;
}
}
.card-models__header {
width: 100%;
display: flex;
justify-content: space-between;
align-items: flex-start;
min-height: 18px;
.name {
font-size: 14px;
font-weight: 500;
color: var(--gray-900);
line-height: 1.4;
flex: 1;
margin-right: 8px;
word-break: break-word;
}
.action {
opacity: 0;
transition: opacity 0.2s ease;
display: flex;
gap: 4px;
flex-shrink: 0;
margin-top: -1px;
button {
padding: 4px;
height: 24px;
width: 24px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
&:hover {
background-color: var(--gray-100);
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
&:hover {
background-color: transparent;
}
}
}
}
}
.api_base {
font-size: 12px;
color: var(--gray-600);
line-height: 1.3;
word-break: break-all;
margin-top: auto;
}
// 添加模型的special样式
&.add-model {
border: 2px dashed var(--gray-300);
background: var(--gray-0);
justify-content: center;
align-items: center;
text-align: center;
min-height: 64px;
&:hover {
border-color: var(--main-color);
background: var(--gray-25);
}
.card-models__header {
justify-content: center;
align-items: center;
text-align: center;
.name {
color: var(--main-color);
font-weight: 600;
margin-right: 0;
text-align: center;
}
}
.api_base {
color: var(--gray-500);
text-align: center;
margin-top: 4px;
}
}
}
}
}
.custom-model-modal {
.ant-form-item {
margin-bottom: 10px;
.form-item-description {
font-size: 12px;
color: var(--gray-600);
margin-bottom: 10px;
}
}
}
.provider-config-modal {
.ant-modal-body {
padding: 16px 0 !important;
.modal-loading-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 200px;
.loading-text {
margin-top: 20px;
color: var(--gray-700);
font-size: 14px;
}
}
.modal-config-content {
max-height: 70vh;
overflow-y: auto;
.modal-config-header {
margin-bottom: 20px;
.description {
font-size: 14px;
color: var(--gray-600);
margin: 0;
}
}
.modal-models-section {
.model-search {
margin-bottom: 10px;
padding: 0 6px;
.ant-input-affix-wrapper {
border-radius: 6px;
&:hover, &:focus {
border-color: var(--main-color);
}
.anticon {
color: var(--gray-500);
}
}
}
.selection-summary {
margin: 0 6px 10px;
font-size: 14px;
color: var(--gray-600);
.filter-info {
color: var(--gray-500);
}
}
.modal-checkbox-list {
max-height: 50vh;
overflow-y: auto;
.modal-checkbox-item {
display: inline-block;
margin-bottom: 4px;
padding: 4px 6px;
border-radius: 6px;
background-color: var(--gray-0);
border: 1px solid var(--gray-150);
&:hover {
background-color: var(--gray-50);
}
}
}
}
}
}
}
// 针对不同状态的额外样式调整
.unconfigured-provider {
.card-body {
.card-models {
opacity: 0.6;
pointer-events: none;
.model_name {
color: var(--gray-500);
}
&:hover {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
border-color: var(--gray-200);
}
}
}
}
// 添加简单的脉冲动画
@keyframes pulse {
0% {
opacity: 0.7;
}
50% {
opacity: 1;
}
100% {
opacity: 0.7;
}
}
// 响应式调整
@media (max-width: 768px) {
.model-provider-card {
margin-bottom: 10px;
.card-body {
grid-template-columns: 1fr;
gap: 8px;
padding: 10px;
}
.card-header {
padding: 10px;
gap: 8px;
.model-icon {
width: 24px;
height: 24px;
}
h3 {
font-size: 14px;
}
.expand-button, .config-button {
height: 30px;
width: 30px;
}
}
}
}
</style>