Files
yunzerwebsiteallinone/platform/src/views/system/aisettings/index.vue
T
2026-11-13 20:26:16 +08:00

289 lines
6.6 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 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?.list || 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>