Преглед изворни кода

feat: 发送消息, 修改密码

chenhao пре 2 година
родитељ
комит
aa6583064a

+ 1 - 0
.eslintrc.cjs

@@ -31,6 +31,7 @@ module.exports = {
     'vue/multi-word-component-names': 0,
     'vue/multi-word-component-names': 0,
     'vue/component-name-in-template-casing': ['error', 'kebab-case'],
     'vue/component-name-in-template-casing': ['error', 'kebab-case'],
     'vue/one-component-per-file': 0,
     'vue/one-component-per-file': 0,
+    'vue/no-v-html': 0,
   },
   },
   globals: {},
   globals: {},
 }
 }

+ 1 - 1
src/components/shared/MarkHeader.vue

@@ -27,7 +27,7 @@
 import { reactive, ref, computed, useAttrs, watch } from 'vue'
 import { reactive, ref, computed, useAttrs, watch } from 'vue'
 import SvgIcon from '@/components/common/SvgIcon.vue'
 import SvgIcon from '@/components/common/SvgIcon.vue'
 import ColorPicker from '@/components/common/ColorPicker.vue'
 import ColorPicker from '@/components/common/ColorPicker.vue'
-import Message from '@/components/shared/Message.vue'
+import Message from '@/components/shared/message/Message.vue'
 import UserInfo from '@/components/shared/UserInfo.vue'
 import UserInfo from '@/components/shared/UserInfo.vue'
 
 
 type ButtonType =
 type ButtonType =

+ 77 - 0
src/components/shared/UpdateUserPwd.vue

@@ -0,0 +1,77 @@
+<template>
+  <base-dialog v-model="visible" title="个人信息" :footer="false" destroy-on-close>
+    <base-form ref="formRef" :model="userModel" :rules="rules" :items="items" :label-width="useVW(72)">
+      <template #form-item-confirm>
+        <confirm-button class="m-t-large" @confirm="onSubmit" @cancel="visible = false"></confirm-button>
+      </template>
+    </base-form>
+  </base-dialog>
+</template>
+
+<script setup lang="ts" name="UpdateUserPwd">
+import { reactive, ref } from 'vue'
+import useFetch from '@/hooks/useFetch'
+import useVModel from '@/hooks/useVModel'
+import useVW from '@/hooks/useVW'
+import useForm from '@/hooks/useForm'
+import useMainStore from '@/store/main'
+import BaseForm from '@/components/element/BaseForm.vue'
+import BaseDialog from '@/components/element/BaseDialog.vue'
+import ConfirmButton from '@/components/common/ConfirmButton.vue'
+
+import type { EpFormItem, EpFormRules } from 'global-type'
+
+const props = defineProps<{
+  modelValue: boolean
+}>()
+
+const visible = useVModel(props, 'modelValue')
+
+const mainStore = useMainStore()
+
+const { formRef, elFormRef } = useForm()
+
+const userModel = reactive<{ name: string; password: string; rePassword?: string }>({
+  name: mainStore?.myUserInfo?.name || '',
+  password: '',
+  rePassword: '',
+})
+
+const rules: EpFormRules = {
+  name: [{ required: true, message: '请填写用户姓名' }],
+  password: [{ required: true, message: '请填写登录密码' }],
+  rePassword: [
+    {
+      required: true,
+      validator(_, value, cb) {
+        if (!value) {
+          cb(new Error('请确认登录密码'))
+        } else if (value !== userModel.password) {
+          cb(new Error('两次输入密码不一致'))
+        } else {
+          cb()
+        }
+      },
+    },
+  ],
+}
+
+const items: EpFormItem[] = [
+  { label: '姓名', prop: 'name', slotType: 'input' },
+  { label: '新密码', prop: 'password', slotType: 'input', slot: { type: 'password' } },
+  { label: '确认密码', prop: 'rePassword', slotType: 'input', slot: { type: 'password' } },
+  { slotName: 'confirm' },
+]
+
+const onSubmit = async () => {
+  try {
+    const valid = await elFormRef?.value?.validate()
+    valid && (await useFetch('updateUserPwd').fetch({ password: userModel.password }))
+    visible.value = false
+  } catch (error) {
+    console.error(error)
+  }
+}
+</script>
+
+<style scoped lang="scss"></style>

