Bladeren bron

公众号推文

wangqi49 1 maand geleden
bovenliggende
commit
0ffddbd88e

+ 3 - 12
webchat-front-admin/src/assets/main.css

@@ -20,15 +20,6 @@ a,
   }
 }
 
-@media (min-width: 1024px) {
-  body {
-    display: flex;
-    place-items: center;
-  }
-
-  #app {
-    display: grid;
-    grid-template-columns: 1fr 1fr;
-    padding: 0 2rem;
-  }
-}
+#app {
+  height: 100%;
+}

+ 1 - 0
webchat-front-admin/src/main.js

@@ -6,6 +6,7 @@ import router from './router'
 import Antd from 'ant-design-vue';
 import 'ant-design-vue/dist/reset.css';
 import * as Icons from '@ant-design/icons-vue'; // 引入图标库
+import { message } from 'ant-design-vue';
 
 const app = createApp(App)
 

+ 1 - 1
webchat-front-admin/src/router/index.js

@@ -20,7 +20,7 @@ const router = createRouter({
     {path: '/account-offical-management', component: AccountOfficalManagement},
     {path: '/account-black-management', component: AccountBlackManagement},
     {path: '/wallet-management', component: WalletManagement},
-    {path: '/article-management', component: OfficialArticleManagement},
+    {path: '/article-management', name: 'officialArticleManagement', component: OfficialArticleManagement},
     {path: '/editArticle', name: 'editArticle', component: EditArticle},
   ],
 })

+ 198 - 86
webchat-front-admin/src/views/editArticle.vue

@@ -1,6 +1,6 @@
 <template>
 
-    <div style="padding-bottom: 200px;">
+    <div style="padding-bottom: 100px;">
       <a-breadcrumb>
         <a-breadcrumb-item>WebChat</a-breadcrumb-item>
         <a-breadcrumb-item>控制台</a-breadcrumb-item>
@@ -15,89 +15,89 @@
             :defaultConfig="toolbarConfig"
             :mode="mode"
         />
-        <div class="edit-body-container" style="min-height: 400px; height: auto;">
-            <input class="edit-title" :value="title" placeholder="文章标题">
-            <Editor id="edit-content"
-                v-model="valueHtml"
-                :defaultConfig="editorConfig"
-                :mode="mode"
-                @onCreated="handleCreated"
-            />
-        </div>
-        <div class="edit-plugin-container">
-          <div class="edit-plugin-container-title">
-            封面和摘要
-          </div>
-          <div style="display: flex;">
-            <a-upload
-             class="article-cover"
-              name="articleCover"
-              :action="uploadUrl"
-              list-type="picture-card"
-              :show-upload-list="false"
-              :before-upload="beforeUpload"
-            >
-              <div v-if="!articleCover">
-                <a-icon type="plus" />
-                <div style="margin-top: 8px">上传封面</div>
-              </div>
-              <div v-else>
-                <img :src="articleCover" alt="封面图" style="width: 100%; display: block" />
-              </div>
-            </a-upload>
-            <a-textarea
-              style=" margin-left: 20px; height: 125px;"
-              v-model:value="value2"
-              placeholder="文章摘要"
-            />
-          </div>
-        </div>
-        <div class="edit-plugin-container">
-          <div class="edit-plugin-container-title">
-            绑定公众号
+        <div class="article-edit-container">
+          <div class="edit-body-container" style="min-height: 400px; height: auto;">
+              <input class="edit-title" v-model="title" placeholder="文章标题">
+              <Editor id="edit-content"
+                  v-model="content"
+                  :defaultConfig="editorConfig"
+                  :mode="mode"
+                  @onCreated="handleCreated"
+              />
           </div>
-          <div>
-            <a-select
-              show-search
-              placeholder="请选择推文公众号"
-              style="width: 200px"
-              :options="officalAccountOptions"
-              @focus="handleFocus"
-              @blur="handleBlur"
-              @change="handleChange"
-            >
-            </a-select>
+          <div class="edit-plugin-container">
+            <div class="edit-plugin-container-title">
+              封面和摘要
+            </div>
+            <div style="display: flex;">
+              <a-upload
+              class="article-cover"
+                name="articleCover"
+                :action="uploadUrl"
+                list-type="picture-card"
+                :show-upload-list="false"
+                :before-upload="beforeUpload"
+              >
+                <div v-if="!articleCover">
+                  <a-icon type="plus" />
+                  <div style="margin-top: 8px">上传封面</div>
+                </div>
+                <div v-else>
+                  <img :src="articleCover" alt="封面图" style="width: 100%; display: block" />
+                </div>
+              </a-upload>
+              <a-textarea
+                style=" margin-left: 20px; height: 125px;"
+                v-model:value="description"
+                placeholder="文章摘要"
+              />
+            </div>
           </div>
-        </div>
-        <div class="edit-plugin-container">
-          <div class="edit-plugin-container-title">
-            计划推送时间
-          </div>
-          <div>
-            <a-date-picker show-time>
-              <template #renderExtraFooter>extra footer</template>
-            </a-date-picker>
+          <div class="edit-plugin-container">
+            <div class="edit-plugin-container-title">
+              绑定公众号
+            </div>
+            <div>
+              <a-select
+                show-search
+                placeholder="请选择推文公众号"
+                style="width: 200px"
+                :options="officalAccountOptions"
+                @change="handleOfficialChange"
+              >
+              </a-select>
+            </div>
           </div>
-        </div>
-        <div class="edit-plugin-container">
-          <div class="edit-plugin-container-title">
-            外部链接(非必填)
+          <div class="edit-plugin-container">
+            <div class="edit-plugin-container-title">
+              计划推送时间
+            </div>
+            <div>
+              <a-date-picker show-time v-model:value="planPushTime">
+                <template #renderExtraFooter>extra footer</template>
+              </a-date-picker>
+            </div>
           </div>
-          <div>
-            <a-input class="edit-input" placeholder="如果指定外部链接则优先跳转到指定链接地址" />
+          <div class="edit-plugin-container">
+            <div class="edit-plugin-container-title">
+              外部链接(非必填)
+            </div>
+            <div>
+              <a-input class="edit-input" v-model:value="redirectUrl" placeholder="如果指定外部链接则优先跳转到指定链接地址" />
+            </div>
           </div>
-        </div>
-        <div class="edit-plugin-container">
-          <div class="edit-plugin-container-title">
-            文章标签
+          <div class="edit-plugin-container">
+            <div class="edit-plugin-container-title">
+              文章标签
+            </div>
+            <div>
+              <a-input class="edit-input" v-model:value="signs" placeholder="多个标签请用英文逗号分割,如:标签A,标签B,标签C" />
+            </div>
           </div>
-          <div>
-            <a-input class="edit-input" placeholder="多个标签请用英文逗号分割,如:标签A,标签B,标签C" />
-          </div>
-        </div>
-        <div class="edit-plugin-container">
-          <div>
-            <a-button type="primary" style="width: 200px; height: 45px; background-color: black;">推送文章</a-button>
+          <div class="edit-plugin-container">
+            <div>
+              <a-button type="primary" @click="submitArticle" style="width: 200px; height: 45px; background-color: black;">推送文章</a-button>
+            </div>
           </div>
         </div>
     </div>
@@ -107,23 +107,35 @@
 
 <script>
 import axios from 'axios';
-import '@wangeditor/editor/dist/css/style.css';
+import { message } from 'ant-design-vue';
+import '@wangeditor/editor/dist/css/style.css' // 引入 css
 import { onBeforeUnmount, ref, shallowRef, onMounted } from 'vue';
 import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
