Browse Source

first commit

Pchen. 1 year ago
commit
2bf167711f

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+.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?

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Pchen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 48 - 0
README.md

@@ -0,0 +1,48 @@
+<h1 align="center">Web Wechat Bot 控制台</h1>
+
+「Web Wechat Bot」是一个基于Wechaty、可通过网页远程登录管理、可接入讯飞星火、ChatGPT、通义千问等大语言模型的微信聊天机器人,使用微信网页版协议。你可以通过网页登录微信、设置回复规则、配置API接口等。
+
+此仓库为「Web Wechat Bot」的前端页面,不包含后端程序。如果你想直接使用Web Wechat Bot而不需要对前端页面进行更改,请前往:[https://github.com/Pchen0/Web-Wechat-Bot](https://github.com/Pchen0/Web-Wechat-Bot)。
+
+
+
+## 🖥主要技术构成
+
+- Vue3
+
+- Vue-router
+
+- ElementPlus
+
+  
+
+## 💡项目文件结构
+
+```
+Web-WechatBot-Dashboard      
+├─ public                               
+│  └─ index.html             
+├─ src                                 
+│  ├─ router                 
+│  │  └─ index.js            
+│  ├─ views                         
+│  │  ├─ acccountConfig.vue  
+│  │  ├─ apiConfig.vue       
+│  │  ├─ Chat.vue            
+│  │  ├─ Config.vue          
+│  │  ├─ Header.vue          
+│  │  ├─ History.vue         
+│  │  ├─ Home.vue            
+│  │  ├─ Login.vue           
+│  │  ├─ wxConfig.vue        
+│  │  └─ Wxlogin.vue         
+│  ├─ App.vue                
+│  └─ main.js                
+├─ babel.config.js           
+├─ jsconfig.json             
+├─ LICENSE                   
+├─ package.json              
+├─ README.md                 
+└─ vue.config.js             
+```
+

+ 5 - 0
babel.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  presets: [
+    '@vue/cli-plugin-babel/preset'
+  ]
+}

+ 19 - 0
jsconfig.json

@@ -0,0 +1,19 @@
+{
+  "compilerOptions": {
+    "target": "es5",
+    "module": "esnext",
+    "baseUrl": "./",
+    "moduleResolution": "node",
+    "paths": {
+      "@/*": [
+        "src/*"
+      ]
+    },
+    "lib": [
+      "esnext",
+      "dom",
+      "dom.iterable",
+      "scripthost"
+    ]
+  }
+}

+ 46 - 0
package.json

@@ -0,0 +1,46 @@
+{
+  "name": "webbot",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.3.1",
+    "axios": "^1.6.5",
+    "core-js": "^3.8.3",
+    "element-plus": "^2.5.2",
+    "vue": "^3.2.13",
+    "vue-router": "^4.0.3"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.12.16",
+    "@babel/eslint-parser": "^7.12.16",
+    "@vue/cli-plugin-babel": "~5.0.0",
+    "@vue/cli-plugin-eslint": "~5.0.0",
+    "@vue/cli-plugin-router": "~5.0.0",
+    "@vue/cli-service": "~5.0.0"
+  },
+  "eslintConfig": {
+    "root": true,
+    "env": {
+      "node": true
+    },
+    "extends": [
+      "plugin:vue/vue3-essential",
+      "eslint:recommended"
+    ],
+    "parserOptions": {
+      "parser": "@babel/eslint-parser"
+    },
+    "rules": {}
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead",
+    "not ie 11"
+  ]
+}

BIN
public/favicon.ico


+ 17 - 0
public/index.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title><%= htmlWebpackPlugin.options.title %></title>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 19 - 0
src/App.vue

@@ -0,0 +1,19 @@
+<template>
+  <div id="app">
+    <router-view></router-view>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'app'
+}
+</script>
+
+<style>
+html,body {
+  margin: 0;
+  padding: 0;
+  background-color: lightblue;
+}
+</style>

+ 22 - 0
src/main.js

@@ -0,0 +1,22 @@
+import { createApp } from 'vue';
+import App from './App.vue';
+import router from './router';
+import ElementPlus from 'element-plus';
+import 'element-plus/dist/index.css';
+import axios from 'axios';
+import * as ElementPlusIconsVue from '@element-plus/icons-vue';
+
+const app = createApp(App)
+
+// axios请求拦截
+axios.interceptors.request.use(config => {
+    // 为请求头对象,添加token验证的Authorization字段
+    config.headers.Authorization = window.localStorage.getItem('token');
+    return config;
+})
+
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+    app.component(key, component);
+}
+
+app.use(router).use(ElementPlus).mount('#app');

+ 61 - 0
src/router/index.js