+ 6 - 2
src/components/shared/UserInfo.vue

@@ -1,13 +1,15 @@
 <template>
 <template>
-  <div class="user-info">
+  <div class="user-info" @click="visibleUpdatePwd = true">
     <span>{{ info.userName }}</span>
     <span>{{ info.userName }}</span>
     <span class="m-l-base">{{ info.roleName }}</span>
     <span class="m-l-base">{{ info.roleName }}</span>
   </div>
   </div>
+  <update-user-pwd v-model="visibleUpdatePwd"></update-user-pwd>
 </template>
 </template>
 
 
 <script setup lang="ts" name="UserInfo">
 <script setup lang="ts" name="UserInfo">
-import { reactive, computed } from 'vue'
+import { ref, computed } from 'vue'
 import useMainStore from '@/store/main'
 import useMainStore from '@/store/main'
+import UpdateUserPwd from '@/components/shared/UpdateUserPwd.vue'
 
 
 const mainStore = useMainStore()
 const mainStore = useMainStore()
 
 
@@ -18,6 +20,8 @@ const info = computed(() => {
     roleName,
     roleName,
   }
   }
 })
 })
+
+const visibleUpdatePwd = ref<boolean>(false)
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

+ 35 - 4
src/components/shared/Message.vue → src/components/shared/message/Message.vue

@@ -26,8 +26,19 @@
             啥可适当就卡省的就卡省的就看撒好的就卡省的就看撒好的金卡和健康大使金卡是框架啥可适当就卡省的就卡省的就看撒好的就卡省的就看撒好的金卡和健康大使金卡是框架
             啥可适当就卡省的就卡省的就看撒好的就卡省的就看撒好的金卡和健康大使金卡是框架啥可适当就卡省的就卡省的就看撒好的就卡省的就看撒好的金卡和健康大使金卡是框架
           </div>
           </div>
         </div>
         </div>
+        <div class="flex message-row">
+          <div class="message-send-user">
+            <div class="user-name">陈明</div>
+            <div class="message-time">20:88:33</div>
+          </div>
+          <div class="flex-1 message-content">
+            啥可适当就卡省的就卡省的就看撒好的就卡省的就看撒好的金卡和健康大使金卡是框架啥可适当就卡省的就卡省的就看撒好的就卡省的就看撒好的金卡和健康大使金卡是框架
+          </div>
+        </div>
       </div>
       </div>
       <confirm-button
       <confirm-button
+        size="small"
+        between
         ok-text="收消息"
         ok-text="收消息"
         cancel-text="发消息"
         cancel-text="发消息"
         @confirm="onReceiveMessage"
         @confirm="onReceiveMessage"
@@ -35,28 +46,38 @@
       ></confirm-button>
       ></confirm-button>
     </div>
     </div>
   </el-popover>
   </el-popover>
+  <message-window v-model="visibleMessageWindow" :type="messageWindowType"></message-window>
 </template>
 </template>
 
 
 <script setup lang="ts" name="Message">
 <script setup lang="ts" name="Message">
+/** 头部消息组件 */
 import { reactive, ref } from 'vue'
 import { reactive, ref } from 'vue'
 import { ElPopover } from 'element-plus'
 import { ElPopover } from 'element-plus'
 import useVW from '@/hooks/useVW'
 import useVW from '@/hooks/useVW'
+import useMessageLoop from '@/hooks/useMessageLoop'
+import MessageWindow from '@/components/shared/message/MessageWindow.vue'
 import ConfirmButton from '@/components/common/ConfirmButton.vue'
 import ConfirmButton from '@/components/common/ConfirmButton.vue'
 import SvgIcon from '@/components/common/SvgIcon.vue'
 import SvgIcon from '@/components/common/SvgIcon.vue'
-import useMessageLoop from '@/hooks/useMessageLoop'
 
 
 const messageIcon = ref<HTMLDivElement>()
 const messageIcon = ref<HTMLDivElement>()
 
 
 const unReadMessages = useMessageLoop()
 const unReadMessages = useMessageLoop()
 
 
