🎉 初始化项目
This commit is contained in:
24
web/src/App.vue
Normal file
24
web/src/App.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<el-config-provider :locale="zhCn">
|
||||
<router-view />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
53
web/src/api/preset.js
Normal file
53
web/src/api/preset.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取预设列表
|
||||
export const getPresetList = (params) => {
|
||||
return request({
|
||||
url: '/app/preset/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 创建预设
|
||||
export const createPreset = (data) => {
|
||||
return request({
|
||||
url: '/app/preset',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新预设
|
||||
export const updatePreset = (data) => {
|
||||
return request({
|
||||
url: '/app/preset',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除预设
|
||||
export const deletePreset = (id) => {
|
||||
return request({
|
||||
url: `/app/preset/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 导入预设
|
||||
export const importPreset = (data) => {
|
||||
return request({
|
||||
url: '/app/preset/import',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 导出预设
|
||||
export const exportPreset = (id) => {
|
||||
return request({
|
||||
url: `/app/preset/${id}/export`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
53
web/src/api/provider.js
Normal file
53
web/src/api/provider.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取提供商列表
|
||||
export const getProviderList = () => {
|
||||
return request({
|
||||
url: '/app/provider/list',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 创建提供商
|
||||
export const createProvider = (data) => {
|
||||
return request({
|
||||
url: '/app/provider',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新提供商
|
||||
export const updateProvider = (data) => {
|
||||
return request({
|
||||
url: '/app/provider',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除提供商
|
||||
export const deleteProvider = (id) => {
|
||||
return request({
|
||||
url: `/app/provider/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 测试连接
|
||||
export const testConnection = (data) => {
|
||||
return request({
|
||||
url: '/app/provider/test',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 获取模型列表
|
||||
export const getModels = (data) => {
|
||||
return request({
|
||||
url: '/app/provider/models',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
53
web/src/api/user.js
Normal file
53
web/src/api/user.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 登录
|
||||
export const login = (data) => {
|
||||
return request({
|
||||
url: '/v1/system/user/login',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
export const getUserInfo = () => {
|
||||
return request({
|
||||
url: '/v1/system/user/info',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
export const getUserList = (params) => {
|
||||
return request({
|
||||
url: '/v1/system/user/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
export const createUser = (data) => {
|
||||
return request({
|
||||
url: '/v1/system/user',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
export const updateUser = (data) => {
|
||||
return request({
|
||||
url: '/v1/system/user',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
export const deleteUser = (id) => {
|
||||
return request({
|
||||
url: `/v1/system/user/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
163
web/src/layout/index.vue
Normal file
163
web/src/layout/index.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<el-container>
|
||||
<!-- 侧边栏 -->
|
||||
<el-aside width="200px">
|
||||
<div class="logo">
|
||||
<h2>AI 中转代理</h2>
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:router="true"
|
||||
background-color="#304156"
|
||||
text-color="#bfcbd9"
|
||||
active-text-color="#409EFF"
|
||||
>
|
||||
<template v-for="route in routes" :key="route.path">
|
||||
<el-sub-menu v-if="route.children && route.children.length > 1" :index="route.path">
|
||||
<template #title>
|
||||
<el-icon><component :is="route.meta.icon" /></el-icon>
|
||||
<span>{{ route.meta.title }}</span>
|
||||
</template>
|
||||
<el-menu-item
|
||||
v-for="child in route.children"
|
||||
:key="child.path"
|
||||
:index="route.path + '/' + child.path"
|
||||
>
|
||||
<el-icon><component :is="child.meta.icon" /></el-icon>
|
||||
<span>{{ child.meta.title }}</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-menu-item v-else :index="route.path">
|
||||
<el-icon><component :is="route.meta?.icon" /></el-icon>
|
||||
<span>{{ route.meta?.title }}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<el-container>
|
||||
<!-- 顶部导航 -->
|
||||
<el-header>
|
||||
<div class="header-content">
|
||||
<div class="breadcrumb">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
||||
<el-breadcrumb-item>{{ currentTitle }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<el-dropdown @command="handleCommand">
|
||||
<span class="el-dropdown-link">
|
||||
<el-icon><User /></el-icon>
|
||||
{{ userInfo.username || 'Admin' }}
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<!-- 内容区 -->
|
||||
<el-main>
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const userInfo = ref({
|
||||
username: localStorage.getItem('username') || 'Admin'
|
||||
})
|
||||
|
||||
const routes = computed(() => {
|
||||
return router.options.routes.filter(r => r.path !== '/login' && r.meta)
|
||||
})
|
||||
|
||||
const activeMenu = computed(() => route.path)
|
||||
const currentTitle = computed(() => route.meta.title || '')
|
||||
|
||||
const handleCommand = (command) => {
|
||||
if (command === 'logout') {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('username')
|
||||
ElMessage.success('退出成功')
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.el-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.el-aside {
|
||||
background-color: #304156;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #2b3a4b;
|
||||
}
|
||||
|
||||
.logo h2 {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.el-header {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0,21,41,.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-dropdown-link {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
background-color: #f0f2f5;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
21
web/src/main.js
Normal file
21
web/src/main.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
// 注册所有图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
|
||||
app.mount('#app')
|
||||
92
web/src/router/index.js
Normal file
92
web/src/router/index.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Layout from '@/layout/index.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/login/index.vue'),
|
||||
meta: { title: '登录' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/dashboard/index.vue'),
|
||||
meta: { title: '仪表盘', icon: 'DataLine' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/system',
|
||||
component: Layout,
|
||||
redirect: '/system/user',
|
||||
meta: { title: '系统管理', icon: 'Setting' },
|
||||
children: [
|
||||
{
|
||||
path: 'user',
|
||||
name: 'User',
|
||||
component: () => import('@/views/system/user/index.vue'),
|
||||
meta: { title: '用户管理', icon: 'User' }
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
name: 'Api',
|
||||
component: () => import('@/views/system/api/index.vue'),
|
||||
meta: { title: 'API管理', icon: 'Connection' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/ai',
|
||||
component: Layout,
|
||||
redirect: '/ai/preset',
|
||||
meta: { title: 'AI管理', icon: 'MagicStick' },
|
||||
children: [
|
||||
{
|
||||
path: 'preset',
|
||||
name: 'Preset',
|
||||
component: () => import('@/views/ai/preset/index.vue'),
|
||||
meta: { title: '预设管理', icon: 'Document' }
|
||||
},
|
||||
{
|
||||
path: 'provider',
|
||||
name: 'Provider',
|
||||
component: () => import('@/views/ai/provider/index.vue'),
|
||||
meta: { title: '提供商管理', icon: 'Platform' }
|
||||
},
|
||||
{
|
||||
path: 'binding',
|
||||
name: 'Binding',
|
||||
component: () => import('@/views/ai/binding/index.vue'),
|
||||
meta: { title: '预设绑定', icon: 'Link' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
if (to.path === '/login') {
|
||||
next()
|
||||
} else {
|
||||
if (!token) {
|
||||
next('/login')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
48
web/src/utils/request.js
Normal file
48
web/src/utils/request.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 30000
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
config => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`
|
||||
config.headers['x-token'] = token
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
response => {
|
||||
const res = response.data
|
||||
|
||||
if (res.code !== 0) {
|
||||
ElMessage.error(res.msg || '请求失败')
|
||||
|
||||
if (res.code === 401) {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/login'
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(res.msg || '请求失败'))
|
||||
}
|
||||
|
||||
return res.data
|
||||
},
|
||||
error => {
|
||||
ElMessage.error(error.message || '网络错误')
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default request
|
||||
240
web/src/views/ai/binding/index.vue
Normal file
240
web/src/views/ai/binding/index.vue
Normal file
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<div class="binding-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>预设绑定配置</span>
|
||||
<el-button type="primary" @click="handleCreate">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新建绑定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-alert
|
||||
title="说明"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px"
|
||||
>
|
||||
为不同的 AI 提供商配置不同的预设,实现灵活的 AI 调用策略
|
||||
</el-alert>
|
||||
|
||||
<el-table :data="bindingList" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column label="预设" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-tag>{{ row.presetName || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提供商" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="success">{{ row.providerName || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="priority" label="优先级" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="warning">{{ row.priority }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isActive ? 'success' : 'info'">
|
||||
{{ row.isActive ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 创建/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="500px"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<el-form-item label="预设" prop="presetId">
|
||||
<el-select v-model="form.presetId" placeholder="请选择预设" style="width: 100%">
|
||||
<el-option
|
||||
v-for="preset in presetOptions"
|
||||
:key="preset.id"
|
||||
:label="preset.name"
|
||||
:value="preset.id"
|
||||
>
|
||||
<span>{{ preset.name }}</span>
|
||||
<span v-if="preset.description" style="color: #8492a6; font-size: 12px; margin-left: 8px">
|
||||
- {{ preset.description }}
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="提供商" prop="providerId">
|
||||
<el-select v-model="form.providerId" placeholder="请选择提供商" style="width: 100%">
|
||||
<el-option
|
||||
v-for="provider in providerOptions"
|
||||
:key="provider.id"
|
||||
:label="provider.name"
|
||||
:value="provider.id"
|
||||
>
|
||||
<span>{{ provider.name }}</span>
|
||||
<span style="color: #8492a6; font-size: 12px; margin-left: 8px">
|
||||
- {{ provider.model }}
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="优先级" prop="priority">
|
||||
<el-input-number v-model="form.priority" :min="0" :max="100" style="width: 100%" />
|
||||
<el-text size="small" type="info" style="margin-top: 5px">
|
||||
数字越小优先级越高,多个预设时按优先级顺序执行
|
||||
</el-text>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-switch v-model="form.isActive" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { getBindingList, createBinding, updateBinding, deleteBinding } from '@/api/binding'
|
||||
import { getPresetList } from '@/api/preset'
|
||||
import { getProviderList } from '@/api/provider'
|
||||
|
||||
const loading = ref(false)
|
||||
const bindingList = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新建绑定')
|
||||
const formRef = ref()
|
||||
|
||||
const presetOptions = ref([])
|
||||
const providerOptions = ref([])
|
||||
|
||||
const form = reactive({
|
||||
id: null,
|
||||
presetId: null,
|
||||
providerId: null,
|
||||
priority: 0,
|
||||
isActive: true
|
||||
})
|
||||
|
||||
const rules = {
|
||||
presetId: [{ required: true, message: '请选择预设', trigger: 'change' }],
|
||||
providerId: [{ required: true, message: '请选择提供商', trigger: 'change' }],
|
||||
priority: [{ required: true, message: '请输入优先级', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getBindingList({ page: 1, pageSize: 100 })
|
||||
bindingList.value = res.list || []
|
||||
} catch (error) {
|
||||
ElMessage.error('获取绑定列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadOptions = async () => {
|
||||
try {
|
||||
// 加载预设列表
|
||||
const presetRes = await getPresetList({ page: 1, pageSize: 100 })
|
||||
presetOptions.value = presetRes.list || []
|
||||
|
||||
// 加载提供商列表
|
||||
const providerRes = await getProviderList()
|
||||
providerOptions.value = providerRes || []
|
||||
} catch (error) {
|
||||
ElMessage.error('加载选项失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreate = async () => {
|
||||
dialogTitle.value = '新建绑定'
|
||||
Object.assign(form, {
|
||||
id: null,
|
||||
presetId: null,
|
||||
providerId: null,
|
||||
priority: 0,
|
||||
isActive: true
|
||||
})
|
||||
|
||||
await loadOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = async (row) => {
|
||||
dialogTitle.value = '编辑绑定'
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
presetId: row.presetId,
|
||||
providerId: row.providerId,
|
||||
priority: row.priority,
|
||||
isActive: row.isActive
|
||||
})
|
||||
|
||||
await loadOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value.validate()
|
||||
|
||||
try {
|
||||
if (form.id) {
|
||||
await updateBinding(form)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createBinding(form)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该绑定吗?', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await deleteBinding(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
274
web/src/views/ai/preset/index.vue
Normal file
274
web/src/views/ai/preset/index.vue
Normal file
@@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<div class="preset-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>AI 预设管理</span>
|
||||
<div>
|
||||
<el-button type="primary" @click="handleCreate">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新建预设
|
||||
</el-button>
|
||||
<el-button type="success" @click="handleImport">
|
||||
<el-icon><Upload /></el-icon>
|
||||
导入预设
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="presetList" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="预设名称" />
|
||||
<el-table-column prop="description" label="描述" show-overflow-tooltip />
|
||||
<el-table-column prop="temperature" label="温度" width="100" />
|
||||
<el-table-column prop="maxTokens" label="最大Token" width="120" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.isDefault" type="success">默认</el-tag>
|
||||
<el-tag v-else-if="row.isPublic" type="info">公开</el-tag>
|
||||
<el-tag v-else>私有</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="250" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="success" @click="handleExport(row)">导出</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="fetchData"
|
||||
@size-change="fetchData"
|
||||
style="margin-top: 20px; justify-content: flex-end"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 导入对话框 -->
|
||||
<el-dialog v-model="importDialogVisible" title="导入预设" width="600px">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".json"
|
||||
:on-change="handleFileChange"
|
||||
:on-exceed="handleExceed"
|
||||
drag
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将 JSON 文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
只能上传 .json 文件,支持 SillyTavern 预设格式
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
|
||||
<el-form v-if="importForm.name" :model="importForm" label-width="100px" style="margin-top: 20px">
|
||||
<el-form-item label="预设名称">
|
||||
<el-input v-model="importForm.name" placeholder="请输入预设名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="importForm.description" placeholder="预设描述(可选)" />
|
||||
</el-form-item>
|
||||
<el-alert
|
||||
title="预览"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 10px"
|
||||
>
|
||||
<div>提示词数量: {{ importForm.promptsCount }}</div>
|
||||
<div>正则脚本数量: {{ importForm.scriptsCount }}</div>
|
||||
</el-alert>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleCancelImport">取消</el-button>
|
||||
<el-button type="primary" @click="confirmImport" :disabled="!importForm.jsonData">确定导入</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getPresetList, deletePreset, importPreset, exportPreset } from '@/api/preset'
|
||||
|
||||
const loading = ref(false)
|
||||
const presetList = ref([])
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const importDialogVisible = ref(false)
|
||||
const uploadRef = ref()
|
||||
const importForm = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
jsonData: '',
|
||||
promptsCount: 0,
|
||||
scriptsCount: 0
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getPresetList(pagination.value)
|
||||
presetList.value = res.list
|
||||
pagination.value.total = res.total
|
||||
} catch (error) {
|
||||
ElMessage.error('获取预设列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
ElMessage.info('创建预设功能开发中')
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
ElMessage.info('编辑预设功能开发中')
|
||||
}
|
||||
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该预设吗?', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await deletePreset(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleImport = () => {
|
||||
importDialogVisible.value = true
|
||||
importForm.value = {
|
||||
name: '',
|
||||
description: '',
|
||||
jsonData: '',
|
||||
promptsCount: 0,
|
||||
scriptsCount: 0
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileChange = (file) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const jsonData = JSON.parse(e.target.result)
|
||||
|
||||
// 自动提取预设名称
|
||||
importForm.value.name = jsonData.name || file.name.replace('.json', '')
|
||||
importForm.value.description = jsonData.description || ''
|
||||
importForm.value.jsonData = e.target.result
|
||||
|
||||
// 统计提示词和脚本数量
|
||||
importForm.value.promptsCount = jsonData.prompts?.length || 0
|
||||
importForm.value.scriptsCount = jsonData.regex_scripts?.length || 0
|
||||
|
||||
ElMessage.success('文件读取成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('JSON 格式错误,请检查文件')
|
||||
importForm.value = {
|
||||
name: '',
|
||||
description: '',
|
||||
jsonData: '',
|
||||
promptsCount: 0,
|
||||
scriptsCount: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
reader.readAsText(file.raw)
|
||||
}
|
||||
|
||||
const handleExceed = () => {
|
||||
ElMessage.warning('只能上传一个文件')
|
||||
}
|
||||
|
||||
const handleCancelImport = () => {
|
||||
importDialogVisible.value = false
|
||||
uploadRef.value?.clearFiles()
|
||||
}
|
||||
|
||||
const confirmImport = async () => {
|
||||
if (!importForm.value.name) {
|
||||
ElMessage.warning('请输入预设名称')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(importForm.value.jsonData)
|
||||
await importPreset({
|
||||
name: importForm.value.name,
|
||||
data
|
||||
})
|
||||
ElMessage.success('导入成功')
|
||||
importDialogVisible.value = false
|
||||
uploadRef.value?.clearFiles()
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
ElMessage.error('导入失败: ' + (error.message || '请检查 JSON 格式'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleExport = async (row) => {
|
||||
try {
|
||||
const data = await exportPreset(row.id)
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${row.name}.json`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
ElMessage.success('导出成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('导出失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-icon--upload {
|
||||
font-size: 67px;
|
||||
color: #409eff;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.el-upload__text {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.el-upload__text em {
|
||||
color: #409eff;
|
||||
font-style: normal;
|
||||
}
|
||||
</style>
|
||||
317
web/src/views/ai/provider/index.vue
Normal file
317
web/src/views/ai/provider/index.vue
Normal file
@@ -0,0 +1,317 @@
|
||||
<template>
|
||||
<div class="provider-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>AI 提供商管理</span>
|
||||
<el-button type="primary" @click="handleCreate">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新建提供商
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="providerList" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="名称" width="150" />
|
||||
<el-table-column prop="baseUrl" label="API地址" show-overflow-tooltip />
|
||||
<el-table-column prop="model" label="模型" width="150" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isActive ? 'success' : 'info'">
|
||||
{{ row.isActive ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="success" @click="handleTest(row)">测试</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 创建/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="600px"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="如: OpenAI" />
|
||||
</el-form-item>
|
||||
<el-form-item label="提供商类型" prop="type">
|
||||
<el-select v-model="form.type" placeholder="请选择类型" style="width: 100%">
|
||||
<el-option label="OpenAI" value="openai" />
|
||||
<el-option label="Claude" value="claude" />
|
||||
<el-option label="Gemini" value="gemini" />
|
||||
<el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="API地址" prop="baseUrl">
|
||||
<el-input v-model="form.baseUrl" placeholder="https://api.openai.com" />
|
||||
</el-form-item>
|
||||
<el-form-item label="API端点" prop="endpoint">
|
||||
<el-input v-model="form.endpoint" placeholder="/v1/chat/completions" />
|
||||
</el-form-item>
|
||||
<el-form-item label="上游API密钥" prop="upstreamKey">
|
||||
<el-input v-model="form.upstreamKey" type="password" show-password placeholder="sk-..." />
|
||||
</el-form-item>
|
||||
<el-form-item label="代理访问密钥" prop="proxyKey">
|
||||
<el-input v-model="form.proxyKey" placeholder="用户访问本代理时使用的密钥" />
|
||||
<el-button size="small" @click="generateProxyKey" style="margin-top: 5px">生成随机密钥</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="默认模型" prop="model">
|
||||
<div style="display: flex; gap: 10px; align-items: flex-start; flex-direction: column; width: 100%">
|
||||
<div style="display: flex; gap: 10px; width: 100%">
|
||||
<el-select
|
||||
v-if="availableModels.length > 0"
|
||||
v-model="form.model"
|
||||
placeholder="选择模型"
|
||||
style="flex: 1"
|
||||
filterable
|
||||
allow-create
|
||||
>
|
||||
<el-option
|
||||
v-for="model in availableModels"
|
||||
:key="model.id"
|
||||
:label="model.name || model.id"
|
||||
:value="model.id"
|
||||
>
|
||||
<span>{{ model.name || model.id }}</span>
|
||||
<span v-if="model.ownedBy" style="color: #8492a6; font-size: 12px; margin-left: 8px">
|
||||
({{ model.ownedBy }})
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="form.model"
|
||||
placeholder="gpt-4"
|
||||
style="flex: 1"
|
||||
/>
|
||||
<el-button @click="handleGetModels" :loading="modelsLoading">
|
||||
{{ availableModels.length > 0 ? '刷新模型' : '获取可用模型' }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-text v-if="availableModels.length > 0" size="small" type="info">
|
||||
已获取 {{ availableModels.length }} 个可用模型,可选择或手动输入
|
||||
</el-text>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-switch v-model="form.isActive" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getProviderList, createProvider, updateProvider, deleteProvider, testConnection, getModels } from '@/api/provider'
|
||||
|
||||
const loading = ref(false)
|
||||
const providerList = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新建提供商')
|
||||
const formRef = ref()
|
||||
|
||||
const form = reactive({
|
||||
id: null,
|
||||
name: '',
|
||||
type: '',
|
||||
baseUrl: '',
|
||||
endpoint: '',
|
||||
upstreamKey: '',
|
||||
proxyKey: '',
|
||||
model: '',
|
||||
isActive: true
|
||||
})
|
||||
|
||||
const modelsLoading = ref(false)
|
||||
const availableModels = ref([])
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择提供商类型', trigger: 'change' }],
|
||||
baseUrl: [{ required: true, message: '请输入API地址', trigger: 'blur' }],
|
||||
upstreamKey: [{ required: true, message: '请输入上游API密钥', trigger: 'blur' }],
|
||||
proxyKey: [{ required: true, message: '请输入代理访问密钥', trigger: ['blur', 'change'] }],
|
||||
model: [{ required: true, message: '请选择或输入模型', trigger: ['blur', 'change'] }]
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getProviderList()
|
||||
providerList.value = res
|
||||
} catch (error) {
|
||||
ElMessage.error('获取提供商列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
dialogTitle.value = '新建提供商'
|
||||
Object.assign(form, {
|
||||
id: null,
|
||||
name: '',
|
||||
type: '',
|
||||
baseUrl: '',
|
||||
endpoint: '',
|
||||
upstreamKey: '',
|
||||
proxyKey: '',
|
||||
model: '',
|
||||
isActive: true
|
||||
})
|
||||
availableModels.value = []
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const generateProxyKey = () => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
let key = 'pk-'
|
||||
for (let i = 0; i < 32; i++) {
|
||||
key += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
form.proxyKey = key
|
||||
// 手动触发验证
|
||||
formRef.value?.validateField('proxyKey')
|
||||
}
|
||||
|
||||
const handleGetModels = async () => {
|
||||
if (!form.baseUrl || !form.upstreamKey || !form.type) {
|
||||
ElMessage.warning('请先填写API地址、上游密钥和提供商类型')
|
||||
return
|
||||
}
|
||||
|
||||
modelsLoading.value = true
|
||||
try {
|
||||
const res = await testConnection({
|
||||
baseUrl: form.baseUrl,
|
||||
upstreamKey: form.upstreamKey,
|
||||
type: form.type
|
||||
})
|
||||
|
||||
if (res.success) {
|
||||
ElMessage.success(`连接成功!延迟: ${res.latency}ms`)
|
||||
// 获取模型列表
|
||||
const models = await getModels({
|
||||
baseUrl: form.baseUrl,
|
||||
upstreamKey: form.upstreamKey,
|
||||
type: form.type
|
||||
})
|
||||
|
||||
if (models && models.length > 0) {
|
||||
availableModels.value = models
|
||||
// 自动选择第一个模型
|
||||
form.model = models[0].id
|
||||
ElMessage.success(`获取到 ${models.length} 个可用模型`)
|
||||
} else {
|
||||
ElMessage.warning('未找到可用模型')
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(`连接失败: ${res.message}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('获取模型失败')
|
||||
} finally {
|
||||
modelsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
dialogTitle.value = '编辑提供商'
|
||||
Object.assign(form, row)
|
||||
availableModels.value = []
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleTest = async (row) => {
|
||||
if (!row.baseUrl || !row.upstreamKey || !row.type) {
|
||||
ElMessage.warning('提供商配置不完整,无法测试')
|
||||
return
|
||||
}
|
||||
|
||||
const loading = ElMessage({
|
||||
message: '正在测试连接...',
|
||||
type: 'info',
|
||||
duration: 0
|
||||
})
|
||||
|
||||
try {
|
||||
const res = await testConnection({
|
||||
baseUrl: row.baseUrl,
|
||||
upstreamKey: row.upstreamKey,
|
||||
type: row.type
|
||||
})
|
||||
|
||||
loading.close()
|
||||
|
||||
if (res.success) {
|
||||
ElMessage.success(`连接成功!延迟: ${res.latency}ms`)
|
||||
} else {
|
||||
ElMessage.error(`连接失败: ${res.message}`)
|
||||
}
|
||||
} catch (error) {
|
||||
loading.close()
|
||||
ElMessage.error('测试失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value.validate()
|
||||
|
||||
try {
|
||||
if (form.id) {
|
||||
await updateProvider(form)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createProvider(form)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该提供商吗?', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await deleteProvider(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
96
web/src/views/dashboard/index.vue
Normal file
96
web/src/views/dashboard/index.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-card>
|
||||
<el-statistic title="总用户数" :value="stats.totalUsers">
|
||||
<template #prefix>
|
||||
<el-icon><User /></el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card>
|
||||
<el-statistic title="AI预设数" :value="stats.totalPresets">
|
||||
<template #prefix>
|
||||
<el-icon><Document /></el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card>
|
||||
<el-statistic title="AI提供商" :value="stats.totalProviders">
|
||||
<template #prefix>
|
||||
<el-icon><Platform /></el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card>
|
||||
<el-statistic title="今日请求" :value="stats.todayRequests">
|
||||
<template #prefix>
|
||||
<el-icon><DataLine /></el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 20px">
|
||||
<el-col :span="24">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>快速开始</span>
|
||||
</template>
|
||||
<el-space wrap>
|
||||
<el-button type="primary" @click="$router.push('/ai/preset')">
|
||||
<el-icon><Document /></el-icon>
|
||||
管理预设
|
||||
</el-button>
|
||||
<el-button type="success" @click="$router.push('/ai/provider')">
|
||||
<el-icon><Platform /></el-icon>
|
||||
配置提供商
|
||||
</el-button>
|
||||
<el-button type="warning" @click="$router.push('/ai/binding')">
|
||||
<el-icon><Link /></el-icon>
|
||||
绑定配置
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const stats = ref({
|
||||
totalUsers: 0,
|
||||
totalPresets: 0,
|
||||
totalProviders: 0,
|
||||
todayRequests: 0
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// TODO: 从API获取统计数据
|
||||
stats.value = {
|
||||
totalUsers: 10,
|
||||
totalPresets: 5,
|
||||
totalProviders: 3,
|
||||
todayRequests: 128
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
106
web/src/views/login/index.vue
Normal file
106
web/src/views/login/index.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<el-card class="login-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>AI 中转代理管理后台</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :model="loginForm" :rules="rules" ref="loginFormRef">
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="用户名"
|
||||
prefix-icon="User"
|
||||
size="large"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
prefix-icon="Lock"
|
||||
size="large"
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
style="width: 100%"
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { login } from '@/api/user'
|
||||
|
||||
const router = useRouter()
|
||||
const loginFormRef = ref()
|
||||
const loading = ref(false)
|
||||
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
await loginFormRef.value.validate()
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await login(loginForm)
|
||||
localStorage.setItem('token', res.token)
|
||||
localStorage.setItem('username', loginForm.username)
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/')
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '登录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
}
|
||||
</style>
|
||||
81
web/src/views/system/api/index.vue
Normal file
81
web/src/views/system/api/index.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="api-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>API 管理</span>
|
||||
</template>
|
||||
|
||||
<el-alert
|
||||
title="API 密钥"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px"
|
||||
>
|
||||
<p>您的 API 密钥: <el-tag>{{ apiKey }}</el-tag></p>
|
||||
<el-button size="small" type="primary" @click="regenerateKey" style="margin-top: 10px">
|
||||
重新生成密钥
|
||||
</el-button>
|
||||
</el-alert>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h3>API 文档</h3>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="聊天补全">
|
||||
<code>POST /v1/chat/completions</code>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="预设列表">
|
||||
<code>GET /app/preset/list</code>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="提供商列表">
|
||||
<code>GET /app/provider/list</code>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h3>使用示例</h3>
|
||||
<el-code-block>
|
||||
<pre>
|
||||
curl -X POST http://localhost:8080/v1/chat/completions \
|
||||
-H "Authorization: Bearer YOUR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"messages": [{"role": "user", "content": "你好"}],
|
||||
"presetId": 1
|
||||
}'
|
||||
</pre>
|
||||
</el-code-block>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const apiKey = ref('ak-xxxxxxxxxxxxxxxx')
|
||||
|
||||
const regenerateKey = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要重新生成 API 密钥吗?旧密钥将失效', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
// TODO: 调用 API 重新生成密钥
|
||||
ElMessage.success('密钥已重新生成')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.api-container pre {
|
||||
background-color: #f5f7fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
87
web/src/views/system/user/index.vue
Normal file
87
web/src/views/system/user/index.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="user-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>用户管理</span>
|
||||
<el-button type="primary" @click="handleCreate">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新建用户
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="userList" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column prop="email" label="邮箱" />
|
||||
<el-table-column prop="role" label="角色" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.role === 'admin' ? 'danger' : 'info'">
|
||||
{{ row.role === 'admin' ? '管理员' : '普通用户' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'active' ? 'success' : 'info'">
|
||||
{{ row.status === 'active' ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getUserList } from '@/api/user'
|
||||
|
||||
const loading = ref(false)
|
||||
const userList = ref([])
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getUserList()
|
||||
userList.value = res.list || []
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
ElMessage.info('新建用户功能开发中')
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
ElMessage.info('编辑用户功能开发中')
|
||||
}
|
||||
|
||||
const handleDelete = (row) => {
|
||||
ElMessage.info('删除用户功能开发中')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user