@@ -0,0 +1,61 @@
+// 导入 Vue 和 VueRouter
+import { createRouter, createWebHashHistory } from 'vue-router';
+
+// 导入组件
+import Home from '../views/Home.vue';
+import Login from '../views/Login.vue';
+import Chat from '../views/Chat.vue'
+import Wxlogin from '../views/Wxlogin.vue'
+import Config from '../views/Config.vue'
+import History from '../views/History.vue'
+
+// 创建路由实例
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes: [
+    {
+      path: '/',
+      component: Home,
+      name: 'Home',
+    },
+    {
+      path: '/login',
+      component: Login,
+      name: 'Login',
+    },
+    {
+      path: '/aichat',
+      component: Chat,
+      name: 'Chat',
+    },
+    {
+      path: '/wxlogin',
+      component: Wxlogin,
+      name: 'Wxlogin',
+    },
+    {
+      path: '/config',
+      component: Config,
+      name: 'Config',
+    },
+    {
+      path: '/history',
+      component: History,
+      name: 'History',
+    },
+  ],
+});
+
+//为路由对象,添加beforeEach导航守卫
+router.beforeEach((to, from, next) => {
+  //如果用户访问的登录页,直接放行
+  if (to.path === '/login') return next()
+  //从localStorage中获取到保存的token值
+  const tokenStr = window.localStorage.getItem('token')
+  //如果没有token,强制跳转到登录页
+  if (!tokenStr) return next('/login')
+  next()
+})
+
+// 导出路由实例,以便在主应用中使用
+export default router;

+ 116 - 0
src/views/Chat.vue

@@ -0,0 +1,116 @@
+<template>
+    <Header />
+    <div id="chat-container">
+        <div id="chat-messages" style="overflow-y: scroll;">
+            <div v-for="(message, index) in messages" :key="index" :class="message.type + '-message-container'">
+                <el-avatar v-if="message.type === 'bot' && isLogin == false">Bot</el-avatar>
+                <el-avatar v-if="message.type === 'bot' && isLogin == true" src="/getavatar" />
+                <el-card :class="message.type + '-message-card'">{{ message.content }}</el-card>
+                <el-avatar v-if="message.type === 'you'">You</el-avatar>
+            </div>
+        </div>
+        <div id="user-input"
+            style="display: flex;justify-content: space-between;align-items: center;position: fixed;bottom: 0;width: 100%;padding: 10px;box-sizing: border-box;background-color:white">
+            <el-input v-model="textarea" clear :autosize="{ minRows: 1, maxRows: 4 }" type="textarea"
+                placeholder="在此输入消息..." style="flex-grow: 1;margin-right: 10px;" />
+            <el-button type="primary" round class="loginbutton" @click="sendMessage()">发送</el-button>
+        </div>
+    </div>
+
+</template>
+
+<script>
+import axios from 'axios'
+import Header from './Header.vue'
+import { ElMessage } from 'element-plus'
+import { ref, onMounted } from 'vue'
+
+export default {
+    components: {
+        Header,
+    },
+    setup() {
+        const textarea = ref('')
+        const messages = ref([])
+        const isLogin = ref(false)
+
+        onMounted(
+            async function () {
+                try {
+                    const response = await axios.get('/getstatus')
+                    if (response.data.status === 200) {
+                        isLogin.value = true
+                    } else if (response.data.status === 401) {
+                        ElMessage.error('登录已过期,请重新登录!')
+                    }
+                } catch (error) {
+                    ElMessage.error('获取登录状态失败!')
+                }
+            })
+
+        const sendMessage = async () => {
+            if (textarea.value === '') {
+                ElMessage.error('不能发送空白消息!')
+                return
+            }
+
+            try {
+                messages.value.push({ content: textarea.value, type: 'you' })
+                const response = await axios.post('/chat', { msg: textarea.value })
+                textarea.value = ''
+                if (response && response.data.msg) {
+                    messages.value.push({ content: response.data.msg, type: 'bot' })
+                } else {
+                    ElMessage.error('无法从服务器获取消息!')
+                }
+            } catch (error) {
+                ElMessage.error('发送消息时出错,请稍后重试!')
+            }
+        }
+
+        return {
+            textarea,
+            messages,
+            sendMessage,
+            isLogin
+        }
+    },
+}
+</script>
+
+<style scoped>
+.bot-message-container {
+    margin-left: 10px;
+    display: flex;
+    margin-top: 8px;
+    margin-bottom: 7px;
+}
+
+.bot-message-card {
+    margin-left: 10px;
+    width: auto;
+    max-width: 70%;
+    height: auto;
+    word-wrap: break-word;
+    padding: 0px;
+    border-radius: 10px;
+}
+
+.you-message-container {
+    justify-content: flex-end;
+    margin-right: 10px;
+    display: flex;
+    margin-bottom: 7px;
+    margin-top: 8px;
+}
+
+.you-message-card {
+    margin-right: 10px;
+    width: auto;
+    max-width: 80%;
+    height: auto;
+    word-wrap: break-word;
+    padding: 0px;
+    border-radius: 10px;
+}
+</style>

+ 56 - 0
src/views/Config.vue

@@ -0,0 +1,56 @@
+<template>
+    <div class="app-container">
+        <Header />
+        <el-tabs v-model="activeName" type="card" style="margin-top: 15px;">
+            <el-tab-pane label="API接口设置" name="first">
+                <apiConfig />
+            </el-tab-pane>
+
+            <el-tab-pane label="WechatBot设置" name="second">
+               <wxConfig />
+            </el-tab-pane>
+
+            <el-tab-pane label="账号设置" name="third">
+              <accountConfig />
+            </el-tab-pane>
+        </el-tabs>
+    </div>
+</template>
+
+<script>
+import Header from './Header.vue'
+import wxConfig from './wxConfig.vue'
+import apiConfig from './apiConfig.vue'
+import accountConfig from './acccountConfig.vue'
+import { ref } from 'vue'
+
+export default{
+    setup() {
+        const activeName = ref('first')
+
+        return{
+            activeName
+        }
+    },
+
+    components: {
+        Header, wxConfig, apiConfig, accountConfig
+    },
+}
+
+</script>
+
+<style>
+.app-container {
+    display: flex;
+    flex-direction: column;
+    height: 100vh;
+    /* 让容器占据整个视窗高度 */
+}
+
+.Header {
+    flex: none;
+    /* 不随内容扩展而扩展 */
+}
+</style>
+

