wangqi49 пре 1 месец
родитељ
комит
87bb6404bc

+ 1 - 1
webchat-front-admin/src/App.vue

@@ -88,7 +88,7 @@ route.rep(wido., "a?aot=wind.")
   padding: 0px;
 }
 .conole-left {
-  position: absolute;
+  position: fixed;
   left: 0px;
   top: 0px;
   width: 250px;

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

@@ -6,7 +6,7 @@ import AccountRobotManagement from '../views/accountRobotManagement.vue'
 import AccountOfficalManagement from '../views/accountOfficalManagement.vue'
 import AccountBlackManagement from '../views/accountBlackManagement.vue'
 import WalletManagement from '../views/walletManagement.vue'
-import OfficalArticleManagement from '../views/officalArticleManagement.vue'
+import OfficialArticleManagement from '../views/officialArticleManagement.vue'
 import EditArticle from '../views/editArticle.vue'
 
 const router = createRouter({
@@ -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: OfficalArticleManagement},
+    {path: '/article-management', component: OfficialArticleManagement},
     {path: '/editArticle', name: 'editArticle', component: EditArticle},
   ],
 })

+ 206 - 14
webchat-front-admin/src/views/editArticle.vue

@@ -1,6 +1,6 @@
 <template>
 
-<div>
+    <div style="padding-bottom: 200px;">
       <a-breadcrumb>
         <a-breadcrumb-item>WebChat</a-breadcrumb-item>
         <a-breadcrumb-item>控制台</a-breadcrumb-item>
@@ -10,46 +10,193 @@
 
       <div class="edit-container">
         <Toolbar
-            style="border: none; margin-top: 20px; background-color: whitesmoke;"
+            style="border: none; margin-top: 20px; background-color: whitesmoke; margin-bottom: 20px;"
             :editor="editorRef"
             :defaultConfig="toolbarConfig"
             :mode="mode"
         />
-        <div class="edit-body-container">
+        <div class="edit-body-container" style="min-height: 400px; height: auto;">
             <input class="edit-title" :value="title" placeholder="文章标题">
-            <Editor
-                style="height: auto; min-height: 500px; width: 100%;"
+            <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>
+          <div>
+            <a-select
+              show-search
+              placeholder="请选择推文公众号"
+              style="width: 200px"
+              :options="officalAccountOptions"
+              @focus="handleFocus"
+              @blur="handleBlur"
+              @change="handleChange"
+            >
+            </a-select>
+          </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>
+        </div>
+        <div class="edit-plugin-container">
+          <div class="edit-plugin-container-title">
+            外部链接(非必填)
+          </div>
+          <div>
+            <a-input class="edit-input" placeholder="如果指定外部链接则优先跳转到指定链接地址" />
+          </div>
+        </div>
+        <div class="edit-plugin-container">
+          <div class="edit-plugin-container-title">
+            文章标签
+          </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>
+        </div>
     </div>
 </div>
 
 </template>
 
-<script >
-import '@wangeditor/editor/dist/css/style.css' // 引入 css
-
-import { onBeforeUnmount, ref, shallowRef, onMounted } from 'vue'
-import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
+<script>
+import axios from 'axios';
+import '@wangeditor/editor/dist/css/style.css';
+import { onBeforeUnmount, ref, shallowRef, onMounted } from 'vue';
+import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
+import { useRoute } from 'vue-router';
 
 export default {
   components: { Editor, Toolbar },
   setup() {
+    const route = useRoute();
+    const oauthCode = ref(route.query.oauthCode || new URLSearchParams(window.location.search).get('oauthCode'));
     // 编辑器实例,必须用 shallowRef
     const editorRef = shallowRef()
-
     // 内容 HTML
     const valueHtml = ref('')
+    const officalAccountOptions = ref([])
+    const articleCover = ref(null); 
+    const uploadUrl = '/admin-service/file/upload';
+
+    const handleArticleCoverUpload = async (file) => {
+        const formData = new FormData();
+        formData.append('file', file);
+        try {
+          const response = await axios.post(uploadUrl, formData, {
+            headers: {
+              'Content-Type': 'multipart/form-data',
+              'oauth-code': oauthCode.value,
+              'origin-url': window.location.href,
+              'upload-path': 'images/avatar'
+            }
+          });
+          if (response.data.code === 40001) {
+            window.location.href = response.data.redirect_url;
+          }
+          // 假设服务器返回头像的 URL
+          articleCover.value = response.data.data.url; 
+        } catch (error) {
+          console.error('Error uploading avatar:', error);
+        }
+      };
+
+      const beforeUpload = async (file) => {
+        // 文件验证逻辑,例如限制文件大小和类型
+        const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
+        const isLt2M = file.size / 1024 / 1024 < 2;
+
+        if (!isJpgOrPng) {
+          message.error('请上传 JPG 或 PNG 格式的图片');
+          return false;
+        }
+        if (!isLt2M) {
+          message.error('图片大小不能超过 2MB');
+          return false;
+        }
+        await handleArticleCoverUpload(file);
+        return false;
+      };
+
+    const listOficalAccounts = async () => {
+        try {
+            const response = await axios.get(`/admin-service/account/page`, {
+              params: {
+                roleCode: 6,
+                pageNo: 1,
+                pageSize: 20
+              },
+              headers: {
+                'oauth-code': oauthCode.value,
+                'origin-url': window.location.href
+              }
+            });
+            if (response.data.code === 40001) {
+              // 未登录
+              window.location.href = response.data.redirect_url;
+            }
+            // 使用 map 方法进行转换
+            officalAccountOptions.value = response.data.data.map(item => ({
+                value: item.userId,
+                label: item.userName
+            }));
+        } catch (error) {
+            message.error('公众号列表加载失败');
+        }
+    }
+
+    listOficalAccounts();
 
     // 模拟 ajax 异步获取内容
     onMounted(() => {
       
-        
+      
     })
 
     const toolbarConfig = {}
@@ -67,12 +214,18 @@ export default {
     }
 
     return {
+      articleCover,
+      uploadUrl,
       editorRef,
       valueHtml,
+      officalAccountOptions,
       mode: 'default', // 或 'simple'
       toolbarConfig,
       editorConfig,
       handleCreated,
+      listOficalAccounts,
+      handleArticleCoverUpload,
+      beforeUpload
     }
   },
 }