-import { useRoute } from 'vue-router';
+import { useRoute, useRouter } from 'vue-router';
 
 export default {
   components: { Editor, Toolbar },
   setup() {
     const route = useRoute();
+    const router = useRouter();
     const oauthCode = ref(route.query.oauthCode || new URLSearchParams(window.location.search).get('oauthCode'));
     // 编辑器实例,必须用 shallowRef
     const editorRef = shallowRef()
+    const title = ref('')
+    const description = ref('')
+    const signs = ref('')
+    const redirectUrl = ref('')
+    const officialAccount = ref('')
+    const planPushTime = ref(null); 
     // 内容 HTML
-    const valueHtml = ref('')
+    const content = ref('')
     const officalAccountOptions = ref([])
     const articleCover = ref(null); 
     const uploadUrl = '/admin-service/file/upload';
+    
+    const handleOfficialChange = (value) => {
+      officialAccount.value = value;
+    }
 
     const handleArticleCoverUpload = async (file) => {
         const formData = new FormData();
@@ -200,7 +212,29 @@ export default {
     })
 
     const toolbarConfig = {}
-    const editorConfig = { placeholder: '请输入内容...' }
+    const editorConfig = {
+      placeholder: '请输入内容...',
+      uploadImage: {
+        server: '/admin-service/file/upload', 
+        fieldName: 'file', 
+        headers: {
+          'oauth-code': oauthCode.value,
+          'origin-url': window.location.href,
+          'upload-path': 'images/editor'
+        },
+        customInsert(res, insertFn) {
+          // 图片上传成功后,将图片插入到编辑器中
+          if (res.code === 200) {
+            insertFn(res.data.url, '', '');
+          } else {
+            message.error(res.message || '图片上传失败');
+          }
+        },
+        maxFileSize: 2 * 1024 * 1024, // 图片最大大小,单位字节
+        allowedFileTypes: ['image/jpeg', 'image/png', 'image/gif'] // 允许上传的图片类型
+      }
+    };
+
 
     // 组件销毁时,也及时销毁编辑器
     onBeforeUnmount(() => {
@@ -213,11 +247,79 @@ export default {
       editorRef.value = editor // 记录 editor 实例,重要!
     }
 
+    // 公众号推文
+    const submitArticle = async () => {
+      if (!title.value) {
+            message.error('标题不能为空');
+            return;
+        }
+        if (!content.value) {
+            message.error('正文不能为空');
+            return;
+        }
+        if (!articleCover.value) {
+            message.error('封面图不能为空');
+            return;
+        }
+        if (!officialAccount.value) {
+            message.error('未绑定公众号');
+            return;
+        }
+        if (!signs.value) {
+            message.error('标签不能为空');
+            return;
+        }
+        if (!description.value) {
+            message.error('摘要不能为空');
+            return;
+        }
+         // 将 planPushTime 转换为 13 位时间戳
+         let planPushTimeStamp = null;
+            if (planPushTime.value) {
+                planPushTimeStamp = planPushTime.value.valueOf();
+            }
+        try {
+            const response = await axios.post('/admin-service/official/article/submit', 
+              {
+                publicAccount: officialAccount.value,
+                title: title.value,
+                signs: signs.value,
+                description: description.value,
+                content: content.value,
+                redirectUrl: redirectUrl.value,
+                planPushTime: planPushTimeStamp,
+                cover: articleCover.value
+              }, 
+              {
+                headers: {
+                  'oauth-code': oauthCode.value,
+                  'origin-url': window.location.href
+                }
+              }
+            );
+            if (response.data.code === 200) {
+                message.success('文章已提交');
+                 // 跳转到文章列表页
+                router.push({ name: 'officialArticleManagement' });
+            } else {
+                message.error(response.data.message || '文章提交失败');
+            }
+        } catch (error) {
+            message.error('文章提交失败');
+        }
+    } 
+
     return {
+      title,
+      description,
+      signs,
+      redirectUrl,
+      officialAccount,
       articleCover,
       uploadUrl,
       editorRef,
-      valueHtml,
+      content,
+      planPushTime,
       officalAccountOptions,
       mode: 'default', // 或 'simple'
       toolbarConfig,
@@ -225,7 +327,9 @@ export default {
       handleCreated,
       listOficalAccounts,
       handleArticleCoverUpload,
-      beforeUpload
+      beforeUpload,
+      submitArticle,
+      handleOfficialChange
     }
   },
 }
@@ -238,8 +342,16 @@ export default {
     justify-content: center;
     align-items: center;
 }
+.article-edit-container {
+  width: 750px;
+  padding: 50px 70px 100px 70px;
+  box-shadow: 0px 0px 5px rgb(222, 221, 221);
+  border-radius: 5px;
+  overflow: hidden;
+  border: 1px solid rgb(241, 239, 239);
+}
 .edit-body-container {
-    width: 70%;
+    width: 100%;
     min-width: 600px;
 }
 .edit-title {
@@ -256,7 +368,7 @@ export default {
   }
   .edit-plugin-container {
     position: relative;
-    width: 70%;
+    width: 100%;
     margin-top: 30px;
   }
   .edit-plugin-container-title {

+ 34 - 17
webchat-front-admin/src/views/officialArticleManagement.vue

@@ -40,10 +40,12 @@
   </template>
   
   <script>
+  import { h } from 'vue';
+  import dayjs from 'dayjs';
   import axios from 'axios';
-import { defineComponent, ref, onMounted, watch, nextTick } from 'vue';
-import { useRoute, useRouter } from 'vue-router';
-import { Avatar } from 'ant-design-vue';
+  import { defineComponent, ref, onMounted, watch, nextTick } from 'vue';
+  import { useRoute, useRouter } from 'vue-router';
+  import { Avatar } from 'ant-design-vue';
 
 export default defineComponent({
   setup() {
@@ -58,27 +60,42 @@ export default defineComponent({
     const pageSize = ref(10); // 每页显示的记录数
     const total = ref(0); // 总记录数
     const pageTotal = ref(0); // 总页数
-
     const columns = [
       {
-        title: '账号ID',
-        dataIndex: 'userId', 
-        key: 'userId',
+        title: '文章ID',
+        dataIndex: 'id', 
+        key: 'id',
+      },
+      {
+        title: '标题',
+        dataIndex: 'title',
+        key: 'title',
       },
       {
-        title: '账号',
-        dataIndex: 'mobile',
-        key: 'mobile',
+        title: '公众号',
+        customRender: ({ text, record }) => {
+          // 使用 record.user.userName 来访问嵌套属性
+          return record.publicAccountInfo?.userName || '';
+        }
       },
       {
-        title: '用户名',
-        dataIndex: 'userName',
-        key: 'userName',
+        title: '计划推送时间',
+        dataIndex: 'planPushTime',
+        customRender: ({ text }) => {
+          return text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '-';
+        }
       },
       {
-        title: '签名',
-        dataIndex: 'signature',
-        key: 'signature',
+        title: '推送状态',
+        dataIndex: 'status',
+        customRender: ({ text }) => {
+          const statusMap = {
+            1: { text: '待推送', color: '#000' },
+            2: { text: '已推送', color: '#017D6F' }
+          };
+          const status = statusMap[text] || {};
+          return h('span', { style: { color: status.color } }, status.text);
+        }
       }
     ];
     const openEditorPage = () => {
@@ -86,7 +103,7 @@ export default defineComponent({
     };
     const fetchData = async (page = 1, pageSize = 10, keyword = '') => {
       try {
-        const response = await axios.get(`/admin-service/article/page`, {
+        const response = await axios.get(`/admin-service/official/article/page`, {
           params: {
             keyword: keyword,
             pageNo: page,

+ 607 - 2
webchat-front-client/package-lock.json

@@ -8,6 +8,7 @@
       "name": "webchat-front-client",
       "version": "0.0.0",
       "dependencies": {
+        "@wangeditor/editor-for-vue": "^5.1.12",
         "ant-design-vue": "^4.0.0-rc.6",
         "axios": "^1.7.9",
         "highlight.js": "^11.11.1",
@@ -17,7 +18,8 @@
         "vue": "^3.5.13",
         "vue-markdown-it": "^0.9.4",
         "vue-router": "^4.5.0",
-        "vue3-emoji-picker": "^1.1.8"
+        "vue3-emoji-picker": "^1.1.8",
+        "wangeditor": "^4.7.15"
       },
       "devDependencies": {
         "@types/markdown-it": "^14.1.2",
@@ -489,6 +491,19 @@
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@babel/runtime-corejs3": {
+      "version": "7.26.7",
+      "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.26.7.tgz",
+      "integrity": "sha512-55gRV8vGrCIYZnaQHQrD92Lo/hYE3Sj5tmbuf0hhHR7sj2CWhEhHU89hbq+UVDXvFG1zUVXJhUkEq1eAfqXtFw==",
+      "license": "MIT",
+      "dependencies": {
+        "core-js-pure": "^3.30.2",
+        "regenerator-runtime": "^0.14.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
     "node_modules/@babel/template": {
       "version": "7.25.9",
       "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
@@ -1377,6 +1392,13 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/@transloadit/prettier-bytes": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz",
+      "integrity": "sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==",
+      "license": "MIT",
+      "peer": true
+    },
     "node_modules/@types/estree": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
@@ -1384,6 +1406,13 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/event-emitter": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/@types/event-emitter/-/event-emitter-0.3.5.tgz",
+      "integrity": "sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==",
+      "license": "MIT",
+      "peer": true
+    },
     "node_modules/@types/linkify-it": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
@@ -1409,6 +1438,66 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@uppy/companion-client": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/@uppy/companion-client/-/companion-client-2.2.2.tgz",
+      "integrity": "sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@uppy/utils": "^4.1.2",
+        "namespace-emitter": "^2.0.1"
+      }
+    },
+    "node_modules/@uppy/core": {
+      "version": "2.3.4",
+      "resolved": "https://registry.npmjs.org/@uppy/core/-/core-2.3.4.tgz",
+      "integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@transloadit/prettier-bytes": "0.0.7",
+        "@uppy/store-default": "^2.1.1",
+        "@uppy/utils": "^4.1.3",
+        "lodash.throttle": "^4.1.1",
+        "mime-match": "^1.0.2",
+        "namespace-emitter": "^2.0.1",
+        "nanoid": "^3.1.25",
+        "preact": "^10.5.13"
+      }
+    },
+    "node_modules/@uppy/store-default": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@uppy/store-default/-/store-default-2.1.1.tgz",
+      "integrity": "sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/@uppy/utils": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-4.1.3.tgz",
+      "integrity": "sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "lodash.throttle": "^4.1.1"
+      }
+    },
+    "node_modules/@uppy/xhr-upload": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz",
+      "integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@uppy/companion-client": "^2.2.2",
+        "@uppy/utils": "^4.1.2",
+        "nanoid": "^3.1.25"
+      },
+      "peerDependencies": {
+        "@uppy/core": "^2.3.3"
+      }
+    },
     "node_modules/@vitejs/plugin-vue": {
       "version": "5.2.1",
       "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz",
@@ -1643,6 +1732,173 @@
       "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
       "license": "MIT"
     },
+    "node_modules/@wangeditor/basic-modules": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz",
+      "integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "is-url": "^1.2.4"
+      },
+      "peerDependencies": {
+        "@wangeditor/core": "1.x",
+        "dom7": "^3.0.0",
+        "lodash.throttle": "^4.1.1",
+        "nanoid": "^3.2.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/code-highlight": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@wangeditor/code-highlight/-/code-highlight-1.0.3.tgz",
+      "integrity": "sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "prismjs": "^1.23.0"
+      },
+      "peerDependencies": {
+        "@wangeditor/core": "1.x",
+        "dom7": "^3.0.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/core": {
+      "version": "1.1.19",
+      "resolved": "https://registry.npmjs.org/@wangeditor/core/-/core-1.1.19.tgz",
+      "integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@types/event-emitter": "^0.3.3",
+        "event-emitter": "^0.3.5",
+        "html-void-elements": "^2.0.0",
+        "i18next": "^20.4.0",
+        "scroll-into-view-if-needed": "^2.2.28",
+        "slate-history": "^0.66.0"
+      },
+      "peerDependencies": {
+        "@uppy/core": "^2.1.1",
+        "@uppy/xhr-upload": "^2.0.3",
+        "dom7": "^3.0.0",
+        "is-hotkey": "^0.2.0",
+        "lodash.camelcase": "^4.3.0",
+        "lodash.clonedeep": "^4.5.0",
+        "lodash.debounce": "^4.0.8",
+        "lodash.foreach": "^4.5.0",
+        "lodash.isequal": "^4.5.0",
+        "lodash.throttle": "^4.1.1",
+        "lodash.toarray": "^4.4.0",
+        "nanoid": "^3.2.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/editor": {
+      "version": "5.1.23",
+      "resolved": "https://registry.npmjs.org/@wangeditor/editor/-/editor-5.1.23.tgz",
+      "integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@uppy/core": "^2.1.1",
+        "@uppy/xhr-upload": "^2.0.3",
+        "@wangeditor/basic-modules": "^1.1.7",
+        "@wangeditor/code-highlight": "^1.0.3",
+        "@wangeditor/core": "^1.1.19",
+        "@wangeditor/list-module": "^1.0.5",
+        "@wangeditor/table-module": "^1.1.4",
+        "@wangeditor/upload-image-module": "^1.0.2",
+        "@wangeditor/video-module": "^1.1.4",
+        "dom7": "^3.0.0",
+        "is-hotkey": "^0.2.0",
+        "lodash.camelcase": "^4.3.0",
+        "lodash.clonedeep": "^4.5.0",
+        "lodash.debounce": "^4.0.8",
+        "lodash.foreach": "^4.5.0",
+        "lodash.isequal": "^4.5.0",
+        "lodash.throttle": "^4.1.1",
+        "lodash.toarray": "^4.4.0",
+        "nanoid": "^3.2.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/editor-for-vue": {
+      "version": "5.1.12",
+      "resolved": "https://registry.npmjs.org/@wangeditor/editor-for-vue/-/editor-for-vue-5.1.12.tgz",
+      "integrity": "sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@wangeditor/editor": ">=5.1.0",
+        "vue": "^3.0.5"
+      }
+    },
+    "node_modules/@wangeditor/list-module": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/@wangeditor/list-module/-/list-module-1.0.5.tgz",
+      "integrity": "sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==",
+      "license": "MIT",
+      "peer": true,
+      "peerDependencies": {
+        "@wangeditor/core": "1.x",
+        "dom7": "^3.0.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/table-module": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/@wangeditor/table-module/-/table-module-1.1.4.tgz",
+      "integrity": "sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==",
+      "license": "MIT",
+      "peer": true,
+      "peerDependencies": {
+        "@wangeditor/core": "1.x",
+        "dom7": "^3.0.0",
+        "lodash.isequal": "^4.5.0",
+        "lodash.throttle": "^4.1.1",
+        "nanoid": "^3.2.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/upload-image-module": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@wangeditor/upload-image-module/-/upload-image-module-1.0.2.tgz",
+      "integrity": "sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==",
+      "license": "MIT",
+      "peer": true,
+      "peerDependencies": {
+        "@uppy/core": "^2.0.3",
+        "@uppy/xhr-upload": "^2.0.3",
+        "@wangeditor/basic-modules": "1.x",
+        "@wangeditor/core": "1.x",
+        "dom7": "^3.0.0",
+        "lodash.foreach": "^4.5.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/video-module": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/@wangeditor/video-module/-/video-module-1.1.4.tgz",
+      "integrity": "sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==",
+      "license": "MIT",
+      "peer": true,
+      "peerDependencies": {
+        "@uppy/core": "^2.1.4",
+        "@uppy/xhr-upload": "^2.0.7",
+        "@wangeditor/core": "1.x",
+        "dom7": "^3.0.0",
+        "nanoid": "^3.2.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
     "node_modules/ant-design-vue": {
       "version": "4.0.0-rc.6",
       "resolved": "https://registry.npmjs.org/ant-design-vue/-/ant-design-vue-4.0.0-rc.6.tgz",
@@ -1864,6 +2120,17 @@
         "url": "https://opencollective.com/core-js"
       }
     },
+    "node_modules/core-js-pure": {
+      "version": "3.40.0",
+      "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.40.0.tgz",
+      "integrity": "sha512-AtDzVIgRrmRKQai62yuSIN5vNiQjcJakJb4fbhVw3ehxx7Lohphvw9SGNWKhLFqSxC4ilD0g/L1huAYFQU3Q6A==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/core-js"
+      }
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.6",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -1885,6 +2152,20 @@
       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
       "license": "MIT"
     },
+    "node_modules/d": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
+      "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
+      "license": "ISC",
+      "peer": true,
+      "dependencies": {
+        "es5-ext": "^0.10.64",
+        "type": "^2.7.2"
+      },
+      "engines": {
+        "node": ">=0.12"
+      }
+    },
     "node_modules/dayjs": {
       "version": "1.11.13",
       "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
@@ -1973,6 +2254,16 @@
       "integrity": "sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==",
       "license": "MIT"
     },
+    "node_modules/dom7": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/dom7/-/dom7-3.0.0.tgz",
+      "integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "ssr-window": "^3.0.0-alpha.1"
+      }
+    },
     "node_modules/electron-to-chromium": {
       "version": "1.5.83",
       "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.83.tgz",
@@ -2002,6 +2293,49 @@
         "url": "https://github.com/sponsors/antfu"
       }
     },
+    "node_modules/es5-ext": {
+      "version": "0.10.64",
+      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
+      "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
+      "hasInstallScript": true,
+      "license": "ISC",
+      "peer": true,
+      "dependencies": {
+        "es6-iterator": "^2.0.3",
+        "es6-symbol": "^3.1.3",
+        "esniff": "^2.0.1",
+        "next-tick": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/es6-iterator": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+      "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "d": "1",
+        "es5-ext": "^0.10.35",
+        "es6-symbol": "^3.1.1"
+      }
+    },
+    "node_modules/es6-symbol": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
+      "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
+      "license": "ISC",
+      "peer": true,
+      "dependencies": {
+        "d": "^1.0.2",
+        "ext": "^1.7.0"
+      },
+      "engines": {
+        "node": ">=0.12"
+      }
+    },
     "node_modules/esbuild": {
       "version": "0.24.2",
       "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
@@ -2053,12 +2387,39 @@
         "node": ">=6"
       }
     },
+    "node_modules/esniff": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
+      "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
+      "license": "ISC",
+      "peer": true,
+      "dependencies": {
+        "d": "^1.0.1",
+        "es5-ext": "^0.10.62",
+        "event-emitter": "^0.3.5",
+        "type": "^2.7.2"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
     "node_modules/estree-walker": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
       "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
       "license": "MIT"
     },
+    "node_modules/event-emitter": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+      "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "d": "1",
+        "es5-ext": "~0.10.14"
+      }
+    },
     "node_modules/execa": {
       "version": "9.5.2",
       "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.2.tgz",
@@ -2086,6 +2447,16 @@
         "url": "https://github.com/sindresorhus/execa?sponsor=1"
       }
     },
+    "node_modules/ext": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
+      "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
+      "license": "ISC",
+      "peer": true,
+      "dependencies": {
+        "type": "^2.7.2"
+      }
+    },
     "node_modules/figures": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
@@ -2249,6 +2620,17 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/html-void-elements": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz",
+      "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==",
+      "license": "MIT",
+      "peer": true,
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm"
+      }
+    },
     "node_modules/human-signals": {
       "version": "8.0.0",
       "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz",
@@ -2259,12 +2641,33 @@
         "node": ">=18.18.0"
       }
     },
+    "node_modules/i18next": {
+      "version": "20.6.1",
+      "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.6.1.tgz",
+      "integrity": "sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@babel/runtime": "^7.12.0"
+      }
+    },
     "node_modules/idb": {
       "version": "7.1.1",
       "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
       "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
       "license": "ISC"
     },
+    "node_modules/immer": {
+      "version": "9.0.21",
+      "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
+      "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
+      "license": "MIT",
+      "peer": true,
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/immer"
+      }
+    },
     "node_modules/immutable": {
       "version": "5.0.3",
       "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
@@ -2288,6 +2691,13 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/is-hotkey": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.2.0.tgz",
+      "integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==",
+      "license": "MIT",
+      "peer": true
+    },
     "node_modules/is-inside-container": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
@@ -2355,6 +2765,13 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/is-url": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
+      "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
+      "license": "MIT",
+      "peer": true
+    },
     "node_modules/is-what": {
       "version": "4.1.16",
       "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
@@ -2464,6 +2881,56 @@
       "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
       "license": "MIT"
     },
