增加ai配置和ai广场功能

This commit is contained in:
hero920103 committed 2026-11-09 16:44:11 +08:00
1 parent 244b4bd9fd
commit d9a50e23dd
27 files changed
+3808 -57

No files matched your search

@@ -0,0 +1,288 @@
<template>
<div class="container-box aisettings">
<div class="header-bar">
<h2>AI 管理</h2>
<el-button type="primary" plain @click="$router.push('/system/aisettings/playground')">
打开 Playground
</el-button>
</div>
<!-- 顶部引导 -->
<div class="ai-guide">
<div class="ai-guide__text">
<h3>统一 AI 能力中台</h3>
<p>
左侧管理已接入的模型配置(OpenAI Chat Completions / OpenAI Responses / Anthropic Messages),
点击「新增」或列表项进行配置;连通性测试与 Playground 对话验证已保存的模型。
OCR 识别配置在右侧统一维护,租户端「OA - OCR 识别」将读取这里的配置。
</p>
</div>
<div class="ai-guide__steps">
<span class="step"><i>1</i>新增模型</span>
<span class="step"><i>2</i>填写 API 信息</span>
<span class="step"><i>3</i>连通性测试</span>
<span class="step"><i>4</i>Playground 验证</span>
</div>
</div>
<div class="ai-layout">
<!-- 左侧:已配置模型列表 -->
<div class="ai-list">
<div class="ai-list__header">
<span class="ai-list__title">模型配置</span>
<el-button type="primary" size="small" @click="openAdd">新增</el-button>
</div>
<div class="ai-list__body" v-loading="listLoading">
<div
v-for="item in list"
:key="item.id"
class="ai-item"
@click="openEdit(item)"
>
<div class="ai-item__main">
<div class="ai-item__name">
{{ item.name }}
<el-tag v-if="item.is_default === 1" type="success" size="small">默认</el-tag>
</div>
<div class="ai-item__meta">
{{ apiTypeLabel(item.api_type) }} · {{ firstModel(item) }}
</div>
</div>
<el-tag :type="item.status === 1 ? 'success' : 'info'" size="small" effect="plain">
{{ item.status === 1 ? '启用' : '禁用' }}
</el-tag>
</div>
<el-empty v-if="!listLoading && !list.length" description="暂无配置,点击新增创建" :image-size="60" />
</div>
</div>
<!-- 右侧:OCR 识别配置 -->
<div class="ai-detail">
<ocr-settings />
</div>
</div>
<!-- 新增 / 编辑弹窗 -->
<provider-dialog
v-model="dialogVisible"
:item="dialogItem"
@saved="onSaved"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { getAiProviderList } from "@/api/aisettings";
import providerDialog from "./components/providerDialog.vue";
import ocrSettings from "./components/ocrSettings.vue";
const listLoading = ref(false);
const list = ref([]);
const dialogVisible = ref(false);
const dialogItem = ref(null);
const API_TYPE_LABELS = {
chat_completions: "Chat Completions",
responses: "Responses",
anthropic: "Anthropic Messages",
};
const apiTypeLabel = (t) => API_TYPE_LABELS[t] || t || "Chat Completions";
const firstModel = (item) => {
const m = (item.models || [])[0];
if (!m) return item.default_model || "未设置模型";
return m.name ? `${m.name}(${m.id})` : m.id;
};
const loadList = async () => {
listLoading.value = true;
try {
const res = await getAiProviderList();
if (res?.code === 200) list.value = res.data || [];
} finally {
listLoading.value = false;
}
};
const openAdd = () => {
dialogItem.value = null;
dialogVisible.value = true;
};
const openEdit = (item) => {
dialogItem.value = item;
dialogVisible.value = true;
};
const onSaved = async () => {
await loadList();
};
onMounted(loadList);
</script>
<style lang="less" scoped>
.aisettings {
padding: 20px;
}
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.ai-guide {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
margin-top: 16px;
padding: 20px 24px;
border-radius: 12px;
background: linear-gradient(120deg, var(--el-color-primary-light-9), var(--el-fill-color-light));
border: 1px solid var(--el-color-primary-light-7);
&__text {
flex: 1;
min-width: 0;
h3 {
margin: 0 0 8px;
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
p {
margin: 0;
font-size: 13px;
line-height: 1.8;
color: var(--el-text-color-secondary);
}
}
&__steps {
display: flex;
flex-wrap: wrap;
gap: 10px;
flex-shrink: 0;
}
.step {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 100px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
font-size: 12.5px;
color: var(--el-text-color-regular);
i {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--el-color-primary);
color: #fff;
font-style: normal;
font-size: 11px;
}
}
}
.ai-layout {
display: flex;
gap: 16px;
margin-top: 16px;
align-items: flex-start;
}
.ai-list {
width: 340px;
flex-shrink: 0;
border: 1px solid var(--el-border-color-lighter);
border-radius: 10px;
background: var(--el-bg-color);
overflow: hidden;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px;
border-bottom: 1px solid var(--el-border-color-lighter);
background: var(--el-fill-color-lighter);
}
&__title {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
&__body {
max-height: 560px;
overflow-y: auto;
padding: 8px;
}
}
.ai-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s ease;
&:hover {
background: var(--el-fill-color-light);
}
&__main {
min-width: 0;
}
&__name {
display: flex;
align-items: center;
gap: 6px;
font-size: 13.5px;
font-weight: 600;
color: var(--el-text-color-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__meta {
margin-top: 4px;
font-size: 12px;
color: var(--el-text-color-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.ai-detail {
flex: 1;
min-width: 0;
}
</style>