feat: webUI2.0 前端介面更新
1. 剩余登陆注册未完成 2. 剩余插件列表&市场未完成
@@ -1,4 +0,0 @@
|
|||||||
> 1%
|
|
||||||
last 2 versions
|
|
||||||
not dead
|
|
||||||
not ie 11
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
[*.{js,jsx,ts,tsx,vue}]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
insert_final_newline = true
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'plugin:vue/vue3-essential',
|
|
||||||
'eslint:recommended',
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
.DS_Store
|
|
||||||
node_modules
|
|
||||||
/dist
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
|
|
||||||
# Log files
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.idea
|
|
||||||
.vscode
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# WebUI
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>LangBot 面板</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"allowJs": true,
|
|
||||||
"target": "es5",
|
|
||||||
"module": "esnext",
|
|
||||||
"baseUrl": "./",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"paths": {
|
|
||||||
"@/*": [
|
|
||||||
"src/*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"lib": [
|
|
||||||
"esnext",
|
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"scripthost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "web",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"lint": "eslint . --fix --ignore-path .gitignore"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@koumoul/vjsf": "^3.0.0-beta.46",
|
|
||||||
"@mdi/font": "7.4.47",
|
|
||||||
"ajv": "^8.17.1",
|
|
||||||
"ajv-dist": "^8.17.1",
|
|
||||||
"ajv-errors": "^3.0.0",
|
|
||||||
"ajv-formats": "^3.0.1",
|
|
||||||
"ajv-i18n": "^4.2.0",
|
|
||||||
"ansi_up": "^6.0.2",
|
|
||||||
"axios": "^1.7.7",
|
|
||||||
"codemirror": "^5.65.18",
|
|
||||||
"core-js": "^3.37.1",
|
|
||||||
"json-editor-vue": "^0.17.3",
|
|
||||||
"roboto-fontface": "*",
|
|
||||||
"vue": "^3.4.31",
|
|
||||||
"vuedraggable": "^4.1.0",
|
|
||||||
"vuetify": "^3.6.11",
|
|
||||||
"vuex": "^4.0.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
|
||||||
"eslint": "^8.57.0",
|
|
||||||
"eslint-config-standard": "^17.1.0",
|
|
||||||
"eslint-plugin-import": "^2.29.1",
|
|
||||||
"eslint-plugin-n": "^16.6.2",
|
|
||||||
"eslint-plugin-node": "^11.1.0",
|
|
||||||
"eslint-plugin-promise": "^6.4.0",
|
|
||||||
"eslint-plugin-vue": "^9.27.0",
|
|
||||||
"sass": "1.77.6",
|
|
||||||
"unplugin-fonts": "^1.1.1",
|
|
||||||
"unplugin-vue-components": "^0.27.2",
|
|
||||||
"unplugin-vue-router": "^0.10.0",
|
|
||||||
"vite": "^5.3.3",
|
|
||||||
"vite-plugin-vuetify": "^2.0.3",
|
|
||||||
"vue-router": "^4.4.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,329 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-app>
|
|
||||||
<v-snackbar v-model="snackbar" :color="color" :timeout="timeout" :location="location">
|
|
||||||
{{ text }}
|
|
||||||
</v-snackbar>
|
|
||||||
|
|
||||||
<div id="app-container" v-if="proxy.$store.state.user.tokenChecked && proxy.$store.state.user.tokenValid">
|
|
||||||
<v-layout>
|
|
||||||
<v-navigation-drawer id="navigation-drawer" :width="160" app permanent rail>
|
|
||||||
<v-list-item id="logo-list-item">
|
|
||||||
<template v-slot:prepend>
|
|
||||||
<div id="logo-container">
|
|
||||||
<v-img id="logo-img" src="@/assets/langbot-logo-block.png" height="32" width="32"></v-img>
|
|
||||||
|
|
||||||
<div id="version-chip"
|
|
||||||
v-tooltip="proxy.$store.state.version + (proxy.$store.state.debug ? ' (调试模式已启用)' : '')"
|
|
||||||
:style="{ 'background-color': proxy.$store.state.debug ? '#27aa27' : '#1e9ae2' }">
|
|
||||||
{{ proxy.$store.state.version }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</v-list-item>
|
|
||||||
<v-divider></v-divider>
|
|
||||||
<v-list density="compact" nav>
|
|
||||||
<v-list-item to="/" title="仪表盘" value="dashboard" prepend-icon="mdi-speedometer" v-tooltip="仪表盘">
|
|
||||||
</v-list-item>
|
|
||||||
<v-list-item to="/settings" title="设置" value="settings" prepend-icon="mdi-toggle-switch-outline"
|
|
||||||
v-tooltip="设置">
|
|
||||||
</v-list-item>
|
|
||||||
<v-list-item to="/logs" title="日志" value="logs" prepend-icon="mdi-file-outline" v-tooltip="日志">
|
|
||||||
</v-list-item>
|
|
||||||
<v-list-item to="/plugins" title="插件" value="plugins" prepend-icon="mdi-puzzle-outline" v-tooltip="插件">
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
<template v-slot:append>
|
|
||||||
<div>
|
|
||||||
<v-list density="compact" nav>
|
|
||||||
<v-dialog max-width="500" persistent v-model="taskDialogShow">
|
|
||||||
<template v-slot:activator="{ props: activatorProps }">
|
|
||||||
<v-list-item id="system-tasks-list-item" title="任务列表" prepend-icon="mdi-align-horizontal-left"
|
|
||||||
v-tooltip="任务列表" v-bind="activatorProps">
|
|
||||||
</v-list-item>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:default="{ isActive }">
|
|
||||||
<TaskDialog :dialog="{ show: isActive }" @close="closeTaskDialog" />
|
|
||||||
</template>
|
|
||||||
</v-dialog>
|
|
||||||
|
|
||||||
<v-list-item id="about-list-item" title="系统信息" prepend-icon="mdi-cog-outline" v-tooltip="系统信息">
|
|
||||||
<v-menu activator="parent" :close-on-content-click="false" location="end">
|
|
||||||
<v-list>
|
|
||||||
<v-list-item @click="showAboutDialog">
|
|
||||||
<v-list-item-title>
|
|
||||||
关于 LangBot
|
|
||||||
|
|
||||||
<v-dialog max-width="400" persistent v-model="aboutDialogShow">
|
|
||||||
<template v-slot:default="{ isActive }">
|
|
||||||
<AboutDialog :dialog="{ show: isActive }" @close="closeAboutDialog" />
|
|
||||||
</template>
|
|
||||||
</v-dialog>
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
|
|
||||||
<v-list-item @click="openDocs">
|
|
||||||
<v-list-item-title>
|
|
||||||
查看文档
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
|
|
||||||
<v-list-item @click="reload('platform')">
|
|
||||||
<v-list-item-title>
|
|
||||||
重载消息平台
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
|
|
||||||
<v-list-item @click="reload('plugin')">
|
|
||||||
<v-list-item-title>
|
|
||||||
重载插件
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
|
|
||||||
<v-list-item @click="reload('provider')">
|
|
||||||
<v-list-item-title>
|
|
||||||
重载 LLM 管理器
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</v-navigation-drawer>
|
|
||||||
|
|
||||||
<v-main style="background-color: #f6f6f6;">
|
|
||||||
<router-view />
|
|
||||||
</v-main>
|
|
||||||
</v-layout>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="loading-container" v-if="!proxy.$store.state.user.tokenChecked">
|
|
||||||
<div id="loading-text">
|
|
||||||
<img src="@/assets/langbot-logo.png" height="32" width="32" />
|
|
||||||
<span id="loading-text-span">正在加载...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="login-container" v-if="proxy.$store.state.user.tokenChecked && !proxy.$store.state.user.tokenValid && proxy.$store.state.user.systemInitialized">
|
|
||||||
<LoginDialog @error="error" @success="success" @checkToken="checkToken" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="uninitialized-container" v-if="proxy.$store.state.user.tokenChecked && !proxy.$store.state.user.systemInitialized">
|
|
||||||
<InitDialog @error="error" @success="success" @checkSystemInitialized="checkSystemInitialized" />
|
|
||||||
</div>
|
|
||||||
</v-app>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { getCurrentInstance } from 'vue'
|
|
||||||
import { provide, ref, watch, onMounted } from 'vue';
|
|
||||||
|
|
||||||
import TaskDialog from '@/components/TaskListDialog.vue'
|
|
||||||
import AboutDialog from '@/components/AboutDialog.vue'
|
|
||||||
import InitDialog from '@/components/InitDialog.vue'
|
|
||||||
import LoginDialog from '@/components/LoginDialog.vue'
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
|
||||||
|
|
||||||
const snackbar = ref(false);
|
|
||||||
const color = ref('success');
|
|
||||||
const text = ref('');
|
|
||||||
const location = ref('top');
|
|
||||||
const timeout = ref(2000);
|
|
||||||
|
|
||||||
function success(content) {
|
|
||||||
snackbar.value = true;
|
|
||||||
color.value = 'success';
|
|
||||||
text.value = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
function error(content) {
|
|
||||||
snackbar.value = true;
|
|
||||||
color.value = 'error';
|
|
||||||
text.value = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
function warning(content) {
|
|
||||||
snackbar.value = true;
|
|
||||||
color.value = 'warning';
|
|
||||||
text.value = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
function info(content) {
|
|
||||||
snackbar.value = true;
|
|
||||||
color.value = 'primary';
|
|
||||||
text.value = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
const taskDialogShow = ref(false)
|
|
||||||
|
|
||||||
function showTaskDialog() {
|
|
||||||
taskDialogShow.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeTaskDialog() {
|
|
||||||
taskDialogShow.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
provide('snackbar', { success, error, warning, info, location, timeout });
|
|
||||||
|
|
||||||
function openDocs() {
|
|
||||||
window.open('https://docs.langbot.app', '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
const reloadScopeLabel = {
|
|
||||||
'platform': "消息平台",
|
|
||||||
'plugin': "插件",
|
|
||||||
'provider': "LLM 管理器"
|
|
||||||
}
|
|
||||||
|
|
||||||
function reload(scope) {
|
|
||||||
let label = reloadScopeLabel[scope]
|
|
||||||
proxy.$axios.post('/system/reload',
|
|
||||||
{ scope: scope },
|
|
||||||
{ headers: { 'Content-Type': 'application/json' } }
|
|
||||||
).then(response => {
|
|
||||||
if (response.data.code === 0) {
|
|
||||||
success(label + '已重载')
|
|
||||||
|
|
||||||
// 关闭菜单
|
|
||||||
} else {
|
|
||||||
error(label + '重载失败:' + response.data.message)
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
error(label + '重载失败:' + err)
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const aboutDialogShow = ref(false)
|
|
||||||
|
|
||||||
function showAboutDialog() {
|
|
||||||
aboutDialogShow.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeAboutDialog() {
|
|
||||||
aboutDialogShow.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkSystemInitialized() {
|
|
||||||
proxy.$axios.get('/user/init').then(response => {
|
|
||||||
if (response.data.code === 0) {
|
|
||||||
proxy.$store.state.user.systemInitialized = response.data.data.initialized
|
|
||||||
} else {
|
|
||||||
error('系统初始化状态检查失败:' + response.data.message)
|
|
||||||
proxy.$store.state.user.systemInitialized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
checkToken()
|
|
||||||
}).catch(err => {
|
|
||||||
error('系统初始化状态检查失败:' + err)
|
|
||||||
proxy.$store.state.user.systemInitialized = true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkToken() {
|
|
||||||
proxy.$axios.get('/user/check-token').then(response => {
|
|
||||||
if (response.data.code === 0) {
|
|
||||||
proxy.$store.state.user.tokenValid = true
|
|
||||||
} else {
|
|
||||||
proxy.$store.state.user.tokenValid = false
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
proxy.$store.state.user.tokenValid = false
|
|
||||||
}).finally(() => {
|
|
||||||
proxy.$store.state.user.tokenChecked = true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
checkSystemInitialized()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
#app-container {
|
|
||||||
display: flex;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loading-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100vh;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loading-text {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loading-text-span {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
padding-top: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#navigation-drawer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
#logo-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-left: -0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#logo-list-item {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#version-chip {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
margin-top: 2.4rem;
|
|
||||||
margin-right: 0.3rem;
|
|
||||||
font-size: 0.55rem;
|
|
||||||
font-weight: 500;
|
|
||||||
padding-inline: 0.4rem;
|
|
||||||
text-align: center;
|
|
||||||
background-color: #c79a47;
|
|
||||||
color: white;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
border: 1px solid #fff;
|
|
||||||
box-shadow: 0 0 0.1rem 0 rgba(0, 0, 0, 0.5);
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#about-list-item {
|
|
||||||
justify-self: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
#about-list-item:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
#about-list-item:active {
|
|
||||||
background-color: #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
#system-tasks-list-item:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
#system-tasks-list-item:active {
|
|
||||||
background-color: #ddd;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 24 KiB |
@@ -1,90 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-card class="about-dialog">
|
|
||||||
|
|
||||||
<div id="content-container">
|
|
||||||
<div id="logo-container">
|
|
||||||
<v-img id="logo-img" src="@/assets/langbot-logo.png" width="64" />
|
|
||||||
|
|
||||||
<div id="text-container">
|
|
||||||
<div id="title">
|
|
||||||
LangBot
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="subtitle">
|
|
||||||
版本 {{ proxy.$store.state.version }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="copyright">
|
|
||||||
版权所有 © 2024 RockChinQ
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-slot:actions>
|
|
||||||
<v-btn id="open-source-btn" text="代码仓库" prepend-icon="mdi-github"
|
|
||||||
@click="openSource"></v-btn>
|
|
||||||
<v-btn class="ml-auto" text="关闭" prepend-icon="mdi-close" @click="close"></v-btn>
|
|
||||||
</template>
|
|
||||||
</v-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { getCurrentInstance } from 'vue'
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
|
||||||
|
|
||||||
const emit = defineEmits(['close'])
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
emit('close')
|
|
||||||
}
|
|
||||||
|
|
||||||
const openSource = () => {
|
|
||||||
window.open('https://github.com/RockChinQ/LangBot', '_blank')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.about-dialog {
|
|
||||||
padding-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#logo-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#logo-img {}
|
|
||||||
|
|
||||||
#text-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#title {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#subtitle {
|
|
||||||
font-size: 0.6rem;
|
|
||||||
color: #3c3c3c;
|
|
||||||
margin-top: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#copyright {
|
|
||||||
font-size: 0.6rem;
|
|
||||||
color: #3c3c3c;
|
|
||||||
margin-top: 0.2rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-dialog v-model="dialog" width="500" persistent>
|
|
||||||
<v-card id="init-dialog">
|
|
||||||
<v-card-title class="d-flex align-center" style="gap: 0.5rem;">
|
|
||||||
<img src="@/assets/langbot-logo.png" height="32" width="32" />
|
|
||||||
<span>系统初始化</span>
|
|
||||||
</v-card-title>
|
|
||||||
|
|
||||||
<v-card-text>
|
|
||||||
<p>请输入初始管理员邮箱和密码。</p>
|
|
||||||
</v-card-text>
|
|
||||||
|
|
||||||
<v-card-text class="d-flex flex-column" style="gap: 0.5rem;">
|
|
||||||
<v-text-field v-model="user" variant="outlined" label="管理员邮箱" :rules="[rules.required, rules.email]"
|
|
||||||
clearable />
|
|
||||||
<v-text-field v-model="password" variant="outlined" label="管理员密码" :rules="[rules.required]"
|
|
||||||
type="password" clearable />
|
|
||||||
</v-card-text>
|
|
||||||
|
|
||||||
<v-card-actions>
|
|
||||||
<v-btn color="primary" variant="flat" @click="initialize" prepend-icon="mdi-check">初始化</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, inject, getCurrentInstance } from 'vue'
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
|
||||||
|
|
||||||
const emit = defineEmits(['error', 'success', 'checkSystemInitialized'])
|
|
||||||
|
|
||||||
const dialog = ref(true)
|
|
||||||
|
|
||||||
const user = ref('')
|
|
||||||
const password = ref('')
|
|
||||||
|
|
||||||
const snackbar = inject('snackbar')
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
required: value => !!value || '必填项',
|
|
||||||
email: value => {
|
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
||||||
return emailRegex.test(value) || '请输入有效的邮箱地址'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function checkEmailValid(email) {
|
|
||||||
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
||||||
return regex.test(email)
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialize = () => {
|
|
||||||
// 检查邮箱和密码是否为空
|
|
||||||
if (user.value == undefined || password.value == undefined) {
|
|
||||||
emit('error', '邮箱和密码不能为空')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.value == '' || password.value == '') {
|
|
||||||
emit('error', '邮箱和密码不能为空')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!checkEmailValid(user.value)) {
|
|
||||||
emit('error', '请输入有效的邮箱地址')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy.$axios.post('/user/init', {
|
|
||||||
user: user.value,
|
|
||||||
password: password.value
|
|
||||||
}).then(res => {
|
|
||||||
emit('success', '系统初始化成功')
|
|
||||||
|
|
||||||
emit('checkSystemInitialized')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
#init-dialog {
|
|
||||||
padding-top: 0.8rem;
|
|
||||||
padding-inline: 0.5rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-dialog v-model="dialog" width="350" persistent>
|
|
||||||
<v-card id="login-dialog">
|
|
||||||
<v-card-title class="d-flex align-center" style="gap: 0.5rem;">
|
|
||||||
<img src="@/assets/langbot-logo.png" height="32" width="32" />
|
|
||||||
<span>登录 LangBot</span>
|
|
||||||
</v-card-title>
|
|
||||||
|
|
||||||
<v-card-text class="d-flex flex-column" style="gap: 0.5rem;margin-bottom: -2rem;margin-top: 1rem;">
|
|
||||||
|
|
||||||
<v-text-field v-model="user" variant="outlined" label="邮箱" :rules="[rules.required, rules.email]"
|
|
||||||
clearable />
|
|
||||||
<v-text-field v-model="password" variant="outlined" label="密码" :rules="[rules.required]"
|
|
||||||
type="password" clearable />
|
|
||||||
</v-card-text>
|
|
||||||
|
|
||||||
<v-card-actions>
|
|
||||||
<v-btn color="primary" variant="flat" @click="login">登录</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, getCurrentInstance } from 'vue'
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
|
||||||
|
|
||||||
const emit = defineEmits(['error', 'success', 'checkToken'])
|
|
||||||
|
|
||||||
const dialog = ref(true)
|
|
||||||
|
|
||||||
const user = ref('')
|
|
||||||
const password = ref('')
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
required: value => !!value || '必填项',
|
|
||||||
email: value => {
|
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
||||||
return emailRegex.test(value) || '请输入有效的邮箱地址'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const login = () => {
|
|
||||||
proxy.$axios.post('/user/auth', {
|
|
||||||
user: user.value,
|
|
||||||
password: password.value
|
|
||||||
}).then(res => {
|
|
||||||
if (res.data.code == 0) {
|
|
||||||
emit('success', '登录成功')
|
|
||||||
localStorage.setItem('user-token', res.data.data.token)
|
|
||||||
setTimeout(() => {
|
|
||||||
location.reload()
|
|
||||||
}, 1000)
|
|
||||||
} else {
|
|
||||||
emit('error', res.data.msg)
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
if (err.response.data.msg) {
|
|
||||||
emit('error', err.response.data.msg)
|
|
||||||
} else {
|
|
||||||
emit('error', '登录失败')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
#login-dialog {
|
|
||||||
padding-top: 0.8rem;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
padding-inline: 0.5rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="plugin-card">
|
|
||||||
<div class="plugin-card-header">
|
|
||||||
<div class="plugin-id">
|
|
||||||
<div class="plugin-card-author">{{ plugin.author }} /</div>
|
|
||||||
<div class="plugin-card-title">{{ plugin.name }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="plugin-card-badges">
|
|
||||||
<v-icon class="plugin-github-source" icon="mdi-github" v-if="plugin.repository != ''"
|
|
||||||
@click="openGithubSource"></v-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="plugin-card-description" >{{ plugin.description }}</div>
|
|
||||||
|
|
||||||
<div class="plugin-card-brief-info">
|
|
||||||
<div class="plugin-card-brief-info-item">
|
|
||||||
<v-icon id="plugin-stars-icon" icon="mdi-star" />
|
|
||||||
<div id="plugin-stars-count">{{ plugin.stars }}</div>
|
|
||||||
</div>
|
|
||||||
<v-btn color="primary" @click="installPlugin" density="compact">安装</v-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
const props = defineProps({
|
|
||||||
plugin: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['install']);
|
|
||||||
|
|
||||||
const openGithubSource = () => {
|
|
||||||
window.open("https://"+props.plugin.repository, '_blank');
|
|
||||||
}
|
|
||||||
|
|
||||||
const installPlugin = () => {
|
|
||||||
emit('install', props.plugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.plugin-card {
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.8rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
margin: 1rem 0;
|
|
||||||
background-color: white;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: 10rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-author {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #666;
|
|
||||||
font-weight: 500;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-title {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-description {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: #666;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-top: -1rem;
|
|
||||||
height: 2rem;
|
|
||||||
/* 超出部分自动换行,最多两行 */
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow-y: hidden;
|
|
||||||
white-space: wrap;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-badges {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-github-source {
|
|
||||||
cursor: pointer;
|
|
||||||
color: #222;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-disabled {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 500;
|
|
||||||
height: 1.3rem;
|
|
||||||
padding-inline: 0.4rem;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.plugin-card-brief-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
/* background-color: #f0f0f0; */
|
|
||||||
gap: 0.8rem;
|
|
||||||
margin-left: -0.2rem;
|
|
||||||
margin-bottom: 0rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-events {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-events-icon {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-events-count {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-functions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-functions-icon {
|
|
||||||
font-size: 1.6rem;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-functions-count {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-brief-info-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#plugin-stars-icon {
|
|
||||||
color: #0073ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
#plugin-stars-count {
|
|
||||||
margin-top: 0.1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0073ff;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-brief-info-item:hover .plugin-card-brief-info-item-icon {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="marketplace-container">
|
|
||||||
<div id="marketplace-search-bar">
|
|
||||||
|
|
||||||
<span style="width: 14rem;">
|
|
||||||
<v-text-field id="marketplace-search-bar-search-input" variant="solo" v-model="proxy.$store.state.marketplaceParams.query" label="搜索"
|
|
||||||
density="compact" @update:model-value="updateSearch" />
|
|
||||||
</span>
|
|
||||||
<!--下拉选择排序-->
|
|
||||||
<span style="width: 10rem;">
|
|
||||||
<v-select id="marketplace-search-bar-sort-select" v-model="sort" :items="sortItems" variant="solo"
|
|
||||||
label="排序" density="compact" @update:model-value="updateSort" />
|
|
||||||
</span>
|
|
||||||
<span style="margin-left: 1rem;">
|
|
||||||
<div id="marketplace-search-bar-total-plugins-count">
|
|
||||||
共 {{ proxy.$store.state.marketplaceTotalPluginsCount }} 个插件
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
<span style="margin-left: 1rem;">
|
|
||||||
<!-- 分页 -->
|
|
||||||
<v-pagination style="width: 14rem;" v-model="proxy.$store.state.marketplaceParams.page"
|
|
||||||
:length="proxy.$store.state.marketplaceTotalPages" variant="solo" density="compact"
|
|
||||||
total-visible="4" @update:model-value="updatePage" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div id="marketplace-plugins-container" ref="pluginsContainer">
|
|
||||||
<div id="marketplace-plugins-container-inner">
|
|
||||||
<MarketPluginCard v-for="plugin in proxy.$store.state.marketplacePlugins" :key="plugin.id" :plugin="plugin" @install="installPlugin" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import MarketPluginCard from './MarketPluginCard.vue'
|
|
||||||
|
|
||||||
import { ref, getCurrentInstance, onMounted } from 'vue'
|
|
||||||
|
|
||||||
import { inject } from "vue";
|
|
||||||
|
|
||||||
const snackbar = inject('snackbar');
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
|
||||||
|
|
||||||
const pluginsContainer = ref(null)
|
|
||||||
|
|
||||||
const sortItems = ref([
|
|
||||||
'最近新增',
|
|
||||||
'最多星标',
|
|
||||||
'最近更新',
|
|
||||||
])
|
|
||||||
|
|
||||||
const sortParams = ref({
|
|
||||||
'最近新增': {
|
|
||||||
sort_by: 'created_at',
|
|
||||||
sort_order: 'DESC',
|
|
||||||
},
|
|
||||||
'最多星标': {
|
|
||||||
sort_by: 'stars',
|
|
||||||
sort_order: 'DESC',
|
|
||||||
},
|
|
||||||
'最近更新': {
|
|
||||||
sort_by: 'pushed_at',
|
|
||||||
sort_order: 'DESC',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const sort = ref(sortItems.value[0])
|
|
||||||
|
|
||||||
proxy.$store.state.marketplaceParams.sort_by = sortParams.value[sort.value].sort_by
|
|
||||||
proxy.$store.state.marketplaceParams.sort_order = sortParams.value[sort.value].sort_order
|
|
||||||
|
|
||||||
const updateSort = (value) => {
|
|
||||||
console.log(value)
|
|
||||||
proxy.$store.state.marketplaceParams.sort_by = sortParams.value[value].sort_by
|
|
||||||
proxy.$store.state.marketplaceParams.sort_order = sortParams.value[value].sort_order
|
|
||||||
proxy.$store.state.marketplaceParams.page = 1
|
|
||||||
|
|
||||||
console.log(proxy.$store.state.marketplaceParams)
|
|
||||||
fetchMarketplacePlugins()
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePage = (value) => {
|
|
||||||
proxy.$store.state.marketplaceParams.page = value
|
|
||||||
fetchMarketplacePlugins()
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateSearch = (value) => {
|
|
||||||
console.log(value)
|
|
||||||
proxy.$store.state.marketplaceParams.query = value
|
|
||||||
proxy.$store.state.marketplaceParams.page = 1
|
|
||||||
fetchMarketplacePlugins()
|
|
||||||
}
|
|
||||||
|
|
||||||
const calculatePluginsPerPage = () => {
|
|
||||||
if (!pluginsContainer.value) return 10
|
|
||||||
|
|
||||||
const containerWidth = pluginsContainer.value.clientWidth
|
|
||||||
const containerHeight = pluginsContainer.value.clientHeight
|
|
||||||
|
|
||||||
console.log(containerWidth, containerHeight)
|
|
||||||
|
|
||||||
// 每个卡片宽度18rem + gap 16px
|
|
||||||
const cardWidth = 18 * 16 + 16 // rem转px
|
|
||||||
// 每个卡片高度9rem + gap 16px
|
|
||||||
const cardHeight = 9 * 16 + 16
|
|
||||||
|
|
||||||
// 计算每行可以放几个卡片
|
|
||||||
const cardsPerRow = Math.floor(containerWidth / cardWidth)
|
|
||||||
// 计算每行可以放几行
|
|
||||||
const rows = Math.floor(containerHeight / cardHeight)
|
|
||||||
|
|
||||||
// 计算每页总数
|
|
||||||
const perPage = cardsPerRow * rows
|
|
||||||
|
|
||||||
proxy.$store.state.marketplaceParams.per_page = perPage > 0 ? perPage : 10
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchMarketplacePlugins = async () => {
|
|
||||||
calculatePluginsPerPage()
|
|
||||||
proxy.$axios.post('https://space.langbot.app/api/v1/market/plugins', {
|
|
||||||
query: proxy.$store.state.marketplaceParams.query,
|
|
||||||
sort_by: proxy.$store.state.marketplaceParams.sort_by,
|
|
||||||
sort_order: proxy.$store.state.marketplaceParams.sort_order,
|
|
||||||
page: proxy.$store.state.marketplaceParams.page,
|
|
||||||
page_size: proxy.$store.state.marketplaceParams.per_page,
|
|
||||||
}).then(response => {
|
|
||||||
console.log(response.data)
|
|
||||||
if (response.data.code != 0) {
|
|
||||||
snackbar.error(response.data.msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析出 name 和 author
|
|
||||||
response.data.data.plugins.forEach(plugin => {
|
|
||||||
plugin.name = plugin.repository.split('/')[2]
|
|
||||||
plugin.author = plugin.repository.split('/')[1]
|
|
||||||
})
|
|
||||||
|
|
||||||
proxy.$store.state.marketplacePlugins = response.data.data.plugins
|
|
||||||
proxy.$store.state.marketplaceTotalPluginsCount = response.data.data.total
|
|
||||||
|
|
||||||
let totalPages = Math.floor(response.data.data.total / proxy.$store.state.marketplaceParams.per_page)
|
|
||||||
if (response.data.data.total % proxy.$store.state.marketplaceParams.per_page != 0) {
|
|
||||||
totalPages += 1
|
|
||||||
}
|
|
||||||
proxy.$store.state.marketplaceTotalPages = totalPages
|
|
||||||
}).catch(error => {
|
|
||||||
snackbar.error(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
calculatePluginsPerPage()
|
|
||||||
fetchMarketplacePlugins()
|
|
||||||
|
|
||||||
// 监听窗口大小变化
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
calculatePluginsPerPage()
|
|
||||||
fetchMarketplacePlugins()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['installPlugin'])
|
|
||||||
|
|
||||||
const installPlugin = (plugin) => {
|
|
||||||
emit('installPlugin', plugin.repository)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
#marketplace-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#marketplace-search-bar {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
gap: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#marketplace-search-bar-search-input {
|
|
||||||
position: relative;
|
|
||||||
left: 1rem;
|
|
||||||
width: 10rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#marketplace-search-bar-total-plugins-count {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
color: #666;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card {
|
|
||||||
width: 18rem;
|
|
||||||
height: 9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#marketplace-plugins-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
margin-inline: 0rem;
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100vh - 16rem);
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#marketplace-plugins-container-inner {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="number-field-data">
|
|
||||||
<div class="number-field-data-item" :style="{ color: color }">
|
|
||||||
{{ number }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="number-field-data-item-title" :style="{ marginTop: link ? '0rem' : '0.1rem', fontSize: link ? '0.9rem' : '1.1rem', marginBottom: link ? '0rem' : '0.5rem' }">
|
|
||||||
{{ title }}
|
|
||||||
</div>
|
|
||||||
<button class="number-field-data-item-link-text" v-if="link" @click="linkClick">{{ linkText }}</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
const props = defineProps({
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
number: {
|
|
||||||
type: Number,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: String,
|
|
||||||
default: '#4271bf'
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
linkText: {
|
|
||||||
type: String,
|
|
||||||
default: '查看详情'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
import { getCurrentInstance } from 'vue'
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
|
||||||
|
|
||||||
const linkClick = (e) => {
|
|
||||||
proxy.$router.push(props.link)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
#number-field-data {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
/* justify-content: center; */
|
|
||||||
}
|
|
||||||
|
|
||||||
.number-field-data-item {
|
|
||||||
font-size: 2.2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.number-field-data-item-title {
|
|
||||||
font-weight: 600;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.number-field-data-item-link-text {
|
|
||||||
text-decoration: none;
|
|
||||||
appearance: none;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #4271bf;
|
|
||||||
font-size: 0.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span class="title-container">
|
|
||||||
<h2 id="page-title">{{ title }}</h2>
|
|
||||||
<v-icon @click="refresh" id="refresh-icon" icon="mdi-refresh" />
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
defineProps({
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['refresh'])
|
|
||||||
|
|
||||||
const refresh = () => {
|
|
||||||
emit('refresh')
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
#page-title {
|
|
||||||
font-weight: 500;
|
|
||||||
margin-left: 1.4rem;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
padding-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#refresh-icon {
|
|
||||||
padding-top: 0.1rem;
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
#refresh-icon:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
#refresh-icon:active {
|
|
||||||
color: #6c6c6c;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="plugin-card">
|
|
||||||
<div class="plugin-card-header">
|
|
||||||
<div class="plugin-id">
|
|
||||||
<div class="plugin-card-author">{{ plugin.author }} /</div>
|
|
||||||
<div class="plugin-card-title">{{ plugin.name }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="plugin-card-badges">
|
|
||||||
<v-icon class="plugin-github-source" icon="mdi-github" v-if="plugin.source != ''"
|
|
||||||
@click="openGithubSource"></v-icon>
|
|
||||||
<v-chip class="plugin-disabled" v-if="!plugin.enabled" color="error" variant="outlined"
|
|
||||||
density="compact">已禁用</v-chip>
|
|
||||||
<v-chip class="plugin-version" color="primary" density="compact" variant="flat">v{{ plugin.version
|
|
||||||
}}</v-chip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="plugin-card-description">{{ plugin.description }}</div>
|
|
||||||
|
|
||||||
<div class="plugin-card-brief-info">
|
|
||||||
<div class="plugin-card-brief-info-item">
|
|
||||||
<v-tooltip text="已注册的事件处理器" location="bottom">
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<div class="plugin-card-events" v-bind="props">
|
|
||||||
<v-icon class="plugin-card-events-icon" icon="mdi-link-box-variant-outline" />
|
|
||||||
<div class="plugin-card-events-count">{{ Object.keys(plugin.event_handlers).length }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</v-tooltip>
|
|
||||||
<v-tooltip text="已注册的内容函数" location="bottom">
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<div class="plugin-card-functions" v-bind="props">
|
|
||||||
<v-icon class="plugin-card-functions-icon" icon="mdi-tools" />
|
|
||||||
<div class="plugin-card-functions-count">{{ plugin.content_functions.length }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</v-tooltip>
|
|
||||||
</div>
|
|
||||||
<v-menu class="plugin-card-menu">
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<v-icon class="plugin-card-menu-btn" icon="mdi-cog" v-bind="props" variant="text" size="small"/>
|
|
||||||
</template>
|
|
||||||
<v-list>
|
|
||||||
<template v-for="item in menuItems" :key="item.title">
|
|
||||||
<v-list-item v-if="item.condition(plugin)" @click="item.action">
|
|
||||||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</template>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
const props = defineProps({
|
|
||||||
plugin: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['toggle', 'update', 'remove']);
|
|
||||||
|
|
||||||
const openGithubSource = () => {
|
|
||||||
window.open(props.plugin.source, '_blank');
|
|
||||||
}
|
|
||||||
|
|
||||||
const togglePlugin = () => {
|
|
||||||
emit('toggle', props.plugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePlugin = () => {
|
|
||||||
emit('update', props.plugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
const removePlugin = () => {
|
|
||||||
emit('remove', props.plugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
const menuItems = [
|
|
||||||
{
|
|
||||||
title: '禁用',
|
|
||||||
condition: (plugin) => plugin.enabled,
|
|
||||||
action: togglePlugin
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '启用',
|
|
||||||
condition: (plugin) => !plugin.enabled,
|
|
||||||
action: togglePlugin
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '更新',
|
|
||||||
condition: (plugin) => plugin.source != '',
|
|
||||||
action: updatePlugin
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '删除',
|
|
||||||
condition: (plugin) => true,
|
|
||||||
action: removePlugin
|
|
||||||
}
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.plugin-card {
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.8rem;
|
|
||||||
padding-left: 1rem;
|
|
||||||
margin: 1rem 0;
|
|
||||||
background-color: white;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 10rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-author {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #666;
|
|
||||||
font-weight: 500;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-title {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-description {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: #666;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-top: 0rem;
|
|
||||||
height: 2rem;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-badges {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-github-source {
|
|
||||||
cursor: pointer;
|
|
||||||
color: #222;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-disabled {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 500;
|
|
||||||
height: 1.3rem;
|
|
||||||
padding-inline: 0.4rem;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-version {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 700;
|
|
||||||
height: 1.3rem;
|
|
||||||
padding-inline: 0.5rem;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-brief-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
/* background-color: #f0f0f0; */
|
|
||||||
gap: 0.8rem;
|
|
||||||
margin-left: -0.2rem;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-events {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-events-icon {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-events-count {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-functions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-functions-icon {
|
|
||||||
font-size: 1.6rem;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-functions-count {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-brief-info-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-brief-info-item:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-brief-info-item:hover .plugin-card-brief-info-item-icon {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-menu {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-menu-btn {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
margin-top: 0.2rem;
|
|
||||||
font-weight: 400;
|
|
||||||
padding: 0rem;
|
|
||||||
color: #3265ba;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-menu-btn:hover {
|
|
||||||
color: #4271bf;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card-menu-btn:active {
|
|
||||||
color: rgb(0, 47, 104);
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -1,287 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-card class="config-tab-toolbar">
|
|
||||||
<v-tooltip :text="currentManagerSchema == null ? '仅配置文件管理器提供了 JSON Schema 时支持可视化配置' : '切换编辑模式'" location="top">
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
|
|
||||||
<v-btn-toggle id="config-type-toggle" color="primary" v-model="configType" mandatory v-bind="props"
|
|
||||||
density="compact">
|
|
||||||
|
|
||||||
<v-btn class="config-type-toggle-btn" value="ui" :readonly="currentManagerSchema == null"
|
|
||||||
density="compact">
|
|
||||||
<v-icon>mdi-view-dashboard-edit-outline</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn class="config-type-toggle-btn" value="json" :readonly="currentManagerSchema == null"
|
|
||||||
density="compact">
|
|
||||||
<v-icon>mdi-code-json</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-btn-toggle>
|
|
||||||
</template>
|
|
||||||
</v-tooltip>
|
|
||||||
|
|
||||||
<div id="file-operation-toolbar">
|
|
||||||
<v-btn @click="reset" color="warning" prepend-icon="mdi-undo"
|
|
||||||
:disabled="!modified && configType == 'json'">重置</v-btn>
|
|
||||||
<v-btn @click="saveAndApply" color="primary" prepend-icon="mdi-content-save-outline"
|
|
||||||
:disabled="!modified && configType == 'json'">应用</v-btn>
|
|
||||||
</div>
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
<div id="config-tab-content">
|
|
||||||
|
|
||||||
<div id="config-tab-content-ui" v-if="configType == 'ui'">
|
|
||||||
<v-form id="config-tab-content-ui-form">
|
|
||||||
<vjsf id="config-tab-content-ui-form-vjsf" :schema="currentManagerSchema" v-model="currentManagerData"
|
|
||||||
:options="VJSFOptions" />
|
|
||||||
</v-form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<v-card id="config-tab-content-json" v-if="configType == 'json'">
|
|
||||||
<JsonEditorVue id="config-tab-content-json-json-editor-vue" v-model="currentManagerData" mode="text"
|
|
||||||
@change="onInput" />
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
<div id="config-tab-json-doc-link"
|
|
||||||
v-if="configType == 'json' && currentManagerDocLink != undefined && currentManagerDocLink != ''">*配置文件格式请查看
|
|
||||||
<a :href="currentManagerDocLink" target="_blank">文档</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
|
|
||||||
import { ref, getCurrentInstance, onMounted, inject } from 'vue'
|
|
||||||
import JsonEditorVue from 'json-editor-vue';
|
|
||||||
|
|
||||||
import Vjsf from '@koumoul/vjsf';
|
|
||||||
|
|
||||||
const VJSFOptions = {
|
|
||||||
"context": {},
|
|
||||||
"width": 1208,
|
|
||||||
"readOnly": false,
|
|
||||||
"summary": false,
|
|
||||||
"density": "comfortable",
|
|
||||||
"indent": true,
|
|
||||||
"titleDepth": 4,
|
|
||||||
"validateOn": "input",
|
|
||||||
"initialValidation": "withData",
|
|
||||||
"updateOn": "input",
|
|
||||||
"debounceInputMs": 300,
|
|
||||||
"defaultOn": "empty",
|
|
||||||
"removeAdditional": "error",
|
|
||||||
"autofocus": false,
|
|
||||||
"readOnlyPropertiesMode": "show",
|
|
||||||
"pluginsOptions": {},
|
|
||||||
"locale": "en",
|
|
||||||
"messages": {
|
|
||||||
"errorOneOf": "请选择一个",
|
|
||||||
"errorRequired": "必填信息",
|
|
||||||
"addItem": "添加",
|
|
||||||
"delete": "删除",
|
|
||||||
"edit": "编辑",
|
|
||||||
"close": "关闭",
|
|
||||||
"duplicate": "复制",
|
|
||||||
"sort": "排序",
|
|
||||||
"up": "向上移动",
|
|
||||||
"down": "向下移动",
|
|
||||||
"showHelp": "显示帮助信息",
|
|
||||||
"mdeLink1": "[链接标题",
|
|
||||||
"mdeLink2": "](链接地址)",
|
|
||||||
"mdeImg1": "",
|
|
||||||
"mdeTable1": "",
|
|
||||||
"mdeTable2": "\n\n| 列 1 | 列 2 | 列 3 |\n| -------- | -------- | -------- |\n| 文本 | 文本 | 文本 |\n\n",
|
|
||||||
"bold": "加粗",
|
|
||||||
"italic": "斜体",
|
|
||||||
"heading": "标题",
|
|
||||||
"quote": "引用",
|
|
||||||
"unorderedList": "无序列表",
|
|
||||||
"orderedList": "有序列表",
|
|
||||||
"createLink": "创建链接",
|
|
||||||
"insertImage": "插入图片",
|
|
||||||
"createTable": "创建表格",
|
|
||||||
"preview": "预览",
|
|
||||||
"mdeGuide": "文档",
|
|
||||||
"undo": "撤销",
|
|
||||||
"redo": "重做"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const snackbar = inject('snackbar');
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
name: String
|
|
||||||
})
|
|
||||||
|
|
||||||
const currentManagerData = ref({})
|
|
||||||
const currentManagerSchema = ref(null)
|
|
||||||
const currentManagerDocLink = ref(null)
|
|
||||||
const configType = ref('')
|
|
||||||
const modified = ref(false)
|
|
||||||
|
|
||||||
const fetchCurrentManagerData = (name) => {
|
|
||||||
console.log(name)
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
proxy.$axios.get(`/settings/${name}`).then(response => {
|
|
||||||
console.log(response.data.data)
|
|
||||||
currentManagerData.value = response.data.data.manager.data
|
|
||||||
currentManagerSchema.value = response.data.data.manager.schema
|
|
||||||
currentManagerDocLink.value = response.data.data.manager.doc_link
|
|
||||||
resolve()
|
|
||||||
}).catch(error => {
|
|
||||||
snackbar.error(error)
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const autoSwitchConfigType = () => {
|
|
||||||
console.log(currentManagerSchema == null)
|
|
||||||
if (currentManagerSchema.value == null) {
|
|
||||||
configType.value = 'json'
|
|
||||||
} else {
|
|
||||||
configType.value = 'ui'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const isJsonValid = ref(true)
|
|
||||||
const errorMessage = ref('')
|
|
||||||
|
|
||||||
const checkJsonValid = () => {
|
|
||||||
try {
|
|
||||||
JSON.parse(currentManagerData.value)
|
|
||||||
isJsonValid.value = true
|
|
||||||
errorMessage.value = ''
|
|
||||||
} catch (error) {
|
|
||||||
isJsonValid.value = false
|
|
||||||
errorMessage.value = error.message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onInput = () => {
|
|
||||||
modified.value = true
|
|
||||||
checkJsonValid()
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveAndApply = () => {
|
|
||||||
if (configType.value == 'json') {
|
|
||||||
checkJsonValid()
|
|
||||||
}
|
|
||||||
if (!isJsonValid.value) {
|
|
||||||
snackbar.error('JSON 格式不正确: ' + errorMessage.value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (configType.value == 'json') {
|
|
||||||
currentManagerData.value = JSON.parse(currentManagerData.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy.$axios.put(`/settings/${props.name}/data`, {
|
|
||||||
data: currentManagerData.value
|
|
||||||
}).then(response => {
|
|
||||||
if (response.data.code != 0) {
|
|
||||||
snackbar.error(response.data.msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fetchCurrentManagerData(props.name).then(() => {
|
|
||||||
modified.value = false
|
|
||||||
snackbar.success('应用成功')
|
|
||||||
}).catch(error => {
|
|
||||||
snackbar.error(error)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
fetchCurrentManagerData(props.name).then(() => {
|
|
||||||
snackbar.success('重置成功')
|
|
||||||
modified.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
console.log(props.name)
|
|
||||||
fetchCurrentManagerData(props.name).then(() => {
|
|
||||||
autoSwitchConfigType()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.config-tab-window {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-tab-toolbar {
|
|
||||||
margin: 0.5rem;
|
|
||||||
height: 3.2rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#file-operation-toolbar {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-type-toggle {
|
|
||||||
margin: 0.5rem;
|
|
||||||
box-shadow: 0 0 0 2px #dddddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-tab-content {
|
|
||||||
margin: 0.2rem;
|
|
||||||
height: calc(100% - 1rem);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-tab-content-ui {
|
|
||||||
margin: 0.5rem;
|
|
||||||
height: calc(100vh - 15rem);
|
|
||||||
margin-top: 1rem;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-tab-content-ui-form {
|
|
||||||
height: 100%;
|
|
||||||
width: calc(100% - 1.5rem);
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-tab-content-ui-form-vjsf {
|
|
||||||
height: 100%;
|
|
||||||
width: calc(100% - 1rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-tab-content-json {
|
|
||||||
margin: 0.5rem;
|
|
||||||
height: calc(100vh - 16rem);
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-tab-content-json-json-editor-vue {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-tab-json-doc-link {
|
|
||||||
margin: 0rem;
|
|
||||||
margin-left: 0.6rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
margin-top: 0.2rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: rgb(26, 98, 214);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="task-card">
|
|
||||||
<div class="task-card-icon">
|
|
||||||
<v-progress-circular :size="25" :width="2" indeterminate v-if="task.runtime.state == 'PENDING'" />
|
|
||||||
<v-icon v-else-if="task.runtime.state == 'FINISHED' && task.runtime.exception == null"
|
|
||||||
style="color: #4caf50;" :size="25" icon="mdi-check" />
|
|
||||||
<v-icon v-else-if="task.runtime.state == 'CANCELLED'" style="color: #f44336;" :size="25" icon="mdi-close" />
|
|
||||||
|
|
||||||
<v-icon v-else-if="task.runtime.state == 'FINISHED' && task.runtime.exception != null" style="color: #f44336;"
|
|
||||||
:size="25" icon="mdi-alert-circle-outline" v-bind="activatorProps" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="task-card-content">
|
|
||||||
<div class="task-card-kind">{{ task.kind }}</div>
|
|
||||||
<div class="task-card-label">{{ task.label }}</div>
|
|
||||||
<v-chip class="task-card-action" color="primary" variant="outlined" size="small" density="compact"
|
|
||||||
v-if="task.runtime.state == 'PENDING'">正在执行: {{ task.task_context.current_action }}</v-chip>
|
|
||||||
<v-chip class="task-card-action" color="success" variant="outlined" size="small" density="compact"
|
|
||||||
v-else-if="task.runtime.state == 'FINISHED' && task.runtime.exception == null">完成</v-chip>
|
|
||||||
|
|
||||||
<v-dialog max-width="500" persistent v-model="exceptionDialogShow">
|
|
||||||
<template v-slot:activator="{ props: activatorProps }">
|
|
||||||
<v-chip class="task-card-action" color="error" variant="outlined" size="small" density="compact"
|
|
||||||
v-if="task.runtime.state == 'FINISHED' && task.runtime.exception != null"
|
|
||||||
v-bind="activatorProps">{{ task.runtime.exception }}</v-chip>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<v-card prepend-icon="mdi-alert-circle-outline"
|
|
||||||
:text="task.runtime.exception_traceback"
|
|
||||||
title="任务执行失败">
|
|
||||||
<template v-slot:actions>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
|
|
||||||
<v-btn @click="exceptionDialogShow = false" prepend-icon="mdi-close">
|
|
||||||
关闭
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="task-card-actions">
|
|
||||||
<!-- <v-icon icon="mdi-details" /> -->
|
|
||||||
<v-dialog max-width="500" persistent v-model="detailsDialogShow">
|
|
||||||
<template v-slot:activator="{ props: activatorProps }">
|
|
||||||
<v-btn icon="mdi-file-outline" variant="text" v-bind="activatorProps" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<v-card prepend-icon="mdi-file-outline" id="task-details-card"
|
|
||||||
:title="'任务执行日志 - '+task.label">
|
|
||||||
|
|
||||||
<div id="task-details-log-container">
|
|
||||||
<textarea id="task-details-log" v-model="task.task_context.log" readonly />
|
|
||||||
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
</div>
|
|
||||||
<template v-slot:actions>
|
|
||||||
|
|
||||||
<v-btn @click="detailsDialogShow = false" prepend-icon="mdi-close">
|
|
||||||
关闭
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
defineProps({
|
|
||||||
task: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
const exceptionDialogShow = ref(false)
|
|
||||||
const detailsDialogShow = ref(false)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.task-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
width: 100%;
|
|
||||||
padding-inline: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card-icon {
|
|
||||||
margin-right: 1rem;
|
|
||||||
margin-left: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card-content {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 0.4rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.02rem;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card-kind {
|
|
||||||
font-size: 0.6rem;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card-label {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card-action {
|
|
||||||
font-size: 0.6rem;
|
|
||||||
width: fit-content;
|
|
||||||
padding-inline: 0.3rem;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card-actions {
|
|
||||||
justify-self: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#task-details-log {
|
|
||||||
width: calc(100% - 2rem);
|
|
||||||
height: 10rem;
|
|
||||||
resize: none;
|
|
||||||
border: none;
|
|
||||||
padding: 0.6rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
outline: none;
|
|
||||||
overflow: auto;
|
|
||||||
appearance: none;
|
|
||||||
background-color: #f6f6f6;
|
|
||||||
margin-inline: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-card class="task-dialog" prepend-icon="mdi-align-horizontal-left" title="任务列表">
|
|
||||||
<v-list id="task-list" v-if="taskList.length > 0">
|
|
||||||
<TaskCard class="task-card" v-for="task in taskList" :key="task.id" :task="task" />
|
|
||||||
</v-list>
|
|
||||||
<div v-else><v-alert color="warning" icon="$warning" title="暂无任务" text="暂无已添加的用户任务项" density="compact" style="margin-inline: 1rem;"></v-alert></div>
|
|
||||||
|
|
||||||
<template v-slot:actions>
|
|
||||||
<v-btn class="ml-auto" text="关闭" prepend-icon="mdi-close" @click="close"></v-btn>
|
|
||||||
</template>
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
defineProps({
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['close'])
|
|
||||||
|
|
||||||
import TaskCard from '@/components/TaskCard.vue'
|
|
||||||
|
|
||||||
import { ref, onMounted, onUnmounted, getCurrentInstance } from 'vue'
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
|
||||||
|
|
||||||
import { inject } from 'vue'
|
|
||||||
|
|
||||||
const snackbar = inject('snackbar')
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
emit('close')
|
|
||||||
}
|
|
||||||
|
|
||||||
const taskList = ref([])
|
|
||||||
|
|
||||||
const refresh = () => {
|
|
||||||
proxy.$axios.get('/system/tasks', {
|
|
||||||
params: {
|
|
||||||
type: 'user'
|
|
||||||
}
|
|
||||||
}).then(response => {
|
|
||||||
if (response.data.code != 0) {
|
|
||||||
snackbar.error(response.data.message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
taskList.value = response.data.data.tasks
|
|
||||||
|
|
||||||
// 倒序
|
|
||||||
taskList.value.reverse()
|
|
||||||
}).catch(error => {
|
|
||||||
snackbar.error(error.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let refreshTask = null
|
|
||||||
onMounted(() => {
|
|
||||||
refresh()
|
|
||||||
refreshTask = setInterval(refresh, 1000)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
clearInterval(refreshTask)
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.task-dialog {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#task-list {
|
|
||||||
max-height: 20rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
margin-inline: 1rem;
|
|
||||||
width: calc(100% - 2.2rem);
|
|
||||||
padding-inline: 0.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-card {
|
|
||||||
/* margin-bottom: 0.1rem; */
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
/* box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.1); */
|
|
||||||
height: 4rem;
|
|
||||||
/* border: 0.08rem solid #ccc; */
|
|
||||||
box-shadow: 0.1rem 0.1rem 0.2rem 0.05rem #ccc;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
/**
|
|
||||||
* main.js
|
|
||||||
*
|
|
||||||
* Bootstraps Vuetify and other plugins then mounts the App`
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Plugins
|
|
||||||
import { registerPlugins } from '@/plugins'
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import App from './App.vue'
|
|
||||||
|
|
||||||
// Composables
|
|
||||||
import { createApp } from 'vue'
|
|
||||||
|
|
||||||
const app = createApp(App)
|
|
||||||
|
|
||||||
registerPlugins(app)
|
|
||||||
|
|
||||||
app.mount('#app')
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
<template>
|
|
||||||
<PageTitle title="仪表盘" @refresh="refresh" />
|
|
||||||
|
|
||||||
<div id="dashboard-content">
|
|
||||||
<div id="first-row">
|
|
||||||
<v-card id="basic-analysis-number-card">
|
|
||||||
<v-card-title class="content-card-title">
|
|
||||||
<v-icon class="content-card-title-icon" icon="mdi-chart-line" />
|
|
||||||
基础数据
|
|
||||||
</v-card-title>
|
|
||||||
|
|
||||||
<div id="basic-analysis-number-card-content">
|
|
||||||
<NumberFieldData title="活跃会话" :number="basicData.active_session_count" />
|
|
||||||
<NumberFieldData title="对话总数" :number="basicData.conversation_count" />
|
|
||||||
<NumberFieldData title="请求总数" :number="basicData.query_count" />
|
|
||||||
</div>
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
<v-card id="message-platform-card">
|
|
||||||
<v-card-title class="content-card-title">
|
|
||||||
<v-icon class="content-card-title-icon" icon="mdi-message-outline" />
|
|
||||||
消息平台
|
|
||||||
</v-card-title>
|
|
||||||
|
|
||||||
<div id="message-platform-card-content">
|
|
||||||
<NumberFieldData title="已启用" :number="proxy.$store.state.enabledPlatformCount" link="/settings" linkText="更改配置" />
|
|
||||||
</div>
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
<v-card id="plugins-amount-card">
|
|
||||||
<v-card-title class="content-card-title">
|
|
||||||
<v-icon class="content-card-title-icon" icon="mdi-puzzle-outline" />
|
|
||||||
插件数量
|
|
||||||
</v-card-title>
|
|
||||||
|
|
||||||
<div id="plugins-amount-card-content">
|
|
||||||
<NumberFieldData title="已加载" :number="pluginsAmount" link="/plugins" linkText="管理插件" />
|
|
||||||
</div>
|
|
||||||
</v-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="dashboard-tips">
|
|
||||||
* 更多图表将在数据持久化功能更新后可用。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
|
|
||||||
import PageTitle from '@/components/PageTitle.vue'
|
|
||||||
import NumberFieldData from '@/components/NumberFieldData.vue'
|
|
||||||
|
|
||||||
import { ref, onMounted, inject, getCurrentInstance } from 'vue'
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
|
||||||
|
|
||||||
const snackbar = inject('snackbar')
|
|
||||||
|
|
||||||
const basicData = ref({
|
|
||||||
active_session_count: 0,
|
|
||||||
conversation_count: 0,
|
|
||||||
query_count: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
const pluginsAmount = ref(0)
|
|
||||||
|
|
||||||
const refresh = () => {
|
|
||||||
proxy.$axios.get('/stats/basic').then(res => {
|
|
||||||
if (res.data.code != 0) {
|
|
||||||
snackbar.error(res.data.msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
basicData.value = res.data.data
|
|
||||||
}).catch(error => {
|
|
||||||
snackbar.error(error)
|
|
||||||
})
|
|
||||||
|
|
||||||
proxy.$axios.get('/plugins').then(res => {
|
|
||||||
if (res.data.code != 0) {
|
|
||||||
snackbar.error(res.data.msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pluginsAmount.value = res.data.data.plugins.length
|
|
||||||
}).catch(error => {
|
|
||||||
snackbar.error(error)
|
|
||||||
})
|
|
||||||
|
|
||||||
proxy.$store.commit('fetchSystemInfo')
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(refresh)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
#dashboard-content {
|
|
||||||
display: table;
|
|
||||||
width: 100%;
|
|
||||||
padding-inline: 1rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#dashboard-tips {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #222;
|
|
||||||
margin-top: 0.7rem;
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#first-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#basic-analysis-number-card {
|
|
||||||
width: 35%;
|
|
||||||
min-width: 16rem;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#basic-analysis-number-card-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-card-title {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-card-title-icon {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#message-platform-card {
|
|
||||||
width: 15%;
|
|
||||||
min-width: 6rem;
|
|
||||||
padding-bottom: 0.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#plugins-amount-card {
|
|
||||||
width: 15%;
|
|
||||||
min-width: 6rem;
|
|
||||||
padding-bottom: 0.7rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
<template>
|
|
||||||
<PageTitle title="日志" @refresh="refresh" />
|
|
||||||
<v-card id="toolbar">
|
|
||||||
<div class="view-operation-components">
|
|
||||||
<v-switch class="toolbar-component" color="primary" :model-value="proxy.$store.state.autoRefreshLog"
|
|
||||||
@update:model-value="proxy.$store.state.autoRefreshLog = $event" label="自动刷新"></v-switch>
|
|
||||||
|
|
||||||
<v-switch class="toolbar-component" color="primary" :model-value="proxy.$store.state.autoScrollLog"
|
|
||||||
@update:model-value="proxy.$store.state.autoScrollLog = $event" label="自动滚动"></v-switch>
|
|
||||||
</div>
|
|
||||||
</v-card>
|
|
||||||
<v-card id="log-card">
|
|
||||||
<v-card-text id="log-card-text">
|
|
||||||
<!-- <textarea id="log-textarea" placeholder="点击标题旁的按钮以刷新日志" v-model="logContentHTML" readonly></textarea> -->
|
|
||||||
<div id="log-content-html" v-html="logContentHTML"></div>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
|
|
||||||
import PageTitle from '@/components/PageTitle.vue'
|
|
||||||
import { ref, getCurrentInstance, onMounted, onUnmounted } from 'vue'
|
|
||||||
|
|
||||||
import {inject} from "vue";
|
|
||||||
|
|
||||||
const snackbar = inject('snackbar');
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
|
||||||
|
|
||||||
const logContent = ref('')
|
|
||||||
const logContentHTML = ref('')
|
|
||||||
|
|
||||||
const refresh = () => {
|
|
||||||
refreshLog()
|
|
||||||
}
|
|
||||||
|
|
||||||
let logPointer = {
|
|
||||||
"start_page_number": 0,
|
|
||||||
"start_offset": 0
|
|
||||||
}
|
|
||||||
|
|
||||||
import { AnsiUp } from 'ansi_up';
|
|
||||||
|
|
||||||
const ansiUp = new AnsiUp()
|
|
||||||
|
|
||||||
const refreshLog = () => {
|
|
||||||
proxy.$axios.get(`/logs`, {
|
|
||||||
params: {
|
|
||||||
start_page_number: logPointer.start_page_number,
|
|
||||||
start_offset: logPointer.start_offset
|
|
||||||
}
|
|
||||||
}).then(response => {
|
|
||||||
if (response.data.code != 0) {
|
|
||||||
snackbar.error(response.data.message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logContent.value += response.data.data.logs
|
|
||||||
logContentHTML.value += ansiUp.ansi_to_html(response.data.data.logs)
|
|
||||||
logPointer.start_page_number = response.data.data.end_page_number
|
|
||||||
logPointer.start_offset = response.data.data.end_offset
|
|
||||||
|
|
||||||
if (proxy.$store.state.autoScrollLog) {
|
|
||||||
// 滚动到最底部
|
|
||||||
document.getElementById('log-content-html').scrollTop = document.getElementById('log-content-html').scrollHeight
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
|
||||||
snackbar.error(error.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let refreshLogTask = null
|
|
||||||
onMounted(() => {
|
|
||||||
refreshLog()
|
|
||||||
|
|
||||||
refreshLogTask = setInterval(() => {
|
|
||||||
if (proxy.$store.state.autoRefreshLog) {
|
|
||||||
refreshLog()
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
clearInterval(refreshLogTask)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
#toolbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin: 1rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
height: 3rem;
|
|
||||||
border-radius: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-component {
|
|
||||||
margin-top: 1.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-operation-components {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-left: 1rem;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#log-card {
|
|
||||||
margin: 1rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
height: calc(100vh - 9.5rem);
|
|
||||||
border-radius: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#log-textarea {
|
|
||||||
/* height: 100%; */
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
resize: none;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
appearance: none;
|
|
||||||
/* background-color: #eee; */
|
|
||||||
}
|
|
||||||
|
|
||||||
#log-card-text {
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
background-color: #343434;
|
|
||||||
}
|
|
||||||
|
|
||||||
#log-content-html {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,325 +0,0 @@
|
|||||||
<template>
|
|
||||||
<PageTitle title="插件" @refresh="refresh" />
|
|
||||||
<v-card id="plugins-toolbar">
|
|
||||||
<div id="view-btns">
|
|
||||||
<v-btn-toggle id="plugins-view-toggle" color="primary" v-model="proxy.$store.state.pluginsView" mandatory density="compact">
|
|
||||||
<v-btn class="plugins-view-toggle-btn" value="installed" density="compact">已安装</v-btn>
|
|
||||||
<v-btn class="plugins-view-toggle-btn" value="market" density="compact">插件市场</v-btn>
|
|
||||||
</v-btn-toggle>
|
|
||||||
</div>
|
|
||||||
<div id="operation-btns">
|
|
||||||
<v-tooltip text="设置插件优先级" location="top">
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<v-btn prepend-icon="mdi-priority-high" v-bind="props" :disabled="plugins.length == 0">
|
|
||||||
编排
|
|
||||||
|
|
||||||
<v-dialog activator="parent" max-width="500" persistent v-model="isOrchestrationDialogActive">
|
|
||||||
<template v-slot:default="{ isActive }">
|
|
||||||
<v-card prepend-icon="mdi-priority-high" text="优先级影响插件的加载、事件触发顺序" title="设置插件优先级">
|
|
||||||
<v-list id="plugin-orchestration-list">
|
|
||||||
<draggable v-model="plugins" item-key="name" group="plugins"
|
|
||||||
@start="drag = true" id="plugin-orchestration-draggable"
|
|
||||||
@end="drag = false">
|
|
||||||
<template #item="{ element }">
|
|
||||||
<div class="plugin-orchestration-item">
|
|
||||||
<div class="plugin-orchestration-item-title">
|
|
||||||
<div class="plugin-orchestration-item-author">
|
|
||||||
{{ element.author }} /
|
|
||||||
</div>
|
|
||||||
<div class="plugin-orchestration-item-name">
|
|
||||||
{{ element.name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="plugin-orchestration-item-action">
|
|
||||||
<v-icon>mdi-drag</v-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</draggable>
|
|
||||||
</v-list>
|
|
||||||
|
|
||||||
<template v-slot:actions>
|
|
||||||
<v-btn class="ml-auto" text="关闭" prepend-icon="mdi-close"
|
|
||||||
@click="cancelOrderChanges"></v-btn>
|
|
||||||
<v-btn color="primary" prepend-icon="mdi-content-save-outline"
|
|
||||||
@click="saveOrder">应用</v-btn>
|
|
||||||
</template>
|
|
||||||
</v-card>
|
|
||||||
</template>
|
|
||||||
</v-dialog>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
</v-tooltip>
|
|
||||||
|
|
||||||
<v-btn color="primary" prepend-icon="mdi-plus">
|
|
||||||
安装
|
|
||||||
|
|
||||||
<v-dialog activator="parent" max-width="500" persistent v-model="isInstallDialogActive">
|
|
||||||
<template v-slot:default="{ isActive }">
|
|
||||||
|
|
||||||
<v-card title="从 GitHub 安装插件" prepend-icon="mdi-github">
|
|
||||||
|
|
||||||
<div id="plugin-install-dialog-content">
|
|
||||||
<div>
|
|
||||||
目前仅支持从 GitHub 安装,插件列表:<a
|
|
||||||
href="https://github.com/stars/RockChinQ/lists/qchatgpt-%E6%8F%92%E4%BB%B6"
|
|
||||||
target="_blank">LangBot 插件</a>
|
|
||||||
</div>
|
|
||||||
<v-text-field v-model="installDialogSource" label="插件源码地址" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-slot:actions>
|
|
||||||
<v-btn class="ml-auto" text="取消" prepend-icon="mdi-close"
|
|
||||||
@click="isInstallDialogActive = false"></v-btn>
|
|
||||||
<v-btn color="primary" prepend-icon="mdi-content-save-outline"
|
|
||||||
@click="installPlugin">安装</v-btn>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
</v-card>
|
|
||||||
</template>
|
|
||||||
</v-dialog>
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
</v-card>
|
|
||||||
<div class="plugins-container" v-if="proxy.$store.state.pluginsView == 'installed'">
|
|
||||||
<v-alert id="no-plugins-alert" v-if="plugins.length == 0" color="warning" icon="$warning" title="暂无插件" text="暂无已安装的插件,请安装插件" density="compact" style="margin-inline: 1rem;"></v-alert>
|
|
||||||
<PluginCard class="plugin-card" v-if="plugins.length > 0" v-for="plugin in plugins" :key="plugin.name" :plugin="plugin"
|
|
||||||
@toggle="togglePlugin" @update="updatePlugin" @remove="removePlugin" />
|
|
||||||
</div>
|
|
||||||
<div class="plugins-container" v-if="proxy.$store.state.pluginsView == 'market'">
|
|
||||||
<Marketplace @installPlugin="installMarketplacePlugin" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
|
|
||||||
import PageTitle from '@/components/PageTitle.vue'
|
|
||||||
import PluginCard from '@/components/PluginCard.vue'
|
|
||||||
import Marketplace from '@/components/Marketplace.vue'
|
|
||||||
|
|
||||||
import draggable from 'vuedraggable'
|
|
||||||
|
|
||||||
import { ref, getCurrentInstance, onMounted } from 'vue'
|
|
||||||
|
|
||||||
import { inject } from "vue";
|
|
||||||
|
|
||||||
const snackbar = inject('snackbar');
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
|
||||||
|
|
||||||
const plugins = ref([])
|
|
||||||
|
|
||||||
const refresh = () => {
|
|
||||||
proxy.$axios.get('/plugins').then(res => {
|
|
||||||
if (res.data.code != 0) {
|
|
||||||
snackbar.error(res.data.msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
plugins.value = res.data.data.plugins
|
|
||||||
}).catch(error => {
|
|
||||||
snackbar.error(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(refresh)
|
|
||||||
|
|
||||||
const togglePlugin = (plugin) => {
|
|
||||||
proxy.$axios.put(`/plugins/${plugin.author}/${plugin.name}/toggle`, {
|
|
||||||
target_enabled: !plugin.enabled
|
|
||||||
}).then(res => {
|
|
||||||
if (res.data.code != 0) {
|
|
||||||
snackbar.error(res.data.msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
refresh()
|
|
||||||
}).catch(error => {
|
|
||||||
snackbar.error(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePlugin = (plugin) => {
|
|
||||||
proxy.$axios.post(`/plugins/${plugin.author}/${plugin.name}/update`).then(res => {
|
|
||||||
if (res.data.code != 0) {
|
|
||||||
snackbar.error(res.data.msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
snackbar.success(`已添加更新任务 请到任务列表查看进度`)
|
|
||||||
}).catch(error => {
|
|
||||||
snackbar.error(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const removePlugin = (plugin) => {
|
|
||||||
proxy.$axios.delete(`/plugins/${plugin.author}/${plugin.name}`).then(res => {
|
|
||||||
if (res.data.code != 0) {
|
|
||||||
snackbar.error(res.data.msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
snackbar.success(`已添加删除任务 请到任务列表查看进度`)
|
|
||||||
}).catch(error => {
|
|
||||||
snackbar.error(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const installMarketplacePlugin = (repository) => {
|
|
||||||
|
|
||||||
installDialogSource.value = 'https://'+repository
|
|
||||||
isInstallDialogActive.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const installPlugin = () => {
|
|
||||||
|
|
||||||
if (installDialogSource.value == '' || installDialogSource.value.trim() == '') {
|
|
||||||
snackbar.error("请输入插件仓库地址")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy.$axios.post(`/plugins/install/github`, {
|
|
||||||
source: installDialogSource.value
|
|
||||||
}).then(res => {
|
|
||||||
if (res.data.code != 0) {
|
|
||||||
snackbar.error(res.data.msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
installDialogSource.value = ''
|
|
||||||
snackbar.success(`已添加插件安装任务 请到任务列表查看进度`)
|
|
||||||
isInstallDialogActive.value = false
|
|
||||||
}).catch(error => {
|
|
||||||
snackbar.error(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const isOrchestrationDialogActive = ref(false)
|
|
||||||
|
|
||||||
const cancelOrderChanges = () => {
|
|
||||||
refresh()
|
|
||||||
isOrchestrationDialogActive.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveOrder = () => {
|
|
||||||
// 为所有插件的 priority 赋值,倒序
|
|
||||||
plugins.value.forEach(plugin => {
|
|
||||||
plugin.priority = plugins.value.length - plugins.value.indexOf(plugin)
|
|
||||||
})
|
|
||||||
|
|
||||||
proxy.$axios.put('/plugins/reorder', {
|
|
||||||
plugins: plugins.value
|
|
||||||
}).then(res => {
|
|
||||||
refresh()
|
|
||||||
snackbar.success('插件优先级已保存')
|
|
||||||
isOrchestrationDialogActive.value = false
|
|
||||||
}).catch(error => {
|
|
||||||
snackbar.error(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const isInstallDialogActive = ref(false)
|
|
||||||
const installDialogSource = ref('')
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
#plugins-toolbar {
|
|
||||||
margin-top: 1rem;
|
|
||||||
margin-inline: 1rem;
|
|
||||||
height: 3.2rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#view-btns {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#plugins-view-toggle {
|
|
||||||
margin: 0.5rem;
|
|
||||||
box-shadow: 0 0 0 2px #dddddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
#operation-btns {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugins-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
margin-inline: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#no-plugins-alert {
|
|
||||||
margin: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-card {
|
|
||||||
width: 18rem;
|
|
||||||
height: 8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#plugin-orchestration-list {
|
|
||||||
max-height: 20rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
margin-inline: 1rem;
|
|
||||||
width: calc(100% - 2rem);
|
|
||||||
/* background-color: aqua; */
|
|
||||||
}
|
|
||||||
|
|
||||||
#plugin-orchestration-draggable {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-orchestration-item {
|
|
||||||
cursor: move;
|
|
||||||
width: calc(100% - 2rem);
|
|
||||||
box-shadow: 0.1rem 0.1rem 0.2rem 0.05rem #ccc;
|
|
||||||
background-color: #ffffff;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
padding: 0.5rem;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-orchestration-item-title {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-orchestration-item-author {
|
|
||||||
color: #666;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-orchestration-item-name {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plugin-orchestration-item-action {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#plugin-install-dialog-content {
|
|
||||||
width: calc(100% - 3rem);
|
|
||||||
margin-inline: 1.5rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
<template>
|
|
||||||
<PageTitle title="设置" @refresh="refresh" />
|
|
||||||
|
|
||||||
<v-card id="settings-card">
|
|
||||||
<v-tabs id="settings-tabs" v-model="proxy.$store.state.settingsPageTab" show-arrows center-active
|
|
||||||
@update:model-value="onTabChange">
|
|
||||||
<v-tooltip v-for="manager in managerList" :key="manager.name" :text="manager.description"
|
|
||||||
location="top">
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<v-tab v-bind="props" :value="manager.name" style="text-transform: none;">{{ manager.name }}</v-tab>
|
|
||||||
</template>
|
|
||||||
</v-tooltip>
|
|
||||||
</v-tabs>
|
|
||||||
<v-tabs-window id="settings-tab-window" v-model="proxy.$store.state.settingsPageTab">
|
|
||||||
<v-tabs-window-item v-for="manager in managerList" :key="manager.name" :value="manager.name"
|
|
||||||
class="config-tab-window">
|
|
||||||
<SettingWindow style="height: 100%;width: 100%;" :name="manager.name" />
|
|
||||||
<!-- {{ manager.name }} -->
|
|
||||||
</v-tabs-window-item>
|
|
||||||
</v-tabs-window>
|
|
||||||
</v-card>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
|
|
||||||
import PageTitle from '@/components/PageTitle.vue'
|
|
||||||
import SettingWindow from '@/components/SettingWindow.vue'
|
|
||||||
import { ref, getCurrentInstance, onMounted } from 'vue'
|
|
||||||
|
|
||||||
import {inject} from "vue";
|
|
||||||
|
|
||||||
const snackbar = inject('snackbar');
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
|
||||||
|
|
||||||
const managerList = ref([])
|
|
||||||
|
|
||||||
const refresh = () => {
|
|
||||||
proxy.$axios.get('/settings').then(response => {
|
|
||||||
|
|
||||||
if (response.data.code != 0) {
|
|
||||||
snackbar.error(response.data.msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
managerList.value = response.data.data.managers
|
|
||||||
|
|
||||||
if (proxy.$store.state.settingsPageTab == '') {
|
|
||||||
proxy.$store.state.settingsPageTab = managerList.value[0].name
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
|
||||||
snackbar.error(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onTabChange = (tab) => {
|
|
||||||
console.log(tab)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
refresh()
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
#settings-card {
|
|
||||||
margin: 1rem;
|
|
||||||
height: calc(100% - 5rem);
|
|
||||||
/* max-height: calc(100% - 5rem); */
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
/* background-color: #f0f0f0; */
|
|
||||||
}
|
|
||||||
|
|
||||||
#settings-tabs {
|
|
||||||
height: 3rem;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settings-tab-window {
|
|
||||||
height: calc(100vh - 9rem);
|
|
||||||
overflow: hidden;
|
|
||||||
/* background-color: aqua; */
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/**
|
|
||||||
* plugins/index.js
|
|
||||||
*
|
|
||||||
* Automatically included in `./src/main.js`
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Plugins
|
|
||||||
import vuetify from './vuetify'
|
|
||||||
import router from '@/router'
|
|
||||||
import store from '@/store'
|
|
||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
export function registerPlugins (app) {
|
|
||||||
app
|
|
||||||
.use(vuetify)
|
|
||||||
.use(router)
|
|
||||||
.use(store)
|
|
||||||
|
|
||||||
// 读取用户令牌
|
|
||||||
const token = localStorage.getItem('user-token')
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
store.state.user.jwtToken = token
|
|
||||||
}
|
|
||||||
|
|
||||||
// 所有axios请求均携带用户令牌
|
|
||||||
axios.defaults.headers.common['Authorization'] = `Bearer ${store.state.user.jwtToken}`
|
|
||||||
|
|
||||||
app.config.globalProperties.$axios = axios
|
|
||||||
store.commit('initializeFetch')
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
/**
|
|
||||||
* plugins/vuetify.js
|
|
||||||
*
|
|
||||||
* Framework documentation: https://vuetifyjs.com`
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Styles
|
|
||||||
import '@mdi/font/css/materialdesignicons.css'
|
|
||||||
import 'vuetify/styles'
|
|
||||||
|
|
||||||
// Composables
|
|
||||||
import { createVuetify } from 'vuetify'
|
|
||||||
|
|
||||||
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
|
||||||
export default createVuetify({
|
|
||||||
theme: {
|
|
||||||
defaultTheme: 'light',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
|
|
||||||
/**
|
|
||||||
* router/index.ts
|
|
||||||
*
|
|
||||||
* Automatic routes for `./src/pages/*.vue`
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Composables
|
|
||||||
import { createRouter, createWebHashHistory } from 'vue-router/auto'
|
|
||||||
import DashBoard from '../pages/DashBoard.vue'
|
|
||||||
import Settings from '../pages/Settings.vue'
|
|
||||||
import Logs from '../pages/Logs.vue'
|
|
||||||
import Plugins from '../pages/Plugins.vue'
|
|
||||||
|
|
||||||
const routes = [
|
|
||||||
{ path: '/', component: DashBoard },
|
|
||||||
{ path: '/settings', component: Settings },
|
|
||||||
{ path: '/logs', component: Logs },
|
|
||||||
{ path: '/plugins', component: Plugins },
|
|
||||||
]
|
|
||||||
|
|
||||||
const router = createRouter({
|
|
||||||
history: createWebHashHistory(),
|
|
||||||
routes,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Workaround for https://github.com/vitejs/vite/issues/11804
|
|
||||||
router.onError((err, to) => {
|
|
||||||
if (err?.message?.includes?.('Failed to fetch dynamically imported module')) {
|
|
||||||
if (!localStorage.getItem('vuetify:dynamic-reload')) {
|
|
||||||
console.log('Reloading page to fix dynamic import error')
|
|
||||||
localStorage.setItem('vuetify:dynamic-reload', 'true')
|
|
||||||
location.assign(to.fullPath)
|
|
||||||
} else {
|
|
||||||
console.error('Dynamic import error, reloading page did not fix it', err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
router.isReady().then(() => {
|
|
||||||
localStorage.removeItem('vuetify:dynamic-reload')
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { createStore } from 'vuex'
|
|
||||||
import router from '@/router'
|
|
||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
export default createStore({
|
|
||||||
state: {
|
|
||||||
// 开发时使用
|
|
||||||
// apiBaseUrl: 'http://localhost:5300/api/v1',
|
|
||||||
apiBaseUrl: '/api/v1',
|
|
||||||
autoRefreshLog: false,
|
|
||||||
autoScrollLog: true,
|
|
||||||
settingsPageTab: '',
|
|
||||||
version: 'v0.0.0',
|
|
||||||
debug: false,
|
|
||||||
enabledPlatformCount: 0,
|
|
||||||
user: {
|
|
||||||
tokenChecked: false,
|
|
||||||
tokenValid: false,
|
|
||||||
systemInitialized: true,
|
|
||||||
jwtToken: '',
|
|
||||||
},
|
|
||||||
pluginsView: 'installed',
|
|
||||||
marketplaceParams: {
|
|
||||||
query: '',
|
|
||||||
page: 1,
|
|
||||||
per_page: 10,
|
|
||||||
sort_by: 'pushed_at',
|
|
||||||
sort_order: 'DESC',
|
|
||||||
},
|
|
||||||
marketplacePlugins: [],
|
|
||||||
marketplaceTotalPages: 0,
|
|
||||||
marketplaceTotalPluginsCount: 0,
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
initializeFetch() {
|
|
||||||
axios.defaults.baseURL = this.state.apiBaseUrl
|
|
||||||
|
|
||||||
axios.get('/system/info').then(response => {
|
|
||||||
this.state.version = response.data.data.version
|
|
||||||
this.state.debug = response.data.data.debug
|
|
||||||
this.state.enabledPlatformCount = response.data.data.enabled_platform_count
|
|
||||||
})
|
|
||||||
},
|
|
||||||
fetchSystemInfo() {
|
|
||||||
axios.get('/system/info').then(response => {
|
|
||||||
this.state.version = response.data.data.version
|
|
||||||
this.state.debug = response.data.data.debug
|
|
||||||
this.state.enabledPlatformCount = response.data.data.enabled_platform_count
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions: {},
|
|
||||||
})
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
/**
|
|
||||||
* src/styles/settings.scss
|
|
||||||
*
|
|
||||||
* Configures SASS variables and Vuetify overwrites
|
|
||||||
*/
|
|
||||||
|
|
||||||
// https://vuetifyjs.com/features/sass-variables/`
|
|
||||||
// @use 'vuetify/settings' with (
|
|
||||||
// $color-pack: false
|
|
||||||
// );
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
// Plugins
|
|
||||||
import Components from 'unplugin-vue-components/vite'
|
|
||||||
import Vue from '@vitejs/plugin-vue'
|
|
||||||
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
|
|
||||||
import ViteFonts from 'unplugin-fonts/vite'
|
|
||||||
import VueRouter from 'unplugin-vue-router/vite'
|
|
||||||
|
|
||||||
import { commonjsDeps } from '@koumoul/vjsf/utils/build.js'
|
|
||||||
|
|
||||||
// Utilities
|
|
||||||
import { defineConfig } from 'vite'
|
|
||||||
import { fileURLToPath, URL } from 'node:url'
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [
|
|
||||||
VueRouter(),
|
|
||||||
Vue({
|
|
||||||
template: { transformAssetUrls }
|
|
||||||
}),
|
|
||||||
// https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
|
|
||||||
Vuetify({
|
|
||||||
autoImport: true,
|
|
||||||
styles: {
|
|
||||||
configFile: 'src/styles/settings.scss',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
Components(),
|
|
||||||
ViteFonts({
|
|
||||||
google: {
|
|
||||||
families: [{
|
|
||||||
name: 'Roboto',
|
|
||||||
styles: 'wght@100;300;400;500;700;900',
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
define: { 'process.env': {} },
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
|
||||||
},
|
|
||||||
extensions: [
|
|
||||||
'.js',
|
|
||||||
'.json',
|
|
||||||
'.jsx',
|
|
||||||
'.mjs',
|
|
||||||
'.ts',
|
|
||||||
'.tsx',
|
|
||||||
'.vue',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
port: 3002,
|
|
||||||
},
|
|
||||||
optimizeDeps: {
|
|
||||||
include: commonjsDeps,
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
commonjsOptions: {
|
|
||||||
transformMixedEsModules: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { FlatCompat } from "@eslint/eslintrc";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
});
|
||||||
|
|
||||||
|
const eslintConfig = [
|
||||||
|
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||||
|
];
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "web_ui",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --turbopack",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||||
|
"antd": "^5.24.6",
|
||||||
|
"next": "15.2.4",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"uuidjs": "^5.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "15.2.4",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 128 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,37 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* 适用于 Firefox 的滚动条 */
|
||||||
|
scrollbar-color: rgba(0, 0, 0, 0.2) transparent; /* 滑块颜色 + 轨道颜色 */
|
||||||
|
scrollbar-width: thin; /* auto | thin | none */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WebKit 内核浏览器定制 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px; /* 垂直滚动条宽度 */
|
||||||
|
height: 6px; /* 水平滚动条高度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent; /* 隐藏轨道背景 */
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(0, 0, 0, 0.2); /* 半透明黑色 */
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.35); /* 悬停加深 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 兼容 Edge */
|
||||||
|
@supports (-ms-ime-align:auto) {
|
||||||
|
body {
|
||||||
|
-ms-overflow-style: -ms-autohiding-scrollbar; /* 自动隐藏滚动条 */
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface ICreateBotField {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
.configPageContainer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer {
|
||||||
|
width: 420px;
|
||||||
|
height: 220px;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.botListContainer {
|
||||||
|
align-self: flex-start;
|
||||||
|
justify-self: flex-start;
|
||||||
|
width: calc(100% - 60px);
|
||||||
|
margin: auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
justify-items: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import {BotCardVO} from "@/app/home/bot-config/components/bot-card/BotCardVO";
|
||||||
|
import styles from "./botCard.module.css";
|
||||||
|
|
||||||
|
export default function BotCard({
|
||||||
|
botCardVO
|
||||||
|
}: {
|
||||||
|
botCardVO: BotCardVO;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.cardContainer}`}>
|
||||||
|
{/* icon和基本信息 */}
|
||||||
|
<div className={`${styles.iconBasicInfoContainer}`}>
|
||||||
|
{/* icon */}
|
||||||
|
<div className={`${styles.icon}`}>
|
||||||
|
ICO
|
||||||
|
</div>
|
||||||
|
{/* bot基本信息 */}
|
||||||
|
<div className={`${styles.basicInfoContainer}`}>
|
||||||
|
<div className={`${styles.basicInfoText} ${styles.bigText}`}>
|
||||||
|
{botCardVO.name}
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.basicInfoText}`}>
|
||||||
|
平台:{botCardVO.adapter}
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.basicInfoText}`}>
|
||||||
|
绑定流水线:{botCardVO.pipelineName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 描述和创建时间 */}
|
||||||
|
<div className={`${styles.urlAndUpdateText}`}>
|
||||||
|
描述:{botCardVO.description}
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.urlAndUpdateText}`}>
|
||||||
|
更新时间:{botCardVO.updateTime}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
export interface IBotCardVO {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
adapter: string;
|
||||||
|
description: string;
|
||||||
|
updateTime: string;
|
||||||
|
pipelineName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BotCardVO implements IBotCardVO {
|
||||||
|
id: string;
|
||||||
|
adapter: string;
|
||||||
|
description: string;
|
||||||
|
name: string;
|
||||||
|
updateTime: string;
|
||||||
|
pipelineName: string;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(props: IBotCardVO) {
|
||||||
|
this.id = props.id;
|
||||||
|
this.name = props.name;
|
||||||
|
this.adapter = props.adapter;
|
||||||
|
this.description = props.description;
|
||||||
|
this.updateTime = props.updateTime;
|
||||||
|
this.pipelineName = props.pipelineName;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
.iconBasicInfoContainer {
|
||||||
|
width: 300px;
|
||||||
|
height: 100px;
|
||||||
|
margin-left: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer {
|
||||||
|
width: 360px;
|
||||||
|
height: 200px;
|
||||||
|
background-color: #FFF;
|
||||||
|
border-radius: 9px;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.4) 0 1px 1px -1px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconBasicInfoContainer {
|
||||||
|
width: 300px;
|
||||||
|
height: 100px;
|
||||||
|
margin-left: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 40px;
|
||||||
|
line-height: 90px;
|
||||||
|
text-align: center;
|
||||||
|
color: #ffffff;
|
||||||
|
background: rgba(96, 149, 209, 0.31);
|
||||||
|
border: 1px solid rgba(96, 149, 209, 0.31);
|
||||||
|
}
|
||||||
|
|
||||||
|
.basicInfoContainer {
|
||||||
|
width: 200px;
|
||||||
|
height: 90px;
|
||||||
|
padding-left: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basicInfoText {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.bigText {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.urlAndUpdateText {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.createCardContainer {
|
||||||
|
font-size: 90px;
|
||||||
|
background: #6062E7;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
import {BotFormEntity, IBotFormEntity} from "@/app/home/bot-config/components/bot-form/BotFormEntity";
|
||||||
|
import {fetchAdapterList} from "@/app/home/mock-api/index"
|
||||||
|
import {Button, Form, Input, Select, Space} from "antd";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {IChooseAdapterEntity} from "@/app/home/bot-config/components/bot-form/ChooseAdapterEntity";
|
||||||
|
import {
|
||||||
|
DynamicFormItemConfig,
|
||||||
|
IDynamicFormItemConfig,
|
||||||
|
parseDynamicFormItemType
|
||||||
|
} from "@/app/home/components/dynamic-form/DynamicFormItemConfig";
|
||||||
|
import {UUID} from 'uuidjs'
|
||||||
|
import DynamicFormComponent from "@/app/home/components/dynamic-form/DynamicFormComponent";
|
||||||
|
import {ICreateLLMField} from "@/app/home/llm-config/ICreateLLMField";
|
||||||
|
|
||||||
|
export default function BotForm({
|
||||||
|
initBotId,
|
||||||
|
onFormSubmit,
|
||||||
|
onFormCancel,
|
||||||
|
}: {
|
||||||
|
initBotId?: string;
|
||||||
|
onFormSubmit: (value: IBotFormEntity) => void;
|
||||||
|
onFormCancel: (value: IBotFormEntity) => void;
|
||||||
|
}) {
|
||||||
|
const [adapterNameToDynamicConfigMap, setAdapterNameToDynamicConfigMap] = useState(new Map<string, IDynamicFormItemConfig[]>())
|
||||||
|
const [form] = Form.useForm<IBotFormEntity>();
|
||||||
|
const [showDynamicForm, setShowDynamicForm] = useState<boolean>(false)
|
||||||
|
const [dynamicForm] = Form.useForm();
|
||||||
|
const [adapterNameList, setAdapterNameList] = useState<IChooseAdapterEntity[]>([])
|
||||||
|
const [dynamicFormConfigList, setDynamicFormConfigList] = useState<IDynamicFormItemConfig[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initBotFormComponent()
|
||||||
|
if (initBotId) {
|
||||||
|
onEditMode()
|
||||||
|
} else {
|
||||||
|
onCreateMode()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
async function initBotFormComponent() {
|
||||||
|
// 拉取adapter
|
||||||
|
const rawAdapterList = await fetchAdapterList()
|
||||||
|
// 初始化适配器选择列表
|
||||||
|
setAdapterNameList(
|
||||||
|
rawAdapterList.map(item => {
|
||||||
|
return {
|
||||||
|
label: item.label.zh_CN,
|
||||||
|
value: item.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// 初始化适配器表单map
|
||||||
|
rawAdapterList.forEach(rawAdapter => {
|
||||||
|
adapterNameToDynamicConfigMap.set(
|
||||||
|
rawAdapter.name,
|
||||||
|
rawAdapter.spec.config.map(item =>
|
||||||
|
new DynamicFormItemConfig({
|
||||||
|
default: item.default,
|
||||||
|
id: UUID.generate(),
|
||||||
|
label: item.label,
|
||||||
|
name: item.name,
|
||||||
|
required: item.required,
|
||||||
|
type: parseDynamicFormItemType(item.type)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
// 拉取初始化表单信息
|
||||||
|
if (initBotId) {
|
||||||
|
getBotFieldById(initBotId).then(val => {
|
||||||
|
form.setFieldsValue(val)
|
||||||
|
handleAdapterSelect(val.adapter)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
form.resetFields()
|
||||||
|
}
|
||||||
|
setAdapterNameToDynamicConfigMap(adapterNameToDynamicConfigMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onCreateMode() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEditMode() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBotFieldById(botId: string): Promise<IBotFormEntity> {
|
||||||
|
return new BotFormEntity({
|
||||||
|
adapter: "telegram",
|
||||||
|
description: "模拟拉取bot",
|
||||||
|
name: "模拟电报bot",
|
||||||
|
adapter_config: {
|
||||||
|
token: "aaabbbccc"
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAdapterSelect(adapterName: string) {
|
||||||
|
if (adapterName) {
|
||||||
|
console.log(adapterNameToDynamicConfigMap)
|
||||||
|
const dynamicFormConfigList = adapterNameToDynamicConfigMap.get(adapterName)
|
||||||
|
if (dynamicFormConfigList) {
|
||||||
|
console.log(dynamicFormConfigList)
|
||||||
|
setDynamicFormConfigList(dynamicFormConfigList)
|
||||||
|
}
|
||||||
|
setShowDynamicForm(true)
|
||||||
|
} else {
|
||||||
|
setShowDynamicForm(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmitButton() {
|
||||||
|
form.submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFormFinish(value: IBotFormEntity) {
|
||||||
|
dynamicForm.submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有通过外层固定表单验证才会走到这里,真正的提交逻辑在这里
|
||||||
|
function onDynamicFormSubmit(value: object) {
|
||||||
|
if (initBotId) {
|
||||||
|
// 编辑提交
|
||||||
|
console.log('submit edit', form.getFieldsValue() ,value)
|
||||||
|
} else {
|
||||||
|
// 创建提交
|
||||||
|
console.log('submit create', form.getFieldsValue() ,value)
|
||||||
|
}
|
||||||
|
onFormSubmit(form.getFieldsValue())
|
||||||
|
setShowDynamicForm(false)
|
||||||
|
form.resetFields()
|
||||||
|
dynamicForm.resetFields()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSaveButton() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
labelCol={{span: 5}}
|
||||||
|
wrapperCol={{span: 18}}
|
||||||
|
layout='vertical'
|
||||||
|
onFinish={handleFormFinish}
|
||||||
|
>
|
||||||
|
<Form.Item<IBotFormEntity>
|
||||||
|
label={"机器人名称"}
|
||||||
|
name={"name"}
|
||||||
|
rules={[{required: true, message: "该项为必填项哦~"}]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="为机器人取个好听的名字吧~"
|
||||||
|
style={{width: 260}}
|
||||||
|
></Input>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item<IBotFormEntity>
|
||||||
|
label={"描述"}
|
||||||
|
name={"description"}
|
||||||
|
rules={[{required: true, message: "该项为必填项哦~"}]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="简单描述一下这个机器人"
|
||||||
|
></Input>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item<IBotFormEntity>
|
||||||
|
label={"平台/适配器选择"}
|
||||||
|
name={"adapter"}
|
||||||
|
rules={[{required: true, message: "该项为必填项哦~"}]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
style={{width: 220}}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleAdapterSelect(value)
|
||||||
|
}}
|
||||||
|
options={adapterNameList}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
{
|
||||||
|
showDynamicForm &&
|
||||||
|
<DynamicFormComponent
|
||||||
|
form={dynamicForm}
|
||||||
|
itemConfigList={dynamicFormConfigList}
|
||||||
|
onSubmit={onDynamicFormSubmit}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
<Space>
|
||||||
|
{
|
||||||
|
!initBotId &&
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="button"
|
||||||
|
onClick={handleSubmitButton}
|
||||||
|
>
|
||||||
|
提交
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
initBotId &&
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
onClick={handleSaveButton}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
<Button htmlType="button" onClick={() => {
|
||||||
|
onFormCancel(form.getFieldsValue())
|
||||||
|
}}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export interface IBotFormEntity {
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
adapter: string,
|
||||||
|
adapter_config: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BotFormEntity implements IBotFormEntity {
|
||||||
|
adapter: string;
|
||||||
|
description: string;
|
||||||
|
name: string;
|
||||||
|
adapter_config: object;
|
||||||
|
|
||||||
|
constructor(props: IBotFormEntity) {
|
||||||
|
this.adapter = props.adapter;
|
||||||
|
this.description = props.description;
|
||||||
|
this.name = props.name;
|
||||||
|
this.adapter_config = props.adapter_config;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface IChooseAdapterEntity {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import styles from "./botConfig.module.css";
|
||||||
|
import EmptyAndCreateComponent from "@/app/home/components/empty-and-create-component/EmptyAndCreateComponent";
|
||||||
|
import {useRouter} from "next/navigation";
|
||||||
|
import {BotCardVO} from "@/app/home/bot-config/components/bot-card/BotCardVO";
|
||||||
|
import {Modal} from "antd";
|
||||||
|
import BotForm from "@/app/home/bot-config/components/bot-form/BotForm";
|
||||||
|
import BotCard from "@/app/home/bot-config/components/bot-card/BotCard";
|
||||||
|
import CreateCardComponent from "@/app/infra/basic-component/create-card-component/CreateCardComponent"
|
||||||
|
|
||||||
|
|
||||||
|
export default function BotConfigPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [pageShowRule, setPageShowRule] = useState<BotConfigPageShowRule>(BotConfigPageShowRule.NO_BOT)
|
||||||
|
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||||
|
const [botList, setBotList] = useState<BotCardVO[]>([])
|
||||||
|
const [isEditForm, setIsEditForm] = useState(false)
|
||||||
|
const [nowSelectedBotCard, setNowSelectedBotCard] = useState<BotCardVO>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// TODO:补齐加载转圈逻辑
|
||||||
|
checkHasLLM().then((hasLLM) => {
|
||||||
|
if (hasLLM) {
|
||||||
|
const botList = getBotList()
|
||||||
|
if (botList.length === 0) {
|
||||||
|
setPageShowRule(BotConfigPageShowRule.NO_BOT)
|
||||||
|
} else {
|
||||||
|
setPageShowRule(BotConfigPageShowRule.HAVE_BOT)
|
||||||
|
}
|
||||||
|
setBotList(botList)
|
||||||
|
} else {
|
||||||
|
setPageShowRule(BotConfigPageShowRule.NO_LLM)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
async function checkHasLLM(): Promise<boolean> {
|
||||||
|
// NOT IMPL
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBotList(): BotCardVO[] {
|
||||||
|
let botList: BotCardVO[] = [
|
||||||
|
new BotCardVO({
|
||||||
|
adapter: "QQ bot",
|
||||||
|
description: "1111",
|
||||||
|
id: "1111",
|
||||||
|
name: "第一个bot",
|
||||||
|
updateTime: "202300001111",
|
||||||
|
pipelineName: "默认流水线",
|
||||||
|
}),
|
||||||
|
new BotCardVO({
|
||||||
|
adapter: "WX bot",
|
||||||
|
description: "22211",
|
||||||
|
id: "2222",
|
||||||
|
name: "第2个bot",
|
||||||
|
updateTime: "2025011011",
|
||||||
|
pipelineName: "默认流水线",
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
// botList = []
|
||||||
|
return botList
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreateBotClick() {
|
||||||
|
setIsEditForm(false)
|
||||||
|
setNowSelectedCard(undefined)
|
||||||
|
setModalOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNowSelectedCard(cardVO: BotCardVO | undefined) {
|
||||||
|
setNowSelectedBotCard(cardVO)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectBot(cardVO: BotCardVO) {
|
||||||
|
setIsEditForm(true)
|
||||||
|
setNowSelectedCard(cardVO)
|
||||||
|
console.log("set now vo", cardVO)
|
||||||
|
setModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.configPageContainer}>
|
||||||
|
<Modal
|
||||||
|
title={isEditForm ? "编辑机器人" : "创建机器人"}
|
||||||
|
centered
|
||||||
|
open={modalOpen}
|
||||||
|
onOk={() => setModalOpen(false)}
|
||||||
|
onCancel={() => setModalOpen(false)}
|
||||||
|
width={700}
|
||||||
|
footer={null}
|
||||||
|
destroyOnClose={true}
|
||||||
|
>
|
||||||
|
<BotForm
|
||||||
|
initBotId={nowSelectedBotCard?.id}
|
||||||
|
onFormSubmit={() => setIsEditForm(false)}
|
||||||
|
onFormCancel={() => setModalOpen(false)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
{pageShowRule === BotConfigPageShowRule.NO_LLM &&
|
||||||
|
<EmptyAndCreateComponent
|
||||||
|
title={"需要先创建大模型才能配置机器人哦~"}
|
||||||
|
subTitle={"快去创建一个吧!"}
|
||||||
|
buttonText={"创建大模型 GO!"}
|
||||||
|
onButtonClick={() => {
|
||||||
|
router.push("/home/llm-config");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{pageShowRule === BotConfigPageShowRule.NO_BOT &&
|
||||||
|
<EmptyAndCreateComponent
|
||||||
|
title={"您还未配置机器人哦~"}
|
||||||
|
subTitle={"快去创建一个吧!"}
|
||||||
|
buttonText={"创建机器人 +"}
|
||||||
|
onButtonClick={handleCreateBotClick}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{pageShowRule === BotConfigPageShowRule.HAVE_BOT &&
|
||||||
|
<div className={`${styles.botListContainer}`}
|
||||||
|
>
|
||||||
|
{botList.map(cardVO => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={cardVO.id}
|
||||||
|
onClick={() => {selectBot(cardVO)}}
|
||||||
|
>
|
||||||
|
<BotCard botCardVO={cardVO} />
|
||||||
|
</div>)
|
||||||
|
})}
|
||||||
|
<CreateCardComponent
|
||||||
|
width={360}
|
||||||
|
height={200}
|
||||||
|
plusSize={90}
|
||||||
|
onClick={handleCreateBotClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BotConfigPageShowRule {
|
||||||
|
NO_LLM,
|
||||||
|
NO_BOT,
|
||||||
|
HAVE_BOT,
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {IDynamicFormItemConfig} from "@/app/home/components/dynamic-form/DynamicFormItemConfig";
|
||||||
|
import {Form, FormInstance} from "antd";
|
||||||
|
import DynamicFormItemComponent from "@/app/home/components/dynamic-form/DynamicFormItemComponent";
|
||||||
|
|
||||||
|
export default function DynamicFormComponent({
|
||||||
|
form,
|
||||||
|
itemConfigList,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
form: FormInstance<object>
|
||||||
|
itemConfigList: IDynamicFormItemConfig[]
|
||||||
|
onSubmit?: (val: object) => unknown
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onFinish={onSubmit}
|
||||||
|
layout={"vertical"}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
itemConfigList.map(config =>
|
||||||
|
<DynamicFormItemComponent
|
||||||
|
key={config.id}
|
||||||
|
config={config}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import {Form, Input, InputNumber, Select, Switch} from "antd";
|
||||||
|
import {DynamicFormItemType, IDynamicFormItemConfig} from "@/app/home/components/dynamic-form/DynamicFormItemConfig";
|
||||||
|
|
||||||
|
export default function DynamicFormItemComponent({
|
||||||
|
config
|
||||||
|
}: {
|
||||||
|
config: IDynamicFormItemConfig
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
label={config.label.zh_CN}
|
||||||
|
name={config.name}
|
||||||
|
rules={[{required: config.required, message: "该项为必填项哦~"}]}
|
||||||
|
initialValue={config.default}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
config.type === DynamicFormItemType.INT &&
|
||||||
|
<InputNumber/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
config.type === DynamicFormItemType.STRING &&
|
||||||
|
<Input/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
config.type === DynamicFormItemType.BOOLEAN &&
|
||||||
|
<Switch defaultChecked/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
config.type === DynamicFormItemType.STRING_ARRAY &&
|
||||||
|
<Select options={[]}/>
|
||||||
|
}
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
export interface IDynamicFormItemConfig {
|
||||||
|
id: string;
|
||||||
|
default: string | number | boolean | Array<unknown>;
|
||||||
|
label: IDynamicFormItemLabel;
|
||||||
|
name: string;
|
||||||
|
required: boolean;
|
||||||
|
type: DynamicFormItemType
|
||||||
|
description?: IDynamicFormItemLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DynamicFormItemConfig implements IDynamicFormItemConfig {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
default: string | number | boolean | Array<unknown>;
|
||||||
|
label: IDynamicFormItemLabel;
|
||||||
|
required: boolean;
|
||||||
|
type: DynamicFormItemType;
|
||||||
|
description?: IDynamicFormItemLabel;
|
||||||
|
|
||||||
|
constructor(params: IDynamicFormItemConfig) {
|
||||||
|
this.id = params.id;
|
||||||
|
this.name = params.name;
|
||||||
|
this.default = params.default;
|
||||||
|
this.label = params.label;
|
||||||
|
this.required = params.required;
|
||||||
|
this.type = params.type;
|
||||||
|
this.description = params.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDynamicFormItemLabel {
|
||||||
|
en_US: string,
|
||||||
|
zh_CN: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DynamicFormItemType {
|
||||||
|
INT = "integer",
|
||||||
|
STRING = "string",
|
||||||
|
BOOLEAN = "boolean",
|
||||||
|
STRING_ARRAY = "array[string]",
|
||||||
|
UNKNOWN = "unknown",
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDynamicFormItemType(value: string): value is DynamicFormItemType {
|
||||||
|
return Object.values(DynamicFormItemType).includes(value as DynamicFormItemType);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseDynamicFormItemType(value: string): DynamicFormItemType {
|
||||||
|
return isDynamicFormItemType(value) ? value : DynamicFormItemType.UNKNOWN;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
DynamicFormItemConfig,
|
||||||
|
DynamicFormItemType,
|
||||||
|
IDynamicFormItemConfig
|
||||||
|
} from "@/app/home/components/dynamic-form/DynamicFormItemConfig";
|
||||||
|
|
||||||
|
export const testDynamicConfigList: IDynamicFormItemConfig[] = [
|
||||||
|
new DynamicFormItemConfig({
|
||||||
|
default: "",
|
||||||
|
id: "111",
|
||||||
|
label: {
|
||||||
|
zh_CN: "测试字段string",
|
||||||
|
en_US: "eng test"
|
||||||
|
},
|
||||||
|
name: "string_test",
|
||||||
|
required: false,
|
||||||
|
type: DynamicFormItemType.STRING
|
||||||
|
}),
|
||||||
|
new DynamicFormItemConfig({
|
||||||
|
default: "",
|
||||||
|
id: "222",
|
||||||
|
label: {
|
||||||
|
zh_CN: "测试字段int",
|
||||||
|
en_US: "int eng test"
|
||||||
|
},
|
||||||
|
name: "int_test",
|
||||||
|
required: true,
|
||||||
|
type: DynamicFormItemType.INT
|
||||||
|
}),
|
||||||
|
new DynamicFormItemConfig({
|
||||||
|
default: "",
|
||||||
|
id: "333",
|
||||||
|
label: {
|
||||||
|
zh_CN: "测试字段boolean",
|
||||||
|
en_US: "boolean eng test"
|
||||||
|
},
|
||||||
|
name: "boolean_test",
|
||||||
|
required: false,
|
||||||
|
type: DynamicFormItemType.BOOLEAN
|
||||||
|
}),
|
||||||
|
]
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import styles from "./emptyAndCreate.module.css";
|
||||||
|
|
||||||
|
export default function EmptyAndCreateComponent({
|
||||||
|
title,
|
||||||
|
subTitle,
|
||||||
|
buttonText,
|
||||||
|
onButtonClick,
|
||||||
|
}: {
|
||||||
|
title: string,
|
||||||
|
subTitle: string,
|
||||||
|
buttonText: string,
|
||||||
|
onButtonClick: () => void,
|
||||||
|
}) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.emptyPageContainer}`}>
|
||||||
|
<div className={`${styles.emptyContainer}`}>
|
||||||
|
<div className={`${styles.emptyInfoContainer}`}>
|
||||||
|
<div className={`${styles.emptyInfoText}`}>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.emptyInfoSubText}`}>
|
||||||
|
{subTitle}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${styles.emptyCreateButton}`}
|
||||||
|
onClick={onButtonClick}
|
||||||
|
>
|
||||||
|
{buttonText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
.emptyPageContainer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #FFF;
|
||||||
|
border: 1px solid #c5c5c5;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyContainer {
|
||||||
|
width: 100%;
|
||||||
|
height: 50%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyCreateButton {
|
||||||
|
width: 200px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: #6062E7;
|
||||||
|
color: #FFF;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 50px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyCreateButton:hover {
|
||||||
|
background-color: #4b4de3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyInfoContainer {
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
color: #353535;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyInfoText {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyInfoSubText {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
.sidebarContainer {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 240px;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #FFF;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.langbotIconContainer {
|
||||||
|
width: 240px;
|
||||||
|
height: 70px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
|
||||||
|
.langbotIcon {
|
||||||
|
width: 54px;
|
||||||
|
height: 54px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #6062E7;
|
||||||
|
color: #fbfbfb;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 36px;
|
||||||
|
line-height: 54px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.langbotText {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.sidebarChildContainer {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 198px;
|
||||||
|
height: 48px;
|
||||||
|
margin: 12px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarSelected {
|
||||||
|
background-color: #6062E7;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarUnselected {
|
||||||
|
background-color: white;
|
||||||
|
color: #6C6C6C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarChildIcon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
margin-left: 16px;
|
||||||
|
margin-right: 6px;
|
||||||
|
background-color: rgba(96, 149, 209, 0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import styles from "./HomeSidebar.module.css"
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {SidebarChild, SidebarChildVO} from "@/app/home/components/home-sidebar/HomeSidebarChild";
|
||||||
|
import {useRouter, usePathname, useSearchParams} from "next/navigation";
|
||||||
|
import {sidebarConfigList} from "@/app/home/components/home-sidebar/sidbarConfigList";
|
||||||
|
|
||||||
|
// TODO 侧边导航栏要加动画
|
||||||
|
export default function HomeSidebar({
|
||||||
|
onSelectedChange
|
||||||
|
}: {
|
||||||
|
onSelectedChange: (sidebarChild: SidebarChildVO) => void
|
||||||
|
}) {
|
||||||
|
// 路由相关
|
||||||
|
const router = useRouter()
|
||||||
|
const pathname = usePathname();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
// 路由被动变化时处理
|
||||||
|
useEffect(() => {
|
||||||
|
handleRouteChange(pathname)
|
||||||
|
}, [pathname, searchParams]);
|
||||||
|
|
||||||
|
const [selectedChild, setSelectedChild] = useState<SidebarChildVO>(sidebarConfigList[0])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('HomeSidebar挂载完成');
|
||||||
|
initSelect()
|
||||||
|
return () => console.log('HomeSidebar卸载');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function handleChildClick(child: SidebarChildVO) {
|
||||||
|
setSelectedChild(child)
|
||||||
|
handleRoute(child)
|
||||||
|
onSelectedChange(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSelect() {
|
||||||
|
handleChildClick(sidebarConfigList[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRoute(child: SidebarChildVO) {
|
||||||
|
console.log(child)
|
||||||
|
router.push(`${child.route}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRouteChange(pathname: string) {
|
||||||
|
// TODO 这段逻辑并不好,未来router封装好后改掉
|
||||||
|
// 判断在home下,并且路由更改的是自己的路由子组件则更新UI
|
||||||
|
const routeList = pathname.split('/')
|
||||||
|
if (
|
||||||
|
routeList[1] === "home" &&
|
||||||
|
sidebarConfigList.find(childConfig =>
|
||||||
|
childConfig.route === pathname
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
console.log("find success")
|
||||||
|
const routeSelectChild = sidebarConfigList.find(childConfig =>
|
||||||
|
childConfig.route === pathname
|
||||||
|
)
|
||||||
|
if (routeSelectChild) {
|
||||||
|
setSelectedChild(routeSelectChild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.sidebarContainer}`}>
|
||||||
|
{/* LangBot、ICON区域 */}
|
||||||
|
<div className={`${styles.langbotIconContainer}`}>
|
||||||
|
{/* icon */}
|
||||||
|
<div className={`${styles.langbotIcon}`}>
|
||||||
|
L
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.langbotText}`}>
|
||||||
|
Langbot
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 菜单列表,后期可升级成配置驱动 */}
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
sidebarConfigList.map(config => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={config.id}
|
||||||
|
onClick={() => {
|
||||||
|
console.log('click:', config.id)
|
||||||
|
handleChildClick(config)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SidebarChild
|
||||||
|
isSelected={selectedChild.id === config.id}
|
||||||
|
icon={config.icon}
|
||||||
|
name={config.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import styles from "./HomeSidebar.module.css";
|
||||||
|
|
||||||
|
export interface ISidebarChildVO {
|
||||||
|
id: string;
|
||||||
|
icon: string;
|
||||||
|
name: string;
|
||||||
|
route: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SidebarChildVO {
|
||||||
|
id: string;
|
||||||
|
icon: string;
|
||||||
|
name: string;
|
||||||
|
route: string;
|
||||||
|
|
||||||
|
constructor(props: ISidebarChildVO) {
|
||||||
|
this.id = props.id;
|
||||||
|
this.icon = props.icon;
|
||||||
|
this.name = props.name;
|
||||||
|
this.route = props.route;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function SidebarChild({
|
||||||
|
icon,
|
||||||
|
name,
|
||||||
|
isSelected,
|
||||||
|
}: {
|
||||||
|
icon: string;
|
||||||
|
name: string;
|
||||||
|
isSelected: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.sidebarChildContainer} ${isSelected ? styles.sidebarSelected : styles.sidebarUnselected}`}>
|
||||||
|
<div className={`${styles.sidebarChildIcon}`}/>
|
||||||
|
<div>{name}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import {SidebarChildVO} from "@/app/home/components/home-sidebar/HomeSidebarChild";
|
||||||
|
|
||||||
|
export const sidebarConfigList = [
|
||||||
|
new SidebarChildVO({
|
||||||
|
id: "llm-config",
|
||||||
|
name: "大模型配置",
|
||||||
|
icon: "",
|
||||||
|
route: "/home/llm-config",
|
||||||
|
}),
|
||||||
|
new SidebarChildVO({
|
||||||
|
id: "platform-config",
|
||||||
|
name: "机器人配置",
|
||||||
|
icon: "",
|
||||||
|
route: "/home/bot-config",
|
||||||
|
}),
|
||||||
|
new SidebarChildVO({
|
||||||
|
id: "plugin-config",
|
||||||
|
name: "插件配置",
|
||||||
|
icon: "",
|
||||||
|
route: "/home/plugin-config",
|
||||||
|
}),
|
||||||
|
new SidebarChildVO({
|
||||||
|
id: "pipeline-config",
|
||||||
|
name: "流水线配置",
|
||||||
|
icon: "",
|
||||||
|
route: "/home/pipeline-config",
|
||||||
|
})
|
||||||
|
]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import styles from "./HomeTittleBar.module.css"
|
||||||
|
|
||||||
|
|
||||||
|
export default function HomeTitleBar({
|
||||||
|
title,
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.titleBarContainer}`}>
|
||||||
|
<div
|
||||||
|
className={`${styles.titleText}`}
|
||||||
|
>{title}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
.titleBarContainer {
|
||||||
|
width: 100%;
|
||||||
|
height: 70px;
|
||||||
|
background-color: #FFF;
|
||||||
|
font-size: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleText {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
.homeLayoutContainer {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #FAFBFB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainContent {
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
height: calc(100% - 110px);
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import '@ant-design/v5-patch-for-react-19';
|
||||||
|
import styles from "./layout.module.css"
|
||||||
|
import HomeSidebar from "@/app/home/components/home-sidebar/HomeSidebar";
|
||||||
|
import HomeTitleBar from "@/app/home/components/home-titlebar/HomeTitleBar";
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import {SidebarChildVO} from "@/app/home/components/home-sidebar/HomeSidebarChild";
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
export default function HomeLayout({
|
||||||
|
children
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [title, setTitle] = useState<string>("")
|
||||||
|
const onSelectedChange = (child: SidebarChildVO) => {
|
||||||
|
setTitle(child.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.homeLayoutContainer}`}>
|
||||||
|
<HomeSidebar
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
<div className={`${styles.main}`}>
|
||||||
|
<HomeTitleBar title={title}/>
|
||||||
|
{/* 主页面 */}
|
||||||
|
<div className={`${styles.mainContent}`}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export interface ICreateLLMField {
|
||||||
|
name: string;
|
||||||
|
model_provider: string;
|
||||||
|
url: string;
|
||||||
|
api_key: string;
|
||||||
|
abilities: string[];
|
||||||
|
extra_args: string[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
.configPageContainer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalContainer {
|
||||||
|
width: 100%;
|
||||||
|
/*height: calc(100vh - 200px);*/
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modelListContainer {
|
||||||
|
align-self: flex-start;
|
||||||
|
justify-self: flex-start;
|
||||||
|
width: calc(100% - 60px);
|
||||||
|
margin: auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
justify-items: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyContainer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContainer {
|
||||||
|
width: 360px;
|
||||||
|
height: 200px;
|
||||||
|
background-color: #FFF;
|
||||||
|
border-radius: 9px;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.4) 0 1px 1px -1px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconBasicInfoContainer {
|
||||||
|
width: 300px;
|
||||||
|
height: 100px;
|
||||||
|
margin-left: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 40px;
|
||||||
|
line-height: 90px;
|
||||||
|
text-align: center;
|
||||||
|
color: #ffffff;
|
||||||
|
background: rgba(96, 149, 209, 0.31);
|
||||||
|
border: 1px solid rgba(96, 149, 209, 0.31);
|
||||||
|
}
|
||||||
|
|
||||||
|
.basicInfoContainer {
|
||||||
|
width: 200px;
|
||||||
|
height: 90px;
|
||||||
|
padding-left: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basicInfoText {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.bigText {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.urlAndUpdateText {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import styles from "../../LLMConfig.module.css"
|
||||||
|
import {LLMCardVO} from "@/app/home/llm-config/component/llm-card/LLMCardVO";
|
||||||
|
|
||||||
|
export default function LLMCard({
|
||||||
|
cardVO
|
||||||
|
}: {
|
||||||
|
cardVO: LLMCardVO
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.cardContainer}`}>
|
||||||
|
{/* icon和基本信息 */}
|
||||||
|
<div className={`${styles.iconBasicInfoContainer}`}>
|
||||||
|
{/* icon */}
|
||||||
|
<div className={`${styles.icon}`}>
|
||||||
|
ICO
|
||||||
|
</div>
|
||||||
|
{/* bot基本信息 */}
|
||||||
|
<div className={`${styles.basicInfoContainer}`}>
|
||||||
|
<div className={`${styles.basicInfoText} ${styles.bigText}`}>
|
||||||
|
{cardVO.name}
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.basicInfoText}`}>
|
||||||
|
使用模型:{cardVO.model}
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.basicInfoText}`}>
|
||||||
|
厂商:{cardVO.company}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* URL和创建时间 */}
|
||||||
|
<div className={`${styles.urlAndUpdateText}`}>
|
||||||
|
URL:{cardVO.URL}
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.urlAndUpdateText}`}>
|
||||||
|
更新时间:{cardVO.updateTime}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export interface ILLMCardVO {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
model: string;
|
||||||
|
company: string;
|
||||||
|
URL: string;
|
||||||
|
updateTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LLMCardVO implements ILLMCardVO {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
model: string;
|
||||||
|
company: string;
|
||||||
|
URL: string;
|
||||||
|
updateTime: string;
|
||||||
|
|
||||||
|
constructor(props: ILLMCardVO) {
|
||||||
|
this.id = props.id;
|
||||||
|
this.name = props.name;
|
||||||
|
this.model = props.model;
|
||||||
|
this.company = props.company;
|
||||||
|
this.URL = props.URL;
|
||||||
|
this.updateTime = props.updateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import styles from "@/app/home/llm-config/LLMConfig.module.css";
|
||||||
|
import {Button, Form, Input, Select, SelectProps, Space} from "antd";
|
||||||
|
import {ICreateLLMField} from "@/app/home/llm-config/ICreateLLMField";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
|
||||||
|
export default function LLMForm({
|
||||||
|
editMode,
|
||||||
|
initLLMId,
|
||||||
|
onFormSubmit,
|
||||||
|
onFormCancel,
|
||||||
|
}: {
|
||||||
|
editMode: boolean;
|
||||||
|
initLLMId?: string;
|
||||||
|
onFormSubmit: (value: ICreateLLMField) => void;
|
||||||
|
onFormCancel: (value: ICreateLLMField) => void;
|
||||||
|
}) {
|
||||||
|
const [form] = Form.useForm<ICreateLLMField>();
|
||||||
|
const extraOptions: SelectProps['options'] = []
|
||||||
|
const [initValue, setInitValue] = useState<ICreateLLMField>()
|
||||||
|
const abilityOptions: SelectProps['options'] = [
|
||||||
|
{
|
||||||
|
label: '函数调用',
|
||||||
|
value: 'func_call',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '图像识别',
|
||||||
|
value: 'vision',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
useEffect(() => {
|
||||||
|
if (editMode && initLLMId) {
|
||||||
|
getLLMConfig(initLLMId).then(val => {
|
||||||
|
form.setFieldsValue(val)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
form.resetFields()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function getLLMConfig(id: string): Promise<ICreateLLMField> {
|
||||||
|
return {
|
||||||
|
name: id,
|
||||||
|
model_provider: "OpenAI",
|
||||||
|
url: "www.aaa.com",
|
||||||
|
api_key: "",
|
||||||
|
abilities: [],
|
||||||
|
extra_args: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFormSubmit(value: ICreateLLMField) {
|
||||||
|
if (editMode) {
|
||||||
|
onSaveEdit(value)
|
||||||
|
} else {
|
||||||
|
onCreateLLM(value)
|
||||||
|
}
|
||||||
|
onFormSubmit(value)
|
||||||
|
form.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSaveEdit(value: ICreateLLMField) {
|
||||||
|
console.log("edit save", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCreateLLM(value: ICreateLLMField) {
|
||||||
|
console.log("create llm", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAbilitiesChange() {
|
||||||
|
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={styles.modalContainer}>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
labelCol={{span: 4}}
|
||||||
|
wrapperCol={{span: 14}}
|
||||||
|
layout='horizontal'
|
||||||
|
initialValues={{
|
||||||
|
...initValue
|
||||||
|
}}
|
||||||
|
onFinish={handleFormSubmit}
|
||||||
|
clearOnDestroy={true}
|
||||||
|
>
|
||||||
|
<Form.Item<ICreateLLMField>
|
||||||
|
label={"模型名称"}
|
||||||
|
name={"name"}
|
||||||
|
rules={[{required: true, message: "该项为必填项哦~"}]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder={"为自己的大模型取个好听的名字~"}
|
||||||
|
style={{width: 260}}
|
||||||
|
></Input>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item<ICreateLLMField>
|
||||||
|
label={"模型供应商"}
|
||||||
|
name={"model_provider"}
|
||||||
|
rules={[{required: true, message: "该项为必填项哦~"}]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
style={{width: 120}}
|
||||||
|
onChange={() => {
|
||||||
|
}}
|
||||||
|
options={[
|
||||||
|
{value: 'OpenAI', label: 'OpenAI'},
|
||||||
|
{value: 'OLAMA', label: 'OLAMA'},
|
||||||
|
{value: 'DeepSeek', label: 'DeepSeek'},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item<ICreateLLMField>
|
||||||
|
label={"请求URL"}
|
||||||
|
name={"url"}
|
||||||
|
rules={[{required: true, message: "该项为必填项哦~"}]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="请求地址,一般是API提供商提供的URL"
|
||||||
|
style={{width: 500}}
|
||||||
|
></Input>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item<ICreateLLMField>
|
||||||
|
label={"开启能力"}
|
||||||
|
name={"abilities"}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
mode="tags"
|
||||||
|
style={{width: 500}}
|
||||||
|
placeholder="选择模型能力,输入回车可自定义能力"
|
||||||
|
onChange={handleAbilitiesChange}
|
||||||
|
options={abilityOptions}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item<ICreateLLMField>
|
||||||
|
label={"其他参数"}
|
||||||
|
name={"extra_args"}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
mode="tags"
|
||||||
|
style={{width: 500}}
|
||||||
|
placeholder="输入后回车可自定义其他参数,例 key:value"
|
||||||
|
onChange={handleAbilitiesChange}
|
||||||
|
options={extraOptions}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
wrapperCol={{offset: 4, span: 14}}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
{
|
||||||
|
!editMode &&
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
提交
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
editMode &&
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
<Button htmlType="button" onClick={() => {
|
||||||
|
onFormCancel(form.getFieldsValue())
|
||||||
|
}}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {useState} from "react";
|
||||||
|
import {LLMCardVO} from "@/app/home/llm-config/component/llm-card/LLMCardVO";
|
||||||
|
import styles from "./LLMConfig.module.css"
|
||||||
|
import EmptyAndCreateComponent from "@/app/home/components/empty-and-create-component/EmptyAndCreateComponent";
|
||||||
|
import {Modal} from "antd";
|
||||||
|
import LLMCard from "@/app/home/llm-config/component/llm-card/LLMCard";
|
||||||
|
import LLMForm from "@/app/home/llm-config/component/llm-form/LLMForm";
|
||||||
|
import CreateCardComponent from "@/app/infra/basic-component/create-card-component/CreateCardComponent";
|
||||||
|
|
||||||
|
export default function LLMConfigPage() {
|
||||||
|
const [cardList, setCardList] = useState<LLMCardVO[]>([
|
||||||
|
new LLMCardVO({
|
||||||
|
id: "1",
|
||||||
|
name: "测试模型",
|
||||||
|
model: "GPT-4o",
|
||||||
|
URL: "www.openai.com",
|
||||||
|
company: "OpenAI",
|
||||||
|
updateTime: "2025.1.2"
|
||||||
|
}),
|
||||||
|
new LLMCardVO({
|
||||||
|
id: "2",
|
||||||
|
name: "测试模型",
|
||||||
|
model: "GPT-4o",
|
||||||
|
URL: "www.openai.com",
|
||||||
|
company: "OpenAI",
|
||||||
|
updateTime: "2025.1.2"
|
||||||
|
}),
|
||||||
|
new LLMCardVO({
|
||||||
|
id: "3",
|
||||||
|
name: "测试模型",
|
||||||
|
model: "GPT-4o",
|
||||||
|
URL: "www.openai.com",
|
||||||
|
company: "OpenAI",
|
||||||
|
updateTime: "2025.1.2"
|
||||||
|
}),
|
||||||
|
new LLMCardVO({
|
||||||
|
id: "4",
|
||||||
|
name: "测试模型",
|
||||||
|
model: "GPT-4o",
|
||||||
|
URL: "www.openai.com",
|
||||||
|
company: "OpenAI",
|
||||||
|
updateTime: "2025.1.2"
|
||||||
|
}),
|
||||||
|
new LLMCardVO({
|
||||||
|
id: "5",
|
||||||
|
name: "测试模型",
|
||||||
|
model: "GPT-4o",
|
||||||
|
URL: "www.openai.com",
|
||||||
|
company: "OpenAI",
|
||||||
|
updateTime: "2025.1.2"
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||||
|
const [isEditForm, setIsEditForm] = useState(false)
|
||||||
|
const [nowSelectedLLM, setNowSelectedLLM] = useState<LLMCardVO | null>(null)
|
||||||
|
|
||||||
|
function selectLLM(cardVO: LLMCardVO) {
|
||||||
|
setIsEditForm(true)
|
||||||
|
setNowSelectedLLM(cardVO)
|
||||||
|
console.log("set now vo", cardVO)
|
||||||
|
setModalOpen(true)
|
||||||
|
}
|
||||||
|
function handleCreateModelClick() {
|
||||||
|
setIsEditForm(false)
|
||||||
|
setNowSelectedLLM(null)
|
||||||
|
setModalOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.configPageContainer}>
|
||||||
|
<Modal
|
||||||
|
title={isEditForm ? "编辑模型" : "创建模型"}
|
||||||
|
centered
|
||||||
|
open={modalOpen}
|
||||||
|
onOk={() => setModalOpen(false)}
|
||||||
|
onCancel={() => setModalOpen(false)}
|
||||||
|
width={700}
|
||||||
|
footer={null}
|
||||||
|
>
|
||||||
|
<LLMForm
|
||||||
|
editMode={isEditForm}
|
||||||
|
initLLMId={nowSelectedLLM?.id}
|
||||||
|
onFormSubmit={() => {
|
||||||
|
setModalOpen(false);
|
||||||
|
}}
|
||||||
|
onFormCancel={() => {
|
||||||
|
setModalOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
{
|
||||||
|
cardList.length > 0 &&
|
||||||
|
<div className={`${styles.modelListContainer}`}
|
||||||
|
>
|
||||||
|
{cardList.map(cardVO => {
|
||||||
|
return <div key={cardVO.id} onClick={() => {selectLLM(cardVO)}}>
|
||||||
|
<LLMCard cardVO={cardVO}></LLMCard>
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
<CreateCardComponent
|
||||||
|
width={360}
|
||||||
|
height={200}
|
||||||
|
plusSize={90}
|
||||||
|
onClick={handleCreateModelClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
cardList.length === 0 &&
|
||||||
|
<div className={`${styles.emptyContainer}`}>
|
||||||
|
<EmptyAndCreateComponent
|
||||||
|
title={"模型列表空空如也~"}
|
||||||
|
subTitle={"快去创建一个吧!"}
|
||||||
|
buttonText={"创建模型 +"}
|
||||||
|
onButtonClick={() => {
|
||||||
|
handleCreateModelClick()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div className={``}>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import {DynamicFormItemConfig} from "@/app/home/components/dynamic-form/DynamicFormItemConfig";
|
||||||
|
|
||||||
|
export interface IPipelineChildFormEntity {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
formItems: DynamicFormItemConfig[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PipelineChildFormEntity implements IPipelineChildFormEntity {
|
||||||
|
formItems: DynamicFormItemConfig[];
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
constructor(props: IPipelineChildFormEntity) {
|
||||||
|
this.form = props.form;
|
||||||
|
this.label = props.label;
|
||||||
|
this.name = props.name;
|
||||||
|
this.formItems = props.formItems;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,512 @@
|
|||||||
|
import {Form, Button, Switch, Select, Input, InputNumber} from "antd";
|
||||||
|
import { CaretLeftOutlined, CaretRightOutlined } from '@ant-design/icons';
|
||||||
|
import {useState} from "react";
|
||||||
|
import styles from "./pipelineFormStyle.module.css"
|
||||||
|
|
||||||
|
export default function PipelineFormComponent({
|
||||||
|
onFinish,
|
||||||
|
onCancel,
|
||||||
|
}: {
|
||||||
|
onFinish: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}) {
|
||||||
|
const [nowFormIndex, setNowFormIndex] = useState<number>(0)
|
||||||
|
// 这里不好,可以改成enum等
|
||||||
|
const formLabelList: FormLabel[] = [
|
||||||
|
{label: "AI能力", name: "ai"},
|
||||||
|
{label: "触发条件", name: "trigger"},
|
||||||
|
{label: "安全能力", name: "safety"},
|
||||||
|
{label: "输出处理", name: "output"},
|
||||||
|
]
|
||||||
|
|
||||||
|
function getNowFormLabel() {
|
||||||
|
return formLabelList[nowFormIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getPreFormLabel(): undefined | FormLabel {
|
||||||
|
if (nowFormIndex !== undefined && nowFormIndex > 0) {
|
||||||
|
return formLabelList[nowFormIndex - 1]
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextFormLabel(): undefined | FormLabel {
|
||||||
|
if (nowFormIndex !== undefined && nowFormIndex < formLabelList.length - 1) {
|
||||||
|
return formLabelList[nowFormIndex + 1]
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFormLabelIndex() {
|
||||||
|
if (nowFormIndex < formLabelList.length - 1) {
|
||||||
|
setNowFormIndex(nowFormIndex + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reduceFormLabelIndex() {
|
||||||
|
if (nowFormIndex > 0) {
|
||||||
|
setNowFormIndex(nowFormIndex - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ maxHeight: '70vh', overflowY: 'auto' }}
|
||||||
|
>
|
||||||
|
<h1>
|
||||||
|
{getNowFormLabel().label}
|
||||||
|
</h1>
|
||||||
|
{/* AI能力表单 ai */}
|
||||||
|
<Form
|
||||||
|
layout={"vertical"}
|
||||||
|
style={{ display: getNowFormLabel().name === "ai" ? 'block' : 'none' }}
|
||||||
|
>
|
||||||
|
{/* Runner 配置区块 */}
|
||||||
|
<div className={`${styles.formItemSubtitle}`}>运行器</div>
|
||||||
|
<Form.Item
|
||||||
|
label="运行器"
|
||||||
|
name={["runner", "runner"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ label: "内置 Agent", value: "local-agent" },
|
||||||
|
{ label: "Dify 服务 API", value: "dify-service-api" },
|
||||||
|
{ label: "阿里云百炼平台 API", value: "dashscope-app-api" }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* 内置 Agent 配置区块 */}
|
||||||
|
<div className={`${styles.formItemSubtitle}`}>配置内置Agent</div>
|
||||||
|
{/* TODO 这里要拉模型 */}
|
||||||
|
<Form.Item
|
||||||
|
label="模型"
|
||||||
|
name={["local-agent", "model"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
tooltip="从模型库中选择"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[]}
|
||||||
|
placeholder="请选择语言模型"
|
||||||
|
showSearch
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="最大回合数"
|
||||||
|
name={["local-agent", "max-round"]}
|
||||||
|
rules={[{
|
||||||
|
required: true,
|
||||||
|
}]}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
precision={0}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
{/* TODO 这里要做转换处理 */}
|
||||||
|
<Form.Item
|
||||||
|
label="提示词"
|
||||||
|
name={["local-agent", "prompt"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
tooltip="按JSON格式输入"
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
rows={4}
|
||||||
|
placeholder={`示例结构:{ "role": "user", "content": "你好" } `}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* Dify 服务 API 区块 */}
|
||||||
|
<div className={`${styles.formItemSubtitle}`}>配置Dify服务API</div>
|
||||||
|
<Form.Item
|
||||||
|
label="基础 URL"
|
||||||
|
name={["dify-service-api", "base-url"]}
|
||||||
|
rules={[
|
||||||
|
{ required: true },
|
||||||
|
{ type: 'url', message: '请输入有效的URL地址' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="应用类型"
|
||||||
|
name={["dify-service-api", "app-type"]}
|
||||||
|
initialValue={"chat"}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ label: "聊天(包括Chatflow)", value: "chat" },
|
||||||
|
{ label: "Agent", value: "agent" },
|
||||||
|
{ label: "工作流", value: "workflow" }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="API 密钥"
|
||||||
|
name={["dify-service-api", "api-key"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Input.Password visibilityToggle={false} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="思维链转换"
|
||||||
|
name={["dify-service-api", "thinking-convert"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ label: "转换成 \<think\>...\<\/think\>", value: "plain" },
|
||||||
|
{ label: "原始", value: "original" },
|
||||||
|
{ label: "移除", value: "remove" }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* 阿里云百炼区块 */}
|
||||||
|
<div className={`${styles.formItemSubtitle}`}>配置阿里云百炼平台 API</div>
|
||||||
|
<Form.Item
|
||||||
|
label="应用类型"
|
||||||
|
name={["dashscope-app-api", "app-type"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ label: "Agent", value: "agent" },
|
||||||
|
{ label: "工作流", value: "workflow" }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="API 密钥"
|
||||||
|
name={["dashscope-app-api", "api-key"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Input.Password visibilityToggle={false} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="应用 ID"
|
||||||
|
name={["dashscope-app-api", "app-id"]}
|
||||||
|
rules={[
|
||||||
|
{ required: true },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="引用文本"
|
||||||
|
name={["dashscope-app-api", "references_quote"]}
|
||||||
|
initialValue={"参考资料来自:"}
|
||||||
|
>
|
||||||
|
<Input.TextArea rows={2} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
{/* 触发条件表单 trigger */}
|
||||||
|
<Form
|
||||||
|
layout={"vertical"}
|
||||||
|
style={{ display: getNowFormLabel().name === "trigger" ? 'block' : 'none' }}
|
||||||
|
>
|
||||||
|
{/* 群响应规则块 */}
|
||||||
|
<Form.Item>
|
||||||
|
群响应规则
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={"是否在消息@机器人时触发"}
|
||||||
|
name={["group-respond-rules", "at"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={"消息前缀"}
|
||||||
|
name={["group-respond-rules", "prefix"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ value: "\"type\": \"string\"", label: "\"type\": \"string\"" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={"正则表达式"}
|
||||||
|
name={["group-respond-rules", "regexp"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
mode="tags"
|
||||||
|
options={[]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={"随机"}
|
||||||
|
name={["group-respond-rules", "random"]}
|
||||||
|
rules={[{ required: false }]}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
max={1}
|
||||||
|
min={0}
|
||||||
|
step={0.05}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
访问控制
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={"模式"}
|
||||||
|
name={["access-control", "mode"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
tooltip={"访问控制模式"}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{label: "黑名单", value: "blacklist"},
|
||||||
|
{label: "白名单", value: "Whitelist"},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={"黑名单"}
|
||||||
|
name={["access-control", "blacklist"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
mode={"tags"}
|
||||||
|
options={[]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={"白名单"}
|
||||||
|
name={["access-control", "whitelist"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
mode={"tags"}
|
||||||
|
options={[]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={"消息忽略规则"}
|
||||||
|
>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={"前缀"}
|
||||||
|
name={["ignore-rules", "whitelist"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
tooltip={"消息前缀"}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
mode={"tags"}
|
||||||
|
options={[]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={"正则表达式"}
|
||||||
|
name={["ignore-rules", "regexp"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
tooltip={"消息正则表达式"}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
mode={"tags"}
|
||||||
|
options={[]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
{/* 安全控制表单 safety */}
|
||||||
|
<Form
|
||||||
|
layout={"vertical"}
|
||||||
|
style={{ display: getNowFormLabel().name === "safety" ? 'block' : 'none' }}
|
||||||
|
>
|
||||||
|
{/* 内容过滤块 content-filter */}
|
||||||
|
<Form.Item
|
||||||
|
label={"检查范围"}
|
||||||
|
name={["content-filter", "scope"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{label: "全部", value: "all"},
|
||||||
|
{label: "传入消息(用户消息)", value: "income-msg"},
|
||||||
|
{label: "传出消息(机器人消息)", value: "output-msg"},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={"检查敏感词"}
|
||||||
|
name={["content-filter", "check-sensitive-words"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Switch/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* 速率限制块 rate-limit */}
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={"窗口长度(秒)"}
|
||||||
|
name={["rate-limit", "window-length"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
initialValue={60}
|
||||||
|
>
|
||||||
|
<InputNumber></InputNumber>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={"限制次数"}
|
||||||
|
name={["rate-limit", "limitation"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
initialValue={60}
|
||||||
|
>
|
||||||
|
<InputNumber/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={"策略"}
|
||||||
|
name={["rate-limit", "strategy"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
initialValue={"drop"}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{label: "丢弃", value: "drop"},
|
||||||
|
{label: "等待", value: "wait"},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
{/* 输出处理控制表单 output */}
|
||||||
|
<Form
|
||||||
|
layout={"vertical"}
|
||||||
|
style={{ display: getNowFormLabel().name === "output" ? 'block' : 'none' }}
|
||||||
|
>
|
||||||
|
{/* 长文本处理区块 */}
|
||||||
|
<Form.Item label="长文本处理" />
|
||||||
|
<Form.Item
|
||||||
|
label="阈值"
|
||||||
|
name={["long-text-processing", "threshold"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="策略"
|
||||||
|
name={["long-text-processing", "strategy"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ label: "转发消息组件", value: "forward" },
|
||||||
|
{ label: "转换为图片", value: "image" }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="字体路径"
|
||||||
|
name={["long-text-processing", "font-path"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* 强制延迟区块 */}
|
||||||
|
<Form.Item label="强制延迟" />
|
||||||
|
<Form.Item
|
||||||
|
label="最小秒数"
|
||||||
|
name={["force-delay", "min"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="最大秒数"
|
||||||
|
name={["force-delay", "max"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* 杂项区块 */}
|
||||||
|
<Form.Item label="杂项" />
|
||||||
|
<Form.Item
|
||||||
|
label="不输出异常信息给用户"
|
||||||
|
name={["misc", "hide-exception"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="在回复中@发送者"
|
||||||
|
name={["misc", "at-sender"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="引用原文"
|
||||||
|
name={["misc", "quote-origin"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="跟踪函数调用"
|
||||||
|
name={["misc", "track-function-calls"]}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<div className={`${styles.changeFormButtonGroupContainer}`}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<CaretLeftOutlined/>}
|
||||||
|
onClick={reduceFormLabelIndex}
|
||||||
|
disabled={!getPreFormLabel()}
|
||||||
|
>
|
||||||
|
{getPreFormLabel()?.label || "暂无更多"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<CaretRightOutlined />}
|
||||||
|
onClick={addFormLabelIndex}
|
||||||
|
disabled={!getNextFormLabel()}
|
||||||
|
iconPosition={"end"}
|
||||||
|
>
|
||||||
|
{getNextFormLabel()?.label || "暂无更多"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PipelineFormRoute {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormPageLabel {
|
||||||
|
formIndex: number,
|
||||||
|
formName: string,
|
||||||
|
formLabel: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormLabel {
|
||||||
|
label: string,
|
||||||
|
name: string,
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.formItemSubtitle {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changeFormButtonGroupContainer {
|
||||||
|
width: 250px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"use client"
|
||||||
|
import {Modal} from "antd";
|
||||||
|
import {useState} from "react";
|
||||||
|
import CreateCardComponent from "@/app/infra/basic-component/create-card-component/CreateCardComponent";
|
||||||
|
import PipelineFormComponent from "./components/pipeline-form/PipelineFormComponent";
|
||||||
|
|
||||||
|
|
||||||
|
export default function PluginConfigPage() {
|
||||||
|
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||||
|
const [isEditForm, setIsEditForm] = useState(false)
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={``}>
|
||||||
|
<Modal
|
||||||
|
title={isEditForm ? "编辑流水线" : "创建流水线"}
|
||||||
|
centered
|
||||||
|
open={modalOpen}
|
||||||
|
onOk={() => setModalOpen(false)}
|
||||||
|
onCancel={() => setModalOpen(false)}
|
||||||
|
width={700}
|
||||||
|
footer={null}
|
||||||
|
>
|
||||||
|
<PipelineFormComponent onFinish={() => {}} onCancel={() => {}}/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<CreateCardComponent width={360} height={200} plusSize={90} onClick={() => {setModalOpen(true)}}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.formItemSubTitle {
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export default function pluginConfigPage() {
|
||||||
|
return (
|
||||||
|
<div className={``}>
|
||||||
|
<h1>PluginConfigPage</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export interface ApiResponse<T> {
|
||||||
|
code: number;
|
||||||
|
data: T;
|
||||||
|
msg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface I18nText {
|
||||||
|
en_US: string;
|
||||||
|
zh_CN: string;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
export interface GetMetaDataResponse {
|
||||||
|
configs: Config[]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface Label {
|
||||||
|
en_US: string;
|
||||||
|
zh_CN: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
label: Label;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConfigItem {
|
||||||
|
default?: boolean | Array<unknown> | number | string;
|
||||||
|
description?: Label;
|
||||||
|
items?: {
|
||||||
|
type?: string;
|
||||||
|
properties?: {
|
||||||
|
[key: string]: {
|
||||||
|
type: string;
|
||||||
|
default?: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
label: Label;
|
||||||
|
name: string;
|
||||||
|
options?: Option[];
|
||||||
|
required: boolean;
|
||||||
|
scope?: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Stage {
|
||||||
|
config: ConfigItem[];
|
||||||
|
description?: Label;
|
||||||
|
label: Label;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
label: Label;
|
||||||
|
name: string;
|
||||||
|
stages: Stage[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import styles from "./createCartComponent.module.css";
|
||||||
|
|
||||||
|
export default function CreateCardComponent({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
plusSize,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
plusSize: number;
|
||||||
|
onClick: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.cardContainer} ${styles.createCardContainer} `}
|
||||||
|
style={{
|
||||||
|
width: `${width}px`,
|
||||||
|
height: `${height}px`,
|
||||||
|
fontSize: `${plusSize}px`
|
||||||
|
}}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
.cardContainer {
|
||||||
|
width: 360px;
|
||||||
|
height: 200px;
|
||||||
|
background-color: #FFF;
|
||||||
|
border-radius: 9px;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.4) 0 1px 1px -1px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.createCardContainer {
|
||||||
|
font-size: 90px;
|
||||||
|
color: #acacac;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface I18NText {
|
||||||
|
en_US: string;
|
||||||
|
zh_CN: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import "./global.css"
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Create Next App",
|
||||||
|
description: "Generated by create next app",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body className={``}>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function LoginLayout({
|
||||||
|
children
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<main>{children}</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div className={``}>
|
||||||
|
<h1>loginpage</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<div className={``}>
|
||||||
|
{/* TODO: @qidongrui 这里404页面有时间要更新*/}
|
||||||
|
<h1>Langbot没有找到该页面喔~</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div className={``}>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||