+ 69 - 0
src/views/Header.vue

@@ -0,0 +1,69 @@
+<template>
+    <el-menu class="el-menu-demo" mode="horizontal" :ellipsis="false" @select="handleSelect" style="background-color: skyblue;">
+        <div class="flex-grow" />
+        <el-sub-menu index="1">
+            <template #title><ChatLineRound style="width: 35px; margin-left: 8px" /></template>
+                <el-menu-item index="1" @click="handleRouter('/')">
+                    <el-icon><HomeFilled /></el-icon>
+                    <span>首页</span>
+                </el-menu-item>
+                <el-menu-item index="2" @click="handleRouter('/aichat')">
+                    <el-icon><Comment /></el-icon>
+                    <span>对话测试</span>
+                </el-menu-item>
+                <el-menu-item index="3" @click="handleRouter('/wxlogin')">
+                    <el-icon><CameraFilled /></el-icon>
+                    <span>登录微信</span>
+                 </el-menu-item>
+                <el-menu-item index="4" @click="handleRouter('/history')">
+                    <el-icon><Promotion /></el-icon>
+                    <span>发送历史</span>
+                </el-menu-item>
+                <el-menu-item index="5" @click="handleRouter('/config')">
+                   <el-icon><Tools /></el-icon>
+                    <span>设置</span>
+                </el-menu-item>
+                <el-menu-item index="6" @click="logout">
+                    <el-icon><CircleCloseFilled /></el-icon>
+                    <span>退出登录</span>
+                </el-menu-item>
+        </el-sub-menu>
+    </el-menu>
+</template>
+
+<script>
+import { ElMessage, ElMessageBox } from 'element-plus'
+
+export default {
+    methods:{
+        handleRouter(route) {
+            this.$router.push(route);
+        },
+
+       logout() {
+            ElMessageBox.confirm(
+                '是否退出登录?',
+                '警告',
+                {
+                    confirmButtonText: '确认',
+                    cancelButtonText: '取消',
+                    type: 'warning',
+                }
+            )
+                .then(() => {
+                    // 清除本地存储的 token
+                    window.localStorage.removeItem('token');
+                    ElMessage.success('退出登录成功!');
+                    this.$router.push('/login');
+                })
+                .catch(() => {
+                    // 用户点击了取消按钮
+                    ElMessage({
+                        type: 'info',
+                        message: '操作已取消',
+                    });
+                });
+        },
+    }
+};
+</script>

+ 82 - 0
src/views/History.vue

@@ -0,0 +1,82 @@
+<template>
+    <div>
+        <Header />
+        <el-button type="primary" @click="open">清空数据</el-button>
+        <el-table :data="tableData" style="width: 100%" table-layout="auto" stripe border>
+            <el-table-column prop="time" label="时间" />
+            <el-table-column prop="type" label="类型" />
+            <el-table-column prop="roomname" label="群聊名称" />
+            <el-table-column prop="name" label="发送人" />
+            <el-table-column prop="recmsg" label="收到消息" />
+            <el-table-column prop="senmsg" label="发送消息" />
+        </el-table>
+    </div>
+</template>
+
+<script>
+import axios from 'axios'
+import { onMounted, ref } from 'vue'
+import Header from './Header.vue'
+import { ElMessage, ElMessageBox} from 'element-plus'
+
+export default {
+    components: {
+        Header,
+    },
+
+    setup() {
+        const tableData = ref([])
+
+        const open = () => {
+            ElMessageBox.confirm(
+                '此操作会清除所有记录,是否继续?',
+                '警告',
+                {
+                    confirmButtonText: '继续',
+                    cancelButtonText: '取消',
+                    type: 'warning',
+                }
+            )
+                .then(clear)
+                .catch(() => {
+                    ElMessage({
+                        type: 'info',
+                        message: '操作已取消',
+                    })
+                })
+        }
+
+        const clear = async () => {
+            try {
+                const response = await axios.post('/clearmessage')
+                const status = response.data.status
+                const msg = response.data.msg
+                if (status === 200) {
+                    ElMessage.success(msg)
+                    window.location.reload()
+                } else {
+                    ElMessage.error('清除失败!' + msg)
+                }
+            } catch (error) {
+                ElMessage.error('清除失败!', error.message)
+            }
+        }
+
+        onMounted(async () => {
+            try {
+                const response = await axios.post('/messagehistory')
+                tableData.value = response.data.msg
+            } catch (error) {
+                ElMessage.error('获取数据失败!')
+            }
+        })
+
+        return {
+            tableData,
+            clear,
+            open,
+           
+        }
+    },
+}
+</script>

+ 154 - 0
src/views/Home.vue

