Explorar el Código

1.2.1代码合并

chenjj hace 3 meses
padre
commit
ab7db8ee8b

+ 1 - 1
.env.development

@@ -4,7 +4,7 @@ VITE_APP_TITLE = 金邻助家后台管理系统
 # 开发环境配置
 VITE_APP_ENV = 'development'
 
-# 若依管理系统/开发环境
+# 金邻助家后台管理系统/开发环境
 VITE_APP_BASE_API = '/dev-api'
 
 #地图key

+ 2 - 2
.env.production

@@ -1,10 +1,10 @@
 # 页面标题
-VITE_APP_TITLE = 若依管理系统
+VITE_APP_TITLE = 金邻助家后台管理系统
 
 # 生产环境配置
 VITE_APP_ENV = 'production'
 
-# 若依管理系统/生产环境
+# 金邻助家后台管理系统/生产环境
 VITE_APP_BASE_API = '/prod-api'
 
 # 是否在打包时开启压缩,支持 gzip 和 brotli

+ 2 - 2
.env.staging

@@ -1,10 +1,10 @@
 # 页面标题
-VITE_APP_TITLE = 若依管理系统
+VITE_APP_TITLE = 金邻助家后台管理系统
 
 # 生产环境配置
 VITE_APP_ENV = 'staging'
 
-# 若依管理系统/生产环境
+# 金邻助家后台管理系统/生产环境
 VITE_APP_BASE_API = '/stage-api'
 
 # 是否在打包时开启压缩,支持 gzip 和 brotli

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 110
README.md


+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "ruoyi",
-  "version": "1.1.0",
+  "version": "1.2.1",
   "description": "金邻助家后台管理系统",
   "author": "金邻助家",
   "license": "MIT",

+ 20 - 0
src/api/finance/wallet.js

@@ -0,0 +1,20 @@
+import request from '@/utils/request'
+
+
+//获取志愿者钱包列表
+export function list(query) {
+  return request({
+    url: '/core/volunteer/account/getVolunteerAccountList',
+    method: 'get',
+    params: query
+  })
+}
+
+//获取平台志愿者钱包统计
+export function walletTotal(query) {
+    return request({
+      url: '/core/volunteer/account/getPlatformVolunteerWalletStatistics',
+      method: 'get',
+      params: query
+    })
+  }

+ 1 - 1
src/components/Pagination/index.vue