@@ -86,7 +239,8 @@ export default {
     align-items: center;
 }
 .edit-body-container {
-    width: 80%;
+    width: 70%;
+    min-width: 600px;
 }
 .edit-title {
     width: 100%;
@@ -100,4 +254,42 @@ export default {
     outline: 0px;
     background-color: white;
   }
+  .edit-plugin-container {
+    position: relative;
+    width: 70%;
+    margin-top: 30px;
+  }
+  .edit-plugin-container-title {
+    text-align: left;
+    font-size: 16px;
+    line-height: 20px;
+    margin-bottom: 20px;
+    color: #353535;
+  }
+  .edit-input {
+    position: relative;
+    width: 100%;
+    height: 45px;
+    line-height: 45px;
+    border-radius: 3px;
+    border: 1px solid #d9d9d9;
+  }
+  .article-cover.ant-upload-wrapper.ant-upload-picture-card-wrapper {
+    width: 250px;
+    height: 125px;
+    overflow: hidden;
+  }
+  .article-cover.ant-upload-wrapper.ant-upload-picture-card-wrapper .ant-upload.ant-upload-select {
+    width: 100%;
+    height: 100%;
+    margin-inline-end: 8px;
+    margin-bottom: 8px;
+    text-align: center;
+    vertical-align: top;
+    background-color: rgba(0, 0, 0, 0.02);
+    border: 1px dashed #d9d9d9;
+    border-radius: 8px;
+    cursor: pointer;
+    transition: border-color 0.3s;
+}
 </style>

+ 0 - 0
webchat-front-admin/src/views/officalArticleManagement.vue → webchat-front-admin/src/views/officialArticleManagement.vue


+ 17 - 4
webchat-front-client/src/views/ChatFile.vue

@@ -33,9 +33,6 @@
             <a-button type="text" @click="handleClick('emoji')">
                 <SmileOutlined style="font-size: 20px; color: black;" />
             </a-button>
-            <a-button type="text" @click="handleClick('redPacket')">
-                <RedEnvelopeOutlined style="font-size: 20px; color: black;" />
-            </a-button>
             <a-button type="text" @click="handleClick('file')">
                 <FolderOpenOutlined style="font-size: 20px; color: black;" />
             </a-button>
@@ -254,8 +251,24 @@
         top: 50px;
         left: 0px;
         width: 100%;
-        height: calc(100% - 270px);
+        height: calc(100% - 249px);
         padding: 10px 30px;
+        overflow-y: scroll;
+        overflow-x: hidden;
+    }
+    .chat-core-container::-webkit-scrollbar {/*滚动条整体样式*/
+        width: 0px;     /*高宽分别对应横竖滚动条的尺寸*/
+        height: 0px;
+    }
+    .chat-core-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-core-container::-webkit-scrollbar-track {/*滚动条里面轨道*/
+        -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0.2);
+        /*border-radius: 10px;*/
+        background: #EDEDED;
     }
     .chat-input {
         position: absolute;

+ 17 - 1
webchat-front-client/src/views/ChatGroup.vue

@@ -306,8 +306,24 @@
         top: 50px;
         left: 0px;
         width: 100%;
-        height: calc(100% - 270px);
+        height: calc(100% - 249px);
         padding: 10px 30px;
+        overflow-y: scroll;
+        overflow-x: hidden;
+    }
+    .chat-core-container::-webkit-scrollbar {/*滚动条整体样式*/
+        width: 0px;     /*高宽分别对应横竖滚动条的尺寸*/
+        height: 0px;
+    }
+    .chat-core-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-core-container::-webkit-scrollbar-track {/*滚动条里面轨道*/
+        -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0.2);
+        /*border-radius: 10px;*/
+        background: #EDEDED;
     }
     .chat-input {
         position: absolute;

+ 85 - 37
webchat-front-client/src/views/ChatOfficial.vue

@@ -5,61 +5,100 @@
 
     </div>
     <div class="chat-core-container" style="background-color: whitesmoke; height: 660px; text-align: center; padding-top: 20px;">
-        <div class="article-card">
+        <div class="article-card" v-for="article in articleArr" >
             <div class="article-card-cover">
-                <img src="https://coderutil.oss-cn-beijing.aliyuncs.com/bbs-image/file_fd603d216ca84e08a6023749170b3bf2.png">
+                <img :src="article.publicAccountArticle?.cover">
             </div>
             <div class="article-card-title">
-                文章标题文章标题文章标题文章标题文章标题文章标题文章标题文章标题文章标题文章标题
+                {{ article.publicAccountArticle?.title }}
             </div>
             <div class="article-card-description">
-                文章标题文章标题文章标题文章标题文章标题文章标题文章标题文章标题文章标题文章标题
+               {{ article.publicAccountArticle?.description }}
             </div>
         </div>
-
-        <div class="article-card">
-            <div class="article-card-cover">
-            
-            </div>
-        </div>
-
-        <div class="article-card">
-            <div class="article-card-cover">
-            
-            </div>
-            <div class="article-card-title">
-                文章标题文章标题文章标题文章标题文章标题文章标题文章标题文章标题文章标题文章标题
-            </div>
-            <div class="article-card-description">
-                文章标题文章标题文章标题文章标题文章标题文章标题文章标题文章标题文章标题文章标题
-            </div>
-        </div>
-
-
-        <div class="article-card">
-            <div class="article-card-cover">
-            
-            </div>
-            <div class="article-card-title">
-                文章标题文章标题文章标题文章标题文章标题文章标题文章标题文章标题文章标题文章标题
-            </div>
-            <div class="article-card-description">
-                文章标题文章标题文章标题文章标题文章标题文章标题文章标题文章标题文章标题文章标题
-            </div>
-        </div>
-
     </div>
   </template>
   
   <script>
   import axios from 'axios';
-  import { defineComponent, ref, onMounted, onUnmounted, watch } from 'vue';
+  import { inject, defineComponent, ref, onMounted, onUnmounted, watch } from 'vue';
   export default defineComponent({
     props: {
         selectChatUserRef: Object
     },
     setup(props) {
         const selectChatUser = ref(props.selectChatUserRef);
+
+        const socket = ref(null);
+        const messageQueue = ref([]); // 存储未发送的消息队列
+        const isConnected = ref(false);
+        const loginUser = ref(inject('loginUser'));
+        const reconnectInterval = ref(5000); // 重连间隔时间,单位:毫秒
+        const heartbeatInterval = ref(30000); // 心跳间隔时间,单位:毫秒
+        const articleArr = ref([]);
+
+        const connectWebSocket = () => {
+            socket.value = new WebSocket(`/connect-service/ws/chat/PC_WEB_CHAT/` + loginUser.value.userId);
+            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('收到WS消息:', event.data);
+                // 在此处理收到的消息
+                const socketMessage = JSON.parse(event.data);
+                if(socketMessage.senderId != selectChatUser.value.userId) {
+                    return;
+                }
+                const messageType = socketMessage.type;
+                if (messageType === 4) {
+                    // 处理对话
+                    articleArr.value.push(socketMessage);
+                }
+            };
+
+            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);
+        };
+        // 链接ws
+        connectWebSocket();
+
+        const sendMessage = (message) => {
+            if (isConnected.value) {
+                socket.value.send(message);
+            } else {
+                messageQueue.value.push(message);
+            }
+        };
+
+
         // 切换对话用户,父组件传递选中用户信息,监听选中用户变化
         watch(() => props.selectChatUserRef, (newValue) => {
           selectChatUser.value = newValue;
@@ -73,6 +112,15 @@
         
         return {
             selectChatUser,
+            isConnected,
+            messageQueue,
+            loginUser,
+            reconnectInterval,
+            heartbeatInterval,
+            articleArr,
+            connectWebSocket,
+            sendMessage,
+            startHeartbeat
         };
     }
   });

+ 1 - 1
webchat-front-client/src/views/ChatRobot.vue

@@ -299,7 +299,7 @@
         top: 50px;
         left: 0px;
         width: 100%;
-        height: calc(100% - 250px);
+        height: calc(100% - 249px);
         padding: 10px 30px;
         overflow-y: scroll;
         overflow-x: hidden;

+ 17 - 1
webchat-front-client/src/views/ChatUser.vue

@@ -282,8 +282,24 @@
         top: 50px;
         left: 0px;
         width: 100%;
-        height: calc(100% - 270px);
+        height: calc(100% - 249px);
         padding: 10px 30px;
+        overflow-y: scroll;
+        overflow-x: hidden;
+    }
+    .chat-core-container::-webkit-scrollbar {/*滚动条整体样式*/
+        width: 0px;     /*高宽分别对应横竖滚动条的尺寸*/
+        height: 0px;
+    }
+    .chat-core-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-core-container::-webkit-scrollbar-track {/*滚动条里面轨道*/
+        -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0.2);
+        /*border-radius: 10px;*/
+        background: #EDEDED;
     }
     .chat-input {
         position: absolute;