@@ -0,0 +1,154 @@
+<template>
+     <div class="common-layout">
+        <el-container>
+        <el-aside width="80px">
+            <el-row class="tac" style="height: 100vh;">
+                <el-menu  class="el-menu" style="background-color: skyblue" collapse>
+                    <el-menu-item index="1" @click="handleRouter('/aichat')">
+                        <el-icon><Comment /></el-icon>
+                    </el-menu-item>
+                    <el-menu-item index="2" @click="handleRouter('/wxlogin')">
+                        <el-icon><CameraFilled /></el-icon>
+                    </el-menu-item>
+                    <el-menu-item index="3" @click="handleRouter('/history')">
+                        <el-icon><Promotion /></el-icon>
+                    </el-menu-item>
+                    <el-menu-item index="4" @click="handleRouter('/config')">
+                        <el-icon><Tools /></el-icon>
+                    </el-menu-item>
+                    <el-menu-item index="5" @click="logout">
+                        <el-icon><CircleCloseFilled /></el-icon>
+                    </el-menu-item>
+                </el-menu>      
+            </el-row>
+          </el-aside>
+     <el-main class="main-container">
+            <div class="centered-content" v-if="!isLogin">
+              <h1>尚未登录微信o(╥﹏╥)o</h1>
+              <el-button type="primary" round class="loginbutton" @click="handleRouter('/wxlogin')">点我登录</el-button>
+              <el-button type="primary" round class="loginbutton" @click="handleRouter('/config')">系统设置</el-button>
+            </div>
+            <div class="centered-content" v-else>
+                  <h1>{{wxname}},欢迎你!(*^▽^*)</h1>
+                  <el-avatar shape="square" :size="100" :fit="fit" src="/getavatar" /><br><br>
+                  <el-button type="primary" round class="loginbutton" @click="handleRouter('/history')">发送历史</el-button>
+                  <el-button type="primary" round class="loginbutton" @click="handleRouter('/config')">系统设置</el-button><br><br>
+                  <el-button type="primary" round class="loginbutton" @click="handleRouter('/aichat')">对话测试</el-button>
+                  <el-button type="primary" round class="loginbutton" @click="wxlogout">退出微信</el-button>
+            </div>
+          </el-main>
+        </el-container>
+      </div>
+</template>
+
+<script>
+import { onMounted, ref } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import axios from 'axios'
+
+export default {
+    setup() {
+        const isLogin = ref(false)
+        const wxname = ref('')
+
+        onMounted(
+            async function () {
+            try {
+                const response = await axios.get('/getstatus')
+                if (response.data.status === 200) {
+                isLogin.value = true
+                const response = await axios.get('/getwxname')
+                wxname.value = response.data.wxname
+                }  else if (response.data.status === 401) {
+                    ElMessage.error('登录已过期,请重新登录!')
+                }
+            } catch (error) {
+                ElMessage.error('获取登录状态失败!')
+            }
+    })
+
+        return {
+            isLogin,
+            wxname
+        }
+
+},
+    methods: {
+        wxlogout() {
+            ElMessageBox.confirm(
+                '是否退出微信登录?',
+                '警告',
+                {
+                    confirmButtonText: '确认',
+                    cancelButtonText: '取消',
+                    type: 'warning',
+                }
+            )
+                .then(async () => {
+                    try {
+                        const response = await axios.get('/stop');
+                        const statusCode = response.status;
+                        if (statusCode === 200) {
+                            ElMessage.success('退出微信成功!')
+                            window.location.reload();
+                        } else {
+                            ElMessage.error('退出微信失败!');
+                        }
+                    } catch (error) {
+                        ElMessage.error('退出微信失败!' + error.message);
+                    }
+                })
+                .catch(() => {
+                    // 用户点击了取消按钮
+                    ElMessage({
+                        type: 'info',
+                        message: '操作已取消',
+                    });
+                });
+        },
+        
+        logout() {
+            ElMessageBox.confirm(
+                '是否退出登录?',
+                '警告',
+                {
+                    confirmButtonText: '确认',
+                    cancelButtonText: '取消',
+                    type: 'warning',
+                }
+            )
+                .then(() => {
+                    // 清除本地存储的 token
+                    window.localStorage.removeItem('token')
+                    ElMessage.success('退出登录成功!')
+                    this.$router.push('/login')
+                })
+                .catch(() => {
+                    // 用户点击了取消按钮
+                    ElMessage({
+                        type: 'info',
+                        message: '操作已取消',
+                    });
+                });
+        },
+
+
+        handleRouter(route) {
+            this.$router.push(route);
+        },
+    },
+}
+</script>
+
+<style scoped>
+.main-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100vh;
+}
+
+.centered-content {
+  text-align: center;
+}
+</style>

+ 112 - 0
src/views/Login.vue