@@ -33,7 +33,7 @@ const props = defineProps({
   pageSizes: {
     type: Array,
     default() {
-      return [10, 20, 30, 50]
+      return [10, 20, 30, 50,100,150,200]
     }
   },
   // 移动端页码按钮的数量端默认值5

+ 116 - 75
src/layout/components/Sidebar/SidebarItem.vue

@@ -1,100 +1,141 @@
 <template>
   <div v-if="!item.hidden">
-    <template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
+    <template
+      v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
       <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
         <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
-          <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"/>
-          <template #title><span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span></template>
+          <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" />
+          <template #title>
+            <span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span>
+            <el-badge v-if="onlyOneChild.meta?.title == '消息中心' && xxtzGqxxtzNum > 0" 
+                  :value="xxtzGqxxtzNum" 
+                  class="item">
+           </el-badge>
+          </template>
         </el-menu-item>
       </app-link>
     </template>
-
+  
     <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
       <template v-if="item.meta" #title>
         <svg-icon :icon-class="item.meta && item.meta.icon" />
         <span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span>
+  
+        <!-- 只有当消息数量 > 0 时才显示 -->
+        <el-badge v-if="item.meta?.title === '消息中心' && xxtzGqxxtzNum > 0" 
+                  :value="xxtzGqxxtzNum" 
+                  class="item">
+        </el-badge>
       </template>
-
+  
       <sidebar-item
-        v-for="(child, index) in item.children"
-        :key="child.path + index"
-        :is-nest="true"
+        v-for="(child, index) in item.children" 
+        :key="child.path + index" 
+        :is-nest="true" 
         :item="child"
-        :base-path="resolvePath(child.path)"
+        :base-path="resolvePath(child.path)" 
         class="nest-menu"
+        :badgeObj="props.badgeObj"
       />
     </el-sub-menu>
   </div>
-</template>
-
-<script setup>
-import { isExternal } from '@/utils/validate'
-import AppLink from './Link'
-import { getNormalPath } from '@/utils/ruoyi'
-
-const props = defineProps({
-  // route object
-  item: {
-    type: Object,
-    required: true
-  },
-  isNest: {
-    type: Boolean,
-    default: false
-  },
-  basePath: {
-    type: String,
-    default: ''
-  }
-})
-
-const onlyOneChild = ref({});
-
-function hasOneShowingChild(children = [], parent) {
-  if (!children) {
-    children = [];
-  }
-  const showingChildren = children.filter(item => {
-    if (item.hidden) {
-      return false
+  
+  </template>
+  
+  <script setup>
+  import { isExternal } from '@/utils/validate'
+  import AppLink from './Link'
+  import { getNormalPath } from '@/utils/ruoyi'
+  import { onMounted } from 'vue';
+  import useUserStore from '@/store/modules/user'
+import { computed } from 'vue';
+  const props = defineProps({
+    // route object
+    item: {
+      type: Object,
+      required: true
+    },
+    isNest: {
+      type: Boolean,
+      default: false
+    },
+    basePath: {
+      type: String,
+      default: ''
+    },
+    badgeObj: {
+      type: Object,
+      default: () => ({})
     }
-    onlyOneChild.value = item
-    return true
   })
 
-  // When there is only one child router, the child router is displayed by default
-  if (showingChildren.length === 1) {
-    return true
-  }
-
-  // Show parent if there are no child router to display
-  if (showingChildren.length === 0) {
-    onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
-    return true
-  }
-
-  return false
-};
-
-function resolvePath(routePath, routeQuery) {
-  if (isExternal(routePath)) {
-    return routePath
+  const userStore = useUserStore()
+  
+  const onlyOneChild = ref({});
+  const unreadCount = ref(0);  // 存储未读消息数量
+  const xxtzGqxxtzNum = computed(()=>{
+  
+    return userStore.xxtzGqxxtzNum
+  })
+  function hasOneShowingChild(children = [], parent) {
+    if (!children) {
+      children = [];
+    }
+    const showingChildren = children.filter(item => {
+      if (item.hidden) {
+        return false
+      } else {
+        // Temp set(will be used if only has one showing child)
+        onlyOneChild.value = item
+        return true
+      }
+    })
+  
+    // When there is only one child router, the child router is displayed by default
+    if (showingChildren.length === 1) {
+      return true
+    }
+  
+    // Show parent if there are no child router to display
+    if (showingChildren.length === 0) {
+      onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
+      return true
+    }
+  
+    return false
+  };
+  
+  function resolvePath(routePath, routeQuery) {
+    if (isExternal(routePath)) {
+      return routePath
+    }
+    if (isExternal(props.basePath)) {
+      return props.basePath
+    }
+    if (routeQuery) {
+      let query = JSON.parse(routeQuery);
+      return { path: getNormalPath(props.basePath + '/' + routePath), query: query }
+    }
+    return getNormalPath(props.basePath + '/' + routePath)
   }
-  if (isExternal(props.basePath)) {
-    return props.basePath
+  
+  function hasTitle(title) {
+    if (title.length > 5) {
+      return title;
+    } else {
+      return "";
+    }
   }
-  if (routeQuery) {
-    let query = JSON.parse(routeQuery);
-    return { path: getNormalPath(props.basePath + '/' + routePath), query: query }
+  
+  
+  </script>
+  <style scoped>
+  .item{
+    top: -18px;
+    margin-left: 15px;
   }
-  return getNormalPath(props.basePath + '/' + routePath)
-}
-
-function hasTitle(title){
-  if (title.length > 5) {
-    return title;
-  } else {
-    return "";
+  .item-content{
+    top: -85px;
+    margin-left: 140px;
   }
-}
-</script>
+  </style>

+ 1 - 1
src/layout/components/Sidebar/qrCode.vue

@@ -65,7 +65,7 @@ const openDialog = async () => {
     try {
         const res = await getInviteQrCode({
             referrerType: areaType.value === '3' ? '2' : '3',//推荐者类型 1用户 2区域公司 3服务中心
-            referrerId: userStore.user.userId,//推荐者id(用户id/区域id/服务中心id)
+            referrerId: userStore.user.deptId,//推荐者id(用户id/区域id/服务中心id)
             page: 'pages/login'
         });
         console.log("TCL: openDialog -> res", res)

+ 28 - 6
src/store/modules/user.js

@@ -2,7 +2,7 @@ import { login, logout, getInfo } from '@/api/login'
 import { getToken, setToken, removeToken } from '@/utils/auth'
 import { isHttp, isEmpty } from "@/utils/validate"
 import defAva from '@/assets/images/profile.jpg'
-
+import WebSocketClient from '@/utils/WebSocketClient'
 const useUserStore = defineStore(
   'user',
   {
@@ -13,9 +13,11 @@ const useUserStore = defineStore(
       avatar: '',
       roles: [],
       permissions: [],
-      phonenumber:'',
-      areaType:'', //区域类型
-      user:{},
+      phonenumber: '',
+      areaType: '', //区域类型
+      user: {},
+      ws: null,
+      xxtzGqxxtzNum: 0
     }),
     actions: {
       // 登录
@@ -54,10 +56,29 @@ const useUserStore = defineStore(
             this.avatar = avatar
 
             this.phonenumber = user.phonenumber;
-            this.areaType =user.dept.areaType;
+            this.areaType = user.dept.areaType;
             this.user = user;
+            if (user.districtCode) {
+              //获取账户时,连接soket 
+              this.ws = new WebSocketClient(user.areaType === '3' ? user.districtCode : '0');
+
+              // 监听消息事件
+              this.ws.on('message', (data) => {
+                // 处理消息逻辑
+                if (data.type === 'msgUnreadCount') {
+                  console.log("处理消息逻辑", data.data)
+                  this.xxtzGqxxtzNum = data.data;
+                }
+              })
+              // 连接WebSocket
+              this.ws.connect()
+              console.log('this.ws', this.ws);
+            }
+
+
             resolve(res)
           }).catch(error => {
+            console.log("TCL: getInfo -> error", error)
             reject(error)
           })
         })
@@ -75,7 +96,8 @@ const useUserStore = defineStore(
             reject(error)
           })
         })
-      }
+      },
+
     }
   })
 

+ 225 - 0
src/utils/WebSocketClient.js

@@ -0,0 +1,225 @@
+import { getToken } from "@/utils/auth";
+class WebSocketClient {
+  constructor(userId, options = {}) {
+    this.system = '3';
+    this.userId = userId;
+    // import.meta.env.VITE_APP_ENV === 'development'?'http://192.168.100.128:9527'
+    // import.meta.env.VITE_APP_BASE_API
+    // const baseURL = 'http://192.168.100.128:9527';
+    // const baseURL = 'https://goldshulin.com/prod-api';
+    const url = import.meta.env.VITE_APP_ENV === 'development' ? '192.168.100.128:9527' : window.location.host;
+    // const url = baseURL.split('/')[2];
+    const header = window.location.protocol === 'https:' ? 'wss' : 'ws';
+    this.url = `${header}://${url}/websocket/${this.system}/${this.userId}?authorization=${"Bearer " + getToken()}`;
+    console.log("TCL: WebSocketClient -> constructor -> this.url", this.url)
+    this.options = {
+      reconnectInterval: 3000, // 重连间隔时间
+      heartbeatInterval: 30000, // 心跳间隔时间
+      heartbeatMessage: 'ping', // 心跳消息
+      ...options
+    }
+
+    this.ws = null
+    this.reconnectTimer = null
+    this.heartbeatTimer = null
+    this.messageCallbacks = new Map()
+    this.isConnected = false
+    this.isReconnecting = false
+
+    this.reconnectCount = 0;
+  }
+
+  // 连接WebSocket
+  connect() {
+    try {
+      this.ws = new WebSocket(this.url)
+      this.bindEvents()
+    } catch (error) {
+      console.error('WebSocket连接错误:', error)
+      this.reconnect()
+    }
+  }
+
+  // 绑定事件
+  bindEvents() {
+    this.ws.onopen = () => {
+      console.log('WebSocket连接成功')
+      this.isConnected = true
+      this.isReconnecting = false
+      this.startHeartbeat()
+      this.emit('connect')
+    }
+
+    this.ws.onclose = (error) => {
+      console.log('WebSocket连接关闭',error)
+      this.isConnected = false
+      this.stopHeartbeat()
+      this.emit('disconnect')
+      this.reconnect()
+    }
+
+    this.ws.onerror = (error) => {
+      console.error('WebSocket错误:', error)
+      this.emit('error', error)
+    }
+
+    this.ws.onmessage = (event) => {
+      try {
+        const message = JSON.parse(event.data)
+        // 处理心跳响应
+        if (message === 'pong') {
+          return
+        }
+        this.handleMessage(message)
+      } catch (error) {
+        console.error('消息解析错误:', error)
+      }
+    }
+  }
+
+  // 重连
+  reconnect() {
+    if (this.isReconnecting) return
+
+
+    this.isReconnecting = true
+    if (this.reconnectCount < 3) {
+      this.reconnectTimer = setTimeout(() => {
+        console.log('尝试重新连接...')
+        this.reconnectCount = this.reconnectCount + 1;
+        this.connect()
+      }, this.options.reconnectInterval)
+    }
+
+  }
+
+  // 开始心跳
+  startHeartbeat() {
+    this.heartbeatTimer = setInterval(() => {
+      this.send(this.options.heartbeatMessage)
+    }, this.options.heartbeatInterval)
+  }
+
+  // 停止心跳
+  stopHeartbeat() {
+    if (this.heartbeatTimer) {
+      clearInterval(this.heartbeatTimer)
+      this.heartbeatTimer = null
+    }
+  }
+
+  // 发送消息
+  send(data) {
+    if (!this.isConnected) {
+      console.warn('WebSocket未连接')
+      return false
+    }
+
+    try {
+      const message = typeof data === 'string' ? data : JSON.stringify(data)
+      this.ws.send(message)
+      return true
+    } catch (error) {
+      console.error('发送消息错误:', error)
+      return false
+    }
+  }
+
+  // 处理接收到的消息
+  handleMessage(message) {
+    const { type, data } = message
+    if (this.messageCallbacks.has(type)) {
+      this.messageCallbacks.get(type).forEach(callback => callback(data))
+    }
+    this.emit('message', message)
+  }
+
+  // 订阅消息
+  on(type, callback) {
+    if (!this.messageCallbacks.has(type)) {
+      this.messageCallbacks.set(type, new Set())
+    }
+    this.messageCallbacks.get(type).add(callback)
+  }
+
+  // 取消订阅
+  off(type, callback) {
+    if (this.messageCallbacks.has(type)) {
+      if (callback) {
+        this.messageCallbacks.get(type).delete(callback)
+      } else {
+        this.messageCallbacks.delete(type)
+      }
+    }
+  }
+
+  // 事件发射器
+  emit(event, data) {
+    if (this.messageCallbacks.has(event)) {
+      this.messageCallbacks.get(event).forEach(callback => callback(data))
+    }
+  }
+
+  // 关闭连接
+  close() {
+    if (this.ws) {
+      this.ws.close()
+    }
+    this.stopHeartbeat()
+    if (this.reconnectTimer) {
+      clearTimeout(this.reconnectTimer)
+      this.reconnectTimer = null
+    }
+  }
+}
+
+// 使用示例
+/*
+const ws = new WebSocketClient('ws://your-websocket-server.com', {
+  reconnectInterval: 3000,
+  heartbeatInterval: 30000,
+  heartbeatMessage: 'ping'
+})
+ 
+// 连接WebSocket
+ws.connect()
+ 
+// 监听连接事件
+ws.on('connect', () => {
+  console.log('连接成功')
+})
+ 
+// 监听断开连接事件
+ws.on('disconnect', () => {
+  console.log('连接断开')
+})
+ 
+// 监听错误事件
+ws.on('error', (error) => {
+  console.error('发生错误:', error)
+})
+ 
+// 监听消息事件
+ws.on('message', (message) => {
+  console.log('收到消息:', message)
+})
+ 
+// 订阅特定类型的消息
+ws.on('chat', (data) => {
+  console.log('收到聊天消息:', data)
+})
+ 
+// 发送消息
+ws.send({
+  type: 'chat',
+  data: {
+    content: 'Hello!',
+    timestamp: Date.now()
+  }
+})
+ 
+// 关闭连接
+ws.close()
+*/
+
+export default WebSocketClient

+ 2 - 2
src/views/components/DialogForm/index.vue

@@ -17,7 +17,8 @@
                         :placeholder="'请输入' + item.label" clearable :disabled="disables[item.prop]" />
                 </el-form-item>
 
-                <el-form-item :label="item.label" v-if="item.type === 'radio' && item.options" :prop="item.prop"
+                <el-form-item :label="item.label" v-if="item.type === 'radio' && item.options" 
+                    :prop="item.prop"
                     :rules="item.rules">
                     <el-radio-group v-model="form[item.prop]" :disabled="disables[item.prop]">
                         <el-radio v-for="dict in item.options" :key="dict.value" :value="dict.value">{{ dict.label
@@ -144,7 +145,6 @@ const formStatus = ref('form') //form:表单 details:详情
 
 const getImages = (key) => {
    
-    console.log(233, key);
     if(typeof key === 'object'){
       return key.map(item =>{
             return  form.value[item]

+ 5 - 3
src/views/components/ListPage/Table.vue

@@ -80,7 +80,7 @@
 </template>
 <script setup>
 import { ref } from 'vue'
-
+import { provide,inject } from 'vue'
 const props = defineProps({
     column: {
         type: Object,
@@ -118,10 +118,12 @@ const props = defineProps({
 const ids = ref([])
 const dialogVisible = ref(false)
 const images = ref([]);
+const injects = inject('selectChange');
+
 /** 多选框选中数据 */
-const handleSelectionChange = (selection) => {
+const handleSelectionChange = (selection,row) => {
     console.log('selection', selection);
-
+    injects(selection)
     ids.value = selection.map(item => item[props.tableKey]);
     console.log('ids', ids);
 

+ 4 - 1
src/views/components/ListPage/index.vue

@@ -10,6 +10,7 @@
             <Table :isScope="isScope" :tableKey="tableKey" ref="tableRef" :column="tableColumn" :data="tableData.list" :loading="loading" :scopeBtns="scopeBtns" :isSelect="isSelect" />
         </div>
         <div class="pagination-div">
+            <slot name="footerLeft"></slot>
             <pagination v-show="tableData.total > 0" :total="tableData.total" v-model:page="queryParams.pageNum"
                 v-model:limit="queryParams.pageSize" @pagination="getList" />
         </div>
@@ -64,10 +65,12 @@ const props = defineProps({
     isScope:{
         type: Boolean,
         default: true
-    }
+    },
 })
 const tabsValue = ref('2')
 
+
+
 const queryParams = reactive({
     pageNum: 1,
     pageSize: 10

+ 4 - 1
src/views/finance/settlement/useRegional.js

@@ -28,7 +28,10 @@ export default ({ proxy, jlzj_area_type }) => {
                 name: item.value
             }
         })
-        return data || [];
+        return [{
+            title: '全部',
+            name: ''
+        }, ...data] || [];
     })
     const openDialog = (data, type) => {
         try {

+ 4 - 1
src/views/finance/settlement/useService.js

@@ -27,7 +27,10 @@ export default ({ proxy, jlzj_area_type }) => {
                 name: item.value
             }
         })
-        return data || [];
+        return [{
+            title: '全部',
+            name: ''
+        }, ...data] || [];
     })
     const openDialog = (data,type) => {
         try {

+ 200 - 0
src/views/finance/wallet/index.vue

@@ -0,0 +1,200 @@
+<template>
+    <div>
+        <div class="card-box">
+            <el-row :gutter="20">
+                <el-col :span="4" v-for="item in cardList" :key="item.key">
+                    <el-card shadow="never">
+                        <div class="card-title">{{ item.name }}: {{ data[item.key] }}</div>
+                    </el-card>
+                </el-col>
+            </el-row>
+        </div>
+        <ListPage :column="listPageData.tableColumn" :tableApi="listPageData.tableApi" :isSelect="listPageData.isSelect"
+            :scopeBtns="listPageData.scopeBtns" :searchBtns="listPageData.searchBtns" ref="userTableRef" :isScope="false" >
+        <template #footerLeft>
+            <el-row :gutter="20">
+                <el-col :span="4">
+                    <div class="card-title">总金额: {{ totalMoney }}</div>
+                </el-col>
+                <el-col :span="4">
+                    <div class="card-title">可提现金额: {{ balanceMoney }}</div>
+                </el-col>
+            </el-row>
+        </template>
+        </ListPage>
+    </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import ListPage from '@/views/components/ListPage/index.vue';
+import { list, walletTotal } from "@/api/finance/wallet.js";
+import { provide,inject } from 'vue'
+const { proxy } = getCurrentInstance();
+const userTableRef = ref();
+const listPageData = reactive({
+    tableColumn: [
+        {
+            label: '金额变动时间',
+            queryLabel: '时间',
+            prop: 'createTime',
+            type: 'date',
+            isSearch: true,
+            keys: ['lastChangeTimeStart', 'lastChangeTimeEnd'],
+        },
+        {
+            label: '区域公司',
+            prop: 'areaName',
+            type: 'input',
+            isSearch: true,
+        },
+        {
+            label: '服务中心',
+            prop: 'serviceCentreName',
+            type: 'input',
+            isSearch: true,
+        },
+        {
+            label: '志愿者姓名',
+            prop: 'volunteerName',
+            queryLabel: '姓名',
+            type: 'input',
+            isSearch: true,
+        },
+        {
+            label: '手机号',
+            prop: 'volunteerPhone',
+            type: 'input',
+            isSearch: true,
+        },
+        {
+            label: '待入账',
+            prop: 'orderFrozenBalance',
+        },
+        {
+            label: '可提现',
+            prop: 'balance',
+        },
+        {
+            label: '提现中',
+            prop: 'beBalance',
+        },
+        {
+            label: '总金额',
+            prop: 'totalAmount',
+        },
+        {
+            label: '已提现',
+            prop: 'withdrawnAmount',
+        },
+        // {
+        //     label: '总余额',
+        //     prop: 'totalBalance',
+        // },
+
+    ],
+    searchBtns: [
+        {
+            label: '导出',
+            func: (parmas) => {
+				console.log("TCL: parmas", parmas)
+                try {
+                    proxy.download("core/volunteer/account/export/VolunteerAccountList", {
+                        ...parmas
+                    }, `钱包管理_${new Date().getTime()}.xlsx`);
+                } catch (error) {
+				console.log("TCL: error", error)
+                }
+            },
+            key: 'export',
+            type: 'primary'
+        },
+    ],
+    tableApi: list,//接口地址
+    isSelect: true,//是否勾选
+    scopeBtns: [],
+
+})
+
+const selectList = ref([]);
+
+//总金额
+const totalMoney = computed(() => {
+    let total = 0;
+    selectList.value.forEach((item) => {
+        total += Number(item.totalAmount);
+    });
+    return total;
+});
+//可提现金额
+const balanceMoney = computed(() => {
+    let total = 0;
+    selectList.value.forEach((item) => {
+        total += Number(item.balance);
+    });
+    return total;
+});
+const selectChange = (rows) => {
+console.log("TCL: selectChange -> rows", rows)
+selectList.value = rows;
+}
+
+provide('selectChange',selectChange)
+
+const cardList = [
+    {
+        name: '总金额',
+        key: 'totalAmount'
+    },
+    {
+        name: '可提现',
+        key: 'balance'
+    },
+    {
+        name: '已提现',
+        key: 'withdrawAmount'
+    },
+    {
+        name: '提现中',
+        key: 'beBalance'
+    },
+    {
+        name: '待入账',
+        key: 'orderFrozenBalance'
+    }
+]
+
+const data = ref({
+    totalAmount: 0.0,
+    balance: 0.0,
+    beBalance: 0.0,
+    withdrawAmount: 0.0,
+    waitAmount: 0.0,
+    orderFrozenBalance: 0.0
+})
+
+const getWalletTotal = async () => {
+    try {
+        const res = await walletTotal()
+        data.value = res.data
+    } catch (error) {
+        console.log("TCL: getWalletTotal -> error", error)
+
+    }
+}
+
+getWalletTotal()
+
+</script>
+
+<style lang='scss' scoped>
+.card-box {
+    padding: 20px 20px 0;
+
+    .card-title {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+    }
+}
+</style>

+ 15 - 14
src/views/finance/withdrawal/useData.js

@@ -26,7 +26,7 @@ export default ({ proxy, jlzj_area_type }) => {
     const dialogVerify =ref(false);
 
     //打开弹窗
-    const openDialog = (data, type) => {
+    const openDialog = (parm, type) => {
         try {
             const disabledData = {
                 alipayAccountNo: true,
@@ -35,9 +35,10 @@ export default ({ proxy, jlzj_area_type }) => {
                 createTime: true,
 
             }
+            const data = JSON.parse(JSON.stringify(parm));
             //审核
             if (type === 'examine') {
-
+                data['appStatus'] = '';
                 dialogFormRef.value.initForm(data, disabledData)
             }
             if (type === 'details') {
@@ -477,18 +478,18 @@ export default ({ proxy, jlzj_area_type }) => {
                     return tabkey.value === '4' && row.paymentStatus === '1'
                 }
             },
-            {
-                label: '创建打款单',
-                type: 'primary',
-                key: 'details',
-                func: (row) => {
-                    console.log(row)
-                    paymentSubmit([row.volunteerTakeRecordId])
-                },
-                show: (row) => {
-                    return tabkey.value === '' && row.appStatus === '2'
-                }
-            },
+            // {
+            //     label: '创建打款单',
+            //     type: 'primary',
+            //     key: 'details',
+            //     func: (row) => {
+            //         console.log(row)
+            //         paymentSubmit([row.volunteerTakeRecordId])
+            //     },
+            //     show: (row) => {
+            //         return tabkey.value === '' && row.appStatus === '2'
+            //     }
+            // },
             {
                 label: '查看打款结果',
                 type: 'primary',

+ 0 - 216
src/views/message/WebSocketClient.js

@@ -1,216 +0,0 @@
-
-class WebSocketClient {
-    constructor(userId, options = {}) {
-      this.system = '3';
-      this.userId = userId;
-      // const baseURL = import.meta.env.VITE_APP_BASE_API
-      const baseURL = 'https://goldshulin.com/prod-api';
-      const url = baseURL.split('/')[2];
-      const header = baseURL.split('/')[0] === 'https:' ? 'wss' : 'ws';
-      this.url = `${header}://${url}/websocket/${this.system}/${this.userId}`;
-			console.log("TCL: WebSocketClient -> constructor -> this.url", this.url)
-      this.options = {
-        reconnectInterval: 3000, // 重连间隔时间
-        heartbeatInterval: 30000, // 心跳间隔时间
-        heartbeatMessage: 'ping', // 心跳消息
-        ...options
-      }
-      
-      this.ws = null
-      this.reconnectTimer = null
-      this.heartbeatTimer = null
-      this.messageCallbacks = new Map()
-      this.isConnected = false
-      this.isReconnecting = false
-    }
-  
-    // 连接WebSocket
-    connect() {
-      try {
-        this.ws = new WebSocket(this.url)
-        this.bindEvents()
-      } catch (error) {
-        console.error('WebSocket连接错误:', error)
-        this.reconnect()
-      }
-    }
-  
-    // 绑定事件
-    bindEvents() {
-      this.ws.onopen = () => {
-        console.log('WebSocket连接成功')
-        this.isConnected = true
-        this.isReconnecting = false
-        this.startHeartbeat()
-        this.emit('connect')
-      }
-  
-      this.ws.onclose = () => {
-        console.log('WebSocket连接关闭')
-        this.isConnected = false
-        this.stopHeartbeat()
-        this.emit('disconnect')
-        this.reconnect()
-      }
-  
-      this.ws.onerror = (error) => {
-        console.error('WebSocket错误:', error)
-        this.emit('error', error)
-      }
-  
-      this.ws.onmessage = (event) => {
-        try {
-          const message = JSON.parse(event.data)
-          // 处理心跳响应
-          if (message === 'pong') {
-            return
-          }
-          this.handleMessage(message)
-        } catch (error) {
-          console.error('消息解析错误:', error)
-        }
-      }
-    }
-  
-    // 重连
-    reconnect() {
-      if (this.isReconnecting) return
-      
-      this.isReconnecting = true
-      this.reconnectTimer = setTimeout(() => {
-        console.log('尝试重新连接...')
-        this.connect()
-      }, this.options.reconnectInterval)
-    }
-  
-    // 开始心跳
-    startHeartbeat() {
-      this.heartbeatTimer = setInterval(() => {
-        this.send(this.options.heartbeatMessage)
-      }, this.options.heartbeatInterval)
-    }
-  
-    // 停止心跳
-    stopHeartbeat() {
-      if (this.heartbeatTimer) {
-        clearInterval(this.heartbeatTimer)
-        this.heartbeatTimer = null
-      }
-    }
-  
-    // 发送消息
-    send(data) {
-      if (!this.isConnected) {
-        console.warn('WebSocket未连接')
-        return false
-      }
-  
-      try {
-        const message = typeof data === 'string' ? data : JSON.stringify(data)
-        this.ws.send(message)
-        return true
-      } catch (error) {
-        console.error('发送消息错误:', error)
-        return false
-      }
-    }
-  
-    // 处理接收到的消息
-    handleMessage(message) {
-      const { type, data } = message
-      if (this.messageCallbacks.has(type)) {
-        this.messageCallbacks.get(type).forEach(callback => callback(data))
-      }
-      this.emit('message', message)
-    }
-  
-    // 订阅消息
-    on(type, callback) {
-      if (!this.messageCallbacks.has(type)) {
-        this.messageCallbacks.set(type, new Set())
-      }
-      this.messageCallbacks.get(type).add(callback)
-    }
-  
-    // 取消订阅
-    off(type, callback) {
-      if (this.messageCallbacks.has(type)) {
-        if (callback) {
-          this.messageCallbacks.get(type).delete(callback)
-        } else {
-          this.messageCallbacks.delete(type)
-        }
-      }
-    }
-  
-    // 事件发射器
-    emit(event, data) {
-      if (this.messageCallbacks.has(event)) {
-        this.messageCallbacks.get(event).forEach(callback => callback(data))
-      }
-    }
-  
-    // 关闭连接
-    close() {
-      if (this.ws) {
-        this.ws.close()
-      }
-      this.stopHeartbeat()
-      if (this.reconnectTimer) {
-        clearTimeout(this.reconnectTimer)
-        this.reconnectTimer = null
-      }
-    }
-  }
-  
-  // 使用示例
-  /*
-  const ws = new WebSocketClient('ws://your-websocket-server.com', {
-    reconnectInterval: 3000,
-    heartbeatInterval: 30000,
-    heartbeatMessage: 'ping'
-  })
-  
-  // 连接WebSocket
-  ws.connect()
-  
-  // 监听连接事件
-  ws.on('connect', () => {
-    console.log('连接成功')
-  })
-  
-  // 监听断开连接事件
-  ws.on('disconnect', () => {
-    console.log('连接断开')
-  })
-  
-  // 监听错误事件
-  ws.on('error', (error) => {
-    console.error('发生错误:', error)
-  })
-  
-  // 监听消息事件
-  ws.on('message', (message) => {
-    console.log('收到消息:', message)
-  })
-  
-  // 订阅特定类型的消息
-  ws.on('chat', (data) => {
-    console.log('收到聊天消息:', data)
-  })
-  
-  // 发送消息
-  ws.send({
-    type: 'chat',
-    data: {
-      content: 'Hello!',
-      timestamp: Date.now()
-    }
-  })
-  
-  // 关闭连接
-  ws.close()
-  */
-  
-  export default WebSocketClient
-  

+ 388 - 250
src/views/message/index.vue

@@ -1,82 +1,95 @@
 <template>
-    <div class="chat-container">
-        <!-- 左侧用户列表 -->
-        <div class="user-list">
-            <div class="user-list-header">
-                <el-input v-model="searchQuery" placeholder="搜索" prefix-icon="el-icon-search" clearable />
-            </div>
-            <div class="user-list-content">
-                <div v-for="user in filteredUsers" :key="user.userId" class="user-item"
-                    :class="{ active: currentUser?.userId === user.userId }" @click="selectUser(user)">
-                    <el-avatar :size="40" :src="user.conversationAvatar" />
-                    <div class="user-info">
-                        <div class="user-name">{{ user.userName }}</div>
-                        <div class="last-message">{{ user.newestMsgContent }}</div>
-                    </div>
-                    <div class="message-time">{{ formatTime(user.newestMsgTime) }}</div>
-                </div>
-            </div>
+  <div class="chat-container">
+    <!-- 左侧用户列表 -->
+    <div class="user-list">
+      <div class="user-list-header">
+        <el-input v-model="searchQuery" placeholder="搜索" prefix-icon="el-icon-search" clearable />
+
+      </div>
+      <div class="user-list-content">
+        <div v-for="user in filteredUsers" :key="user.userId" class="user-item"
+          :class="{ active: currentUser?.userId === user.userId }" @click="selectUser(user)">
+          <el-avatar :size="40" :src="user.conversationAvatar || logo" />
+          <div class="user-info">
+            <div class="user-name">{{ user.conversationTitle }}</div>
+            <div class="last-message">{{ user.newestMsgContent }}</div>
+          </div>
+          <div class="message-time2">
+            <div class="newestMsgTime">{{ formatTime(user.newestMsgTime) }}</div>
+            <div class="msgUnreadCount" v-if="user.msgUnreadCount && user.msgUnreadCount>0">{{ user.msgUnreadCount }}</div>
+          </div>
         </div>
+      </div>
+    </div>
 
-        <!-- 右侧聊天区域 -->
-        <div class="chat-area">
-            <template v-if="currentUser">
-                <div class="chat-header">
-                    <span>{{ currentUser.nickname }}</span>
-                </div>
-                <div class="message-list" ref="messageList">
-                    <div v-for="message in currentMessages.vos" :key="message.senderId" class="message-item"
-                        :class="{ 'message-self': message.senderId === userId }">
-                        <el-avatar :size="40"
-                            :src="message.senderId === userId ? userStore.avatar : currentMessages.conversationAvatar" />
-                        <div class="message-content">
-                            <div class="message-time"
-                                :style="{ textAlign: message.senderId === userId ? 'right' : 'left' }">{{
-                                   message.msgSendTime }}</div>
-                            <div class="message-bubble">
-                                <template v-if="message.msgType === '1'">
-                                    {{ message.msgContent }}
-                                </template>
-                                <template v-else-if="message.type === '2'">
-                                    <el-image :src="message.msgContent" :preview-src-list="[message.msgContent]"
-                                        fit="cover" class="message-image" />
-                                </template>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-                <div class="message-input">
-                    <!-- <div class="input-toolbar">
+    <!-- 右侧聊天区域 -->
+    <div class="chat-area">
+      <template v-if="currentUser">
+        <div class="chat-header">
+          <span>{{ currentUser.nickname }}</span>
+        </div>
+        <div class="message-list" ref="messageList">
+          <div v-if="isLoading" class="loading-messages">
+            <el-icon class="is-loading">
+              <Loading />
+            </el-icon>
+            <span>加载更多消息...</span>
+          </div>
+          <div v-for="message in messages" :key="message.senderId" class="message-item"
+            :class="{ 'message-self': message.senderId === districtCode }">
+            <el-avatar :size="40"
+              :src="message.senderId === districtCode ? userStore.avatar : currentMessages.conversationAvatar || logo" />
+            <div class="message-content">
+              <div class="message-time" :style="{ textAlign: message.senderId === districtCode ? 'right' : 'left' }">{{
+                message.msgSendTime }}</div>
+              <div class="message-bubble">
+                <template v-if="message.msgType === '1'">
+                  {{ message.msgContent }}
+                </template>
+                <template v-else-if="message.type === '2'">
+                  <el-image :src="message.msgContent" :preview-src-list="[message.msgContent]" fit="cover"
+                    class="message-image" />
+                </template>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="message-input">
+          <!-- <div class="input-toolbar">
                         <el-upload action="#" :auto-upload="false" :show-file-list="false"
                             :on-change="handleImageUpload">
                             <el-button type="text" :icon="Picture" title="发送图片">图片</el-button>
                         </el-upload>
                     </div> -->
-                    <div class="input-area">
-                        <el-input v-model="messageInput" type="textarea" :rows="3" placeholder="请输入消息"
-                            @keyup.enter.native="sendMessage" />
-                        <el-button type="primary" @click="sendMessage('text')">发送</el-button>
-                    </div>
-                </div>
-            </template>
-            <div v-else class="no-chat-selected">
-                请选择一个聊天对象
-            </div>
+          <div class="input-area">
+            <el-input v-model="messageInput" type="textarea" :rows="3" placeholder="请输入消息"
+              @keyup.enter.native="sendMessage" />
+            <el-button type="primary" @click="sendMessage('text')">发送</el-button>
+          </div>
         </div>
+      </template>
+      <div v-else class="no-chat-selected">
+        请选择一个聊天对象
+      </div>
     </div>
+  </div>
 </template>
 
 <script setup>
-import { ref, computed, onMounted, nextTick } from 'vue'
+import { ref, computed, onMounted, nextTick, onUnmounted } from 'vue'
 import dayjs from 'dayjs'
 import { ElMessage } from 'element-plus'
-import { getList, sendMsg, getListConversationInfo, setRead } from '@/api/conversation'
+import { getList, sendMsg, getListConversationInfo, setRead, getHistoryMsg } from '@/api/conversation'
+// import WebSocketClient from './WebSocketClient.js';
+import { debounce } from '@/utils/index.js';
 import useUserStore from '@/store/modules/user'
-import WebSocketClient from './WebSocketClient.js';
 const userStore = useUserStore();
-
+import logo from '@/assets/logo/logo.png'
 const userId = computed(() => {
-    return userStore.user.deptId
+  return userStore.user.userId
+})
+const districtCode = computed(() => {
+  return userStore.user.districtCode
 })
 console.log("TCL: userId -> userId", userId)
 
@@ -88,92 +101,157 @@ const searchQuery = ref('')
 const currentUser = ref(null)
 const messageInput = ref('')
 const messageList = ref(null)
+const isLoading = ref(false)
+const currentPage = ref(1)
+const hasMore = ref(true)
 
 // 用户列表
 const filteredUsers = ref([]);
 
 // 当前用户的消息
 const currentMessages = ref({
-    vos: []
+  vos: []
 })
 
 const isToday = (date) => {
-    return dayjs(date).isSame(dayjs(), 'day');
+  return dayjs(date).isSame(dayjs(), 'day');
 };
 
 const isYesterday = (date) => {
-    return dayjs(date).isSame(dayjs().subtract(1, 'day'), 'day');
+  return dayjs(date).isSame(dayjs().subtract(1, 'day'), 'day');
 };
 const handlerData = (dates) => {
-    const date = dayjs(dates);
-    if (isToday(dates)) {
-        return date.format('HH:MM');;
-    } else if (isYesterday(dates)) {
-        return '昨天';
-    } else {
-        return date.format('YY/MM/DD'); // 或者其他格式如 'YYYY年MM月DD日'
-    }
+  const date = dayjs(dates);
+  if (isToday(dates)) {
+    return date.format('HH:MM');;
+  } else if (isYesterday(dates)) {
+    return '昨天';
+  } else {
+    return date.format('YY/MM/DD'); // 或者其他格式如 'YYYY年MM月DD日'
+  }
 }
 // 格式化时间
 const formatTime = (time) => {
-    return handlerData(time)
+  return handlerData(time)
 }
 
 // 选择用户
-const selectUser = async(user) => {
-    try {
-        console.log("TCL: selectUser -> user", user)
-        const res = await getListConversationInfo({
-            conversationRecordId:  user.conversationRecordId,
-        })
-        const data = res.data;
-        data.vos = data.vos.reverse(); //倒叙处理
-        currentMessages.value = data;
-        currentUser.value = user
+const selectUser = async (user) => {
+  try {
+    console.log("TCL: selectUser -> user", user)
+    currentPage.value = 1
+    hasMore.value = true
+    await setRead({...user, conversationRecordId: user.conversationRecordId, system: 3, }); //设置已读
+    const res = await getListConversationInfo({
+      conversationRecordId: user.conversationRecordId,
+      pageNum: currentPage.value,
+      pageSize: 20
+    })
+    const data = res.data;
+    data.vos = data.vos.reverse(); //倒叙处理
+    currentMessages.value = data;
+    messages.value = data.vos;
+    currentUser.value = user
     nextTick(() => {
-        scrollToBottom()
+      scrollToBottom()
     })
-    } catch (error) {
-        
+  } catch (error) {
+   console.log("TCL: selectUser -> error", error)
+   
+  }
+}
+
+// 加载更多消息
+const loadMoreMessages = debounce(async () => {
+  if (isLoading.value || !hasMore.value || !currentUser.value) return
+
+  // isLoading.value = true
+  // console.log('xx');
+  // setTimeout(() => {
+  //   isLoading.value = false
+  // }, 1000)
+
+  // return
+
+  try {
+    isLoading.value = true
+    const oldScrollHeight = messageList.value.scrollHeight
+    const conversationMsgRecordId = messages.value[messages.value.length - 1].conversationMsgRecordId;
+    const res = await getHistoryMsg({
+      conversationMsgRecordId,
+      conversationRecordId: currentUser.value.conversationRecordId
+    })
+
+    const data = res.data
+    console.log("TCL: loadMoreMessages -> data", data)
+    if (data && data.length > 0) {
+      const data_reverse = data.reverse()
+      console.log("TCL: loadMoreMessages -> data_reverse", data_reverse)
+      const mes = [...data, ...messages.value]
+      messages.value = mes;
+      console.log("TCL: loadMoreMessages -> messages.value", messages.value)
+
+    } else {
+      hasMore.value = false
     }
-	
+
+    nextTick(() => {
+      const newScrollHeight = messageList.value.scrollHeight
+      messageList.value.scrollTop = newScrollHeight - oldScrollHeight
+    })
+  } catch (error) {
+    console.error('加载更多消息失败:', error)
+  } finally {
+    isLoading.value = false
+  }
+}, 500)
+
+// 处理滚动事件
+const handleScroll = () => {
+  if (!messageList.value) return
+
+  const { scrollTop } = messageList.value
+
+  if (scrollTop < 50 && !isLoading.value) {
+
+    loadMoreMessages()
+  }
 }
 
 // 发送消息
 const sendMessage = async (contentType) => {
-    try {
-        if (!messageInput.value.trim()) {
-            ElMessage.warning('消息不能为空')
-            return
-        }
-
-        if (!currentUser.value) {
-            ElMessage.warning('请先选择一个聊天对象')
-            return
-        }
-
-        const newMessage = {
-            conversationRecordId: currentUser.value.conversationRecordId,
-            msgContent: messageInput.value,
-            msgSendTime: dayjs(new Date().getTime()).format('YYYY-MM-DD HH:MM:SS') ,
-            msgType: contentType == 'text' ? '1' : '2',//消息类型 1文字消息 2图片消息
-            userId:  userId.value,
-            senderId: userId.value,
-            system: 3,
-        }
-
-    
-
-        await sendMsg(newMessage);
-        currentMessages.value.vos.push(newMessage);
-        messageInput.value = ''
-        nextTick(() => {
-            scrollToBottom()
-        })
-    } catch (error) {
-        console.log("TCL: sendMessage -> error", error)
+  try {
+    if (!messageInput.value.trim()) {
+      ElMessage.warning('消息不能为空')
+      return
+    }
+
+    if (!currentUser.value) {
+      ElMessage.warning('请先选择一个聊天对象')
+      return
+    }
 
+    const newMessage = {
+      ...currentUser.value,
+      conversationRecordId: currentUser.value.conversationRecordId,
+      msgContent: messageInput.value,
+      msgSendTime: dayjs(new Date().getTime()).format('YYYY-MM-DD HH:MM:SS'),
+      msgType: contentType == 'text' ? '1' : '2',//消息类型 1文字消息 2图片消息
+      system: 3,
     }
+
+
+
+    await sendMsg(newMessage);
+    currentMessages.value.vos.push(newMessage);
+    messageInput.value = ''
+    nextTick(() => {
+      scrollToBottom()
+    })
+  } catch (error) {
+    console.log("TCL: sendMessage -> error", error)
+
+  }
 }
 
 
@@ -206,238 +284,298 @@ const sendMessage = async (contentType) => {
 
 // 滚动到底部
 const scrollToBottom = () => {
-    if (messageList.value) {
-        messageList.value.scrollTop = messageList.value.scrollHeight
-    }
+  if (messageList.value) {
+    messageList.value.scrollTop = messageList.value.scrollHeight
+  }
 }
 
-const getMList = async () => {
-    try {
-        const res = await getList({
-            system: 3
-        })
-        filteredUsers.value = res.rows;
-        res.rows[0] && selectUser(res.rows[0])
-    } catch (error) {
+const getMList = async (type) => {
+  try {
+    const res = await getList({
+      system: 3
+    })
+    filteredUsers.value = res.rows;
+    type && res.rows[0] && selectUser(res.rows[0])
+  } catch (error) {
 
-    }
+  }
 }
 let ws = null;
 
 
 const soketInit = () => {
-    try {
-        //获取账户时,连接soket 
-        ws = new WebSocketClient(userId.value);
-        // 连接WebSocket
-        ws.connect()
-
-        // 监听连接事件
-        ws.on('connect', () => {
-            console.log('连接成功')
-        })
-
-        // 监听断开连接事件
-        ws.on('disconnect', () => {
-            console.log('连接断开')
-        })
-        // 监听错误事件
-        ws.on('error', (error) => {
-            console.error('发生错误:', error)
-        })
-        // 监听消息事件
-        ws.on('message', (res) => {
-            console.log('收到消息:', res)
-            const data = JSON.parse(res.data);
-			console.log("TCL: soketInit -> data", data)
-            if (res.type === 'msgNew') {
-                currentMessages.value.push({...res.data})
-			}
+  ws = userStore.ws;
+	console.log("TCL: soketInit -> ws", ws)
+  try {
+    // //获取账户时,连接soket 
+    // ws = new WebSocketClient(userId.value);
+    // // 连接WebSocket
+    // ws.connect()
+
+    // // 监听连接事件
+    // ws.on('connect', () => {
+    //   console.log('连接成功')
+    // })
+
+    // // 监听断开连接事件
+    // ws.on('disconnect', () => {
+    //   console.log('连接断开')
+    // })
+    // // 监听错误事件
+    // ws.on('error', (error) => {
+    //   console.error('发生错误:', error)
+    // })
+    // 监听消息事件
+    ws.on('message', (res) => {
+      console.log('收到消息:', res,currentUser.value.conversationRecordId , res.data.conversationRecordId)
+      // const data = JSON.parse(res.data);
+
+      if (res.type === 'msgNew' && currentUser.value.conversationRecordId === res.data.conversationRecordId) {
+        console.log("TCL: soketInit -> data", res.data)
+        messages.value.push(res.data)
+        nextTick(() => {
+            scrollToBottom()
         })
+      }
+      if(res.type ==='msgUnreadCount' || currentUser.value.conversationRecordId !== res.data.conversationRecordId){
+        getMList(false);//获取用户列表
+      }
+    })
 
-    } catch (error) {
-        console.log("TCL: soketInit -> error", error)
+  } catch (error) {
+    console.log("TCL: soketInit -> error", error)
 
-    }
+  }
 }
 
 onMounted(() => {
+ 
+  getMList(true);//获取用户列表
 
+  setTimeout(() => {
+    // 添加滚动监听
+    if (messageList.value) {
+      messageList.value.addEventListener('scroll', handleScroll)
+    }
     soketInit();
-
-
-
-
-    getMList();//获取用户列表
+  }, 500)
 })
-</script>
+
+onUnmounted(() => {
+  // 移除滚动监听
+  if (messageList.value) {
+    messageList.value.removeEventListener('scroll', handleScroll)
+    }
+  })
+  </script>
 
 <style scoped>
 .chat-container {
-    display: flex;
-    height: 100vh;
-    background-color: #f5f5f5;
+  display: flex;
+  height: calc(100vh - 130px);
+  background-color: #f5f5f5;
 }
 
 .user-list {
-    width: 300px;
-    background-color: #fff;
-    border-right: 1px solid #e6e6e6;
-    display: flex;
-    flex-direction: column;
+  width: 300px;
+  background-color: #fff;
+  border-right: 1px solid #e6e6e6;
+  display: flex;
+  flex-direction: column;
 }
 
 .user-list-header {
-    padding: 10px;
-    border-bottom: 1px solid #e6e6e6;
+  padding: 10px;
+  border-bottom: 1px solid #e6e6e6;
 }
 
 .user-list-content {
-    flex: 1;
-    overflow-y: auto;
+  flex: 1;
+  overflow-y: auto;
 }
 
 .user-item {
-    display: flex;
-    align-items: center;
-    padding: 10px;
-    cursor: pointer;
-    transition: background-color 0.3s;
+  display: flex;
+  align-items: center;
+  padding: 10px;
+  cursor: pointer;
+  transition: background-color 0.3s;
 }
 
 .user-item:hover {
-    background-color: #f5f5f5;
+  background-color: #f5f5f5;
 }
 
 .user-item.active {
-    background-color: #e6f7ff;
+  background-color: #e6f7ff;
 }
 
 .user-info {
-    flex: 1;
-    margin-left: 10px;
-    overflow: hidden;
+  flex: 1;
+  margin-left: 10px;
+  overflow: hidden;
 }
 
 .user-name {
-    font-weight: bold;
-    margin-bottom: 4px;
+  font-weight: bold;
+  margin-bottom: 4px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 
 .last-message {
-    color: #999;
-    font-size: 12px;
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
+  color: #999;
+  font-size: 12px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 
 .message-time {
-    font-size: 12px;
-    color: #999;
+  font-size: 12px;
+  color: #999;
 
 }
 
 .chat-area {
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-    background-color: #fff;
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  background-color: #fff;
 }
 
 .chat-header {
-    padding: 10px;
-    border-bottom: 1px solid #e6e6e6;
-    font-weight: bold;
+  padding: 10px;
+  border-bottom: 1px solid #e6e6e6;
+  font-weight: bold;
 }
 
 .message-list {
-    flex: 1;
-    overflow-y: auto;
-    padding: 20px;
+  flex: 1;
+  overflow-y: auto;
+  padding: 20px;
 }
 
 .message-item {
-    display: flex;
-    margin-bottom: 20px;
+  display: flex;
+  margin-bottom: 20px;
 }
 
 .message-item.message-self {
-    flex-direction: row-reverse;
+  flex-direction: row-reverse;
 }
 
 .message-content {
-    margin: 0 10px;
-    max-width: 60%;
+  margin: 0 10px;
+  max-width: 60%;
 }
 
 .message-time {
-    font-size: 12px;
-    color: #999;
-    margin-bottom: 4px;
-    text-align: left;
+  font-size: 12px;
+  color: #999;
+  margin-bottom: 4px;
+  text-align: left;
+
+}
+.message-time2 {
+  font-size: 12px;
+  color: #999;
+  margin-bottom: 4px;
+  text-align: left;
+
+  display: flex;
+  align-items: end;
+  justify-content: center;
+  flex-direction: column;
+}
+.msgUnreadCount {
+  background: red;
+    color: #fff;
+    margin-top: 4px;
+    width: 15px;
+    height: 15px;
+
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
 }
-
 .message-bubble {
-    background-color: #f5f5f5;
-    padding: 10px;
-    border-radius: 4px;
-    word-break: break-all;
-   
+  background-color: #f5f5f5;
+  padding: 10px;
+  border-radius: 4px;
+  word-break: break-all;
+
 }
 
 .message-self .message-bubble {
-    background-color: #95ec69;
-    text-align: right;
-    width: 100%;
+  background-color: #95ec69;
+  text-align: right;
+  width: 100%;
 }
 
 .message-image {
-    max-width: 200px;
-    max-height: 200px;
-    border-radius: 4px;
+  max-width: 200px;
+  max-height: 200px;
+  border-radius: 4px;
 }
 
 .message-input {
-    border-top: 1px solid #e6e6e6;
-    padding: 10px;
-    display: flex;
+  border-top: 1px solid #e6e6e6;
+  padding: 10px;
+  display: flex;
 }
 
 .input-toolbar {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    padding: 8px 0;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 0;
 }
 
 .input-toolbar .el-button {
-    padding: 4px 8px;
-    font-size: 14px;
+  padding: 4px 8px;
+  font-size: 14px;
 }
 
 .input-toolbar .el-button:hover {
-    background-color: #f5f5f5;
-    border-radius: 4px;
+  background-color: #f5f5f5;
+  border-radius: 4px;
 }
 
 .input-area {
-    display: flex;
-    gap: 10px;
-    padding-top: 10px;
-    flex: 1;
+  display: flex;
+  gap: 10px;
+  padding-top: 10px;
+  flex: 1;
 }
 
 .input-area .el-textarea {
-    flex: 1;
+  flex: 1;
 }
 
 .no-chat-selected {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    height: 100%;
-    color: #999;
-    font-size: 16px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  color: #999;
+  font-size: 16px;
+}
+
+.loading-messages {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 10px;
+  color: #999;
+  font-size: 14px;
+  gap: 8px;
 }
+
+.loading-messages .el-icon{
+    font-size: 16px;
+  }
+  
 </style>

+ 6 - 4
src/views/staff/volunteer/manage/index.vue

@@ -235,6 +235,9 @@ const dialogData = reactive({
             label: '驳回原因',
             prop: 'rejectReason',
             type: 'textarea',
+            rules: [
+                { required: true, message: '请填写驳回原因', trigger: 'blur' }
+            ],
             show: (form) => {
                 console.log('驳回原因', form);
                 return form.appStatus === '3'
@@ -273,7 +276,7 @@ const openDialog = (data, type) => {
         if (type) {
             //审核
             title.value = '审核'
-
+            row.appStatus = '';
         } else {
             disabledData['appStatus'] = true;
             disabledData['rejectReason'] = true;
@@ -304,6 +307,8 @@ const submitForm = async (parmas) => {
             })
             if (res.code === 200) {
                 proxy.$modal.msgSuccess("审核成功");
+                userTableRef.value.resetForm();
+                dialogFormRef.value.handleDialog(false);
                 return;
             }
             proxy.$modal.msgSuccess(res.msg);
@@ -311,9 +316,6 @@ const submitForm = async (parmas) => {
     } catch (error) {
         console.log('error', error);
 
-    } finally {
-        userTableRef.value.resetForm();
-        dialogFormRef.value.handleDialog(false);
     }
 
 }

+ 3 - 2
vite.config.js

@@ -38,9 +38,10 @@ export default defineConfig(({ mode, command }) => {
         // https://cn.vitejs.dev/config/#server-proxy
         '/dev-api': {
           // target: 'http://192.168.100.139:9527',
-          // target: 'https://zybooks.tech/prod-api', 
+          target: 'http://192.168.100.122:9527',
+          // target: 'https://yongc.top/prod-api', 
           // target: 'http://192.168.100.128:9527',
-          target: 'https://goldshulin.com/prod-api',
+          // target: 'https://goldshulin.com/prod-api',
           changeOrigin: true,
           rewrite: (p) => p.replace(/^\/dev-api/, '')
         },