Files
Yi.Admin/Yi.Ai.Vue3/src/pages/console/channel/index.vue
2026-01-01 18:53:26 +08:00

549 lines
16 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.
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Delete, Edit, Plus, Refresh, View } from '@element-plus/icons-vue';
import type { AiAppDto, AiModelDto } from '@/api/channel/types';
import {
getAppList,
createApp,
updateApp,
deleteApp,
getModelList,
createModel,
updateModel,
deleteModel,
} from '@/api/channel';
// ==================== 应用管理 ====================
const appList = ref<AiAppDto[]>([]);
const appLoading = ref(false);
const selectedAppId = ref<string>('');
// 应用对话框
const appDialogVisible = ref(false);
const appDialogTitle = ref('');
const appForm = ref<Partial<AiAppDto>>({});
const appDetailDialogVisible = ref(false);
const appDetailData = ref<AiAppDto | null>(null);
// 获取应用列表
async function fetchAppList() {
appLoading.value = true;
try {
const res = await getAppList({
skipCount: 0,
maxResultCount: 100,
});
appList.value = res.data.items;
// 默认选中第一个应用
if (appList.value.length > 0 && !selectedAppId.value) {
selectedAppId.value = appList.value[0].id;
fetchModelList();
}
}
catch (error: any) {
ElMessage.error(error.message || '获取应用列表失败');
}
finally {
appLoading.value = false;
}
}
// 选择应用
function handleSelectApp(appId: string) {
selectedAppId.value = appId;
fetchModelList();
}
// 查看应用详情
function handleViewAppDetail(app: AiAppDto) {
appDetailData.value = app;
appDetailDialogVisible.value = true;
}
// 打开应用对话框
function openAppDialog(type: 'create' | 'edit', row?: AiAppDto) {
appDialogTitle.value = type === 'create' ? '创建应用' : '编辑应用';
if (type === 'create') {
appForm.value = {
name: '',
endpoint: '',
extraUrl: '',
apiKey: '',
orderNum: 0,
};
}
else {
appForm.value = { ...row };
}
appDialogVisible.value = true;
}
// 保存应用
async function saveApp() {
try {
if (appForm.value.id) {
await updateApp(appForm.value as any);
ElMessage.success('更新成功');
}
else {
await createApp(appForm.value as any);
ElMessage.success('创建成功');
}
appDialogVisible.value = false;
fetchAppList();
}
catch (error: any) {
ElMessage.error(error.message || '保存失败');
}
}
// 删除应用
async function handleDeleteApp(row: AiAppDto) {
try {
await ElMessageBox.confirm('确定要删除该应用吗?删除后该应用下的所有模型将无法使用。', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await deleteApp(row.id);
ElMessage.success('删除成功');
// 如果删除的是当前选中的应用,清空选中状态
if (selectedAppId.value === row.id) {
selectedAppId.value = '';
modelList.value = [];
}
fetchAppList();
}
catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.message || '删除失败');
}
}
}
// ==================== 模型管理 ====================
const modelList = ref<AiModelDto[]>([]);
const modelLoading = ref(false);
const modelSearchKey = ref('');
const modelDialogVisible = ref(false);
const modelDialogTitle = ref('');
const modelForm = ref<Partial<AiModelDto>>({});
// 获取模型列表
async function fetchModelList() {
if (!selectedAppId.value) {
modelList.value = [];
return;
}
modelLoading.value = true;
try {
const res = await getModelList({
aiAppId: selectedAppId.value,
searchKey: modelSearchKey.value,
skipCount: 0,
maxResultCount: 100,
});
modelList.value = res.data.items;
}
catch (error: any) {
ElMessage.error(error.message || '获取模型列表失败');
}
finally {
modelLoading.value = false;
}
}
// 打开模型对话框
function openModelDialog(type: 'create' | 'edit', row?: AiModelDto) {
if (!selectedAppId.value) {
ElMessage.warning('请先选择一个应用');
return;
}
modelDialogTitle.value = type === 'create' ? '创建模型' : '编辑模型';
if (type === 'create') {
modelForm.value = {
handlerName: '',
modelId: '',
name: '',
description: '',
orderNum: 0,
aiAppId: selectedAppId.value,
extraInfo: '',
modelType: 0,
modelApiType: 0,
multiplier: 1,
multiplierShow: 1,
providerName: '',
iconUrl: '',
isPremium: false,
};
}
else {
modelForm.value = { ...row };
}
modelDialogVisible.value = true;
}
// 保存模型
async function saveModel() {
try {
if (modelForm.value.id) {
await updateModel(modelForm.value as any);
ElMessage.success('更新成功');
}
else {
await createModel(modelForm.value as any);
ElMessage.success('创建成功');
}
modelDialogVisible.value = false;
fetchModelList();
}
catch (error: any) {
ElMessage.error(error.message || '保存失败');
}
}
// 删除模型
async function handleDeleteModel(row: AiModelDto) {
try {
await ElMessageBox.confirm('确定要删除该模型吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await deleteModel(row.id);
ElMessage.success('删除成功');
fetchModelList();
}
catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.message || '删除失败');
}
}
}
// 初始化
onMounted(() => {
fetchAppList();
});
</script>
<template>
<div class="channel-management">
<div class="channel-container">
<!-- 左侧应用列表 -->
<div class="app-list-panel">
<div class="panel-header">
<h3>应用列表</h3>
<el-button type="primary" size="small" :icon="Plus" @click="openAppDialog('create')">
新建
</el-button>
</div>
<el-scrollbar class="app-list-scrollbar">
<div v-loading="appLoading" class="app-list">
<div
v-for="app in appList"
:key="app.id"
class="app-item"
:class="{ active: selectedAppId === app.id }"
@click="handleSelectApp(app.id)"
>
<div class="app-item-content">
<div class="app-name">{{ app.name }}</div>
<div class="app-actions">
<el-button
link
type="primary"
size="small"
:icon="View"
@click.stop="handleViewAppDetail(app)"
>
详情
</el-button>
<el-button
link
type="primary"
size="small"
:icon="Edit"
@click.stop="openAppDialog('edit', app)"
>
编辑
</el-button>
<el-button
link
type="danger"
size="small"
:icon="Delete"
@click.stop="handleDeleteApp(app)"
>
删除
</el-button>
</div>
</div>
</div>
<el-empty v-if="!appLoading && appList.length === 0" description="暂无应用" />
</div>
</el-scrollbar>
</div>
<!-- 右侧模型列表 -->
<div class="model-list-panel">
<div class="panel-header">
<h3>模型列表</h3>
<div class="header-actions">
<el-input
v-model="modelSearchKey"
placeholder="搜索模型"
style="width: 200px; margin-right: 10px"
clearable
@keyup.enter="fetchModelList"
/>
<el-button type="primary" size="small" :icon="Plus" @click="openModelDialog('create')">
新建
</el-button>
<el-button size="small" :icon="Refresh" @click="fetchModelList">
刷新
</el-button>
</div>
</div>
<div v-if="!selectedAppId" class="empty-tip">
<el-empty description="请先选择左侧的应用" />
</div>
<el-table
v-else
v-loading="modelLoading"
:data="modelList"
border
stripe
height="calc(100vh - 220px)"
>
<el-table-column prop="name" label="模型名称" min-width="150" />
<el-table-column prop="modelId" label="模型ID" min-width="200" show-overflow-tooltip />
<el-table-column prop="handlerName" label="处理名" min-width="120" />
<el-table-column prop="providerName" label="供应商" width="100" />
<el-table-column label="是否尊享" width="100">
<template #default="{ row }">
<el-tag :type="row.isPremium ? 'warning' : 'info'">
{{ row.isPremium ? '尊享' : '普通' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="multiplierShow" label="显示倍率" width="100" />
<el-table-column prop="orderNum" label="排序" width="80" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :icon="Edit" @click="openModelDialog('edit', row)">
编辑
</el-button>
<el-button link type="danger" :icon="Delete" @click="handleDeleteModel(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 应用详情对话框 -->
<el-dialog v-model="appDetailDialogVisible" title="应用详情" width="600px">
<el-descriptions v-if="appDetailData" :column="1" border>
<el-descriptions-item label="应用名称">{{ appDetailData.name }}</el-descriptions-item>
<el-descriptions-item label="终结点">{{ appDetailData.endpoint }}</el-descriptions-item>
<el-descriptions-item label="额外URL">{{ appDetailData.extraUrl || '-' }}</el-descriptions-item>
<el-descriptions-item label="API Key">
<el-input :model-value="appDetailData.apiKey" type="textarea" readonly />
</el-descriptions-item>
<el-descriptions-item label="排序">{{ appDetailData.orderNum }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ appDetailData.creationTime }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
<!-- 应用编辑对话框 -->
<el-dialog v-model="appDialogVisible" :title="appDialogTitle" width="600px">
<el-form :model="appForm" label-width="120px">
<el-form-item label="应用名称" required>
<el-input v-model="appForm.name" placeholder="请输入应用名称" />
</el-form-item>
<el-form-item label="终结点" required>
<el-input v-model="appForm.endpoint" placeholder="请输入应用终结点URL" />
</el-form-item>
<el-form-item label="额外URL">
<el-input v-model="appForm.extraUrl" placeholder="请输入额外URL可选" />
</el-form-item>
<el-form-item label="API Key" required>
<el-input v-model="appForm.apiKey" type="textarea" placeholder="请输入API Key" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="appForm.orderNum" :min="0" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="appDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveApp">保存</el-button>
</template>
</el-dialog>
<!-- 模型编辑对话框 -->
<el-dialog v-model="modelDialogVisible" :title="modelDialogTitle" width="700px">
<el-form :model="modelForm" label-width="120px">
<el-form-item label="模型名称" required>
<el-input v-model="modelForm.name" placeholder="请输入模型名称" />
</el-form-item>
<el-form-item label="模型ID" required>
<el-input v-model="modelForm.modelId" placeholder="请输入模型ID" />
</el-form-item>
<el-form-item label="处理名" required>
<el-input v-model="modelForm.handlerName" placeholder="请输入处理名" />
</el-form-item>
<el-form-item label="供应商名称">
<el-input v-model="modelForm.providerName" placeholder="如OpenAI、Anthropic等" />
</el-form-item>
<el-form-item label="模型描述">
<el-input v-model="modelForm.description" type="textarea" placeholder="请输入模型描述" />
</el-form-item>
<el-form-item label="是否尊享模型">
<el-switch v-model="modelForm.isPremium" />
</el-form-item>
<el-form-item label="模型倍率">
<el-input-number v-model="modelForm.multiplier" :min="0.01" :step="0.1" />
</el-form-item>
<el-form-item label="显示倍率">
<el-input-number v-model="modelForm.multiplierShow" :min="0.01" :step="0.1" />
</el-form-item>
<el-form-item label="模型类型" required>
<el-select v-model="modelForm.modelType" placeholder="请选择模型类型">
<el-option label="聊天" :value="0" />
<el-option label="图片" :value="1" />
<el-option label="嵌入" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="API类型" required>
<el-select v-model="modelForm.modelApiType" placeholder="请选择API类型">
<el-option label="OpenAI" :value="0" />
<el-option label="Claude" :value="1" />
</el-select>
</el-form-item>
<el-form-item label="图标URL">
<el-input v-model="modelForm.iconUrl" placeholder="请输入模型图标URL" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="modelForm.orderNum" :min="0" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="modelDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveModel">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped lang="scss">
.channel-management {
padding: 20px;
height: calc(100vh - 40px);
.channel-container {
display: flex;
gap: 20px;
height: 100%;
}
.app-list-panel {
width: 350px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
.model-list-panel {
flex: 1;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
.panel-header {
padding: 16px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.header-actions {
display: flex;
gap: 10px;
}
}
.app-list-scrollbar {
flex: 1;
height: 0;
}
.app-list {
padding: 10px;
}
.app-item {
padding: 12px 16px;
margin-bottom: 8px;
border: 1px solid #e4e7ed;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #409eff;
background: #f0f9ff;
}
&.active {
border-color: #409eff;
background: #ecf5ff;
}
.app-item-content {
.app-name {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
color: #303133;
}
.app-actions {
display: flex;
gap: 8px;
}
}
}
.empty-tip {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>