@@ -0,0 +1,112 @@
+<template>
+    <div class="login-container">
+        <el-card class="box-card">
+          <div class="item">
+            <h2>用户登录</h2>
+            <el-input v-model="usernameinput" placeholder="用户名" class="usernameinput" />
+            <el-input v-model="passwordinput" class="passwordinput" type="password" placeholder="密码" show-password />
+            <el-checkbox v-model="remember" label="记住我" class="remember" /></div><div class="item">
+            <el-button type="primary" round @click="userlogin" >登录</el-button>
+          </div>
+          
+        </el-card>
+    </div>
+
+</template>
+
+<script>
+import { ElMessage } from 'element-plus'
+import {ref} from 'vue'
+import axios from 'axios'
+
+export default {
+    setup() {
+        const passwordinput = ref('')
+        const usernameinput = ref('')
+        const remember = ref(false)
+    
+        return { usernameinput , passwordinput, remember }
+    },
+
+    methods:{
+        async userlogin() {
+            try {
+                // 获取输入框内容
+                const username = this.usernameinput
+                const password = this.passwordinput
+                const remember = this.remember
+
+                if(username.length===0||password.length===0){
+                  ElMessage.error('用户名和密码不能为空!')
+                  return
+                }
+
+                // 发送登录请求
+                const response = await axios.post('/userlogin', {
+                    username: username,
+                    password: password,
+                    remember: remember
+                })
+
+                if (response) {
+                  const status = response.data.status
+                  const msg = response.data.msg
+                  if (status === 500) {
+                    ElMessage.error('登录失败!'+ msg)
+                  } else if (status ===200){
+                    const token = response.data.token
+                    localStorage.setItem('token', token)
+                    ElMessage.success('登录成功!')
+                    this.$router.push('/')
+                  } else {
+                      ElMessage.error('登录失败!请稍后再试')
+                  }
+                } else {
+                  ElMessage.error('登录失败!服务器无响应')
+                }
+
+            } catch (err) {
+                ElMessage.error('登录失败!请稍后再试')
+            }
+        },
+    }
+};
+</script>
+
+<style scoped>
+h2 {
+  color: #409eff;
+  text-align: center;
+}
+
+.usernameinput {
+  margin-bottom: 25px;
+
+}
+
+.remember {
+  float: right;
+}
+
+.item {
+  padding: 20px;
+  text-align: center;
+}
+
+.login-container {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 100vh; 
+}
+
+.box-card {
+  width: 80%;
+}
+
+@media screen and (min-width: 768px) {
+  .box-card {
+    width: 30%;
+  }
+}
+</style>

+ 170 - 0
src/views/Wxlogin.vue