+    "node_modules/lodash.camelcase": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/lodash.clonedeep": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+      "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/lodash.debounce": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+      "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/lodash.foreach": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
+      "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/lodash.isequal": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+      "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+      "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/lodash.throttle": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
+      "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
+      "license": "MIT",
+      "peer": true
+    },
+    "node_modules/lodash.toarray": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
+      "integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==",
+      "license": "MIT",
+      "peer": true
+    },
     "node_modules/loose-envify": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -2599,6 +3066,16 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/mime-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/mime-match/-/mime-match-1.0.2.tgz",
+      "integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==",
+      "license": "ISC",
+      "peer": true,
+      "dependencies": {
+        "wildcard": "^1.1.0"
+      }
+    },
     "node_modules/mime-types": {
       "version": "2.1.35",
       "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
@@ -2635,6 +3112,13 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/namespace-emitter": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/namespace-emitter/-/namespace-emitter-2.0.1.tgz",
+      "integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==",
+      "license": "MIT",
+      "peer": true
+    },
     "node_modules/nanoid": {
       "version": "3.3.8",
       "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
@@ -2659,6 +3143,13 @@
       "integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==",
       "license": "MIT"
     },
+    "node_modules/next-tick": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
+      "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
+      "license": "ISC",
+      "peer": true
+    },
     "node_modules/node-releases": {
       "version": "2.0.19",
       "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
@@ -2805,6 +3296,17 @@
         "node": "^10 || ^12 || >=14"
       }
     },
+    "node_modules/preact": {
+      "version": "10.25.4",
+      "resolved": "https://registry.npmjs.org/preact/-/preact-10.25.4.tgz",
+      "integrity": "sha512-jLdZDb+Q+odkHJ+MpW/9U5cODzqnB+fy2EiHSZES7ldV5LK7yjlVzTp7R8Xy6W6y75kfK8iWYtFVH7lvjwrCMA==",
+      "license": "MIT",
+      "peer": true,
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/preact"
+      }
+    },
     "node_modules/prettier": {
       "version": "2.8.8",
       "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
@@ -2837,6 +3339,16 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/prismjs": {
+      "version": "1.29.0",
+      "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
+      "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/proxy-from-env": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -3394,6 +3906,61 @@
         "node": ">=18"
       }
     },
+    "node_modules/slate": {
+      "version": "0.72.8",
+      "resolved": "https://registry.npmjs.org/slate/-/slate-0.72.8.tgz",
+      "integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "immer": "^9.0.6",
+        "is-plain-object": "^5.0.0",
+        "tiny-warning": "^1.0.3"
+      }
+    },
+    "node_modules/slate-history": {
+      "version": "0.66.0",
+      "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.66.0.tgz",
+      "integrity": "sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "is-plain-object": "^5.0.0"
+      },
+      "peerDependencies": {
+        "slate": ">=0.65.3"
+      }
+    },
+    "node_modules/slate-history/node_modules/is-plain-object": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+      "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/slate/node_modules/is-plain-object": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+      "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/snabbdom": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.6.2.tgz",
+      "integrity": "sha512-ig5qOnCDbugFntKi6c7Xlib8bA6xiJVk8O+WdFrV3wxbMqeHO0hXFQC4nAhPVWfZfi8255lcZkNhtIBINCc4+Q==",
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">=12.17.0"
+      }
+    },
     "node_modules/source-map": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -3428,6 +3995,13 @@
       "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
       "license": "BSD-3-Clause"
     },
+    "node_modules/ssr-window": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-3.0.0.tgz",
+      "integrity": "sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==",
+      "license": "MIT",
+      "peer": true
+    },
     "node_modules/strip-final-newline": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
@@ -3514,6 +4088,13 @@
         "node": ">=12.22"
       }
     },
+    "node_modules/tiny-warning": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
+      "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
+      "license": "MIT",
+      "peer": true
+    },
     "node_modules/totalist": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
@@ -3528,9 +4109,15 @@
       "version": "2.8.1",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
       "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
-      "dev": true,
       "license": "0BSD"
     },
+    "node_modules/type": {
+      "version": "2.7.3",
+      "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
+      "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
+      "license": "ISC",
+      "peer": true
+    },
     "node_modules/uc.micro": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
@@ -3931,6 +4518,17 @@
         "node": ">=16.0.0"
       }
     },
+    "node_modules/wangeditor": {
+      "version": "4.7.15",
+      "resolved": "https://registry.npmjs.org/wangeditor/-/wangeditor-4.7.15.tgz",
+      "integrity": "sha512-aPTdREd8BxXVyJ5MI+LU83FQ7u1EPd341iXIorRNYSOvoimNoZ4nPg+yn3FGbB93/owEa6buLw8wdhYnMCJQLg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.2",
+        "@babel/runtime-corejs3": "^7.11.2",
+        "tslib": "^2.1.0"
+      }
+    },
     "node_modules/warning": {
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
@@ -3956,6 +4554,13 @@
         "node": ">= 8"
       }
     },
+    "node_modules/wildcard": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-1.1.2.tgz",
+      "integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==",
+      "license": "MIT",
+      "peer": true
+    },
     "node_modules/yallist": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

+ 3 - 1
webchat-front-client/package.json

@@ -9,6 +9,7 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "@wangeditor/editor-for-vue": "^5.1.12",
     "ant-design-vue": "^4.0.0-rc.6",
     "axios": "^1.7.9",
     "highlight.js": "^11.11.1",
@@ -18,7 +19,8 @@
     "vue": "^3.5.13",
     "vue-markdown-it": "^0.9.4",
     "vue-router": "^4.5.0",
