chore: initialize wygj monorepo
This commit is contained in:
@@ -0,0 +1,915 @@
|
||||
<template>
|
||||
<div class="model-config-page">
|
||||
<section class="page-head">
|
||||
<div>
|
||||
<h1>模型配置</h1>
|
||||
<p>统一维护供应商 Key、Base URL 与可用模型,供 RAG、面试出题、对练调用。</p>
|
||||
</div>
|
||||
<div class="head-actions">
|
||||
<el-button type="danger" :icon="Plus" @click="openProviderDialog()">新增供应商</el-button>
|
||||
<el-button type="danger" :icon="Plus" @click="openModelDialog()">新增模型</el-button>
|
||||
<el-button :icon="Connection" :loading="testing" @click="runConnectionTest">测试连接</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="model-layout">
|
||||
<aside class="panel provider-panel">
|
||||
<div class="panel-title">
|
||||
<h2>供应商</h2>
|
||||
</div>
|
||||
<el-input v-model="keyword" class="provider-search" placeholder="搜索供应商名称或编码" :prefix-icon="Search" clearable />
|
||||
|
||||
<div v-loading="loading" class="provider-list">
|
||||
<button
|
||||
v-for="provider in filteredProviders"
|
||||
:key="provider.providerCode"
|
||||
class="provider-item"
|
||||
:class="{ active: provider.providerCode === selectedProviderCode }"
|
||||
@click="selectedProviderCode = provider.providerCode"
|
||||
>
|
||||
<span class="provider-logo" :class="`logo-${provider.providerCode}`">{{ provider.logo }}</span>
|
||||
<span class="provider-text">
|
||||
<strong>{{ provider.providerName }}</strong>
|
||||
<small>{{ provider.providerCode }}</small>
|
||||
</span>
|
||||
<span class="status-tag" :class="providerTag(provider).className">{{ providerTag(provider).label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<footer>共 {{ providers.length }} 个供应商</footer>
|
||||
</aside>
|
||||
|
||||
<main class="detail-stack">
|
||||
<section class="panel detail-panel">
|
||||
<div class="panel-title with-actions">
|
||||
<h2>供应商详情</h2>
|
||||
<div>
|
||||
<el-button :icon="EditPen" @click="openProviderDialog(activeProvider)">编辑</el-button>
|
||||
<el-button type="danger" plain :icon="Connection" :loading="testing" @click="runConnectionTest">测试</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-grid">
|
||||
<div class="field-list">
|
||||
<div class="field-row">
|
||||
<span>供应商名称</span>
|
||||
<strong>{{ activeProvider.providerName }}</strong>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>供应商编码</span>
|
||||
<strong>{{ activeProvider.providerCode }}</strong>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>Base URL</span>
|
||||
<strong>{{ activeProvider.apiHost }}</strong>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>API Key</span>
|
||||
<strong>
|
||||
{{ keyText }}
|
||||
<el-button link :icon="showKey ? View : Hide" @click="showKey = !showKey" />
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-list side">
|
||||
<div class="field-row">
|
||||
<span>状态</span>
|
||||
<el-switch
|
||||
:model-value="activeProvider.status === '0'"
|
||||
active-color="#e60012"
|
||||
inactive-color="#d7dee8"
|
||||
@change="(value) => toggleProviderStatus(Boolean(value))"
|
||||
/>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>最近测试时间</span>
|
||||
<strong>{{ lastTestAt }}</strong>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>请求方式</span>
|
||||
<strong>OpenAI Compatible</strong>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>备注</span>
|
||||
<strong>{{ activeProvider.providerDesc || `${activeProvider.providerName} OpenAI 兼容接口` }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel model-panel">
|
||||
<div class="panel-title">
|
||||
<h2>模型列表</h2>
|
||||
</div>
|
||||
<el-table :data="modelRows" class="model-table" size="large">
|
||||
<el-table-column prop="category" label="类型" width="130" />
|
||||
<el-table-column prop="modelName" label="模型名" min-width="190" />
|
||||
<el-table-column prop="modelDescribe" label="用途" min-width="280" />
|
||||
<el-table-column label="维度" width="110">
|
||||
<template #default="{ row }">{{ row.modelDimension || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.enabled"
|
||||
active-color="#e60012"
|
||||
inactive-color="#d7dee8"
|
||||
@change="(value) => toggleModelEnabled(row, Boolean(value))"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="danger" @click="openModelDialog(row)">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<div class="tip-bar">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
<span>调用优先级:模型配置覆盖供应商默认值;未配置时回退 seed,接口不返回 API Key。</span>
|
||||
</div>
|
||||
</main>
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="providerDialogVisible" :title="providerForm.id > 0 ? '编辑供应商' : '新增供应商'" width="560px">
|
||||
<el-form label-width="104px">
|
||||
<el-form-item label="供应商名称" required>
|
||||
<el-input v-model="providerForm.providerName" placeholder="DeepSeek" />
|
||||
</el-form-item>
|
||||
<el-form-item label="供应商编码" required>
|
||||
<el-input v-model="providerForm.providerCode" placeholder="deepseek" :disabled="providerForm.id > 0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Base URL">
|
||||
<el-input v-model="providerForm.apiHost" placeholder="https://api.deepseek.com" />
|
||||
</el-form-item>
|
||||
<el-form-item label="API Key">
|
||||
<el-input v-model="providerForm.apiKey" type="password" show-password placeholder="留空则保留原密钥" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="providerForm.providerDesc" type="textarea" :rows="2" placeholder="OpenAI 兼容接口" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-switch v-model="providerEnabled" active-text="启用" inactive-text="停用" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="providerDialogVisible = false">取消</el-button>
|
||||
<el-button type="danger" :loading="saving" @click="submitProvider">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="modelDialogVisible" :title="modelForm.id > 0 ? '编辑模型' : '新增模型'" width="560px">
|
||||
<el-form label-width="104px">
|
||||
<el-form-item label="模型类型" required>
|
||||
<el-select v-model="modelForm.category">
|
||||
<el-option label="对话 chat" value="chat" />
|
||||
<el-option label="向量 vector" value="vector" />
|
||||
<el-option label="重排 rerank" value="rerank" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型名称" required>
|
||||
<el-input v-model="modelForm.modelName" placeholder="deepseek-chat" />
|
||||
</el-form-item>
|
||||
<el-form-item label="供应商" required>
|
||||
<el-select v-model="modelForm.providerCode">
|
||||
<el-option v-for="provider in providers" :key="provider.providerCode" :label="provider.providerName" :value="provider.providerCode" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="用途">
|
||||
<el-input v-model="modelForm.modelDescribe" placeholder="用于 Demo 对话与 RAG 生成" />
|
||||
</el-form-item>
|
||||
<el-form-item label="维度">
|
||||
<el-input-number v-model="modelForm.modelDimension" :min="0" :controls="false" placeholder="向量模型填写" />
|
||||
</el-form-item>
|
||||
<el-form-item label="覆盖 URL">
|
||||
<el-input v-model="modelForm.apiHost" placeholder="留空则使用供应商 Base URL" />
|
||||
</el-form-item>
|
||||
<el-form-item label="覆盖 Key">
|
||||
<el-input v-model="modelForm.apiKey" type="password" show-password placeholder="留空则使用供应商 Key 或保留原值" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="modelForm.enabled" active-text="启用" inactive-text="停用" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="modelDialogVisible = false">取消</el-button>
|
||||
<el-button type="danger" :loading="saving" @click="submitModel">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="SystemModelConfig" lang="ts">
|
||||
import { Connection, EditPen, Hide, InfoFilled, Plus, Search, View } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import {
|
||||
createModelConfig,
|
||||
createModelProvider,
|
||||
getModelConfigs,
|
||||
getModelProviders,
|
||||
testModelChat,
|
||||
updateModelConfig,
|
||||
updateModelConfigEnabled,
|
||||
updateModelProvider,
|
||||
updateModelProviderStatus,
|
||||
type ModelConfig,
|
||||
type ModelConfigPayload,
|
||||
type ModelProvider,
|
||||
type ModelProviderPayload
|
||||
} from '@/api/aihr/model';
|
||||
|
||||
type ProviderView = ModelProvider & { logo: string };
|
||||
type ProviderFormState = ModelProviderPayload & {
|
||||
id: number;
|
||||
originalProviderCode: string;
|
||||
apiHost: string;
|
||||
apiKey: string;
|
||||
providerDesc: string;
|
||||
sortOrder: number;
|
||||
};
|
||||
type ModelFormState = ModelConfigPayload & {
|
||||
id: number;
|
||||
apiHost: string;
|
||||
apiKey: string;
|
||||
modelDescribe: string;
|
||||
};
|
||||
|
||||
const seedProviders: ProviderView[] = [
|
||||
{
|
||||
id: -2,
|
||||
providerName: 'DeepSeek',
|
||||
providerCode: 'deepseek',
|
||||
providerDesc: 'DeepSeek 官方 API',
|
||||
apiHost: 'https://api.deepseek.com',
|
||||
status: '0',
|
||||
sortOrder: 0,
|
||||
source: 'seed',
|
||||
configured: false,
|
||||
logo: 'DS'
|
||||
},
|
||||
{
|
||||
id: -3,
|
||||
providerName: '智谱AI',
|
||||
providerCode: 'zhipu',
|
||||
providerDesc: '智谱 AI 大模型服务',
|
||||
apiHost: 'https://open.bigmodel.cn',
|
||||
status: '0',
|
||||
sortOrder: 1,
|
||||
source: 'seed',
|
||||
configured: false,
|
||||
logo: '智'
|
||||
},
|
||||
{
|
||||
id: -4,
|
||||
providerName: '阿里云百炼',
|
||||
providerCode: 'qianwen',
|
||||
providerDesc: '阿里云百炼大模型服务',
|
||||
apiHost: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
status: '0',
|
||||
sortOrder: 2,
|
||||
source: 'seed',
|
||||
configured: false,
|
||||
logo: '百'
|
||||
},
|
||||
{
|
||||
id: -1,
|
||||
providerName: 'OpenAI兼容接口',
|
||||
providerCode: 'custom_api',
|
||||
providerDesc: '自定义 OpenAI 兼容接口',
|
||||
apiHost: '自定义',
|
||||
status: '1',
|
||||
sortOrder: 3,
|
||||
source: 'seed',
|
||||
configured: false,
|
||||
logo: 'AI'
|
||||
},
|
||||
{
|
||||
id: -5,
|
||||
providerName: 'Ollama(本地)',
|
||||
providerCode: 'ollama',
|
||||
providerDesc: '本地 Ollama 服务',
|
||||
apiHost: 'http://127.0.0.1:11434/v1',
|
||||
status: '0',
|
||||
sortOrder: 4,
|
||||
source: 'seed',
|
||||
configured: false,
|
||||
logo: 'OL'
|
||||
}
|
||||
];
|
||||
|
||||
const seedModels: ModelConfig[] = [
|
||||
{
|
||||
id: -1,
|
||||
category: 'chat',
|
||||
modelName: 'deepseek-chat',
|
||||
providerCode: 'deepseek',
|
||||
modelDescribe: '用于 Demo 对话与 RAG 生成',
|
||||
modelShow: 'Y',
|
||||
apiHost: '',
|
||||
enabled: true,
|
||||
apiKeyConfigured: false,
|
||||
configured: true
|
||||
},
|
||||
{
|
||||
id: -2,
|
||||
category: 'chat',
|
||||
modelName: 'deepseek-reasoner',
|
||||
providerCode: 'deepseek',
|
||||
modelDescribe: '用于复杂评分解释',
|
||||
modelShow: 'Y',
|
||||
apiHost: '',
|
||||
enabled: true,
|
||||
apiKeyConfigured: false,
|
||||
configured: true
|
||||
},
|
||||
{
|
||||
id: -3,
|
||||
category: 'vector',
|
||||
modelName: 'embedding-3',
|
||||
providerCode: 'zhipu',
|
||||
modelDescribe: '用于知识片段量化',
|
||||
modelDimension: 2048,
|
||||
modelShow: 'N',
|
||||
apiHost: '',
|
||||
enabled: false,
|
||||
apiKeyConfigured: false,
|
||||
configured: false
|
||||
},
|
||||
{
|
||||
id: -4,
|
||||
category: 'rerank',
|
||||
modelName: 'rerank',
|
||||
providerCode: 'zhipu',
|
||||
modelDescribe: '用于召回片段重排',
|
||||
modelShow: 'N',
|
||||
apiHost: '',
|
||||
enabled: false,
|
||||
apiKeyConfigured: false,
|
||||
configured: false
|
||||
}
|
||||
];
|
||||
|
||||
const providers = ref<ProviderView[]>(seedProviders);
|
||||
const modelRows = ref<ModelConfig[]>(seedModels);
|
||||
const selectedProviderCode = ref('deepseek');
|
||||
const keyword = ref('');
|
||||
const loading = ref(false);
|
||||
const testing = ref(false);
|
||||
const saving = ref(false);
|
||||
const showKey = ref(false);
|
||||
const providerDialogVisible = ref(false);
|
||||
const modelDialogVisible = ref(false);
|
||||
const lastTestAt = ref('未测试');
|
||||
|
||||
const providerForm = reactive<ProviderFormState>({
|
||||
id: 0,
|
||||
originalProviderCode: '',
|
||||
providerName: '',
|
||||
providerCode: '',
|
||||
providerDesc: '',
|
||||
apiHost: '',
|
||||
apiKey: '',
|
||||
status: '0',
|
||||
sortOrder: 0
|
||||
});
|
||||
|
||||
const modelForm = reactive<ModelFormState>({
|
||||
id: 0,
|
||||
category: 'chat',
|
||||
modelName: '',
|
||||
providerCode: '',
|
||||
modelDescribe: '',
|
||||
modelDimension: undefined,
|
||||
modelShow: 'Y',
|
||||
apiHost: '',
|
||||
apiKey: '',
|
||||
enabled: true
|
||||
});
|
||||
|
||||
const filteredProviders = computed(() => {
|
||||
const word = keyword.value.trim().toLowerCase();
|
||||
if (!word) return providers.value;
|
||||
return providers.value.filter((item) => `${item.providerName} ${item.providerCode}`.toLowerCase().includes(word));
|
||||
});
|
||||
|
||||
const activeProvider = computed<ProviderView>(
|
||||
() => providers.value.find((item) => item.providerCode === selectedProviderCode.value) ?? providers.value[0] ?? seedProviders[0]
|
||||
);
|
||||
const activeChatModel = computed(() => modelRows.value.find((item) => item.category === 'chat' && item.enabled) ?? modelRows.value[0]);
|
||||
const keyText = computed(() => {
|
||||
if (!activeProvider.value.configured) return '未配置';
|
||||
return showKey.value ? '已配置,接口不回传密钥' : 'sk-************';
|
||||
});
|
||||
const providerEnabled = computed({
|
||||
get: () => providerForm.status === '0',
|
||||
set: (value: boolean) => {
|
||||
providerForm.status = value ? '0' : '1';
|
||||
}
|
||||
});
|
||||
|
||||
const providerTag = (provider: ProviderView) => {
|
||||
if (provider.providerCode.includes('ollama')) return { label: '本地', className: 'neutral' };
|
||||
if (provider.status !== '0') return { label: '未启用', className: 'warning' };
|
||||
if (!provider.configured) return { label: '未配置', className: 'warning' };
|
||||
return { label: '已启用', className: 'success' };
|
||||
};
|
||||
|
||||
const normalizeProviders = (items: ModelProvider[]): ProviderView[] => {
|
||||
if (!items.length) return seedProviders;
|
||||
const seedMap = new Map(seedProviders.map((item) => [item.providerCode, item]));
|
||||
return items.map((item) => {
|
||||
const seed = seedMap.get(item.providerCode);
|
||||
return {
|
||||
...item,
|
||||
logo: seed?.logo ?? item.providerName.slice(0, 2).toUpperCase()
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const loadData = async (preferredProviderCode = selectedProviderCode.value) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [providerResult, configResult] = await Promise.all([getModelProviders(), getModelConfigs()]);
|
||||
providers.value = normalizeProviders(providerResult.data ?? []);
|
||||
modelRows.value = configResult.data?.length ? configResult.data : seedModels;
|
||||
selectedProviderCode.value =
|
||||
providers.value.find((item) => item.providerCode === preferredProviderCode)?.providerCode ?? providers.value[0]?.providerCode ?? 'deepseek';
|
||||
} catch {
|
||||
providers.value = seedProviders;
|
||||
modelRows.value = seedModels;
|
||||
selectedProviderCode.value = preferredProviderCode || 'deepseek';
|
||||
ElMessage.warning('模型配置 API 不可用,已展示本地 seed 原型数据');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const runConnectionTest = async () => {
|
||||
testing.value = true;
|
||||
try {
|
||||
const result = await testModelChat({
|
||||
model: activeChatModel.value?.modelName,
|
||||
prompt: '请用一句话说明模型配置已连接。'
|
||||
});
|
||||
lastTestAt.value = new Date().toLocaleString('zh-CN', { hour12: false });
|
||||
if (result.data?.configured) {
|
||||
ElMessage.success(`连接成功:${result.data.modelName}`);
|
||||
} else {
|
||||
ElMessage.warning('后端返回 seed fallback,请先在数据库配置供应商 URL 和 Key');
|
||||
}
|
||||
} catch {
|
||||
ElMessage.warning('模型测试接口不可用,当前仅展示页面原型数据');
|
||||
} finally {
|
||||
testing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openProviderDialog = (provider?: ProviderView) => {
|
||||
const source = provider ?? undefined;
|
||||
Object.assign(providerForm, {
|
||||
id: source && source.id > 0 ? source.id : 0,
|
||||
originalProviderCode: source && source.id > 0 ? source.providerCode : '',
|
||||
providerName: source?.providerName ?? '',
|
||||
providerCode: source?.providerCode ?? '',
|
||||
providerDesc: source?.providerDesc ?? '',
|
||||
apiHost: source?.apiHost === '自定义' ? '' : (source?.apiHost ?? ''),
|
||||
apiKey: '',
|
||||
status: source?.status ?? '0',
|
||||
sortOrder: source?.sortOrder ?? providers.value.length
|
||||
});
|
||||
providerDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const providerPayload = (): ModelProviderPayload | undefined => {
|
||||
const providerName = providerForm.providerName.trim();
|
||||
const providerCode = providerForm.providerCode.trim();
|
||||
if (!providerName || !providerCode) {
|
||||
ElMessage.warning('请填写供应商名称和编码');
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
providerName,
|
||||
providerCode,
|
||||
providerDesc: providerForm.providerDesc.trim(),
|
||||
apiHost: providerForm.apiHost.trim(),
|
||||
apiKey: providerForm.apiKey.trim(),
|
||||
status: providerForm.status,
|
||||
sortOrder: providerForm.sortOrder
|
||||
};
|
||||
};
|
||||
|
||||
const submitProvider = async () => {
|
||||
const payload = providerPayload();
|
||||
if (!payload) return;
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
if (providerForm.id > 0) {
|
||||
await updateModelProvider(providerForm.originalProviderCode || providerForm.providerCode, payload);
|
||||
} else {
|
||||
await createModelProvider(payload);
|
||||
}
|
||||
providerDialogVisible.value = false;
|
||||
await loadData(payload.providerCode);
|
||||
ElMessage.success('供应商配置已保存');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleProviderStatus = async (enabled: boolean) => {
|
||||
const provider = activeProvider.value;
|
||||
if (provider.id < 0) {
|
||||
ElMessage.warning('seed 供应商请先保存后再启停');
|
||||
openProviderDialog(provider);
|
||||
return;
|
||||
}
|
||||
|
||||
const status = enabled ? '0' : '1';
|
||||
try {
|
||||
await updateModelProviderStatus(provider.providerCode, status);
|
||||
await loadData(provider.providerCode);
|
||||
ElMessage.success(enabled ? '供应商已启用' : '供应商已停用');
|
||||
} catch {
|
||||
await loadData(provider.providerCode);
|
||||
}
|
||||
};
|
||||
|
||||
const openModelDialog = (model?: ModelConfig) => {
|
||||
Object.assign(modelForm, {
|
||||
id: model && model.id > 0 ? model.id : 0,
|
||||
category: model?.category ?? 'chat',
|
||||
modelName: model?.modelName ?? '',
|
||||
providerCode: model?.providerCode ?? selectedProviderCode.value,
|
||||
modelDescribe: model?.modelDescribe ?? '',
|
||||
modelDimension: model?.modelDimension,
|
||||
modelShow: model?.modelShow ?? 'Y',
|
||||
apiHost: model?.apiHost ?? '',
|
||||
apiKey: '',
|
||||
enabled: model?.enabled ?? true
|
||||
});
|
||||
modelDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const modelPayload = (): ModelConfigPayload | undefined => {
|
||||
const modelName = modelForm.modelName.trim();
|
||||
const providerCode = modelForm.providerCode.trim();
|
||||
if (!modelName || !providerCode) {
|
||||
ElMessage.warning('请填写模型名称和供应商');
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
category: modelForm.category,
|
||||
modelName,
|
||||
providerCode,
|
||||
modelDescribe: modelForm.modelDescribe.trim(),
|
||||
modelDimension: modelForm.modelDimension && modelForm.modelDimension > 0 ? modelForm.modelDimension : undefined,
|
||||
modelShow: modelForm.modelShow,
|
||||
apiHost: modelForm.apiHost.trim(),
|
||||
apiKey: modelForm.apiKey.trim(),
|
||||
enabled: modelForm.enabled
|
||||
};
|
||||
};
|
||||
|
||||
const submitModel = async () => {
|
||||
const payload = modelPayload();
|
||||
if (!payload) return;
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
if (modelForm.id > 0) {
|
||||
await updateModelConfig(modelForm.id, payload);
|
||||
} else {
|
||||
await createModelConfig(payload);
|
||||
}
|
||||
modelDialogVisible.value = false;
|
||||
await loadData(payload.providerCode);
|
||||
ElMessage.success('模型配置已保存');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleModelEnabled = async (row: ModelConfig, enabled: boolean) => {
|
||||
if (row.id < 0) {
|
||||
row.enabled = !enabled;
|
||||
ElMessage.warning('seed 模型请先保存后再启停');
|
||||
openModelDialog(row);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateModelConfigEnabled(row.id, enabled);
|
||||
row.enabled = enabled;
|
||||
ElMessage.success(enabled ? '模型已启用' : '模型已停用');
|
||||
} catch {
|
||||
row.enabled = !enabled;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(loadData);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.model-config-page {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 28px 32px 34px;
|
||||
background: #f4f7fb;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
h1 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 30px;
|
||||
line-height: 1.2;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #5f6f86;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.head-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-top: 2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.model-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 330px) minmax(0, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 24px rgb(15 23 42 / 5%);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 22px 24px 14px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
.provider-panel {
|
||||
overflow: hidden;
|
||||
|
||||
footer {
|
||||
padding: 18px 24px 22px;
|
||||
color: #64748b;
|
||||
border-top: 1px solid #edf1f6;
|
||||
}
|
||||
}
|
||||
|
||||
.provider-search {
|
||||
width: calc(100% - 40px);
|
||||
margin: 4px 20px 18px;
|
||||
}
|
||||
|
||||
.provider-list {
|
||||
min-height: 420px;
|
||||
}
|
||||
|
||||
.provider-item {
|
||||
display: grid;
|
||||
grid-template-columns: 44px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 82px;
|
||||
padding: 14px 22px;
|
||||
border: 0;
|
||||
border-top: 1px solid #edf1f6;
|
||||
border-left: 4px solid transparent;
|
||||
background: #fff;
|
||||
color: #172033;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
border-left-color: #e60012;
|
||||
background: linear-gradient(90deg, #fff1f2 0%, #fff 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.provider-logo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: #eef4ff;
|
||||
color: #2264d1;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.logo-zhipu {
|
||||
background: #f3e8ff;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.logo-qianwen {
|
||||
background: #e0f2fe;
|
||||
color: #0891b2;
|
||||
}
|
||||
|
||||
.logo-custom_api {
|
||||
background: #f1f5f9;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.logo-ollama {
|
||||
background: #f8fafc;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.provider-text {
|
||||
min-width: 0;
|
||||
|
||||
strong,
|
||||
small {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
small {
|
||||
margin-top: 5px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 52px;
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
|
||||
&.success {
|
||||
color: #17834d;
|
||||
background: #dcfce7;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: #c2410c;
|
||||
background: #ffedd5;
|
||||
}
|
||||
|
||||
&.neutral {
|
||||
color: #475569;
|
||||
background: #eef2f7;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detail-panel,
|
||||
.model-panel {
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
.with-actions {
|
||||
padding-bottom: 18px;
|
||||
border-bottom: 1px solid #edf1f6;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 0.72fr);
|
||||
gap: 34px;
|
||||
padding: 22px 24px 6px;
|
||||
}
|
||||
|
||||
.field-list {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
|
||||
&.side {
|
||||
padding-left: 32px;
|
||||
border-left: 1px solid #edf1f6;
|
||||
}
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: grid;
|
||||
grid-template-columns: 116px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
span {
|
||||
color: #64748b;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
color: #25324a;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.model-table {
|
||||
width: calc(100% - 40px);
|
||||
margin: 4px 20px 0;
|
||||
border: 1px solid #edf1f6;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tip-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 54px;
|
||||
padding: 0 18px;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 8px;
|
||||
background: #fff7f7;
|
||||
color: #8a2f33;
|
||||
}
|
||||
|
||||
:deep(.el-button--danger) {
|
||||
--el-button-bg-color: #e60012;
|
||||
--el-button-border-color: #e60012;
|
||||
--el-button-hover-bg-color: #f0202f;
|
||||
--el-button-hover-border-color: #f0202f;
|
||||
}
|
||||
|
||||
:deep(.el-table th.el-table__cell) {
|
||||
background: #f8fafc;
|
||||
color: #334155;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.page-head,
|
||||
.head-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.model-layout,
|
||||
.detail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.field-list.side {
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user