Procházet zdrojové kódy

feat: 消息列表逻辑完善

chenhao před 2 roky
rodič
revize
282d0b22ab

+ 69 - 0
src/components/common/ContentEditAble.vue

@@ -0,0 +1,69 @@
+<template>
+  <component
+    :is="tagName"
+    ref="contenteditableEle"
+    :contenteditable="contenteditable"
+    @input="inputChange"
+    @blur="inputChange"
+  >
+  </component>
+</template>
+
+<script setup lang="ts" name="ContentEditAble">
+import { defineProps, ref, onMounted, withDefaults, watch } from 'vue'
+import useVModel from '@/hooks/useVModel'
+
+const props = withDefaults(
+  defineProps<{
+    modelValue?: string
+    tagName?: string
+    contenteditable?: boolean | string
+    autofocus?: boolean
+  }>(),
+  {
+    modelValue: '',
+    tagName: 'div',
+    contenteditable: true,
+    autofocus: true,
+  }
+)
+
+const contentValue = useVModel(props, 'modelValue')
+
+const contenteditableEle = ref<HTMLElement | null>()
+
+function updateContent(content: string) {
+  if (contenteditableEle.value) {
+    contenteditableEle.value.innerHTML = content
+  }
+}
+
+function focus() {
+  if (contenteditableEle.value) {
+    contenteditableEle.value?.focus()
+  }
+}
+
+function inputChange(event: Event) {
+  const target = event.target as HTMLElement
+  contentValue.value = target.innerHTML
+}
+
+watch(
+  () => props.modelValue,
+  () => {
+    if (props.modelValue !== contenteditableEle.value?.innerHTML) {
+      updateContent(props.modelValue ?? '')
+    }
+  }
+)
+
+onMounted(() => {
+  updateContent(contentValue.value ?? '')
+  setTimeout(() => {
+    focus()
+  })
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 9 - 0
src/components/shared/CurrentServerTime.vue

@@ -0,0 +1,9 @@
+<template>
+  <div class=""></div>
+</template>
+
+<script setup lang="ts" name="">
+import { reactive, ref } from 'vue'
+</script>
+
+<style scoped lang="scss"></style>

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

@@ -99,7 +99,7 @@ watch(
     /** reset scores */
     scoreValues.value = []
     /** auto show */
-    modalVisible.value = true
+    modalVisible.value = !!props.mainNumber
     if (props.mainNumber) {
       resetQuestionStruct()
       getQuestionStruct({ mainNumber: props.mainNumber })

+ 1 - 7
src/components/shared/ScoringPanelWithConfirm.vue

@@ -18,13 +18,7 @@
     <div class="text-center m-t-base confirm-text">确认提交?</div>
     <template #footer>
       <div class="flex items-center justify-between">
-        <el-button
-          ref="confirmButtonRef"
-          class="confirm-button"
-          autofocus="autofocus"
-          type="primary"
-          @click="onConfirmSubmit"
-        >
+        <el-button ref="confirmButtonRef" class="confirm-button" type="primary" @click="onConfirmSubmit">
           是(Y)
         </el-button>
         <el-button class="confirm-button" plain @click="onCancelSubmit">否(N)</el-button>

+ 11 - 16
src/components/shared/message/Message.vue

@@ -18,23 +18,17 @@
           <span>条消息</span>
         </div>
         <div class="message-list">
-          <div class="flex message-row">
+          <div
+            v-for="message in unReadMessages?.messages"
+            :key="message.sendUserId"
+            class="flex message-row"
+            @click="onReceiveMessage"
+          >
             <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 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 class="user-name">{{ message.sendUserName }}</div>
+              <div class="message-time">{{ dayjs(message.sendTime).format('HH:mm') }}</div>
             </div>
+            <pre class="flex-1 message-content" v-html="message.content"></pre>
           </div>
         </div>
         <confirm-button
@@ -47,7 +41,7 @@
         ></confirm-button>
       </div>
     </el-popover>
-    <message-window v-model="visibleMessageWindow" :type="messageWindowType"></message-window>
+    <message-window v-model="visibleMessageWindow" v-model:type="messageWindowType"></message-window>
   </message-component>
 </template>
 
@@ -55,6 +49,7 @@
 /** 头部消息组件 */
 import { defineComponent, ref, useSlots } from 'vue'
 import { ElPopover } from 'element-plus'
+import dayjs from 'dayjs'
 import useVW from '@/hooks/useVW'
 import useMessageLoop from '@/hooks/useMessageLoop'
 import useMainStore from '@/store/main'

+ 12 - 7
src/components/shared/message/MessageHistory.vue

@@ -5,11 +5,11 @@
     </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>
+        <div class="m-b-mini message-header">
+          <span class="m-r-base user-name">{{ message.sendUserName }}</span>
           <span class="message-time">{{ message.sendTime }}</span>
         </div>
-        <div class="message-info-content" v-html="message.content"></div>
+        <pre class="message-info-content" v-html="message.content"></pre>
       </div>
     </template>
   </div>
@@ -21,15 +21,15 @@ import { watch } from 'vue'
 import useFetch from '@/hooks/useFetch'
 import Empty from '@/components/common/Empty.vue'
 
-const props = defineProps<{ userId?: number }>()
+const props = defineProps<{ sendUserId?: number }>()
 
 const { fetch: getMessageHistory, result: history } = useFetch('getMessageHistory')
 
 watch(
-  () => props.userId,
+  () => props.sendUserId,
   () => {
-    if (props.userId) {
-      getMessageHistory({ sendUserId: props.userId })
+    if (props.sendUserId) {
+      getMessageHistory({ sendUserId: props.sendUserId })
     }
   },
   { immediate: true }
@@ -42,5 +42,10 @@ watch(
   border-top: $OnePixelLine;
   border-radius: 0 0 6px 6px;
   background-color: $color--white;
+  .message-info {
+    .message-header {
+      color: $color--primary;
+    }
+  }
 }
 </style>

+ 68 - 12
src/components/shared/message/MessageList.vue

@@ -3,10 +3,17 @@
     <div class="message-list p-base scroll-y-auto">
       <div
         v-for="message in messageList"
-        :key="message.sendUserId"
-        class="message-item"
+        :key="message.id"
+        class="radius-base fill-blank p-base message-item"
+        :class="{ active: currentMessage?.sendUserId === message.sendUserId }"
         @click="checkMessage(message)"
-      ></div>
+      >
+        <div class="flex items-center m-b-base message-title">
+          <div class="message-send-user">{{ message.sendUserName }}</div>
+          <div class="m-l-auto message-send-time">{{ dayjs(message.sendTime).format('HH:mm') }}</div>
+        </div>
+        <pre class="message-content" v-html="message.content"></pre>
+      </div>
     </div>
     <div class="radius-base message-info-container">
       <div class="flex direction-column message-info">
@@ -20,24 +27,26 @@
           </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>
+          <pre
+            class="full-h radius-base p-extra-base scroll-y-auto message-info-content"
+            v-html="currentMessage?.content"
+          ></pre>
         </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>
+          <el-button size="small" plain :disabled="!currentMessage" @click="toggleHistory">历史消息</el-button>
+          <el-button size="small" type="primary" :disabled="!currentMessage" @click="onReply">回复</el-button>
         </div>
       </div>
-      <message-history v-if="showHistory" :id="currentMessage?.sendUserId"></message-history>
+      <message-history v-if="showHistory" :send-user-id="currentMessage?.sendUserId"></message-history>
     </div>
   </div>
 </template>
 
 <script setup lang="ts" name="MessageList">
 /** 消息列表*/
-import { reactive, ref } from 'vue'
+import { ref, watch } from 'vue'
 import { ElButton, ElIcon } from 'element-plus'
+import dayjs from 'dayjs'
 import { Close } from '@element-plus/icons-vue'
 import useFetch from '@/hooks/useFetch'
 import MessageHistory from '@/components/shared/message/MessageHistory.vue'
@@ -46,7 +55,7 @@ import type { ExtractApiResponse } from 'api-type'
 
 type MessageType = ExtractArrayValue<ExtractApiResponse<'getMessageList'>>
 
-defineEmits(['close', 'change-type'])
+const emits = defineEmits(['close', 'change-type', 'reply'])
 
 const showHistory = ref<boolean>(false)
 
@@ -54,6 +63,12 @@ const { fetch: getMessageList, result: messageList } = useFetch('getMessageList'
 
 const currentMessage = ref<MessageType>()
 
+watch(currentMessage, () => {
+  if (currentMessage.value) {
+    useFetch('handleReadMessage').fetch({ id: currentMessage.value.id })
+  }
+})
+
 const checkMessage = (message: MessageType) => {
   currentMessage.value = message
 }
@@ -62,7 +77,14 @@ const toggleHistory = () => {
   showHistory.value = !showHistory.value
 }
 
-getMessageList()
+const onReply = () => {
+  if (currentMessage.value) {
+    emits('change-type', 'send')
+    emits('reply', currentMessage.value.sendUserId)
+  }
+}
+
+getMessageList().then((result) => (currentMessage.value = result?.[0]))
 </script>
 
 <style scoped lang="scss">
@@ -74,6 +96,40 @@ getMessageList()
     height: 446px;
     background: #fafafa;
     box-shadow: 0px 6px 6px 0px rgba(0, 0, 0, 0.1);
+    .message-item {
+      &.active {
+        background-color: $color--primary;
+        color: $color--white;
+      }
+      &:not(.active) {
+        .message-title {
+          color: $NormalColor;
+          .message-send-time {
+            color: $RegularFontColor;
+          }
+        }
+        .message-content {
+          color: $RegularFontColor;
+        }
+      }
+      .message-title {
+        font-size: $BaseFont;
+        .message-send-time {
+          font-size: $SmallFont;
+          font-weight: 400;
+        }
+      }
+      .message-content {
+        font-weight: 400;
+        word-break: break-all;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        display: -webkit-box;
+        -webkit-line-clamp: 2;
+        -webkit-box-orient: vertical;
+        font-size: $SmallFont;
+      }
+    }
   }
   .message-info-container {
     width: 600px;

+ 73 - 20
src/components/shared/message/MessageSend.vue

@@ -6,6 +6,7 @@
         <el-tree
           ref="treeRef"
           show-checkbox
+          node-key="id"
           :filter-node-method="filterTree"
           :data="markerTree"
           :props="treeProp"
@@ -18,7 +19,7 @@
         <div class="flex items-center p-base message-info-header">
           <div class="flex flex-1 items-center overflow-hidden send-user">
             <span class="m-r-mini">收件人</span>
-            <span class="flex flex-1 overflow-hidden items-center justify-between radius-base user-name">
+            <span class="flex flex-1 overflow-hidden items-center justify-between radius-base m-r-base user-name">
               <span class="flex-1 checked-users"> {{ viewCheckedUser }}</span>
               <el-button class="m-l-base" size="small" type="primary" @click="toggleCheckUser"> 选择收件人 </el-button>
             </span>
@@ -29,46 +30,60 @@
         </div>
         <div class="flex-1 p-base overflow-hidden">
           <div class="full-h radius-base scroll-y-auto message-info-content">
-            <el-input
-              v-model="messageContent"
-              type="textarea"
-              class="full-h"
-              resize="none"
-              :maxlength="1000"
-              show-word-limit
-            ></el-input>
+            <content-edit-able v-model="messageContent" class="full-h"></content-edit-able>
           </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>
+          <el-button size="small" type="primary" :disabled="!allowSend" :loading="loading" @click="onSendMessage">
+            发送
+          </el-button>
         </div>
       </div>
     </div>
   </div>
+  <image-preview v-model="previewModalVisible" :url="previewPath"></image-preview>
 </template>
 
-<script setup lang="ts" name="MessageSend">
-import { reactive, ref, computed, watch } from 'vue'
-import { ElInput, ElButton, ElTree, ElIcon } from 'element-plus'
+<script setup lang="tsx" name="MessageSend">
+import { ref, computed, watch, nextTick, defineComponent, createApp } from 'vue'
+import { ElInput, ElButton, ElTree, ElIcon, ElMessage } from 'element-plus'
 import { Close } from '@element-plus/icons-vue'
+import ContentEditAble from '@/components/common/ContentEditAble.vue'
+import ImagePreview from '../ImagePreview.vue'
 import useFetch from '@/hooks/useFetch'
+import useVModel from '@/hooks/useVModel'
 
 import type { ExtractApiResponse } from 'api-type'
 
 type MarkerItem = ExtractArrayValue<ExtractApiResponse<'getUserGroup'>['chiffGroup']>
 type TreeNode = ExtractArrayValue<ExtractApiResponse<'getUserGroup'>['markerGroup']> & { label: string }
 
-defineEmits(['close'])
+const props = defineProps<{
+  replyUserId: number
+}>()
 
-const showCheckUser = ref<boolean>(false)
+const emit = defineEmits(['close'])
 
-const messageContent = ref<string>()
+const replyUserId = useVModel(props, 'replyUserId')
 
-const checkedUsers = ref<MarkerItem[]>()
+/** 图片预览 */
+const previewModalVisible = ref<boolean>(false)
+
+const previewPath = ref<string>()
+
+const showCheckUser = ref<boolean>(true)
+
+const messageContent = ref<string>('')
+
+const checkedUsers = ref<MarkerItem[]>([])
 
 const filterText = ref<string>('')
 
+const allowSend = computed<boolean>(() => {
+  return !!(messageContent.value && checkedUsers.value.length)
+})
+
 const treeRef = ref<InstanceType<typeof ElTree>>()
 
 const viewCheckedUser = computed(() => {
@@ -116,17 +131,55 @@ const markerTree = computed<TreeNode[]>(() => {
   ] as TreeNode[]
 })
 
+watch(
+  [replyUserId, markerTree],
+  () => {
+    nextTick(() => {
+      replyUserId.value && markerTree.value.length && treeRef?.value?.setCheckedKeys([replyUserId.value])
+    })
+  },
+  { immediate: true }
+)
+
 const onCheckChange = () => {
   checkedUsers.value = (treeRef?.value?.getCheckedNodes(true) as MarkerItem[]) || []
 }
 
+const onPreviewImage = (path: string) => {
+  previewPath.value = path
+  previewModalVisible.value = true
+}
+
 /** 发送当前试卷 */
 const sendCurrentPaper = () => {
-  console.log('发送当前试卷')
+  const ImgLink = defineComponent({
+    render() {
+      return (
+        <div class="inline" contenteditable={false}>
+          <span class="pointer" type="primary" link onClick={() => onPreviewImage('xxxxx')}>
+            当前试卷
+          </span>
+        </div>
+      )
+    },
+  })
+  messageContent.value += createApp(ImgLink).mount(document.createElement('div')).$el.outerHTML
 }
+
+const { fetch: sendMessage, loading } = useFetch('sendMessage')
+
 /** 发送消息 */
-const onSendMessage = () => {
-  console.log('发消息')
+const onSendMessage = async () => {
+  try {
+    await sendMessage({
+      content: messageContent.value,
+      receiveUserIds: checkedUsers.value.map((u) => u.id),
+    })
+    ElMessage.success('发送成功')
+    emit('close')
+  } catch (error) {
+    console.error(error)
+  }
 }
 
 getUserGroup()

+ 18 - 3
src/components/shared/message/MessageWindow.vue

@@ -1,12 +1,18 @@
 <template>
-  <base-dialog v-model="visible" unless :footer="false" class="message-dialog">
-    <component :is="MessageWindowContent" @close="onClose" @change-type="onChangeType"></component>
+  <base-dialog v-model="visible" unless :footer="false" class="message-dialog" destroy-on-close>
+    <component
+      :is="MessageWindowContent"
+      :reply-user-id="replyUserId"
+      @close="onClose"
+      @change-type="onChangeType"
+      @reply="onReply"
+    ></component>
   </base-dialog>
 </template>
 
 <script setup lang="ts" name="MessageWindow">
 /** 发送/回复消息 */
-import { reactive, ref, computed, withDefaults } from 'vue'
+import { 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'
@@ -23,19 +29,28 @@ const props = withDefaults(
 )
 
 const visible = useVModel(props, 'modelValue')
+
 const modalType = useVModel(props, 'type')
 
+const replyUserId = ref<number>()
+
 const MessageWindowContent = computed(() => {
   return modalType.value === 'send' ? MessageSend : MessageHistory
 })
 
 const onClose = () => {
   visible.value = false
+  replyUserId.value = void 0
 }
 
 const onChangeType = (type: ModalType) => {
   modalType.value = type
 }
+
+/** 回复 */
+const onReply = (userId: number) => {
+  replyUserId.value = userId
+}
 </script>
 
 <style lang="scss">

+ 1 - 0
src/hooks/useMessageLoop.ts

@@ -14,6 +14,7 @@ const useMessageLoop = () => {
     () => mainStore?.myUserInfo?.role,
     () => {
       if (mainStore?.myUserInfo?.role && mainStore?.myUserInfo?.role !== 'ADMIN') {
+        getUnReadMessage()
         useIntervalFn(getUnReadMessage, 10 * 1000)
       }
     },

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

@@ -41,7 +41,7 @@ const columns: EpTableColumn<ExtractArrayValue<ExtractApiResponse<'getGroupMonit
   { label: '已浏览问题卷数', prop: 'problemCount' },
   { label: '已给分问题卷数', prop: 'problemReScoreCount' },
   { label: '已浏览自定义抽查卷数', prop: 'customCheckCount' },
-  { label: '已给分自定抽查卷数', prop: 'customCheckReScoreCount' },
+  { label: '已给分自定抽查卷数', prop: 'customCheckReScoreCount' },
   {
     label: '已浏览试卷总数',
     prop: 'totalCount',

+ 12 - 8
src/modules/marking/mark/index.vue

@@ -2,9 +2,12 @@
   <div class="flex direction-column full">
     <mark-header :exclude-operations="['delete', 'bookmark']" @click="onOperationClick">
       <div class="flex items-center m-l-auto">
-        <span class="data-item">已评: {{ markStatus?.markedCount || 0 }} / {{ markStatus?.totalCount || 0 }}</span>
-        <span class="data-item">密号: {{ currentTask?.secretNumber }}</span>
-        <span class="data-item">{{ currentTask?.mainNumber }}-{{ currentTask?.mainTitle }}</span>
+        <span class="data-item">
+          已评: {{ markStatus?.personCount || 0 }} /
+          {{ minus(markStatus?.totalCount || 0, markStatus?.markedCount || 0) }}
+        </span>
+        <span v-show="currentTask" class="data-item">密号: {{ currentTask?.secretNumber }}</span>
+        <span v-show="currentTask" lass="data-item">{{ currentTask?.mainNumber }}-{{ currentTask?.mainTitle }}</span>
         <span class="data-item">停留: {{}}</span>
         <span class="data-item">北京时间: {{}}</span>
       </div>
@@ -61,10 +64,11 @@
 
 <script setup lang="ts" name="MarkingMark">
 /** 阅卷-正式评卷 */
-import { computed, reactive, ref, watch } from 'vue'
+import { computed, nextTick, ref, watch } from 'vue'
 import { useRouter } from 'vue-router'
 import { debounce } from 'lodash-es'
 import { ElButton, ElRadioGroup, ElRadioButton, ElRadio } from 'element-plus'
+import { minus } from '@/utils/common'
 import { useSetImgBg } from '@/hooks/useSetImgBg'
 import useFetch from '@/hooks/useFetch'
 import useVW from '@/hooks/useVW'
@@ -84,7 +88,6 @@ import SampleBStatus from '@/assets/images/status-sample-b.png'
 import type { SetImgBgOption } from '@/hooks/useSetImgBg'
 import type { ExtractApiResponse } from 'api-type'
 import type { MarkHeaderInstance } from 'global-type'
-
 const { push, replace } = useRouter()
 
 const { getSpentTime, resume } = useTime()
@@ -133,9 +136,12 @@ const { fetch: getMarkingTask, loading, result: taskPool } = useFetch('getMarkin
 const { fetch: submitMarkTask, loading: submitting, result: submitMarkStatus } = useFetch('submitMarkTask')
 const { fetch: getMarkStatus, result: markStatus } = useFetch('getMarkStatus')
 
-const historyTaskChange = (task: TaskInfoType) => {
+const historyTaskChange = (task: HistoryTaskType) => {
   currentTask.value = task
   currentTaskType.value = 'remarking'
+  nextTick(() => {
+    modelScore.value = task.markerScores
+  })
 }
 
 const debounceGetMarkingTask = debounce(getMarkingTask, 5000)
@@ -297,8 +303,6 @@ const { drawing, dataUrl } = useSetImgBg(imgOption)
 
 watch(currentTask, () => {
   resume()
-  modelScore.value = (currentTask?.value as HistoryTaskType)?.markerScores || []
-  scoringPanelVisible.value = !!currentTask?.value
 })
 
 getMarkStatus()

+ 0 - 1
src/plugins/request/index.ts

@@ -1,4 +1,3 @@
-import { h } from 'vue'
 import axios, { AxiosError } from 'axios'
 import { ElMessage } from 'element-plus'
 import { setupAuthSign, transformRequestData } from '@/plugins/request/plugin'

+ 1 - 0
types/api.d.ts

@@ -231,6 +231,7 @@ declare module 'api-type' {
 
   export namespace Message {
     interface BaseMessageResponse {
+      id: number
       content: string
       receiveUserId: number
       receiveUserName: string