-    "vue3-emoji-picker": "^1.1.8"
+    "vue3-emoji-picker": "^1.1.8",
+    "wangeditor": "^4.7.15"
   },
   "devDependencies": {
     "@types/markdown-it": "^14.1.2",

+ 15 - 839
webchat-front-client/src/App.vue

@@ -1,844 +1,20 @@
-<template >
-
-  <h3 style="position: absolute; left: 50px; top: 30px;">
-      <i>WebChat</i>
-  </h3>
-
-  <div class="chat-container">
-    <div class="chat-left-container">
-      <div class="mac-dots-container">
-        <div class="mac-dot red"></div>
-        <div class="mac-dot yellow"></div>
-        <div class="mac-dot green"></div>
-      </div>
-      <a-avatar :src="photo" :size="40" style="margin-left: 10px; margin-top: 20px; border: 2px solid white; border-radius: 50%;"/>
+<script setup>
+import { RouterLink, RouterView } from 'vue-router'
+</script>
 
-       <div class="chat-menus">
-        <MessageOutlined 
-          @click="handleIconClick('message')" 
-          :style="{ color: iconColors.message }" 
-          @mouseover="handleIconHover('message')"
-          @mouseout="handleIconLeave('message')"
-        />
-        <UserOutlined 
-          @click="handleIconClick('user')" 
-          :style="{ color: iconColors.user }" 
-          @mouseover="handleIconHover('user')"
-          @mouseout="handleIconLeave('user')"
-        />
-        <div class="bell-with-notification" :width="20">
-          <BellOutlined 
-          @click="handleIconClick('bell')" 
-          :style="{ color: iconColors.bell }" 
-          @mouseover="handleIconHover('bell')"
-          @mouseout="handleIconLeave('bell')"
-          />
-          <span class="message-red-point" :style="{ display: redPointStatus }"></span>
-        </div>
-        <WechatOutlined 
-          @click="handleIconClick('moment')" 
-          :style="{ color: iconColors.moment }" 
-          @mouseover="handleIconHover('moment')"
-          @mouseout="handleIconLeave('moment')"
-        />
-        <LoginOutlined style="position: absolute; bottom: 20px"
-          @click="handleIconClick('logout')" 
-          :style="{ color: iconColors.setting }" 
-          @mouseover="handleIconHover('logout')"
-          @mouseout="handleIconLeave('logout')"
-        />
-       </div>
-    </div>
-    <div class="chat-center-container">
-      <div class="chat-center-header-container">
-        <a-input v-model:value="searchInput" placeholder="搜索" class="search-input" />
-        <a-dropdown v-model:visible="dropdownVisible">
-          <a-button type="primary" class="search-button">
-            <PlusOutlined />
-          </a-button>
-          <template #overlay>
-            <a-menu>
-              <a-menu-item key="1" @click="createGroup">创建群聊</a-menu-item>
-              <a-menu-item key="2" @click="subscribe">加人/订阅</a-menu-item>
-            </a-menu>
-          </template>
-        </a-dropdown>
-      </div>
-      <div class="chat-center-body-container">
-        <component :is="currentCenterComponent" 
-          :friendsData="friendsData" 
-          :waitConfirmList="waitConfirmList" 
-          :chattingList="chattingList" 
-          :messageList="messageList"
-          :autoSelectedChatting="autoSelectedChatting"
-          @select-chat-user="handleSelectChatUser" >
-        </component>
-      </div>
-    </div>
-    <div class="chat-right-container">
-        <component :is="currentChatComponent" :selectChatUser="selectChatUser" > </component>
-    </div>
+<template>
+  <div class="client-container">
+    <RouterView />
   </div>
-
-  <!-- 添加好友/机器人 -->
-  <a-modal
-      v-model:visible="subscribeModalVisible"
-      title="搜索好友/机器人/订阅公众号"
-      :okButtonProps="{ hidden: true }"
-      :cancelButtonProps="{ hidden: true }"
-    >
-      <a-input
-        style="height: 40px; line-height: 40px; margin-top: 20px; text-indent: 2em;"
-        v-model:value="subscribeQueryInput"
-        placeholder="请输入用户名"
-        @keydown.enter="searchAccount"
-      />
-      <p style="color: #666; margin-top: 20px;">搜索结果:</p>
-      <div v-if="searchAccountInfo" class="search-account-card">
-        <div>
-          <a-avatar :src="searchAccountInfo.photo" :size="60" style="border: 2px solid white; border-radius: 50%;"/>
-        </div>
-        <div style="margin-left: 10px;">
-          <div style="font-weight: 500; font-size: 15px; color: black;">{{ searchAccountInfo.userName }}</div>
-          <div style="margin-top: 5px;">{{ searchAccountInfo.signature }}</div>
-          <a-button type="primary" @click="doSubscribe" class="doSubscribeBtn">
-            {{ searchAccountInfo.roleCode <= 2 ? '添加好友' : searchAccountInfo.roleCode == 5 ? '添加机器人' : '订阅公众号'}}
-          </a-button>
-        </div>
-      </div>
-    </a-modal>
-    <!-- 创建群聊 -->
-    <a-modal
-      title="新建群聊"
-      style="height: 400px; width: 600px; border-radius: 3px;"
-      :visible="createGroupVisible"
-      okText = "创 建"
-      cancelText = "取 消"
-      :okButtonProps="{ createGroupLoading }"
-      @ok="handleCreateGroup"
-      @cancel="handleCancelGroup"
-    >
-    <div style="display: flex; gap: 20px; margin-top: 30px;">
-       <!-- 左侧好友列表 -->
-       <div style="flex: 1; border-right: 1px solid #eee; padding-right: 20px;">
-        <a-checkbox-group v-model:value="selectedFriends">
-          <a-checkbox v-for="friend in friendsData.users?.accounts" 
-                :key="friend.userId"
-                :value="friend.userId" 
-                class="group-friend-item">
-              <img :src="friend.photo" alt="avatar" class="group-friend-avatar" />
-              <span class="group-friend-name">{{ friend.userName }}</span>
-          </a-checkbox>
-        </a-checkbox-group>
-      </div>
-      <!-- 右侧已选列表 -->
-      <div style="flex: 1; padding-left: 10px;">
-        <div style="font-weight: 500; margin-bottom: 10px;">已选成员({{ selectedFriends.length }}人)</div>
-        <div style="height: 300px; overflow-y: auto;">
-          <div 
-            v-for="userId in selectedFriends" 
-            :key="userId"
-            style="display: flex; align-items: center; padding: 6px;"
-          >
-            <a-avatar :src="getFriendPhoto(userId)" size="small" />
-            <span style="margin-left: 8px;">{{ getFriendName(userId) }}</span>
-          </div>
-          <div 
-            v-if="selectedFriends.length === 0"
-            style="color: #999; text-align: center; margin-top: 50px;"
-          >
-            请从左侧选择好友
-          </div>
-        </div>
-      </div>
-    </div>
-  </a-modal>
+  
 </template>
 
-<script>
-import './css/custom-antd.css';
-import axios from 'axios';
-import { provide, defineComponent, ref, onMounted, onUnmounted, watch } from 'vue';
-import { PlusOutlined } from '@ant-design/icons-vue';
-import { Avatar, Input, Button, Dropdown, Menu, MenuItem, message } from 'ant-design-vue';
-import MessageList from './views/MessageList.vue';
-import FriendList from './views/FriendList.vue';
-import ChattingList from './views/Chatting.vue';
-import WaitConfirmList from './views/WaitConfirmList.vue';
-import ChatCore from './views/ChatCore.vue';
-
-export default defineComponent({
-  components: {
-    'a-input': Input,
-    PlusOutlined,
-    Input,
-    Button,
-    Dropdown,
-    Menu,
-    MenuItem,
-    Avatar,
-    MessageList,
-    WaitConfirmList,
-    FriendList,
-    ChattingList,
-    ChatCore
-  },
-  setup() {
-    const userId = ref('');
-    const loginUser = ref({});
-    const photo = ref('');
-    const searchInput = ref('');
-    const dropdownVisible = ref(false);
-    const oauthCode = ref(null);
-    const socket = ref(null);
-    const reconnectInterval = ref(5000); // 重连间隔时间,单位:毫秒
-    const heartbeatInterval = ref(30000); // 心跳间隔时间,单位:毫秒
-    const isConnected = ref(false);
-    const messageQueue = ref([]); // 存储未发送的消息队列
-    const inputRef = ref(null);
-    const inputValue = ref('');
-    const searchAccountInfo = ref(null)
-    const subscribeModalVisible = ref(false)
-    const subscribeQueryInput = ref('');
-    const redPointStatus = ref('none');
-    const currentCenterComponent = ref('ChattingList'); 
-    const currentChatComponent = ref('ChatCore'); 
-    // 定义响应式数据
-    const activeIcon = ref('');
-    const friendsData = ref({});
-    const chattingList = ref([]);
-    const messageList = ref([]);
-    const waitConfirmList = ref([])
-    const lastChatTime = ref('')
-    const autoSelectedChatting = ref(true)
-    const createGroupVisible = ref(false)
-    const createGroupLoading = ref(false)
-    // 用对象记录每个图标的颜色状态
-    const iconColors = ref({
-      message: '#28c940',
-      user: 'white',
-      bell: 'white',
-      moment: 'white',
-      setting: 'white'
-    });
-    // 存储当前对话选中的账号角色类型
-    const selectChatUser = ref({});
-
-    provide('loginUser', loginUser);
-
-    const selectedFriends = ref([]);
-    const friendsMap = ref(new Map()); // 用于快速查找好友信息
-
-    // 获取好友信息映射
-    const buildFriendsMap = () => {
-      friendsMap.value.clear();
-      friendsData.value.users?.accounts?.forEach(friend => {
-        friendsMap.value.set(friend.userId, friend);
-      });
-    };
-
-    // 获取好友名称
-    const getFriendName = (userId) => {
-      return friendsMap.value.get(userId)?.userName || '未知用户';
-    };
-
-    // 获取好友头像
-    const getFriendPhoto = (userId) => {
-      return friendsMap.value.get(userId)?.photo || '';
-    };
-    
-    watch(friendsData, () => {
-      buildFriendsMap();
-    });
-
-    // 处理对话选择事件
-    const handleSelectChatUser = (selectUser) => {
-      selectChatUser.value = selectUser;
-    };
-    // 处理图标点击事件
-    const handleIconClick = (iconName) => {
-      activeIcon.value = iconName;
-      // 将所有图标颜色重置为白色
-      for (const key in iconColors.value) {
-        iconColors.value[key] = 'white';
-      }
-      // 将点击的图标颜色设为绿色
-      iconColors.value[iconName] = '#28c940';
-      if(iconName === 'bell') {
-        redPointStatus.value = 'none';
-      }
-      switch (iconName) {
-        case 'message':
-          currentCenterComponent.value = 'ChattingList';
-          break;
-        case 'user':
-          currentCenterComponent.value = 'FriendList';
-          break;
-        case 'bell':
-          currentCenterComponent.value = 'WaitConfirmList';
-          break;
-        case 'logout':
-          logout();
-          break;
-        default:
-          currentCenterComponent.value = 'ChattingList';
-      }
-    };
-
-    // 处理鼠标悬停事件
-    const handleIconHover = (iconName) => {
-      if (activeIcon.value!== iconName) {
-        iconColors.value[iconName] = '#28c940';
-      }
-    };
-
-    // 处理鼠标离开事件
-    const handleIconLeave = (iconName) => {
-      if (activeIcon.value!== iconName) {
-        iconColors.value[iconName] = 'white';
-      }
-    };
-
-    const handleSendMessage = (event) => {
-      if (inputValue.value) {
-        console.log('发送消息:', inputValue.value);
-        // 在这里添加发送消息的逻辑,例如将消息发送到服务器等
-        inputValue.value = ''; // 清空输入框
-        // 重新聚焦输入框
-        inputRef.value.focus();
-        event.preventDefault(); // 阻止回车键的默认行为(如换行)
-      }
-    };
-
-    const createGroup = () => {
-      console.log('创建群组操作');
-      // 在此添加创建群组的具体逻辑
-      dropdownVisible.value = false; // 关闭下拉菜单
-      createGroupVisible.value = true;
-    };
-    const subscribe = () => {
-      // 显示订阅/添加好友弹窗
-      subscribeModalVisible.value = true;
-      // 在此添加订阅的具体逻辑
-      dropdownVisible.value = false; // 关闭下拉菜单
-    };
-    
-    // 将 doSubscribe 定义为异步函数
-    const doSubscribe = async () => {
-        try {
-            const response = await axios.post(`/client-service/chat/account-relation/subscribe/` + searchAccountInfo.value.userId, {
-              headers: {
-                'oauth-code': oauthCode.value,
-                'origin-url': window.location.href
-              }
-            });
-            if (response.data.code === 40001) {
-              // 未登录
-              window.location.href = response.data.redirect_url;
-            } else if (response.data.code === 200) {
-               message.info(searchAccountInfo.value.roleCode <= 2 ? '好友申请已发出' : searchAccountInfo.value.roleCode == 5 ? '成功添加机器人' : '成功订阅公众号'); 
-               // 显示订阅/添加好友弹窗
-               subscribeModalVisible.value = true;
-            } else {
-              message.error(response.data.msg);
-            }
-        } catch (error) {
-            message.error('订阅失败');
-        }
-    }
-
-    const logout = async () => {
-        try {
-            const response = await axios.get(`/client-service/chat/account/logout`, {
-              headers: {
-                'oauth-code': oauthCode.value,
-                'origin-url': window.location.href
-              }
-            });
-            if (response.data.code === 200) {
-              window.location.href = response.data.redirect_url;
-            }
-        } catch (error) {
-            message.error('退出失败');
-        }
-    }
-
-    const searchAccount = () => {
-      oauthCode.value = new URLSearchParams(window.location.search).get('oauthCode');
-      axios.get(`/client-service/chat/account/query`, {
-        // 这里可以添加请求的配置,例如 headers 或 params
-        params: {
-          account: subscribeQueryInput.value
-        },
-        headers: {
-          'Content-Type': 'application/json',
-          'origin-url': window.location.href,
-          'oauth-code': oauthCode.value
-        }
-      }).then(function (response) {
-        if (response.data.code === 40001) {
-          // 未登录
-          window.location.href = response.data.redirect_url;
-        } else if (response.data.code === 200 && response.data.data) {
-          searchAccountInfo.value = response.data.data;
-          if (searchAccountInfo.value === null) {
-            message.error("账号不存在");
-          }
-        }
-      }).catch(function (error) {
-        // 处理错误
-        console.error(error);
-      });
-    }
-
-    // 加载待审核的好友申请列表
-    const loadWaitConfirmList = () => {
-      axios.get(`/client-service/chat/account-relation/wait-confirm/list`, {
-        // 这里可以添加请求的配置,例如 headers 或 params
-        headers: {
-          'Content-Type': 'application/json',
-          'origin-url': window.location.href,
-          'oauth-code': oauthCode.value
-        }
-      }).then(function (response) {
-        if (response.data.code === 40001) {
-          // 未登录
-          window.location.href = response.data.redirect_url;
-        } else if (response.data.code === 200) {
-          waitConfirmList.value = response.data.data;
-          if(waitConfirmList.value.length > 0) {
-            redPointStatus.value = "block"
-          }
-        }
-      }).catch(function (error) {
-        // 处理错误
-        console.error(error);
-      });
-    }
-
-    const loadChattingList = () => {
-      axios.get(`/client-service/chat/message/chatting/list`, {
-        // 这里可以添加请求的配置,例如 headers 或 params
-        params: {
-            "lastChatTime": lastChatTime.value
-        },
-        headers: {
-          'Content-Type': 'application/json',
-          'origin-url': window.location.href,
-          'oauth-code': oauthCode.value
-        }
-      }).then(function (response) {
-        if (response.data.code === 40001) {
-          // 未登录
-          window.location.href = response.data.redirect_url;
-        } else if (response.data.code === 200) {
-          chattingList.value = response.data.data;
-        }
-      }).catch(function (error) {
-        // 处理错误
-        console.error(error);
-      });
-    }
-
-    const loadFriendList = () => {
-      axios.get(`/client-service/chat/account-relation/list`, {
-        // 这里可以添加请求的配置,例如 headers 或 params
-        headers: {
-          'Content-Type': 'application/json',
-          'origin-url': window.location.href,
-          'oauth-code': oauthCode.value
-        }
-      }).then(function (response) {
-        if (response.data.code === 40001) {
-          // 未登录
-          window.location.href = response.data.redirect_url;
-        } else if (response.data.code === 200) {
-          friendsData.value = response.data.data;
-        }
-      }).catch(function (error) {
-        // 处理错误
-        console.error(error);
-      });
-    }
-
-    const loadCurrentUserInfo = () => {
-      oauthCode.value = new URLSearchParams(window.location.search).get('oauthCode');
-      axios.get(`/client-service/chat/account/current/info`, {
-        // 这里可以添加请求的配置,例如 headers 或 params
-        headers: {
-          'Content-Type': 'application/json',
-          'origin-url': window.location.href,
-          'oauth-code': oauthCode.value
-        }
-      }).then(function (response) {
-        if (response.data.code === 40001) {
-          // 未登录
-          window.location.href = response.data.redirect_url;
-        } else if (response.data.code === 200) {
-          userId.value = response.data.data.userId;
-          photo.value = response.data.data.photo;
-          loginUser.value = response.data.data;
-          loadChattingList();
-          loadFriendList();
-          loadWaitConfirmList();
-          connectWebSocket();
-        }
-      }).catch(function (error) {
-        // 处理错误
-        console.error(error);
-      });
-    }
-
-    onMounted(() => {
-      
-    });
-    onUnmounted(() => {
-      if (eventSource) {
-        eventSource.close();
-      }
-    });    
-
-    const connectWebSocket = () => {
-      socket.value = new WebSocket(`/connect-service/ws/chat/PC_WEB_INDEX/`+userId.value);
-      socket.value.onopen = () => {
-        console.log('WebSocket 已连接');
-        isConnected.value = true;
-        // 发送队列中的消息
-        messageQueue.value.forEach(message => socket.value.send(message));
-        messageQueue.value = [];
-        startHeartbeat();
-      };
-
-      socket.value.onmessage = (event) => {
-        console.log('App组件收到消息:', event.data);
-        // 在此处理收到的消息
-        const messageType = JSON.parse(event.data).type;
-        if (messageType === 5) {
-            // 申请添加好友,红点提醒
-            redPointStatus.value = "block";
-        }
-        else if (messageType === 7) {
-            autoSelectedChatting.value = false;
-            loadChattingList();
-        }
-      };
-
-      socket.value.onerror = (error) => {
-        console.error('WebSocket 错误:', error);
-        isConnected.value = false;
-        // 关闭当前连接
-        socket.value.close();
-        // 尝试重连
-        setTimeout(connectWebSocket, reconnectInterval.value);
-      };
-
-      socket.value.onclose = () => {
-        console.log('WebSocket 已关闭');
-        isConnected.value = false;
-        // 尝试重连
-        setTimeout(connectWebSocket, reconnectInterval.value);
-      };
-    };
-
-    const startHeartbeat = () => {
-      const heartbeat = () => {
-        if (isConnected.value) {
-          socket.value.send('ping'); // 发送心跳包
-        }
-      };
-      setInterval(heartbeat, heartbeatInterval.value);
-    };
-
-    const sendMessage = (message) => {
-      if (isConnected.value) {
-        socket.value.send(message);
-      } else {
-        messageQueue.value.push(message);
-      }
-    };
-
-    const handleCreateGroup = async () => {
-      if (selectedFriends.value.length < 3) {
-        message.error("请至少选择3位好友");
-        return;
-      }
-      createGroupLoading.value = true;
-      try {
-        const response = await axios.post(`/client-service/chat/account-relation/createGroup`, 
-        {
-          userIds: selectedFriends.value
-        }, 
-        {
-          headers: {
-            'oauth-code': oauthCode.value,
-            'origin-url': window.location.href
-          }
-        });
-        if (response.data.code === 200) {
-          createGroupVisible.value = false;
-          selectedFriends.value = [];
-          createGroupLoading.value = false;
-        } else {
-          message.error(response.data.msg);
-          createGroupLoading.value = false;
-        }
-      } catch (error) {
-        message.error("创建群聊失败");
-        createGroupLoading.value = false;
-      }
-    };
-
-    const handleCancelGroup = () => {
-      createGroupVisible.value = false;
-    }
-
-    onMounted(() => {
-      loadCurrentUserInfo();
-    });
-    onUnmounted(() => {
-      if (socket.value) {
-        socket.value.close();
-      }
-    });
-    
-    return {
-      userId,
-      photo,
-      loginUser,
-      searchInput,
-      dropdownVisible,
-      subscribeQueryInput,
-      searchAccountInfo,
-      subscribeModalVisible,
-      oauthCode,
-      inputRef,
-      inputValue,
-      activeIcon,
-      iconColors,
-      redPointStatus,
-      chattingList,
-      messageList,
-      waitConfirmList,
-      currentCenterComponent,
-      currentChatComponent,
-      friendsData,
-      lastChatTime,
-      selectChatUser,
-      autoSelectedChatting,
-      createGroupVisible,
-      selectedFriends,
-      createGroupLoading,
-      getFriendName,
-      getFriendPhoto,
-      handleCreateGroup,
-      getFriendName,
-      getFriendPhoto,
-      handleCreateGroup,
-      handleCancelGroup,
-      logout,
-      handleIconClick,
-      handleIconHover,
-      handleIconLeave,
-      sendMessage,
-      createGroup,
-      subscribe,
-      handleSendMessage,
-      searchAccount,
-      doSubscribe,
-      loadFriendList,
-      loadCurrentUserInfo,
-      loadChattingList,
-      handleSelectChatUser
-    };
-  }
-});
-</script>
-
 <style scoped>
-* {
-  margin: 0px;
-  padding: 0px;
-}
-.mac-dots-container {
-  margin-top: 10px;
-  display: flex;
-  justify-content: flex-start;
-  align-items: center;
-  padding: 5px;
-  width: 60px;
-  margin-left: 5px;
-}
-.mac-dot {
-  width: 10px;
-  height: 10px;
-  border-radius: 50%;
-  margin-right: 5px;
-}
-.red {
-  background-color: #ff5f57;
-}
-.yellow {
-  background-color: #ffbd2e;
-}
-.green {
-  background-color: #28c940;
-}
-.chat-container {
-  position: absolute;
-  width: 1100px;
-  height: 700px;
-  left: 50%;
-  top: 55%;
-  margin-left: -550px;
-  margin-top: -400px;
-  border-radius: 6px;
-  overflow: hidden;
-  /* box-shadow: 0px 0px 10px #eeeded; */
-  background-color: white;
-  border: 1px solid #e3e3e3;
-}
-.chat-left-container {
-  position: absolute;
-  top: 0px;
-  left: 0px;
-  width: 60px;
-  height: 100%;
-  background-color: black;
-}
-.chat-center-container {
-  position: absolute;
-  top: 0px;
-  left: 60px;
-  width: 270px;
-  height: 100%;
-  border-right: 1px solid #e3e3e3;
-  background-color: #f4f4f4;
-}
-.chat-right-container {
-  position: absolute;
-  top: 0px;
-  right: 0px;
-  width: calc(100% - 330px);
-  height: 100%;
-}
-.chat-center-header-container {
-  position: absolute;
-  top: 0px;
-  left: 0px;
-  width: 100%;
-  height: 50px;
-  border-bottom: 1px solid #e3e3e3;
-  background-color: #f9f9f9;
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-}
-.search-input {
-  height: 30px;
-  width: 210px;
-  margin-left: 10px;
-  background-color: #e5e5e5;
-  border: none;
-}
-.search-button {
-  width: 28px;
-  height: 28px;
-  margin-right: 10px;
-  background-color: black;
-}
-.search-account-card {
-  position: relative;
-  margin-top: 10px; 
-  display: flex; 
-  align-items: center;
-  border: 1px solid rgb(239, 236, 236);
-  padding: 20px;
-}
-.doSubscribeBtn {
-  position: absolute;
-  background-color: black;
-  right: 8px;
-  bottom: 8px;
-  padding: 2px 10px;
-}
-.chat-menus {
-  display: flex;
-  flex-direction: column;
-  margin-top: 30px;
-  width: 18px;
-  margin-left: 21px;
-}
-.chat-menus .anticon {
-  color: white;
-  font-size: 19px;
-  margin-bottom: 30px;
-}
-.bell-with-notification {
-  width: 20px;
-  height: auto;
-  position: relative;
-}
-.message-red-point {
-  position: absolute;
-  content: '';
-  right: -4px;
-  top: -4px;
-  width: 8px;
-  height: 8px;
-  background-color: red;
-  border-radius: 50%;
-  display: none;
-}
-.chat-center-body-container {
-  position: relative;
-  width: 100%;
-  height: 100%;
-  margin-top: 50px;
-  overflow-x: hidden;
-}
-.chat-center-body-container::-webkit-scrollbar {/*滚动条整体样式*/
-  width: 0px;     /*高宽分别对应横竖滚动条的尺寸*/
-  height: 0px;
-}
-.chat-center-body-container::-webkit-scrollbar-thumb {/*滚动条里面小方块*/
-    border-radius: 10px;
-    background-color: transparent;
-    background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, transparent 75%, transparent);
-}
-.chat-center-body-container::-webkit-scrollbar-track {/*滚动条里面轨道*/
-    -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0.2);
-    /*border-radius: 10px;*/
-    background: #EDEDED;
-}
-.group-friend-avatar {
-  width: 30px;
-  border-radius: 5px;
-  margin-right: 5px;
-}
-.group-friend-item {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-  width: 100%;
-  margin-bottom: 10px; /* 添加间距 */
-}
-.group-friends-list {
-  max-height: 350px;
-  overflow-x: hidden;
-  overflow-y: scroll;
-}
-  .group-friends-list::-webkit-scrollbar {/*滚动条整体样式*/
-        width: 0px;     /*高宽分别对应横竖滚动条的尺寸*/
-        height: 0px;
-    }
-  .group-friends-list::-webkit-scrollbar-thumb {/*滚动条里面小方块*/
-      border-radius: 10px;
-      background-color: transparent;
-      background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, transparent 75%, transparent);
-  }
-  .group-friends-list::-webkit-scrollbar-track {/*滚动条里面轨道*/
-      -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0.2);
-      /*border-radius: 10px;*/
-      background: #EDEDED;
-  }
-</style>
+/* .client-container { */
+  /* display: flex; */
+  /* align-items: center; */
+  /* justify-content: center; */
+  /* height: 100%; */
+  /* width: 100%; */
+/* } */
+</style>