+const messageWindowType = ref<'view' | 'send'>('view')
+
+/** 发送/查看消息Modal */
+const visibleMessageWindow = ref<boolean>(false)
+
 /** 收消息 */
 /** 收消息 */
 const onReceiveMessage = () => {
 const onReceiveMessage = () => {
-  console.log('收消息')
+  messageWindowType.value = 'view'
+  visibleMessageWindow.value = true
 }
 }
 
 
 /** 发消息 */
 /** 发消息 */
 const onSendMessage = () => {
 const onSendMessage = () => {
-  console.log('发消息')
+  messageWindowType.value = 'send'
+  visibleMessageWindow.value = true
 }
 }
 </script>
 </script>
 
 
@@ -77,6 +98,7 @@ const onSendMessage = () => {
   }
   }
 }
 }
 .message-popover-content {
 .message-popover-content {
+  color: $NormalColor;
   .title {
   .title {
     .unread-count {
     .unread-count {
       font-size: $MediumFont;
       font-size: $MediumFont;
@@ -86,19 +108,28 @@ const onSendMessage = () => {
   }
   }
   .message-list {
   .message-list {
     border-bottom: $OnePixelLine;
     border-bottom: $OnePixelLine;
+    margin: 10px 0 40px;
     .message-row {
     .message-row {
       padding: 10px 4px;
       padding: 10px 4px;
       height: 52px;
       height: 52px;
-      font-weight: 500;
+      font-weight: 400;
       border-top: $OnePixelLine;
       border-top: $OnePixelLine;
+      &:hover {
+        background-color: #f5f5f5;
+      }
       .message-send-user {
       .message-send-user {
         width: 49px;
         width: 49px;
         margin-right: 0.5em;
         margin-right: 0.5em;
         font-size: $SmallFont;
         font-size: $SmallFont;
         .user-name {
         .user-name {
+          font-weight: 500;
           text-align: justify;
           text-align: justify;
           text-align-last: justify;
           text-align-last: justify;
         }
         }
+        .message-time {
+          text-align: center;
+          font-weight: 300;
+        }
       }
       }
       .message-content {
       .message-content {
         word-break: break-all;
         word-break: break-all;

+ 46 - 0
src/components/shared/message/MessageHistory.vue

@@ -0,0 +1,46 @@
+<template>
+  <div class="p-base scroll-y-auto message-history-container">
+    <template v-if="!history?.length">
+      <empty></empty>
+    </template>
+    <template v-else>
+      <div v-for="(message, index) in history" :key="index" class="m-b-base message-info">
+        <div class="message-header">
+          <span class="user-name">{{ message.sendUserName }}</span>
+          <span class="message-time">{{ message.sendTime }}</span>
+        </div>
+        <div class="message-info-content" v-html="message.content"></div>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script setup lang="ts" name="MessageHistory">
+/** 历史消息 */
+import { watch } from 'vue'
+import useFetch from '@/hooks/useFetch'
+import Empty from '@/components/common/Empty.vue'
+
+const props = defineProps<{ userId?: number }>()
+
+const { fetch: getMessageHistory, result: history } = useFetch('getMessageHistory')
+
+watch(
+  () => props.userId,
+  () => {
+    if (props.userId) {
+      getMessageHistory({ sendUserId: props.userId })
+    }
+  },
+  { immediate: true }
+)
+</script>
+
+<style scoped lang="scss">
+.message-history-container {
+  max-height: 400px;
+  border-top: $OnePixelLine;
+  border-radius: 0 0 6px 6px;
+  background-color: $color--white;
+}
+</style>

+ 113 - 0
src/components/shared/message/MessageList.vue

@@ -0,0 +1,113 @@
+<template>
+  <div class="flex radius-base overflow-hidden message-list-modal">
+    <div class="message-list p-base scroll-y-auto">
+      <div
+        v-for="message in messageList"
+        :key="message.sendUserId"
+        class="message-item"
+        @click="checkMessage(message)"
+      ></div>
+    </div>
+    <div class="radius-base message-info-container">
+      <div class="flex direction-column message-info">
+        <div class="flex items-center p-base message-info-header">
+          <div class="flex items-center send-user">
+            <span class="m-r-mini">发件人</span>
+            <span class="radius-base user-name">{{ currentMessage?.sendUserName }}</span>
+          </div>
+          <div class="grid pointer m-l-auto close-icon" @click="$emit('close')">
+            <el-icon><close /></el-icon>
+          </div>
+        </div>
+        <div class="flex-1 overflow-hidden p-base">
+          <div class="full-h radius-base p-extra-base scroll-y-auto message-info-content">
+            {{ currentMessage?.content }}
+          </div>
+        </div>
+        <div class="p-base flex items-center justify-end">
+          <el-button size="small" plain @click="toggleHistory">历史消息</el-button>
+          <el-button size="small" type="primary" @click="$emit('change-type', 'send')">回复</el-button>
+        </div>
+      </div>
+      <message-history v-if="showHistory" :id="currentMessage?.sendUserId"></message-history>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts" name="MessageList">
+/** 消息列表*/
+import { reactive, ref } from 'vue'
+import { ElButton, ElIcon } from 'element-plus'
+import { Close } from '@element-plus/icons-vue'
+import useFetch from '@/hooks/useFetch'
+import MessageHistory from '@/components/shared/message/MessageHistory.vue'
+
+import type { ExtractApiResponse } from 'api-type'
+
+type MessageType = ExtractArrayValue<ExtractApiResponse<'getMessageList'>>
+
+defineEmits(['close', 'change-type'])
+
+const showHistory = ref<boolean>(false)
+
+const { fetch: getMessageList, result: messageList } = useFetch('getMessageList')
+
+const currentMessage = ref<MessageType>()
+
+const checkMessage = (message: MessageType) => {
+  currentMessage.value = message
+}
+
+const toggleHistory = () => {
+  showHistory.value = !showHistory.value
+}
+
+getMessageList()
+</script>
+
+<style scoped lang="scss">
+.message-list-modal {
+  width: 880px;
+  background-color: transparent;
+  .message-list {
+    width: 280px;
+    height: 446px;
+    background: #fafafa;
+    box-shadow: 0px 6px 6px 0px rgba(0, 0, 0, 0.1);
+  }
+  .message-info-container {
+    width: 600px;
+    .message-info {
+      height: 446px;
+      background-color: $color--white;
+      .message-info-header {
+        border-bottom: $OnePixelLine;
+        .send-user {
+          font-size: $SmallFont;
+          color: $RegularFontColor;
+          .user-name {
+            display: inline-block;
+            width: 160px;
+            padding: 10px 12px;
+            border: $OnePixelLine;
+          }
+        }
+        .close-icon {
+          width: 20px;
+          height: 20px;
+          place-items: center;
+          font-size: 18px;
+          color: $RegularFontColor;
+          &:hover {
+            color: $NormalColor;
+          }
+        }
+      }
+      .message-info-content {
+        border: $OnePixelLine;
+        font-size: $SmallFont;
+      }
+    }
+  }
+}
+</style>

+ 149 - 0
src/components/shared/message/MessageSend.vue

@@ -0,0 +1,149 @@
+<template>
+  <div class="flex radius-base overflow-hidden message-list-modal">
+    <div v-show="showCheckUser" class="flex direction-column p-base fill-lighter tree-box">
+      <el-input v-model="filterText" placeholder="输入评卷员账号或名称筛选" clearable></el-input>
+      <div class="flex-1 m-t-base scroll-y-auto">
+        <el-tree
+          ref="treeRef"
+          v-model="markerIds"
+          show-checkbox
+          :filter-node-method="filterTree"
+          :data="markerTree"
+          :props="treeProp"
+        ></el-tree>
+      </div>
+    </div>
+    <div class="message-info-container">
+      <div class="flex direction-column message-info">
+        <div class="flex items-center p-base message-info-header">
+          <div class="flex items-center send-user">
+            <span class="m-r-mini">收件人</span>
+            <span class="flex items-center justify-between radius-base user-name">
+              <span> {{ checkedUsers }}</span>
+              <el-button size="small" type="primary" @click="toggleCheckUser"> 选择收件人 </el-button>
+            </span>
+          </div>
+          <div class="grid pointer m-l-auto close-icon" @click="$emit('close')">
+            <el-icon><close /></el-icon>
+          </div>
+        </div>
+        <div class="flex-1 p-base overflow-hidden">
+          <div class="full-h radius-base p-extra-base scroll-y-auto message-info-content">
+            <el-input v-model="messageContent" type="textarea"></el-input>
+          </div>
+        </div>
+        <div class="p-base flex items-center justify-end">
+          <el-button size="small" plain @click="sendCurrentPaper">发送当前试卷</el-button>
+          <el-button size="small" type="primary" @click="onSendMessage">发送</el-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts" name="MessageSend">
+import { reactive, ref, computed } from 'vue'
+import { ElInput, ElButton, ElTree, ElIcon } from 'element-plus'
+import { Close } from '@element-plus/icons-vue'
+import useFetch from '@/hooks/useFetch'
+
+import type { ExtractApiResponse } from 'api-type'
+
+type MarkerItem = ExtractArrayValue<ExtractArrayValue<ExtractApiResponse<'getMarkerTree'>>['markers']>
+
+defineEmits(['close'])
+
+const showCheckUser = ref<boolean>(false)
+
+const messageContent = ref<string>()
+const markerIds = ref<number[]>()
+
+const filterText = ref<string>('')
+const treeRef = ref<InstanceType<typeof ElTree>>()
+
+const checkedUsers = computed(() => {
+  return 'xxx'
+})
+
+const toggleCheckUser = () => {
+  showCheckUser.value = !showCheckUser.value
+}
+
+function isMarker(x: any): x is MarkerItem {
+  return !!x.loginName
+}
+
+const treeProp = {
+  children: 'markers',
+  label(treeNode: ExtractArrayValue<ExtractApiResponse<'getMarkerTree'>>) {
+    if (isMarker(treeNode)) {
+      return treeNode.name || treeNode.loginName
+    }
+    return `第${treeNode.markingGroupNumber}组`
+  },
+} as unknown as InstanceType<typeof ElTree>['props']
+
+const filterTree = ((value: string, data: MarkerItem) => {
+  if (!value) return true
+  return data.name?.includes(value) || data.loginName?.includes(value)
+}) as unknown as InstanceType<typeof ElTree>['filterNodeMethod']
+
+const { fetch: getMarkerTree, result: markerTree } = useFetch('getMarkerTree')
+
+/** 发送当前试卷 */
+const sendCurrentPaper = () => {
+  console.log('发送当前试卷')
+}
+/** 发送消息 */
+const onSendMessage = () => {
+  console.log('发消息')
+}
+</script>
+
+<style scoped lang="scss">
+.message-list-modal {
+  width: 880px;
+  background-color: transparent;
+  .tree-box {
+  }
+  .message-list {
+    width: 280px;
+    height: 446px;
+    background: #fafafa;
+    box-shadow: 0px 6px 6px 0px rgba(0, 0, 0, 0.1);
+  }
+  .message-info-container {
+    width: 600px;
+    .message-info {
+      height: 446px;
+      background-color: $color--white;
+      .message-info-header {
+        border-bottom: $OnePixelLine;
+        .send-user {
+          font-size: $SmallFont;
+          color: $RegularFontColor;
+          .user-name {
+            min-width: 320px;
+            padding: 10px 12px;
+            border: $OnePixelLine;
+          }
+        }
+        .close-icon {
+          width: 20px;
+          height: 20px;
+          place-items: center;
+          font-size: 18px;
+          color: $RegularFontColor;
+          &:hover {
+            color: $NormalColor;
+          }
+        }
+      }
+      .message-info-content {
+        border: $OnePixelLine;
+        font-size: $SmallFont;
+      }
+    }
+  }
+}
+</style>

+ 49 - 0
src/components/shared/message/MessageWindow.vue

@@ -0,0 +1,49 @@
+<template>
+  <base-dialog v-model="visible" unless :footer="false" class="message-dialog">
+    <component :is="MessageWindowContent" @close="onClose" @change-type="onChangeType"></component>
+  </base-dialog>
+</template>
+
+<script setup lang="ts" name="MessageWindow">
+/** 发送/回复消息 */
+import { reactive, ref, computed, withDefaults } from 'vue'
+import useVModel from '@/hooks/useVModel'
+import BaseDialog from '@/components/element/BaseDialog.vue'
+import MessageHistory from '@/components/shared/message/MessageList.vue'
+import MessageSend from '@/components/shared/message/MessageSend.vue'
+
+type ModalType = 'view' | 'send'
+
+const props = withDefaults(
+  defineProps<{
+    modelValue: boolean
+    type: ModalType
+  }>(),
+  { type: 'view' }
+)
+
+const visible = useVModel(props, 'modelValue')
+const modalType = useVModel(props, 'type')
+
+const MessageWindowContent = computed(() => {
+  return modalType.value === 'send' ? MessageSend : MessageHistory
+})
+
+const onClose = () => {
+  visible.value = false
+}
+
+const onChangeType = (type: ModalType) => {
+  modalType.value = type
+}
+</script>
+
+<style lang="scss">
+.message-dialog {
+  background-color: transparent;
+  box-shadow: none;
+  .el-dialog__body {
+    padding: 0 !important;
+  }
+}
+</style>

+ 1 - 1
src/hooks/useMessageLoop.ts

@@ -8,7 +8,7 @@ import type {} from 'global-type'
 
 
 const useMessageLoop = () => {
 const useMessageLoop = () => {
   const { fetch: getUnReadMessage, result: unReadMessage } = uesFetch('getUnReadMessage')
   const { fetch: getUnReadMessage, result: unReadMessage } = uesFetch('getUnReadMessage')
-  useIntervalFn(getUnReadMessage, 300 * 1000)
+  useIntervalFn(getUnReadMessage, 10 * 1000)
 
 
   return unReadMessage
   return unReadMessage
 }
 }

+ 1 - 1
src/layout/main/MainHeader.vue

@@ -25,7 +25,7 @@ import { ElIcon } from 'element-plus'
 import { Fold, Expand, Close } from '@element-plus/icons-vue'
 import { Fold, Expand, Close } from '@element-plus/icons-vue'
 import { logout } from '@/utils/shared'
 import { logout } from '@/utils/shared'
 import useMainLayoutStore from '@/store/layout'
 import useMainLayoutStore from '@/store/layout'
-import Message from '@/components/shared/Message.vue'
+import Message from '@/components/shared/message/Message.vue'
 import UserInfo from '@/components/shared/UserInfo.vue'
 import UserInfo from '@/components/shared/UserInfo.vue'
 
 
 const mainLayoutStore = useMainLayoutStore()
 const mainLayoutStore = useMainLayoutStore()

+ 1 - 1
src/modules/analysis/group-monitoring-detail/index.vue

@@ -68,7 +68,7 @@ import useTable from '@/hooks/useTable'
 import useTableCheck from '@/hooks/useTableCheck'
 import useTableCheck from '@/hooks/useTableCheck'
 import VueEcharts from 'vue-echarts'
 import VueEcharts from 'vue-echarts'
 import BaseTable from '@/components/element/BaseTable.vue'
 import BaseTable from '@/components/element/BaseTable.vue'
-import Message from '@/components/shared/Message.vue'
+import Message from '@/components/shared/message/Message.vue'
 import UserInfo from '@/components/shared/UserInfo.vue'
 import UserInfo from '@/components/shared/UserInfo.vue'
 import ScoringPanelWithConfirmVue from '@/components/shared/ScoringPanelWithConfirm.vue'
 import ScoringPanelWithConfirmVue from '@/components/shared/ScoringPanelWithConfirm.vue'
 import ImagePreview from '@/components/shared/ImagePreview.vue'
 import ImagePreview from '@/components/shared/ImagePreview.vue'