From 6bd214aafe6a7e2fdc6d862ce2fafadbc8d40617 Mon Sep 17 00:00:00 2001 From: ronger Date: Fri, 26 May 2023 15:34:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(components):=20=E5=AE=9E=E7=8E=B0=20chatGP?= =?UTF-8?q?T=20=E6=B5=81=E5=BC=8F=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nuxt.config.js | 16 +++++---- package.json | 7 ++-- pages/chats/_account.vue | 76 ++++++++++++++++++++++++++++++++++++++-- plugins/vue-sse.js | 15 ++++++++ 4 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 plugins/vue-sse.js diff --git a/nuxt.config.js b/nuxt.config.js index d4ddde2..7043019 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -57,7 +57,8 @@ export default { {src: '~/plugins/extend'}, {src: '~/plugins/axios'}, {src: '~/plugins/element-ui'}, - {src: '~/plugins/vditor', ssr: false} + {src: '~/plugins/vditor', ssr: false}, + {src: '~/plugins/vue-sse'} // {src: '~/plugins/vue-cropper', ssr: false} ], /* @@ -99,14 +100,15 @@ export default { autoFetch: false }, endpoints: { - login: { url: '/api/auth/login', method: 'post' }, - logout: { url: '/api/auth/logout', method: 'post' }, - refresh: { url: '/api/auth/refresh-token', method: 'post' }, - user: { url: '/api/auth/user', method: 'get' } + login: {url: '/api/auth/login', method: 'post'}, + logout: {url: '/api/auth/logout', method: 'post'}, + refresh: {url: '/api/auth/refresh-token', method: 'post'}, + user: {url: '/api/auth/user', method: 'get'} }, autoLogout: false } - } + }, + plugins: [{src: '~/plugins/axios', ssr: true}] }, axios: { proxy: true // 开启proxy @@ -139,7 +141,7 @@ export default { config.module.rules[2].use[0].options.plugins = ['lodash'] }, babel: { - presets({ envName }) { + presets({envName}) { return [ [ '@nuxt/babel-preset-app', diff --git a/package.json b/package.json index aaae1ff..160353b 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "@nuxtjs/auth-next": "^5.0.0-1667386184.dfbbb54", "@nuxtjs/axios": "^5.13.1", "babel-plugin-lodash": "^3.3.4", + "core-js": "^2.6.12", + "defu": "^5.0.1", "echarts": "^4.9.0", "element-ui": "^2.15.12", "lodash": "^4.17.21", @@ -26,9 +28,8 @@ "simple-icons": "^6.23.0", "vditor": "^3.8.18", "vue-cropperjs": "^4.2.0", - "vuejs-avataaars": "^4.0.1", - "core-js": "^2.6.12", - "defu": "^5.0.1" + "vue-sse": "^2.5.2", + "vuejs-avataaars": "^4.0.1" }, "devDependencies": { "@commitlint/cli": "^17.4.0", diff --git a/pages/chats/_account.vue b/pages/chats/_account.vue index 0798f47..8f1b41f 100644 --- a/pages/chats/_account.vue +++ b/pages/chats/_account.vue @@ -8,6 +8,16 @@ 发送 + + + + + +
+
+
+
+
@@ -39,6 +49,7 @@ import Vue from 'vue'; import {mapState} from 'vuex'; import apiConfig from '~/config/api.config'; +import 'vditor/dist/css/content-theme/light.css'; export default { name: "Chat", @@ -61,7 +72,10 @@ export default { isShow: true, loading: false, to: {}, - messages: [] + messages: [], + vueSse: null, + customEvents: null, + message: '' } }, watch: { @@ -159,6 +173,7 @@ export default { }, async send() { let _ts = this; + _ts.message = ''; const message = { to: _ts.to.account, from: _ts.user.account, @@ -173,18 +188,59 @@ export default { _ts.contentEditor.setValue('') _ts.$axios.$post('/api/openai/chat', { message: message.content - }).then(res => { + }).then(async res => { + const html = await Vue.Vditor.md2html(_ts.message); _ts.messages.push({ to: _ts.user.account, from: _ts.to.account, dataType: 1, dataId: new Date().getTime(), - content: res[0].message.content + content: html }); _ts.messages.sort((a, b) => { return b.dataId - a.dataId; }); + _ts.message = ''; }); + }, + close() { + let _ts = this; + //浏览器关闭SSE连接 + _ts.vueSse.disconnect(); + _ts.$axios.$get(`/api/sse/close/${_ts.user.idUser}`); + }, + init() { + //初始化 vue-sse + let _ts = this; + let vueSse = _ts.vueSse; + //监听 message + vueSse.on('message', this.handleMessage); + //监听 customEvents + vueSse.once('customEvents', this.handleCustomEvents); + + //里面的 on、once、off 是用了发布订阅模式, + //源码 once 方法这有点小问题,写文章时改了 + //源码但还没提PR(主要是没提过,不会弄) + //执行 connect() 返回个Promise, + vueSse + .connect() + .then((sse) => { + console.log("We're connected!", sse); + }) + .catch((err) => console.error('Failed make initial connection:', err)); + }, + //message回调 + handleMessage(res) { + if (typeof res !== "undefined") { + this.message += res; + } + }, + //handleCustomEvents回调 + handleCustomEvents(res) { + console.log('customEvents22:', res) + let { data } = res; + console.info('customEvents:', data); + this.customEvents = data; } }, async mounted() { @@ -215,6 +271,12 @@ export default { content: '伟大的"坦格利安家族的风暴降生丹妮莉丝 · 铁王座的合法继承人 · 安达尔人和先民的合法女王 · 七国的守护者 · 草海上的卡丽熙 · 不焚者 · 解放者 · 傲之追猎者 · 悠米"为你服务' } _ts.messages.push(message); + _ts.vueSse = _ts.$sse.create({ + url: `/api/sse/subscribe/${_ts.user.idUser}`, + format: 'json', + withCredentials: true + }); + this.init(); } if (!_ts.initEditor) { @@ -230,6 +292,14 @@ export default { }); }, 500); } + }, + sse: { + //配置后自动添加断开连接事件,源码里面是做了判断, + //然后加在组件 beforeDestroy 生命周期里 + cleanup: true, + }, + beforeDestroy() { + this.close(); } } diff --git a/plugins/vue-sse.js b/plugins/vue-sse.js new file mode 100644 index 0000000..46f3d83 --- /dev/null +++ b/plugins/vue-sse.js @@ -0,0 +1,15 @@ +import Vue from 'vue' +import VueSSE from 'vue-sse'; + +// using defaults +Vue.use(VueSSE); + +// OR specify custom defaults (described below) +Vue.use(VueSSE, { + format: 'json', //数据格式 + url: '/', //路径 + withCredentials: true, //标识为open? // withCredentials should be set after "open" for Safari and Chrome (< 19 ?) + forcePolyfill:false, //强制使用原生SSE,使用另一个库event-source-polyfill + polyfill: true, //支持旧版浏览器 + polyfillOptions:null, //配置参数 +});