+ 3 - 12
webchat-front-client/src/assets/main.css

@@ -20,15 +20,6 @@ a,
   }
 }
 
-@media (min-width: 1024px) {
-  body {
-    display: flex;
-    place-items: center;
-  }
-
-  #app {
-    display: grid;
-    grid-template-columns: 1fr 1fr;
-    padding: 0 2rem;
-  }
-}
+#app {
+  height: 100%;
+}

+ 19 - 2
webchat-front-client/src/router/index.js

@@ -1,9 +1,26 @@
 import { createRouter, createWebHistory } from 'vue-router'
 
+import Chat from '../views/chat.vue'
+
 const router = createRouter({
   history: createWebHistory(import.meta.env.BASE_URL),
   routes: [
-    
+    {
+      path: '/',
+      name: '/',
+      component: Chat,
+    },
+    {
+      path: '/chat',
+      name: 'chat',
+      component: Chat,
+    },
+    {
+      path: '/article',
+      name: 'article',
+      component: () => import('@/views/article.vue')
+    },
   ],
 })
-export default router
+
+export default router;

+ 38 - 2
webchat-front-client/src/views/ChatOfficial.vue

@@ -5,7 +5,7 @@
 
     </div>
     <div class="chat-core-container" style="background-color: whitesmoke; height: 660px; text-align: center; padding-top: 20px;">
