增加ai配置和ai广场功能
This commit is contained in:
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>
|
||||
Reference in new issue
Block a user