aiWithWechat前端部分是采用目前非常流程的VUE+ElementUI实现,其中图形展示采用eCharts相关技术实现。aiWithWechat使用最新的前端技术栈,提供各类经过各种项目验证的实用的组件方便在业务开发时的调用。aiWithWechat的核心业务功能是实现微信机器人,在微信中提问按照预制的答案进行回答,感兴趣或者有相关技术能力的公司或个人可以基于此项目开发出适合自己的衍生项目。
本文档的作用只是起到抛砖引玉的作用,供广大爱好者或者相关行业工作者学习或借鉴。
aiwithwechat-front ├── package.json #node.js项目的配置文件,管理各种第三方包 ├── public #存放静态文件的目录 | ├── element-theme #相关的elementUI主题 | ├── favicon.ico #网站标识的图标文件 | └── index.html #项目入口页面 ├── README.md #项目介绍 ├── src #存放项目源码的目录 | ├── App.vue #vue的根组件 | ├── assets #静态资源(会被打包) | ├── components #组件库 | ├── echartsTheme #echarts主题 | ├── element-ui #当前element-ui主题样式 | ├── i18n #多语言配置 | ├── icons #图标目录 | ├── main.js #应用程序的入口文件 | ├── mixins #存放混入的文件,方便类似功能复用 | ├── router #存放路由相关的文件 | ├── store #VUEX管理应用程序的状态 | ├── utils #公共的自定义的通用函数库 | └── views #业务相关功能的视图 ├── vue.config.js #vue-cli配置
├── main.vue #登录后主页面视图 ├── modules #业务模块 | ├── qamgt #问答管理 | | ├── qa-add-or-update.vue #添加问答页面 | | └── qa.vue #文档管理主页面 | ├── home.vue #登录后工作区域内容视图 | ├── oadocument #文档管理(类似于云盘) | | ├── document.vue #文档管理 | | └── documenttype.vue #文档类型管理 | └── sys #系统管理 | ├── choose-dept-tree.vue #部门树 | ├── dept-add-or-update.vue #部门增加或修改 | ├── dept-set-leader-list.vue #设置部门负责人 | ├── dept.vue #部门管理 | ├── dict-data.vue #字典管理 | ├── dict-type.vue #字典类型管理 | ├── log-error.vue #错误日志 | ├── log-login.vue #登录日志 | ├── log-operation.vue #操作日志 | ├── menu.vue #菜单管理 | ├── params.vue #全局参数管理 | ├── region.vue #区域管理 | ├── role.vue #角色管理 | └── sysuser.vue #用户管理 └── pages ├── 404.vue #404页面视图 └── login.vue #登录页面视图
一个成熟的项目包含很多技术框架类的内容,例如组件库,自定义函数库,路由处理,多语言,UI样式等等。由于此aiWithWechat是一个完整的项目,虽然业务功能点不多,但技术框架该有的基础功能已经基本涵盖(路由,cookie,图表,小组件等等),以下从二个功能点代码进行讲解,起到抛砖引玉的作用,完整的代码学习还需要自行在项目中或者自行debug进行学习。
一般大部分的应用系统都有登录功能,为了安全性或者不同角色使用不同的功能使用。互联网上的应用很多使用了第三方的一键登录,例如微信扫码登录、关注公众号登录、支付宝账号登录、短信息验证登录等等。但企业内部的应用系统,基本还是采用用户名及密码登录,为了防止暴力破解,一般会加一个验证码进行验证。前端代码如下:
<template> <div class="aui-wrapper aui-page__login"> <div class="aui-content__wrappers"> <div class="logo"> <div class="logo-login-img"> <img src="../../assets/img/logo-login.png" alt="" /> </div> </div> </div> <div class="aui-content__wrapper"> <main class="aui-content"> <h2 class="login-brand">{{ $t('brand.lg') }}</h2> <div class="login-body"> <img src="../../assets/img/login-blue.png" alt="" class="img-login"/> <h3 class="login-title">{{ $t('login.title') }}</h3> <el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmitHandle()" status-icon > <el-form-item prop="username"> <el-input v-model="dataForm.username" :placeholder="$t('login.username')" > <span slot="prefix" class="el-input__icon"> <svg class="icon-svg" aria-hidden="true"> <use xlink:href="#icon-user"></use> </svg> </span> </el-input> </el-form-item> <el-form-item prop="password"> <el-input v-model="dataForm.password" type="password" :placeholder="$t('login.password')" > <span slot="prefix" class="el-input__icon"> <svg class="icon-svg" aria-hidden="true"> <use xlink:href="#icon-lock"></use> </svg> </span> </el-input> </el-form-item> <el-form-item prop="captcha"> <el-row :gutter="20"> <el-col :span="14"> <el-input v-model="dataForm.captcha" :placeholder="$t('login.captcha')" > <span slot="prefix" class="el-input__icon"> <svg class="icon-svg" aria-hidden="true"> <use xlink:href="#icon-safetycertificate"></use> </svg> </span> </el-input> </el-col> <el-col :span="10" class="login-captcha"> <img :src="captchaPath" @click="getCaptcha()" /> </el-col> </el-row> </el-form-item> <el-form-item> <el-button type="primary" @click="dataFormSubmitHandle()" class="w-percent-100" >{{ $t('login.title') }}</el-button > </el-form-item> </el-form> </div> </main> </div> <div class="login-footer"> <p>{{ $t('login.copyright') }} 2014 - 2025 ©</p> </div> <!-- 广告区域 --> <div class="publicize"> <!-- <div class="publicize_Switch"></div> --> <div class="publicize_text"> <div> <img src="~@/assets/img/erwei.jpg" alt="" /> <p>联系我们</p> </div> </div> </div> </div> </template> <script> import Cookies from 'js-cookie'; import debounce from 'lodash/debounce'; import { messages } from '@/i18n'; import { getUUID } from '@/utils'; export default { data() { return { i18nMessages: messages, captchaPath: '', dataForm: { username: '', password: '', uuid: '', captcha: '', }, }; }, computed: { dataRule() { return { username: [ { required: true, message: this.$t('validate.required'), trigger: 'blur', }, ], password: [ { required: true, message: this.$t('validate.required'), trigger: 'blur', }, ], captcha: [ { required: true, message: this.$t('validate.required'), trigger: 'blur', }, ], }; }, }, created() { this.getCaptcha(); }, mounted() { this.bgSwitch(); }, methods: { // 获取验证码 getCaptcha() { this.dataForm.uuid = getUUID(); this.captchaPath = `${window.SITE_CONFIG['apiURL']}/captcha?uuid=${this.dataForm.uuid}`; }, // 表单提交 dataFormSubmitHandle: debounce( function() { this.$refs['dataForm'].validate((valid) => { if (!valid) { return false; } this.$http .post('/login', this.dataForm) .then(({ data: res }) => { if (res.code !== 0) { this.getCaptcha(); return this.$message.error(res.msg); } Cookies.set('token', res.data.token); this.$router.replace({ name: 'home' }); }) .catch(() => {}); }); }, 1000, { leading: true, trailing: false } ), // 背景轮播 bgSwitch() { // 获取 class 为 aui-page__login 的标签 var logins = document.querySelector('.aui-content__wrappers'); let bgswitch = () => { // 设置一个随机1-3的正整数 let random = Math.floor(Math.random() * 3 + 1); logins.style.backgroundImage = 'url(' + require('@/assets/img/login_dark' + random + '.jpg') + ')'; }; // 设置一个定时器,每隔5秒钟切换一次背景图片 let timer = setInterval(bgswitch, 5000); // 监听页面可见性变化,并在页面隐藏时暂停动画,在页面显示时恢复动画 document.visibilitychange = () => { if (document.hidden) { // 停止定时器 clearInterval(timer); } else { // 重新调用定时器函数,实现自动轮播功能 timer = setInterval(bgswitch, 5000); } }; }, }, }; </script>
页面效果如下:
以上代码包含了登录页面的布局和逻辑。在模板部分,使用了Element UI库的组件来构建登录表单。其中包括了用户名、密码和动态验证码的输入框,以及一个提交按钮。通过v-model指令将输入框的值与data中的dataForm对象进行双向绑定,使得输入框的值可以实时更新到dataForm对象中。
在脚本部分,首先引入了一些依赖,包括js-cookie库和lodash库的debounce函数。然后定义了组件的data属性,包括了多语言消息、验证码图片和表单数据。其中,dataForm对象包含了用户名、密码、uuid和验证码。
computed属性定义了表单的验证规则,主要是用户名、密码的必填规则。
在created生命周期中,调用了getCaptcha方法来获取验证码图片。
dataFormSubmitHandle方法是表单提交的处理函数,使用了debounce函数来防止抖动。this.http.post方法向服务器发送登录请求,根据服务器返回的结果进行处理,如果登录成功则将token保存到Cookies中,并跳转到首页,否则重新获取验证码并显示错误信息。
bgSwitch方法是处理背景图的轮播效果,设置了一个定时器,每隔5秒钟换一下背景图。
要实现用预制的答案在微信中回答问题,首先需要将问答信息维护到数据库中,其次通过OpenAI把问题进行向量化并存储到向量数据库中,然后才能在微信中进行问与答。以下是问答管理的前端代码:
<template> <el-card shadow="never" class="aui-card--fill"> <div class="mod-qamgt__qa}"> <el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()"> <el-form-item> <el-input v-model="dataForm.question" placeholder="问题" clearable></el-input> </el-form-item> <el-form-item> <el-input v-model="dataForm.answer" placeholder="答案" clearable></el-input> </el-form-item> <el-form-item> <el-button @click="getDataList()" icon="el-icon-search">{{ $t('query') }}</el-button> </el-form-item> <el-form-item> <el-button v-if="$hasPermission('qamgt:qa:save')" type="primary" @click="addOrUpdateHandle()" icon="el-icon-plus">{{ $t('add') }}</el-button> </el-form-item> <el-form-item> <el-button v-if="$hasPermission('qamgt:qa:delete')" type="danger" @click="deleteHandle()" icon="el-icon-delete">{{ $t('deleteBatch') }}</el-button> </el-form-item> </el-form> <el-table stripe @sort-change="dataListSortChangeHandle" v-loading="dataListLoading" :data="dataList" border @selection-change="dataListSelectionChangeHandle" style="width: 100%;"> <el-table-column type="selection" header-align="center" align="center" width="50"></el-table-column> <el-table-column prop="question" label="问题" header-align="center" align="center" min-width="160" show-overflow-tooltip></el-table-column> <el-table-column prop="answer" label="答案" header-align="center" align="center" min-width="300" show-overflow-tooltip></el-table-column> <el-table-column prop="tokens" label="tokens" header-align="center" align="center" min-width="80" show-overflow-tooltip></el-table-column> <el-table-column :label="$t('handle')" fixed="right" header-align="center" align="center" width="200"> <template slot-scope="scope"> <el-button v-if="$hasPermission('qamgt:qa:info')" type="text" size="small" @click="viewInfoHandle(scope.row.id)" icon="el-icon-view">{{ $t('info') }}</el-button> <el-button v-if="$hasPermission('qamgt:qa:update')" type="text" size="small" @click="addOrUpdateHandle(scope.row.id)" icon="el-icon-edit">{{ $t('update') }}</el-button> <el-button v-if="$hasPermission('qamgt:qa:delete')" type="text" size="small" @click="deleteHandle(scope.row.id)" icon="el-icon-delete">{{ $t('delete') }}</el-button> </template> </el-table-column> </el-table> <el-pagination :current-page="page" :page-sizes="[10, 20, 50, 100]" :page-size="limit" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="pageSizeChangeHandle" @current-change="pageCurrentChangeHandle"> </el-pagination> <!-- 弹窗, 新增 / 修改 --> <add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update> <view-info v-if="viewInfoVisible" ref="viewInfo" @refreshDataList="getDataList"></view-info> </div> </el-card> </template> <script> import mixinViewModule from '@/mixins/view-module' import AddOrUpdate from './qa-add-or-update' import ViewInfo from './qa-view-info' export default { mixins: [mixinViewModule], data () { return { mixinViewModuleOptions: { getDataListURL: '/qamgt/qa/page', getDataListIsPage: true, deleteURL: '/qamgt/qa', deleteIsBatch: true }, loading: {}, dataForm: { question: '', answer: '' } } }, components: { AddOrUpdate, ViewInfo }, methods: { } } </script>
页面效果如下:
从以上代码可以看出,使用了Element UI库的常用组件,包括el-card、el-form、el-input、el-row、el-col、el-button、el-table、el-table-column和el-pagination等。其中,el-card是一个卡片组件,el-form是一个表单组件,el-input是一个输入框组件,el-row和el-col是用来布局的组件,el-button是一个按钮组件,el-table是一个表格组件,el-table-column是表格的列组件,el-pagination是一个分页组件。
<el-form>是一个表单组件,用于展示问答设置的表单。@keyup.enter.native="getDataList()"表示在按下回车键时触发相关搜索的方法。
<el-row>和<el-col>是用于布局的组件,:span属性表示占据的列数,:gutter属性表示列之间的间隔。
<el-table>用于展示问答列表。:data属性绑定了dataList属性,表示表格的数据来源。
<el-table>中,使用了<el-table-column>组件定义了表格的列。prop属性表示列对应的数据字段,label属性表示列的标题。
点击新增按钮时,弹出新增问答的dialog,代码如下:
<template> <el-dialog :visible.sync="visible" :title="!dataForm.id ? $t('add') : $t('update')" :close-on-click-modal="false" :close-on-press-escape="false"> <el-form v-loading="loading" :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmitHandle()" :label-width="$i18n.locale === 'en-US' ? '120px' : '80px'"> <el-form-item label="问题" prop="question"> <el-input type="textarea" :rows="4" v-model="dataForm.question" placeholder="问题" maxlength="65535"></el-input> </el-form-item> <el-form-item label="答案" prop="answer"> <el-input type="textarea" :rows="16" v-model="dataForm.answer" placeholder="答案" maxlength="65535"></el-input> </el-form-item> </el-form> <template slot="footer"> <el-button @click="visible = false" icon="el-icon-close">{{ $t('cancel') }}</el-button> <el-button type="primary" @click="dataFormSubmitHandle()" icon="el-icon-check">{{ $t('confirm') }}</el-button> </template> </el-dialog> </template> <script> import debounce from 'lodash/debounce' export default { data () { return { visible: false, dataForm: { id: '', question: '', answer: '' }, loading: false } }, computed: { dataRule () { return { question: [ { required: true, message: this.$t('validate.required'), trigger: 'blur' } ], answer: [ { required: true, message: this.$t('validate.required'), trigger: 'blur' } ], } } }, methods: { init () { this.visible = true this.$nextTick(() => { this.$refs['dataForm'].resetFields() if (this.dataForm.id) { this.getInfo() } }) }, // 获取信息 getInfo () { this.$http.get(`/qamgt/qa/${this.dataForm.id}`).then(({ data: res }) => { if (res.code !== 0) { return this.$message.error(res.msg) } this.dataForm = { ...this.dataForm, ...res.data } }).catch(() => {}) }, // 表单提交 dataFormSubmitHandle: debounce(function () { this.$refs['dataForm'].validate((valid) => { if (!valid) { return false } this.loading = true this.$http[!this.dataForm.id ? 'post' : 'put']('/qamgt/qa/', this.dataForm).then(({ data: res }) => { if (res.code !== 0) { this.loading = false return this.$message.error(res.msg) } this.$message({ message: this.$t('prompt.success'), type: 'success', duration: 1000, onClose: () => { this.loading = false this.visible = false this.$emit('refreshDataList') } }) }).catch(() => {this.loading = false}) }) }, 1000, { 'leading': true, 'trailing': false }) } } </script>
页面效果如下:
以上代码的主要逻辑是为了实现问答维护的功能,以下是部分代码解释:
<el-dialog>是一个对话框组件,用于展示问答设置的弹窗。:visible.sync="visible"表示通过visible属性控制对话框的显示和隐藏,:title属性根据dataForm.id的值来确定对话框的标题是"添加"还是"更新",:close-on-click-modal="false"和:close-on-press-escape="false"表示点击对话框外部和按下ESC键时不关闭对话框。
<el-form>表示是一个表单组件,用于实现问答的问和答。v-loading="loading"属性绑定了一个loading变量,当为true时,则显示loading图标和对应的遮罩层,主要是因为要调用第三方的接口获得向量值,由于网络问题可能会响应比较慢,因此加一个loading样式。
methods中的方法解释:
init方法用于初始化对话框,设置对话框的显示状态,并重置表单的字段。
getInfo方法用于获取问答的相关信息(在修改时),发送HTTP请求并根据返回结果进行相应的操作。
dataFormSubmitHandle方法用于表单的提交,先进行表单的校验,然后发送HTTP请求进行数据的添加或更新。
此处只是前端维护问答设置的页面,真正的使用是在微信当中。微信中的效果如下:
如上图所示,当提问的问题是预制的问答时,系统将直接响应匹配到的答案(可以开出提的问题并不是和设置的问题完全一样,而是类似,这就是AI中向量匹配相关的技术),如果非预制问题时,或者匹配度达不到设置的分值时,则直接由大语言模型进行回答。
扫码关注不迷路!!!
郑州升龙商业广场B座25层
service@iqiqiqi.cn
联系电话:187-0363-0315
联系电话:199-3777-5101