-        <div class="article-card" v-for="article in articleArr" >
+        <div class="article-card" v-for="article in articleArr" @click="viewArticle(article.publicAccountArticle?.articleId)">
             <div class="article-card-cover">
                 <img :src="article.publicAccountArticle?.cover">
             </div>
@@ -20,6 +20,7 @@
   </template>
   
   <script>
+  import { useRouter } from 'vue-router';
   import axios from 'axios';
   import { inject, defineComponent, ref, onMounted, onUnmounted, watch } from 'vue';
   export default defineComponent({
@@ -27,6 +28,9 @@
         selectChatUserRef: Object
     },
     setup(props) {
+
+        const router = useRouter();
+
         const selectChatUser = ref(props.selectChatUserRef);
 
         const socket = ref(null);
@@ -37,6 +41,36 @@
         const heartbeatInterval = ref(30000); // 心跳间隔时间,单位:毫秒
         const articleArr = ref([]);
 
+        const loadChatMessages = (chatUserId) => {
+            // 先清空
+            axios.get(`/client-service/chat/message/list/`+chatUserId, {})
+            .then(function (response) {
+                if (response.data.code === 40001) {
+                    // 未登录
+                    window.location.href = response.data.redirect_url;
+                } else if (response.data.code === 200) {
+                    if (response.data.data.length > 0) {
+                        articleArr.value = response.data.data;
+                    }
+                }
+            }).catch(function (error) {
+                // 处理错误
+                console.error(error);
+            });
+        }
+        loadChatMessages( selectChatUser.value.userId);
+
+        const viewArticle = (articleId) => {
+            // 使用 router.resolve 生成完整的 URL
+            const routeData = router.resolve({
+                name: 'article',
+                query: { id: articleId }
+            });
+
+            // 使用 window.open 在新窗口中打开链接
+            window.open(routeData.href, '_blank');
+        }
+
         const connectWebSocket = () => {
             socket.value = new WebSocket(`/connect-service/ws/chat/PC_WEB_CHAT/` + loginUser.value.userId);
             socket.value.onopen = () => {
@@ -120,7 +154,9 @@
             articleArr,
             connectWebSocket,
             sendMessage,
-            startHeartbeat
+            startHeartbeat,
+            loadChatMessages,
+            viewArticle
         };
     }
   });

+ 73 - 0
webchat-front-client/src/views/article.vue

@@ -0,0 +1,73 @@
+<template>
+    <div class="article-container">
+        <div class="article-title">{{articleData?.title}}</div>
+        <div class="article-content" v-html="articleData?.content"></div>
+    </div>
+</template>
+
+<script>
+import axios from 'axios';
+import '@wangeditor/editor/dist/css/style.css';
+
+export default {
+  data() {
+    return {
+      articleId: null,
+      articleData: null
+    };
+  },
+  mounted() {
+    // 正确获取路由参数的方式
+    this.articleId = this.$route.query.id;
+    this.loadArticle();
+  },
+  watch: {
+    // 修正监听器写法
+    '$route.query.id': function(newId) {
+      this.articleId = newId;
+      this.loadArticle();
+    }
+  },
+  methods: {
+    async loadArticle() {
+      if (!this.articleId) return;
+      try {
+        const response = await axios.get(`/client-service/chat/article/detail/${this.articleId}`, {
+          headers: {
+            'origin-url': window.location.href
+          }
+        });
+        if (response.data.code === 40001) {
+          window.location.href = response.data.redirect_url;
+          return;
+        }
+        // 修正数据赋值方式
+        this.articleData = response.data.data;
+      } catch (error) {
+        console.error('加载文章失败:', error);
+      }
+    }
+  }
+};
+</script>
+
+<style scoped>
+    /* 样式保持不变 */
+    .article-container {
+        position: absolute;
+        width: 800px;
+        min-height: 200px;
+        left: 50%;
+        top: 30px;
+        margin-left: -400px;
+        border-radius: 6px;
+        overflow: hidden;
+        background-color: white;
+    }
+    .article-title {
+        font-size: 22px;
+    }
+    .article-content {
+        margin-top: 2px;
+    }
+</style>

+ 844 - 0
webchat-front-client/src/views/chat.vue