@@ -0,0 +1,170 @@
+<template>
+  <div class="app-container">
+    <Header />
+    <div class="login-container">
+      <el-card class="box-card">
+        <div class="text item">
+          <h2 v-if="!scanSuccessNotified">请扫描二维码登录微信</h2>
+          <h2 v-else-if="!loginSuccessNotified ||scanSuccessNotified">请在手机上确认登录</h2>
+          <h2 v-else-if="loginSuccessNotified">微信登录成功</h2>
+          <img :src="qrcodeUrl" class="image" v-if="!scanSuccessNotified" />
+          <el-icon v-else class="image" style="height: 100px;"><CircleCheckFilled /></el-icon>
+          <div class="bottom">
+            <el-button type="primary" round class="loginbutton" @click="refresh">刷新二维码</el-button><br>
+            <el-button type="primary" round class="loginbutton" @click="handleRouter('/')">返回首页</el-button>
+          </div>
+        </div>
+      </el-card>
+    </div>
+  </div>
+</template>
+<script>
+import Header from './Header.vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { ref, onMounted } from 'vue'
+import axios from 'axios'
+
+export default {
+    setup() {
+      const qrcodeUrl = ref("")
+      const scanSuccessNotified = ref(false)
+      const loginSuccessNotified = ref(false)
+
+
+      onMounted(
+        async () =>{
+        ElMessageBox.confirm(
+          '若已有微信登录,继续获取二维码会使当前登录微信下线,是否继续?',
+          '警告',
+          {
+            confirmButtonText: '继续',
+            cancelButtonText: '取消',
+            type: 'warning',
+          }
+        )
+          .then(getqrcode)
+          .catch(() => {
+            ElMessage({
+              type: 'info',
+              message: '操作已取消',
+            })
+          })
+        }
+      )
+
+    const getqrcode = async () => {
+      try {
+        getStatus()
+        const response = await axios.get('/getqrcode');
+        qrcodeUrl.value = response.data.qrcode
+      } catch (error) {
+        ElMessage.error('获取二维码失败!')
+      }
+
+      //对扫描状态进行轮询
+      getStatus()
+
+      setInterval(() => {
+        getStatus()
+      }, 500)
+    }
+
+    const refresh = async () => {
+      try {
+        const response = await axios.get('/stop')
+        const statusCode = response.data.Status
+        if(statusCode === 200) {
+          window.location.reload()
+        } else {
+          ElMessage.error('刷新二维码失败!')
+        }
+      } catch (error) {
+        ElMessage.error('刷新二维码失败!' + error.message)
+      }
+    }
+
+    const getStatus = async () => {
+      try {
+        const response = await axios.get('/getstatus')
+        const statusCode = response.data.status
+        if (statusCode === 3) {
+          // 扫描成功提醒
+          if (!scanSuccessNotified.value) {
+            ElMessage.success('扫描成功,请在手机上确认登录')
+            // 提醒后将变量设为 true
+            scanSuccessNotified.value = true
+          }
+          return;
+        }
+        if (statusCode === 200) {
+          // 登录成功提醒
+          if (!loginSuccessNotified.value) {
+            ElMessage.success('微信登录成功!')
+            loginSuccessNotified.value = true
+          }
+          return
+         }      
+         } catch (error) {
+          return
+      }
+    }
+
+    return {
+      qrcodeUrl,
+      scanSuccessNotified,
+      loginSuccessNotified,
+      refresh
+    }
+  },
+
+    methods:{
+      handleRouter(route) {
+      this.$router.push(route);
+    },
+    },
+
+    components: {
+        Header
+    }
+
+}
+</script>
+<style scoped>
+h2 {
+  color: #409eff;
+  text-align: center;
+}
+
+.app-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh; 
+}
+
+.item {
+  padding: 20px;
+  text-align: center;
+}
+
+.login-container {
+  flex: 1; 
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.box-card {
+  width: 60%;
+
+}
+
+.loginbutton {
+  margin-top: 10px;
+}
+
+@media screen and (min-width: 768px) {
+  .box-card {
+    width: 20%;
+  }
+}
+</style>

+ 109 - 0
src/views/acccountConfig.vue

@@ -0,0 +1,109 @@
+<template>
+    <div class="centered-card">
+        <el-card class="box-card">
+            <div class="text item">
+                <el-form label-position="top" :model="userForm" :size="formSize" label-width="auto" status-icon>
+                    <el-form-item label="更改用户名">
+                        <el-input v-model="userForm.newusername" />
+                    </el-form-item>
+                    <el-form-item label="旧密码">
+                        <el-input type="password" v-model="userForm.oldpassword" />
+                    </el-form-item>
+                    <el-form-item label="新密码">
+                        <el-input type="password" v-model="userForm.newpassword1" />
+                    </el-form-item>
+                    <el-form-item label="再次输入新密码">
+                        <el-input type="password" v-model="userForm.newpassword2" />
+                    </el-form-item>
+                    <el-form-item>
+                        <el-button type="primary" @click="saveuser">
+                            保存
+                        </el-button>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </el-card>
+    </div>
+</template>
+<script>
+import { onMounted, ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import axios from 'axios'
+
+export default {
+setup() {
+    const formSize = ref('default')
+    const activeName = ref('first')
+
+    const userForm = ref({
+        newusername: '',
+        oldpassword: '',
+        newpassword1: '',
+        newpassword2: '',
+        newpassword: '',
+    });
+
+    const saveuser = async () => {
+        if (userForm.value.newpassword1 === userForm.value.newpassword2) {
+            userForm.value.newpassword = userForm.value.newpassword1
+            try {
+                const response = await axios.post('/changeaccount', userForm.value)
+                const status = response.data.status
+                const msg = response.data.msg
+                if (status === 200) {
+                    ElMessage.success(msg)
+                } else {
+                    ElMessage.error('修改失败!' + msg)
+                }
+            } catch (error) {
+                ElMessage.error('修改失败!', error.message)
+            }
+        } else {
+            ElMessage.error('两次输入的密码不匹配!')
+        }
+    }
+
+    onMounted(async () => {
+        try {
+            const userresponse = await axios.post('/getusername')
+
+            userForm.value.newusername = userresponse.data.msg
+
+        } catch (error) {
+            ElMessage.error('查询失败!', error.message)
+        }
+    })
+
+    return {
+        formSize,
+        userForm,
+        activeName,
+        saveuser
+    };
+}
+}
+
+</script>
+<style scoped>
+.centered-card {
+    flex: 1; 
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.item {
+    padding: 20px;
+    text-align: center;
+}
+
+.box-card {
+    width: 100%;
+}
+
+@media screen and (min-width: 768px) {
+    .box-card {
+        width: 30%;
+    }
+}
+</style>

+ 248 - 0
src/views/apiConfig.vue

@@ -0,0 +1,248 @@
+<template>
+      <el-tabs v-model="activeName" class="demo-tabs" style="margin-left: 20px;margin-right: 20px">
+        <el-tab-pane label="讯飞星火" name="xunfei">
+            <div class="centered-card">
+            <el-card class="box-card">
+                                <div class="text item">
+                                    <el-form label-position="top" :model="gptForm" :size="formSize"
+                                        label-width="auto" status-icon>
+                                        <el-form-item label="API接口地址">
+                                            <el-input v-model="xfForm.APIUrl" placeholder="如ws://spark-api.xf-yun.com/v3.1/chat"/>
+                                        </el-form-item>
+                                        <el-form-item label="APIKey">
+                                            <el-input v-model="xfForm.APIKey" placeholder="在控制台获取"/>
+                                        </el-form-item>
+                                        <el-form-item label="APISecret">
+                                            <el-input v-model="xfForm.APISecret" placeholder="在控制台获取"/>
+                                        </el-form-item>
+                                        <el-form-item label="APPID">
+                                            <el-input v-model="xfForm.app_id" placeholder="在控制台获取"/>
+                                        </el-form-item>
+                                        <el-form-item label="模型版本(请与接口地址保持一致)">
+                                            <el-input v-model="xfForm.domain" placeholder="如generalv2,generalv3,generalv3.5"/>
+                                        </el-form-item>
+                                        <el-form-item label="最大token">
+                                                <el-input v-model="xfForm.max_tokens" placeholder="模型回答的tokens的最大长度"/>
+                                        </el-form-item>
+                                        <el-form-item label="温度(决定结果随机性)">
+                                            <el-input v-model="xfForm.temperature" placeholder="取值范围 (0,1] ,默认值0.5"/>
+                                        </el-form-item>
+                                        <el-form-item>
+                                            <el-button type="primary" @click="savexf">
+                                                保存
+                                            </el-button>
+                                        </el-form-item>
+                                    </el-form>
+                                </div>
+                            </el-card></div>
+                        
+        </el-tab-pane>
+        <el-tab-pane label="ChatGPT" name="chatgpt">
+            <div class="centered-card">
+                <el-card class="box-card">
+                    <div class="text item">
+                                <el-form label-position="top" :model="gptForm" :size="formSize"
+                                    label-width="auto" status-icon>
+                                    <el-form-item label="API接口地址">
+                                        <el-input v-model="gptForm.apiUrl" placeholder="如https://api.openai.com/v1/chat/completions"/>
+                                    </el-form-item>
+                                    <el-form-item label="APIKey">
+                                        <el-input v-model="gptForm.apiKey" placeholder="在控制台获取"/>
+                                    </el-form-item>
+                                    <el-form-item label="app_code">
+                                        <el-input v-model="gptForm.app_code" placeholder="为部分中转平台设置,可不填"/>
+                                    </el-form-item>
+                                    <el-form-item label="模型名称">
+                                        <el-input v-model="gptForm.model" placeholder="如gpt-3.5-turbo"/>
+                                    </el-form-item>
+                                    <el-form-item>
+                                        <el-button type="primary" @click="savegpt">
+                                            保存
+                                        </el-button>
+                                    </el-form-item>
+                                </el-form>
+                            </div>
+                        </el-card>
+                    </div>
+                </el-tab-pane>
+                <el-tab-pane label="通义千问" name="tongyi">
+                    <div class="centered-card">
+                        <el-card class="box-card">
+                            <div class="text item">
+                                    <el-form label-position="top" :model="tyForm" :size="formSize"
+                                        label-width="auto" status-icon>
+                                        <el-form-item label="API接口地址">
+                                            <el-input v-model="tyForm.apiUrl" placeholder="如https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"/>
+                                        </el-form-item>
+                                        <el-form-item label="APIKey">
+                                            <el-input v-model="tyForm.apiKey" placeholder="在阿里云开通DashScope并创建API-KEY"/>
+                                        </el-form-item>
+                                        <el-form-item label="温度">
+                                            <el-input v-model="tyForm.temperature" placeholder="取值范围:[0, 2),用于控制随机性和多样性的程度"/>
+                                        </el-form-item>
+                                        <el-form-item label="模型名称">
+                                            <el-input v-model="tyForm.model" placeholder="如qwen-turbo"/>
+                                        </el-form-item>
+                                        <el-form-item label="最大token">
+                                            <el-input v-model="tyForm.max_tokens" placeholder="模型回答的tokens的最大长度"/>
+                                        </el-form-item>
+                                        <el-form-item label="预设内容">
+                                            <el-input v-model="tyForm.presets" type="textarea" placeholder="请填写你对机器人的设定和要求"/>
+                                        </el-form-item>
+                                        <el-form-item>
+                                            <el-button type="primary" @click="savety">
+                                                保存
+                                            </el-button>
+                                        </el-form-item>
+                                    </el-form>
+                                </div>
+                            </el-card>
+                        </div>
+                </el-tab-pane>
+      </el-tabs>
+
+</template>
+<script>
+import { onMounted, ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import axios from 'axios'
+
+export default {
+    setup() {
+        const formSize = ref('default')
+        const activeName = ref('xunfei')
+
+        const xfForm = ref({
+            APIUrl: '',
+            APIKey: '',
+            app_id: '',
+            model: '',
+            APISecret: '',
+            max_tokens: '',
+            temperature: ''
+        })
+        
+        const gptForm = ref({
+            apiUrl: '',
+            apiKey: '',
+            app_code: '',
+            model: '',
+        })
+
+        const tyForm = ref({
+            apiUrl: '',
+            apiKey: '',
+            temperature: '',
+            model: '',
+            max_tokens: '',
+            presets: '',
+        })
+
+        const savexf = async () => {
+            try {
+                const response = await axios.post('/xfconfig', xfForm.value)
+                const status = response.data.status
+                const msg = response.data.msg
+                if (status === 200) {
+                    ElMessage.success(msg)
+                } else {
+                    ElMessage.error('修改失败!' + msg)
+                }
+            } catch (error) {
+                ElMessage.error('修改失败!', error.message)
+            }
+        }
+
+        const savegpt = async () => {
+            try {
+                const response = await axios.post('/gptconfig', gptForm.value)
+                const status = response.data.status
+                const msg = response.data.msg
+                if (status === 200) {
+                    ElMessage.success(msg)
+                } else {
+                    ElMessage.error('修改失败!' + msg)
+                }
+            } catch (error) {
+                ElMessage.error('修改失败!', error.message)
+            }
+        }
+
+        const savety = async () => {
+            try {
+                const response = await axios.post('/tyconfig', tyForm.value)
+                const status = response.data.status
+                const msg = response.data.msg
+                if (status === 200) {
+                    ElMessage.success(msg)
+                } else {
+                    ElMessage.error('修改失败!' + msg)
+                }
+            } catch (error) {
+                ElMessage.error('修改失败!', error.message)
+            }
+        }
+
+        onMounted(async () => {
+            try {
+                const xfresponse = await axios.post('/getxfconfig')
+                const xfconfigData = xfresponse.data.msg
+                // 更新 Form 的值
+                xfconfigData.forEach(item => {
+                    xfForm.value[item.config] = item.value
+                })
+
+                const gptresponse = await axios.post('/getgptconfig')
+                const gptconfigData = gptresponse.data.msg
+                gptconfigData.forEach(item => {
+                    gptForm.value[item.config] = item.value
+                })
+                
+                const tyresponse = await axios.post('/gettyconfig')
+                const tyconfigData = tyresponse.data.msg
+                tyconfigData.forEach(item => {
+                    tyForm.value[item.config] = item.value
+                })
+
+            } catch (error) {
+                ElMessage.error('查询失败!', error.message)
+            }
+        })
+
+        return {
+            formSize,
+            gptForm,
+            activeName,
+            savegpt,
+            xfForm,
+            savexf,
+            savety,
+            tyForm
+        }
+    },
+}
+
+</script>
+<style scoped>
+.centered-card {
+    flex: 1; 
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.item {
+    padding: 20px;
+    text-align: center;
+}
+
+.box-card {
+    width: 100%;
+}
+
+@media screen and (min-width: 768px) {
+    .box-card {
+        width: 30%;
+    }
+}
+</style>

+ 151 - 0
src/views/wxConfig.vue

@@ -0,0 +1,151 @@
+<template>
+<div class="centered-card">
+    <el-card class="box-card">
+        <div class="text item">
+            <el-form label-position="top" :model="apiForm" :size="formSize" label-width="auto" status-icon>
+                <el-form-item label="选择模型">
+                    <el-select v-model="wxForm.usemodel" placeholder="请选择使用模型" clearable>
+                        <el-option label="讯飞星火" value="xunfei" />
+                        <el-option label="ChatGPT" value="chatgpt" />
+                        <el-option label="通义千问" value="tongyi" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="自动回复前缀">
+                    <el-input v-model="wxForm.prefix" />
+                </el-form-item>
+                <el-form-item label="自动回复后缀(\n换行)">
+                    <el-input v-model="wxForm.suffix" />
+                </el-form-item>
+                <el-divider />
+                <el-form-item label="是否开启私聊自动回复">
+                    <el-switch v-model="wxForm.autoReplySingle" />
+                </el-form-item>
+                <el-form-item label="私聊黑名单(用英文逗号分隔)">
+                    <el-input v-model="wxForm.blackName" />
+                </el-form-item>
+                <el-divider />
+                <el-form-item label="群聊关键字回复(用英文逗号分隔)">
+                    <el-input v-model="wxForm.keyWords" />
+                </el-form-item>
+                <el-form-item label="群聊白名单(用英文逗号分隔,不填会在所有群聊回复)">
+                    <el-input v-model="wxForm.whiteRoom" />
+                </el-form-item>                               
+
+                <el-form-item label="是否在被@时回复">
+                    <el-switch v-model="wxForm.atReply" />
+                </el-form-item>
+                <el-form-item>
+                    <el-divider />
+                <el-button type="primary" @click="savewx">
+                    保存
+                </el-button>
+                </el-form-item>
+            </el-form>
+        </div>
+    </el-card>
+</div>
+</template>
+
+<script>
+import { onMounted, ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import axios from 'axios'
+
+export default {
+    setup() {
+        const formSize = ref('default')
+        const activeName = ref('first')
+
+        const wxForm = ref({
+            usemodel:'',
+            autoReplySingle: '',
+            prefix: '',
+            suffix: '',
+            atReply: '',
+            keyWords:'',
+            whiteRoom:'',
+            blackName:'',
+        })
+
+        const savewx = async () => {
+            try {
+                // 在上传之前将布尔值转换为字符串
+                const dataToSend = { ...wxForm.value }
+
+                // 遍历 wxForm 中的属性,将布尔值转换为字符串
+                for (const key in dataToSend) {
+                    if (typeof dataToSend[key] === 'boolean') {
+                        dataToSend[key] = dataToSend[key].toString()
+                    }
+                }
+
+                // 发送转换后的数据
+                const response = await axios.post('/wxconfig', dataToSend)
+
+                const status = response.data.status
+                const msg = response.data.msg
+
+                if (status === 200) {
+                    ElMessage.success(msg);
+                } else {
+                    ElMessage.error('修改失败!' + msg)
+                }
+            } catch (error) {
+                ElMessage.error('修改失败!', error.message)
+            }
+        }
+
+
+        onMounted(async () => {
+            try {
+                const wxresponse = await axios.post('/getwxconfig')
+                const wxconfigData = wxresponse.data.msg
+
+                wxconfigData.forEach(item => {
+                    // 判断是否是字符串形式的 "true" 或 "false"
+                    if (item.value === "true" || item.value === "false") {
+                        // 将字符串转为布尔值
+                        wxForm.value[item.config] = JSON.parse(item.value)
+                    } else {
+                        // 如果不是布尔值字符串,直接赋值
+                        wxForm.value[item.config] = item.value
+                    }
+                })
+            } catch (error) {
+                ElMessage.error('查询失败!', error.message)
+            }
+        })
+
+        return {
+            formSize,
+            wxForm,
+            activeName,
+            savewx,
+        };
+    },
+}
+
+</script>
+<style scoped>
+.centered-card {
+    flex: 1; 
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.item {
+    padding: 20px;
+    text-align: center;
+}
+
+.box-card {
+    width: 100%;
+}
+
+@media screen and (min-width: 768px) {
+    .box-card {
+        width: 30%;
+    }
+}
+</style>

+ 6 - 0
vue.config.js

@@ -0,0 +1,6 @@
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  transpileDependencies: true,
+  lintOnSave:false,
+  publicPath: ''
+})