@@ -0,0 +1,844 @@
+<template >
+
+  <h3 style="position: absolute; left: 50px; top: 30px;">
+      <i>WebChat</i>
+  </h3>
+
+  <div class="chat-container">
+    <div class="chat-left-container">
+      <div class="mac-dots-container">
+        <div class="mac-dot red"></div>
+        <div class="mac-dot yellow"></div>
+        <div class="mac-dot green"></div>
+      </div>
+      <a-avatar :src="photo" :size="40" style="margin-left: 10px; margin-top: 20px; border: 2px solid white; border-radius: 50%;"/>
+
+       <div class="chat-menus">
+        <MessageOutlined 
+          @click="handleIconClick('message')" 
+          :style="{ color: iconColors.message }" 
+          @mouseover="handleIconHover('message')"
+          @mouseout="handleIconLeave('message')"
+        />
+        <UserOutlined 
+          @click="handleIconClick('user')" 
+          :style="{ color: iconColors.user }" 
+          @mouseover="handleIconHover('user')"
+          @mouseout="handleIconLeave('user')"
+        />
+        <div class="bell-with-notification" :width="20">
+          <BellOutlined 
+          @click="handleIconClick('bell')" 
+          :style="{ color: iconColors.bell }" 
+          @mouseover="handleIconHover('bell')"
+          @mouseout="handleIconLeave('bell')"
+          />
+          <span class="message-red-point" :style="{ display: redPointStatus }"></span>
+        </div>
+        <WechatOutlined 
+          @click="handleIconClick('moment')" 
+          :style="{ color: iconColors.moment }" 
+          @mouseover="handleIconHover('moment')"
+          @mouseout="handleIconLeave('moment')"
+        />
+        <LoginOutlined style="position: absolute; bottom: 20px"
+          @click="handleIconClick('logout')" 
+          :style="{ color: iconColors.setting }" 
+          @mouseover="handleIconHover('logout')"
+          @mouseout="handleIconLeave('logout')"
+        />
+       </div>
+    </div>
+    <div class="chat-center-container">
+      <div class="chat-center-header-container">
+        <a-input v-model:value="searchInput" placeholder="搜索" class="search-input" />
+        <a-dropdown v-model:visible="dropdownVisible">
+          <a-button type="primary" class="search-button">
+            <PlusOutlined />
+          </a-button>
+          <template #overlay>
+            <a-menu>
+              <a-menu-item key="1" @click="createGroup">创建群聊</a-menu-item>
+              <a-menu-item key="2" @click="subscribe">加人/订阅</a-menu-item>
+            </a-menu>
+          </template>
+        </a-dropdown>
+      </div>
+      <div class="chat-center-body-container">
+        <component :is="currentCenterComponent" 
+          :friendsData="friendsData" 
+          :waitConfirmList="waitConfirmList" 
+          :chattingList="chattingList" 
+          :messageList="messageList"
+          :autoSelectedChatting="autoSelectedChatting"
+          @select-chat-user="handleSelectChatUser" >
+        </component>
+      </div>
+    </div>
+    <div class="chat-right-container">
+        <component :is="currentChatComponent" :selectChatUser="selectChatUser" > </component>
+    </div>
+  </div>
+
+  <!-- 添加好友/机器人 -->
+  <a-modal
+      v-model:visible="subscribeModalVisible"
+      title="搜索好友/机器人/订阅公众号"
+      :okButtonProps="{ hidden: true }"
+      :cancelButtonProps="{ hidden: true }"
+    >
+      <a-input
+        style="height: 40px; line-height: 40px; margin-top: 20px; text-indent: 2em;"
+        v-model:value="subscribeQueryInput"
+        placeholder="请输入用户名"
+        @keydown.enter="searchAccount"
+      />
+      <p style="color: #666; margin-top: 20px;">搜索结果:</p>
+      <div v-if="searchAccountInfo" class="search-account-card">
+        <div>
+          <a-avatar :src="searchAccountInfo.photo" :size="60" style="border: 2px solid white; border-radius: 50%;"/>
+        </div>
+        <div style="margin-left: 10px;">
+          <div style="font-weight: 500; font-size: 15px; color: black;">{{ searchAccountInfo.userName }}</div>
+          <div style="margin-top: 5px;">{{ searchAccountInfo.signature }}</div>
+          <a-button type="primary" @click="doSubscribe" class="doSubscribeBtn">
+            {{ searchAccountInfo.roleCode <= 2 ? '添加好友' : searchAccountInfo.roleCode == 5 ? '添加机器人' : '订阅公众号'}}
+          </a-button>
+        </div>
+      </div>
+    </a-modal>
+    <!-- 创建群聊 -->
+    <a-modal
+      title="新建群聊"
+      style="height: 400px; width: 600px; border-radius: 3px;"
+      :visible="createGroupVisible"
+      okText = "创 建"
+      cancelText = "取 消"
+      :okButtonProps="{ createGroupLoading }"
+      @ok="handleCreateGroup"
+      @cancel="handleCancelGroup"
+    >
+    <div style="display: flex; gap: 20px; margin-top: 30px;">
+       <!-- 左侧好友列表 -->
+       <div style="flex: 1; border-right: 1px solid #eee; padding-right: 20px;">
+        <a-checkbox-group v-model:value="selectedFriends">
+          <a-checkbox v-for="friend in friendsData.users?.accounts" 
+                :key="friend.userId"
+                :value="friend.userId" 
+                class="group-friend-item">
+              <img :src="friend.photo" alt="avatar" class="group-friend-avatar" />
+              <span class="group-friend-name">{{ friend.userName }}</span>
+          </a-checkbox>
+        </a-checkbox-group>
+      </div>
+      <!-- 右侧已选列表 -->
+      <div style="flex: 1; padding-left: 10px;">
+        <div style="font-weight: 500; margin-bottom: 10px;">已选成员({{ selectedFriends.length }}人)</div>
+        <div style="height: 300px; overflow-y: auto;">
+          <div 
+            v-for="userId in selectedFriends" 
+            :key="userId"
+            style="display: flex; align-items: center; padding: 6px;"
+          >
+            <a-avatar :src="getFriendPhoto(userId)" size="small" />
+            <span style="margin-left: 8px;">{{ getFriendName(userId) }}</span>
+          </div>
+          <div 
+            v-if="selectedFriends.length === 0"
+            style="color: #999; text-align: center; margin-top: 50px;"
+          >
+            请从左侧选择好友
+          </div>
+        </div>
+      </div>
+    </div>
+  </a-modal>
+</template>
+
+<script>
+import '../css/custom-antd.css';
+import axios from 'axios';
+import { provide, defineComponent, ref, onMounted, onUnmounted, watch } from 'vue';
+import { PlusOutlined } from '@ant-design/icons-vue';
+import { Avatar, Input, Button, Dropdown, Menu, MenuItem, message } from 'ant-design-vue';
+import MessageList from '../views/MessageList.vue';
+import FriendList from '../views/FriendList.vue';
+import ChattingList from '../views/Chatting.vue';
+import WaitConfirmList from '../views/WaitConfirmList.vue';
+import ChatCore from '../views/ChatCore.vue';
+
+export default defineComponent({
+  components: {
+    'a-input': Input,
+    PlusOutlined,
+    Input,
+    Button,
+    Dropdown,
+    Menu,
+    MenuItem,
+    Avatar,
+    MessageList,
+    WaitConfirmList,
+    FriendList,
+    ChattingList,
+    ChatCore
+  },
+  setup() {
+    const userId = ref('');
+    const loginUser = ref({});
+    const photo = ref('');
+    const searchInput = ref('');
+    const dropdownVisible = ref(false);
+    const oauthCode = ref(null);
+    const socket = ref(null);
+    const reconnectInterval = ref(5000); // 重连间隔时间,单位:毫秒
+    const heartbeatInterval = ref(30000); // 心跳间隔时间,单位:毫秒
+    const isConnected = ref(false);
+    const messageQueue = ref([]); // 存储未发送的消息队列
+    const inputRef = ref(null);
+    const inputValue = ref('');
+    const searchAccountInfo = ref(null)
+    const subscribeModalVisible = ref(false)
+    const subscribeQueryInput = ref('');
+    const redPointStatus = ref('none');
+    const currentCenterComponent = ref('ChattingList'); 
+    const currentChatComponent = ref('ChatCore'); 
+    // 定义响应式数据
+    const activeIcon = ref('');
+    const friendsData = ref({});
+    const chattingList = ref([]);
+    const messageList = ref([]);
+    const waitConfirmList = ref([])
+    const lastChatTime = ref('')
+    const autoSelectedChatting = ref(true)
+    const createGroupVisible = ref(false)
+    const createGroupLoading = ref(false)
+    // 用对象记录每个图标的颜色状态
+    const iconColors = ref({
+      message: '#28c940',
+      user: 'white',
+      bell: 'white',
+      moment: 'white',
+      setting: 'white'
+    });
+    // 存储当前对话选中的账号角色类型
+    const selectChatUser = ref({});
+
+    provide('loginUser', loginUser);
+
+    const selectedFriends = ref([]);
+    const friendsMap = ref(new Map()); // 用于快速查找好友信息
+
+    // 获取好友信息映射
+    const buildFriendsMap = () => {
+      friendsMap.value.clear();
+      friendsData.value.users?.accounts?.forEach(friend => {
+        friendsMap.value.set(friend.userId, friend);
+      });
+    };
+
+    // 获取好友名称
+    const getFriendName = (userId) => {
+      return friendsMap.value.get(userId)?.userName || '未知用户';
+    };
+
+    // 获取好友头像
+    const getFriendPhoto = (userId) => {
+      return friendsMap.value.get(userId)?.photo || '';
+    };
+    
+    watch(friendsData, () => {
+      buildFriendsMap();
+    });
+
+    // 处理对话选择事件
+    const handleSelectChatUser = (selectUser) => {
+      selectChatUser.value = selectUser;
+    };
+    // 处理图标点击事件
+    const handleIconClick = (iconName) => {
+      activeIcon.value = iconName;
+      // 将所有图标颜色重置为白色
+      for (const key in iconColors.value) {
+        iconColors.value[key] = 'white';
+      }
+      // 将点击的图标颜色设为绿色
+      iconColors.value[iconName] = '#28c940';
+      if(iconName === 'bell') {
+        redPointStatus.value = 'none';
+      }
+      switch (iconName) {
+        case 'message':
+          currentCenterComponent.value = 'ChattingList';
+          break;
+        case 'user':
+          currentCenterComponent.value = 'FriendList';
+          break;
+        case 'bell':
+          currentCenterComponent.value = 'WaitConfirmList';
+          break;
+        case 'logout':
+          logout();
+          break;
+        default:
+          currentCenterComponent.value = 'ChattingList';
+      }
+    };
+
+    // 处理鼠标悬停事件
+    const handleIconHover = (iconName) => {
+      if (activeIcon.value!== iconName) {
+        iconColors.value[iconName] = '#28c940';
+      }
+    };
+
+    // 处理鼠标离开事件
+    const handleIconLeave = (iconName) => {
+      if (activeIcon.value!== iconName) {
+        iconColors.value[iconName] = 'white';
+      }
+    };
+
+    const handleSendMessage = (event) => {
+      if (inputValue.value) {
+        console.log('发送消息:', inputValue.value);
+        // 在这里添加发送消息的逻辑,例如将消息发送到服务器等
+        inputValue.value = ''; // 清空输入框
+        // 重新聚焦输入框
+        inputRef.value.focus();
+        event.preventDefault(); // 阻止回车键的默认行为(如换行)
+      }
+    };
+
+    const createGroup = () => {
+      console.log('创建群组操作');
+      // 在此添加创建群组的具体逻辑
+      dropdownVisible.value = false; // 关闭下拉菜单
+      createGroupVisible.value = true;
+    };
+    const subscribe = () => {
+      // 显示订阅/添加好友弹窗
+      subscribeModalVisible.value = true;
+      // 在此添加订阅的具体逻辑
+      dropdownVisible.value = false; // 关闭下拉菜单
+    };
+    
+    // 将 doSubscribe 定义为异步函数
+    const doSubscribe = async () => {
+        try {
+            const response = await axios.post(`/client-service/chat/account-relation/subscribe/` + searchAccountInfo.value.userId, {
+              headers: {
+                'oauth-code': oauthCode.value,
+                'origin-url': window.location.href
+              }
+            });
+            if (response.data.code === 40001) {
+              // 未登录
+              window.location.href = response.data.redirect_url;
+            } else if (response.data.code === 200) {
+               message.info(searchAccountInfo.value.roleCode <= 2 ? '好友申请已发出' : searchAccountInfo.value.roleCode == 5 ? '成功添加机器人' : '成功订阅公众号'); 
+               // 显示订阅/添加好友弹窗
+               subscribeModalVisible.value = true;
+            } else {
+              message.error(response.data.msg);
+            }
+        } catch (error) {
+            message.error('订阅失败');
+        }
+    }
+
+    const logout = async () => {
+        try {
+            const response = await axios.get(`/client-service/chat/account/logout`, {
+              headers: {
+                'oauth-code': oauthCode.value,
+                'origin-url': window.location.href
+              }
+            });
+            if (response.data.code === 200) {
+              window.location.href = response.data.redirect_url;
+            }
+        } catch (error) {
+            message.error('退出失败');
+        }
+    }
+
+    const searchAccount = () => {
+      oauthCode.value = new URLSearchParams(window.location.search).get('oauthCode');
+      axios.get(`/client-service/chat/account/query`, {
+        // 这里可以添加请求的配置,例如 headers 或 params
+        params: {
+          account: subscribeQueryInput.value
+        },
+        headers: {
+          'Content-Type': 'application/json',
+          'origin-url': window.location.href,
+          'oauth-code': oauthCode.value
+        }
+      }).then(function (response) {
+        if (response.data.code === 40001) {
+          // 未登录
+          window.location.href = response.data.redirect_url;
+        } else if (response.data.code === 200 && response.data.data) {
+          searchAccountInfo.value = response.data.data;
+          if (searchAccountInfo.value === null) {
+            message.error("账号不存在");
+          }
+        }
+      }).catch(function (error) {
+        // 处理错误
+        console.error(error);
+      });
+    }
+
+    // 加载待审核的好友申请列表
+    const loadWaitConfirmList = () => {
+      axios.get(`/client-service/chat/account-relation/wait-confirm/list`, {
+        // 这里可以添加请求的配置,例如 headers 或 params
+        headers: {
+          'Content-Type': 'application/json',
+          'origin-url': window.location.href,
+          'oauth-code': oauthCode.value
+        }
+      }).then(function (response) {
+        if (response.data.code === 40001) {
+          // 未登录
+          window.location.href = response.data.redirect_url;
+        } else if (response.data.code === 200) {
+          waitConfirmList.value = response.data.data;
+          if(waitConfirmList.value.length > 0) {
+            redPointStatus.value = "block"
+          }
+        }
+      }).catch(function (error) {
+        // 处理错误
+        console.error(error);
+      });
+    }
+
+    const loadChattingList = () => {
+      axios.get(`/client-service/chat/message/chatting/list`, {
+        // 这里可以添加请求的配置,例如 headers 或 params
+        params: {
+            "lastChatTime": lastChatTime.value
+        },
+        headers: {
+          'Content-Type': 'application/json',
+          'origin-url': window.location.href,
+          'oauth-code': oauthCode.value
+        }
+      }).then(function (response) {
+        if (response.data.code === 40001) {
+          // 未登录
+          window.location.href = response.data.redirect_url;
+        } else if (response.data.code === 200) {
+          chattingList.value = response.data.data;
+        }
+      }).catch(function (error) {
+        // 处理错误
+        console.error(error);
+      });
+    }
+
+    const loadFriendList = () => {
+      axios.get(`/client-service/chat/account-relation/list`, {
+        // 这里可以添加请求的配置,例如 headers 或 params
+        headers: {
+          'Content-Type': 'application/json',
+          'origin-url': window.location.href,
+          'oauth-code': oauthCode.value
+        }
+      }).then(function (response) {
+        if (response.data.code === 40001) {
+          // 未登录
+          window.location.href = response.data.redirect_url;
+        } else if (response.data.code === 200) {
+          friendsData.value = response.data.data;
+        }
+      }).catch(function (error) {
+        // 处理错误
+        console.error(error);
+      });
+    }
+
+    const loadCurrentUserInfo = () => {
+      oauthCode.value = new URLSearchParams(window.location.search).get('oauthCode');
+      axios.get(`/client-service/chat/account/current/info`, {
+        // 这里可以添加请求的配置,例如 headers 或 params
+        headers: {
+          'Content-Type': 'application/json',
+          'origin-url': window.location.href,
+          'oauth-code': oauthCode.value
+        }
+      }).then(function (response) {
+        if (response.data.code === 40001) {
+          // 未登录
+          window.location.href = response.data.redirect_url;
+        } else if (response.data.code === 200) {
+          userId.value = response.data.data.userId;
+          photo.value = response.data.data.photo;
+          loginUser.value = response.data.data;
+          loadChattingList();
+          loadFriendList();
+          loadWaitConfirmList();
+          connectWebSocket();
+        }
+      }).catch(function (error) {
+        // 处理错误
+        console.error(error);
+      });
+    }
+
+    onMounted(() => {
+      
+    });
+    onUnmounted(() => {
+      if (eventSource) {
+        eventSource.close();
+      }
+    });    
+
+    const connectWebSocket = () => {
+      socket.value = new WebSocket(`/connect-service/ws/chat/PC_WEB_INDEX/`+userId.value);
+      socket.value.onopen = () => {
+        console.log('WebSocket 已连接');
+        isConnected.value = true;
+        // 发送队列中的消息
+        messageQueue.value.forEach(message => socket.value.send(message));
+        messageQueue.value = [];
+        startHeartbeat();
+      };
+
+      socket.value.onmessage = (event) => {
+        console.log('App组件收到消息:', event.data);
+        // 在此处理收到的消息
+        const messageType = JSON.parse(event.data).type;
+        if (messageType === 5) {
+            // 申请添加好友,红点提醒
+            redPointStatus.value = "block";
+        }
+        else if (messageType === 7) {
+            autoSelectedChatting.value = false;
+            loadChattingList();
+        }
+      };
+
+      socket.value.onerror = (error) => {
+        console.error('WebSocket 错误:', error);
+        isConnected.value = false;
+        // 关闭当前连接
+        socket.value.close();
+        // 尝试重连
+        setTimeout(connectWebSocket, reconnectInterval.value);
+      };
+
+      socket.value.onclose = () => {
+        console.log('WebSocket 已关闭');
+        isConnected.value = false;
+        // 尝试重连
+        setTimeout(connectWebSocket, reconnectInterval.value);
+      };
+    };
+
+    const startHeartbeat = () => {
+      const heartbeat = () => {
+        if (isConnected.value) {
+          socket.value.send('ping'); // 发送心跳包
+        }
+      };
+      setInterval(heartbeat, heartbeatInterval.value);
+    };
+
+    const sendMessage = (message) => {
+      if (isConnected.value) {
+        socket.value.send(message);
+      } else {
+        messageQueue.value.push(message);
+      }
+    };
+
+    const handleCreateGroup = async () => {
+      if (selectedFriends.value.length < 3) {
+        message.error("请至少选择3位好友");
+        return;
+      }
+      createGroupLoading.value = true;
+      try {
+        const response = await axios.post(`/client-service/chat/account-relation/createGroup`, 
+        {
+          userIds: selectedFriends.value
+        }, 
+        {
+          headers: {
+            'oauth-code': oauthCode.value,
+            'origin-url': window.location.href
+          }
+        });
+        if (response.data.code === 200) {
+          createGroupVisible.value = false;
+          selectedFriends.value = [];
+          createGroupLoading.value = false;
+        } else {
+          message.error(response.data.msg);
+          createGroupLoading.value = false;
+        }
+      } catch (error) {
+        message.error("创建群聊失败");
+        createGroupLoading.value = false;
+      }
+    };
+
+    const handleCancelGroup = () => {
+      createGroupVisible.value = false;
+    }
+
+    onMounted(() => {
+      loadCurrentUserInfo();
+    });
+    onUnmounted(() => {
+      if (socket.value) {
+        socket.value.close();
+      }
+    });
+    
+    return {
+      userId,
+      photo,
+      loginUser,
+      searchInput,
+      dropdownVisible,
+      subscribeQueryInput,
+      searchAccountInfo,
+      subscribeModalVisible,
+      oauthCode,
+      inputRef,
+      inputValue,
+      activeIcon,
+      iconColors,
+      redPointStatus,
+      chattingList,
+      messageList,
+      waitConfirmList,
+      currentCenterComponent,
+      currentChatComponent,
+      friendsData,
+      lastChatTime,
+      selectChatUser,
+      autoSelectedChatting,
+      createGroupVisible,
+      selectedFriends,
+      createGroupLoading,
+      getFriendName,
+      getFriendPhoto,
+      handleCreateGroup,
+      getFriendName,
+      getFriendPhoto,
+      handleCreateGroup,
+      handleCancelGroup,
+      logout,
+      handleIconClick,
+      handleIconHover,
+      handleIconLeave,
+      sendMessage,
+      createGroup,
+      subscribe,
+      handleSendMessage,
+      searchAccount,
+      doSubscribe,
+      loadFriendList,
+      loadCurrentUserInfo,
+      loadChattingList,
+      handleSelectChatUser
+    };
+  }
+});
+</script>
+
+<style scoped>
+* {
+  margin: 0px;
+  padding: 0px;
+}
+.mac-dots-container {
+  margin-top: 10px;
+  display: flex;
+  justify-content: flex-start;
+  align-items: center;
+  padding: 5px;
+  width: 60px;
+  margin-left: 5px;
+}
+.mac-dot {
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+  margin-right: 5px;
+}
+.red {
+  background-color: #ff5f57;
+}
+.yellow {
+  background-color: #ffbd2e;
+}
+.green {
+  background-color: #28c940;
+}
+.chat-container {
+  position: absolute;
+  width: 1100px;
+  height: 700px;
+  left: 50%;
+  top: 55%;
+  margin-left: -550px;
+  margin-top: -400px;
+  border-radius: 6px;
+  overflow: hidden;
+  /* box-shadow: 0px 0px 10px #eeeded; */
+  background-color: white;
+  border: 1px solid #e3e3e3;
+}
+.chat-left-container {
+  position: absolute;
+  top: 0px;
+  left: 0px;
+  width: 60px;
+  height: 100%;
+  background-color: black;
+}
+.chat-center-container {
+  position: absolute;
+  top: 0px;
+  left: 60px;
+  width: 270px;
+  height: 100%;
+  border-right: 1px solid #e3e3e3;
+  background-color: #f4f4f4;
+}
+.chat-right-container {
+  position: absolute;
+  top: 0px;
+  right: 0px;
+  width: calc(100% - 330px);
+  height: 100%;
+}
+.chat-center-header-container {
+  position: absolute;
+  top: 0px;
+  left: 0px;
+  width: 100%;
+  height: 50px;
+  border-bottom: 1px solid #e3e3e3;
+  background-color: #f9f9f9;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.search-input {
+  height: 30px;
+  width: 210px;
+  margin-left: 10px;
+  background-color: #e5e5e5;
+  border: none;
+}
+.search-button {
+  width: 28px;
+  height: 28px;
+  margin-right: 10px;
+  background-color: black;
+}
+.search-account-card {
+  position: relative;
+  margin-top: 10px; 
+  display: flex; 
+  align-items: center;
+  border: 1px solid rgb(239, 236, 236);
+  padding: 20px;
+}
+.doSubscribeBtn {
+  position: absolute;
+  background-color: black;
+  right: 8px;
+  bottom: 8px;
+  padding: 2px 10px;
+}
+.chat-menus {
+  display: flex;
+  flex-direction: column;
+  margin-top: 30px;
+  width: 18px;
+  margin-left: 21px;
+}
+.chat-menus .anticon {
+  color: white;
+  font-size: 19px;
+  margin-bottom: 30px;
+}
+.bell-with-notification {
+  width: 20px;
+  height: auto;
+  position: relative;
+}
+.message-red-point {
+  position: absolute;
+  content: '';
+  right: -4px;
+  top: -4px;
+  width: 8px;
+  height: 8px;
+  background-color: red;
+  border-radius: 50%;
+  display: none;
+}
+.chat-center-body-container {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  margin-top: 50px;
+  overflow-x: hidden;
+}
+.chat-center-body-container::-webkit-scrollbar {/*滚动条整体样式*/
+  width: 0px;     /*高宽分别对应横竖滚动条的尺寸*/
+  height: 0px;
+}
+.chat-center-body-container::-webkit-scrollbar-thumb {/*滚动条里面小方块*/
+    border-radius: 10px;
+    background-color: transparent;
+    background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, transparent 75%, transparent);
+}
+.chat-center-body-container::-webkit-scrollbar-track {/*滚动条里面轨道*/
+    -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0.2);
+    /*border-radius: 10px;*/
+    background: #EDEDED;
+}
+.group-friend-avatar {
+  width: 30px;
+  border-radius: 5px;
+  margin-right: 5px;
+}
+.group-friend-item {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  width: 100%;
+  margin-bottom: 10px; /* 添加间距 */
+}
+.group-friends-list {
+  max-height: 350px;
+  overflow-x: hidden;
+  overflow-y: scroll;
+}
+  .group-friends-list::-webkit-scrollbar {/*滚动条整体样式*/
+        width: 0px;     /*高宽分别对应横竖滚动条的尺寸*/
+        height: 0px;
+    }
+  .group-friends-list::-webkit-scrollbar-thumb {/*滚动条里面小方块*/
+      border-radius: 10px;
+      background-color: transparent;
+      background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, transparent 75%, transparent);
+  }
+  .group-friends-list::-webkit-scrollbar-track {/*滚动条里面轨道*/
+      -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0.2);
+      /*border-radius: 10px;*/
+      background: #EDEDED;
+  }
+</style>