刘洋 2 лет назад
Родитель
Сommit
659da7f1b5

+ 2 - 2
server.config.ts

@@ -3,11 +3,11 @@ import type { ServerOptions } from 'vite'
 const server: ServerOptions = {
   proxy: {
     '^/?(api|file)/': {
-      // target: 'http://192.168.10.41:8200',
+      target: 'http://192.168.10.41:8200',
       // target: 'http://192.168.10.178:8200',
       // target: 'http://192.168.10.107:8200',
       // target: 'http://cet-test.markingtool.cn',
-      target: 'http://192.168.10.136:80',
+      // target: 'http://192.168.10.136:80',
       // target: 'http://cet-dev.markingtool.cn:8200',
     },
   },

+ 2 - 0
src/api/api-types/exam.d.ts

@@ -44,6 +44,8 @@ export namespace Exam {
     /** 用户姓名收集 */
     userNameCollect: boolean
     enable: boolean
+    rejectReason: boolean
+    scoreEffective: string
   }
 
   /** 新增/修改考试 */

+ 1 - 1
src/api/api-types/user.d.ts

@@ -130,7 +130,7 @@ export namespace User {
   /** 修改用户姓名 */
   type UpdateUserName = BaseDefine<{ name: string }>
   /** 重置指定用户密码 */
-  type ResetUsersPwd = BaseDefine<{ userIds: number[] }>
+  type ResetUsersPwd = BaseDefine<{ userIds: number[]; password: string }>
   /** 启用/禁用用户 */
   type ToggleEnableUsers = BaseDefine<{ ids: number[]; enable: boolean }>
 

+ 9 - 1
src/components/common/ConfirmButton.vue

@@ -7,7 +7,14 @@
       'justify-end': props.right,
     }"
   >
-    <el-button v-bind="props.okProps" type="primary" :loading="props.loading" :size="size" @click="emit('confirm')">
+    <el-button
+      :disabled="!!props.disabled"
+      v-bind="props.okProps"
+      type="primary"
+      :loading="props.loading"
+      :size="size"
+      @click="emit('confirm')"
+    >
       {{ props.okText }}
     </el-button>
     <el-button v-bind="props.cancelProps" plain :size="size" @click="emit('cancel')">{{ props.cancelText }}</el-button>
@@ -31,6 +38,7 @@ const props = withDefaults(
     okProps?: Partial<ButtonProps>
     cancelProps?: Partial<ButtonProps>
     right?: boolean
+    disabled?: boolean
   }>(),
   {
     okText: '确认',

+ 86 - 15
src/components/shared/message/MessageList.vue

@@ -29,26 +29,49 @@
       <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="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">
-          <pre
+        <div ref="historyMsgWrap" class="flex-1 overflow-hidden p-base scroll-auto">
+          <!-- <pre
             class="full-h radius-base p-extra-base scroll-y-auto message-info-content"
             @click="onContentClick"
             v-html="currentMessage?.content"
-          ></pre>
+          ></pre> -->
+          <div v-for="(message, index) in history" :key="index" class="m-b-base history-box">
+            <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="history-message-info-content" @click="onContentClick" v-html="message.content"></div>
+          </div>
+        </div>
+        <div class="p-base" style="padding-top: 0">
+          <div class="p-base radius-base overflow-hidden msg-content-box">
+            <div style="height: calc(100% - 18px)" class="scroll-y-auto">
+              <content-edit-able
+                v-model="messageContent"
+                class="full content-edit-able"
+                @click="onContentClick"
+              ></content-edit-able>
+            </div>
+
+            <div class="limit-tip">{{ messageContent.length }} / 2000</div>
+          </div>
         </div>
         <div class="p-base flex items-center justify-end">
-          <el-button size="small" type="primary" :disabled="!currentMessage" @click="onReply">回复</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> -->
+          <!-- <el-button size="small" plain :disabled="!currentMessage" @click="toggleHistory">历史消息</el-button> -->
+          <el-button size="small" type="primary" :disabled="!allowSend" :loading="sendLoading" @click="onSendMessage"
+            >发送</el-button
+          >
         </div>
       </div>
-      <message-history v-if="showHistory" :send-user-id="currentMessage?.sendUserId"></message-history>
+      <!-- <message-history v-if="showHistory" :send-user-id="currentMessage?.sendUserId"></message-history> -->
     </div>
   </div>
   <image-preview
@@ -61,8 +84,8 @@
 
 <script setup lang="ts" name="MessageList">
 /** 消息列表*/
-import { ref, watch, onUnmounted } from 'vue'
-import { ElButton, ElIcon } from 'element-plus'
+import { ref, watch, onUnmounted, computed, nextTick } from 'vue'
+import { ElButton, ElIcon, ElMessage } from 'element-plus'
 import { Close } from '@element-plus/icons-vue'
 import useFetch from '@/hooks/useFetch'
 import MessageHistory from '@/components/shared/message/MessageHistory.vue'
@@ -71,13 +94,16 @@ import useMainStore from '@/store/main'
 import { setLastMsgs } from '@/hooks/useMessageLoop'
 import bus from '@/utils/bus'
 import type { ExtractApiResponse } from '@/api/api'
+import ContentEditAble from '@/components/common/ContentEditAble.vue'
 type MessageType = ExtractArrayValue<ExtractApiResponse<'getMessageList'>>
+const historyMsgWrap = ref()
 const mainStore = useMainStore()
 const emits = defineEmits(['close', 'change-type', 'reply'])
-
+const { fetch: getMessageHistory, result: history, loading } = useFetch('getMessageHistory')
 /** 图片预览 */
 const previewModalVisible = ref<boolean>(false)
-
+/** 消息内容 */
+const messageContent = ref<string>('')
 /** 图片路径 */
 const paperPath = ref<string>('')
 
@@ -86,19 +112,48 @@ const showHistory = ref<boolean>(false)
 const { fetch: getMessageList, result: messageList } = useFetch('getMessageList')
 
 const currentMessage = ref<MessageType>()
-
+watch(history, () => {
+  nextTick(() => {
+    console.log(historyMsgWrap.value.scrollHeight)
+  })
+})
 watch(currentMessage, () => {
   if (currentMessage.value) {
     useFetch('handleReadMessage')
       .fetch({ id: currentMessage.value.id })
       .then(() => {
         if (currentMessage.value?.unReadCount) {
-          currentMessage.value.unReadCount--
+          // currentMessage.value.unReadCount--
+          currentMessage.value.unReadCount = 0
+        }
+        if (currentMessage.value?.sendUserId) {
+          getMessageHistory({ sendUserId: currentMessage.value.sendUserId })
         }
       })
   }
 })
+const allowSend = computed<boolean>(() => {
+  return !!(messageContent.value && currentMessage.value?.sendUserId)
+})
+const { fetch: sendMessage, loading: sendLoading } = useFetch('sendMessage')
+/** 发送消息 */
+const onSendMessage = async () => {
+  try {
+    if (messageContent.value.length > 2000) {
+      return ElMessage.error('输入内容过长')
+    }
 
+    await sendMessage({
+      content: messageContent.value,
+      receiveUserIds: [currentMessage.value?.sendUserId as number],
+    })
+    ElMessage.success('发送成功')
+    messageContent.value = ''
+    // emit('close')
+  } catch (error) {
+    console.error(error)
+  }
+}
 // const checkMessage = (message: MessageType) => {
 //   currentMessage.value = message
 // }
@@ -111,8 +166,6 @@ const toggleHistory = () => {
 }
 
 const onReply = () => {
-  console.log('ssss', currentMessage.value)
-
   if (currentMessage.value) {
     emits('change-type', 'send')
     emits('reply', currentMessage.value.sendUserId, currentMessage.value.sendUserName)
@@ -165,6 +218,24 @@ onUnmounted(() => {
     // width: 600px;
     width: calc(100% - 260px);
     position: relative;
+    .history-box {
+      .message-header {
+        color: $color--primary;
+      }
+      .history-message-info-content {
+        white-space: pre-wrap;
+      }
+    }
+    .msg-content-box {
+      height: 100px;
+      font-size: 12px;
+      border: 1px solid #0091ff;
+      .limit-tip {
+        text-align: right;
+        height: 18px;
+        padding-top: 5px;
+      }
+    }
     .message-info {
       height: 446px;
       background-color: $color--white;

+ 41 - 23
src/components/shared/message/MessageSend.vue

@@ -85,7 +85,9 @@ import type { ExtractApiResponse } from '@/api/api'
 
 type MarkerItem = ExtractArrayValue<ExtractApiResponse<'getUserGroup'>['chiffGroup']>
 type TreeNode = ExtractArrayValue<ExtractApiResponse<'getUserGroup'>['markerGroup']> & { label: string }
-
+function createNum(m: number, n: number) {
+  return Math.floor(Math.random() * (m - n)) + n
+}
 const props = defineProps<{
   replyUserId?: number | null
   paperPath?: string | null
@@ -139,10 +141,12 @@ const allowSend = computed<boolean>(() => {
 const treeRef = ref<InstanceType<typeof ElTree>>()
 
 const viewCheckedUser = computed(() => {
-  let arr = checkedUsers?.value?.filter((v) => !!v.id)?.map((d) => `${d.loginName}-${d.name}`)
-  if (!!replyUserName.value && !arr.includes(replyUserName.value)) {
-    arr.unshift(replyUserName.value)
-  }
+  // let arr = checkedUsers?.value?.filter((v) => !!v.id)?.map((d) => `${d.loginName}-${d.name}`)
+  // if (!!replyUserName.value && !arr.includes(replyUserName.value)) {
+  //   arr.unshift(replyUserName.value)
+  // }
+  let arr = checkedUsers?.value?.filter((v) => !!v.id)?.map((d) => d.name)
+
   return arr.join(';')
 
   // return checkedUsers?.value
@@ -160,14 +164,15 @@ function isMarker(x: any): x is MarkerItem {
 }
 
 const treeProp = {
-  children: 'markers',
-  label(treeNode: TreeNode) {
-    if (isMarker(treeNode)) {
-      // return treeNode.name || treeNode.loginName
-      return `${treeNode.loginName}-${treeNode.name}`
-    }
-    return treeNode.label
-  },
+  children: 'subNodes',
+  // label(treeNode: TreeNode) {
+  //   if (isMarker(treeNode)) {
+  //     // return treeNode.name || treeNode.loginName
+  //     return `${treeNode.loginName}-${treeNode.name}`
+  //   }
+  //   return treeNode.label
+  // },
+  label: 'name',
   class: customNodeClass,
 } as unknown as InstanceType<typeof ElTree>['props']
 
@@ -181,19 +186,32 @@ watch(filterText, () => {
 })
 
 const { fetch: getUserGroup, result: userGroup } = useFetch('getUserGroup')
-
+function treeNodeAddId(data: any) {
+  data.forEach((item: any) => {
+    if (!item.data) {
+      item.id = createNum(1, 50000) * -1
+    } else {
+      item.id = item.data.id
+    }
+    if (item.subNodes) {
+      treeNodeAddId(item.subNodes)
+    }
+  })
+  return data
+}
 const markerTree = computed<TreeNode[]>(() => {
   if (!userGroup?.value) {
     return []
   }
-  const { chiffGroup, deputyGroup, markerGroup } = userGroup.value
-  return [
-    chiffGroup?.length ? { label: '大组长', markingGroupNumber: -1, markers: chiffGroup } : null,
-    deputyGroup?.length ? { label: '小组长', markingGroupNumber: -1, markers: deputyGroup } : null,
-    markerGroup?.length
-      ? { label: '评卷小组', markers: markerGroup.map((d) => ({ ...d, label: `第${d.markingGroupNumber}组` })) }
-      : null,
-  ].filter(Boolean) as TreeNode[]
+  return treeNodeAddId(userGroup.value || [])
+  // const { chiffGroup, deputyGroup, markerGroup } = userGroup.value
+  // return [
+  //   chiffGroup?.length ? { label: '大组长', markingGroupNumber: -1, markers: chiffGroup } : null,
+  //   deputyGroup?.length ? { label: '小组长', markingGroupNumber: -1, markers: deputyGroup } : null,
+  //   markerGroup?.length
+  //     ? { label: '评卷小组', markers: markerGroup.map((d) => ({ ...d, label: `第${d.markingGroupNumber}组` })) }
+  //     : null,
+  // ].filter(Boolean) as TreeNode[]
 })
 
 watch(
@@ -373,7 +391,7 @@ const toggleHistory = () => {
         .limit-tip {
           text-align: right;
           height: 18px;
-          padding-top: 3px;
+          padding-top: 5px;
         }
       }
     }

+ 6 - 0
src/constants/dicts.ts

@@ -20,6 +20,12 @@ export const TrueOrFalse = [
   { label: '否', value: false },
 ]
 
+/** 分析计算取值  */
+export const scoreEffectiveOption = [
+  { label: '按最终成绩计算', value: 'FINAL_MARK' },
+  { label: '按一评给分计算', value: 'FIRST_MARK' },
+]
+
 /** 试卷类型 */
 
 export const PaperMap: Record<PaperType, string> = {

+ 7 - 1
src/modules/admin-exam/edit-exam/index.vue

@@ -20,7 +20,7 @@ import ConfirmButton from '@/components/common/ConfirmButton.vue'
 import useForm from '@/hooks/useForm'
 import useFetch from '@/hooks/useFetch'
 import useVW from '@/hooks/useVW'
-import { TrueOrFalse, StatusMap } from '@/constants/dicts'
+import { TrueOrFalse, StatusMap, scoreEffectiveOption } from '@/constants/dicts'
 
 import type { EpFormItem, EpFormRules } from 'global-type'
 import type { ExtractApiParams } from '@/api/api'
@@ -42,6 +42,8 @@ const initModel: ExtractApiParams<'saveExamInfo'> = {
   spotCheckReject: true,
   userNameCollect: true,
   enable: true,
+  rejectReason: true,
+  scoreEffective: 'FINAL_MARK',
 }
 
 const model = reactive<ExtractApiParams<'saveExamInfo'>>(initModel)
@@ -61,11 +63,13 @@ const rules: EpFormRules = {
   ],
   spotCheckReject: [{ required: true, message: '抽查卷是否允许打回' }],
   doubtReject: [{ required: true, message: '问题卷是否允许打回' }],
+  rejectReason: [{ required: true, message: '打回时是否需要原因' }],
   maxTaskRecover: [
     { required: true, message: '请设置自动任务回收的时间' },
     { type: 'number', min: 1, max: 1440, message: '任务回收时间限制1-1440分钟' },
   ],
   userNameCollect: [{ required: true, message: '是否收集用户姓名' }],
+  scoreEffective: [{ required: true, message: '分析计算取值方式' }],
   enable: [{ required: true, message: '是否启用考试' }],
 }
 
@@ -87,6 +91,7 @@ const items: EpFormItem[] = [
   }),
   span8({ label: '抽查卷允许打回', slotType: 'select', prop: 'spotCheckReject', slot: { options: TrueOrFalse } }),
   span8({ label: '问题卷允许打回', slotType: 'select', prop: 'doubtReject', slot: { options: TrueOrFalse } }),
+  span8({ label: '打回时需要原因', slotType: 'select', prop: 'rejectReason', slot: { options: TrueOrFalse } }),
   span8({
     label: '自动任务回收(分钟)',
     slotType: 'inputNumber',
@@ -94,6 +99,7 @@ const items: EpFormItem[] = [
     slot: { stepStrictly: true, step: 1 },
   }),
   span8({ label: '用户姓名收集', slotType: 'select', prop: 'userNameCollect', slot: { options: TrueOrFalse } }),
+  span8({ label: '分析计算取值', slotType: 'select', prop: 'scoreEffective', slot: { options: scoreEffectiveOption } }),
   span8({ label: '状态', slotType: 'select', prop: 'enable', slot: { options: StatusMap } }),
 ]
 

+ 1 - 1
src/modules/admin-user/bulk-add-user/index.vue

@@ -151,7 +151,7 @@ const items = computed<EpFormItem[]>(() => {
       slotType: 'select',
       prop: 'role',
       slot: {
-        options: ROLE_OPTION,
+        options: ROLE_OPTION.value,
         placeholder: '设置用户角色',
       },
     }),

+ 1 - 1
src/modules/admin-user/edit-user/index.vue

@@ -128,7 +128,7 @@ const items = computed<EpFormItem[]>(() => {
       slotType: 'select',
       prop: 'role',
       slot: {
-        options: ROLE_OPTION,
+        options: ROLE_OPTION.value,
         placeholder: '设置用户角色',
         disabled: isEdit,
       },

+ 46 - 13
src/modules/admin-user/manage/index.vue

@@ -34,7 +34,7 @@
         <div class="flex items-center m-b-base">
           <el-button size="small" type="primary" @click="onAddUser">新增</el-button>
           <el-button size="small" type="primary" @click="onBulkAddUser">批量新增</el-button>
-          <el-popconfirm
+          <!-- <el-popconfirm
             v-if="hasSelected"
             :width="'220px'"
             hide-icon
@@ -44,8 +44,8 @@
             <template #reference>
               <el-button size="small" type="primary">重置密码</el-button>
             </template>
-          </el-popconfirm>
-          <el-button v-else size="small" type="primary" @click="checkSelected('reset')">重置密码</el-button>
+          </el-popconfirm> -->
+          <el-button size="small" type="primary" @click="checkSelected('reset', 'mult')">重置密码</el-button>
           <el-popconfirm
             v-if="hasSelected"
             :width="'220px'"
@@ -72,11 +72,13 @@
         >
           <template #column-operation="{ row }">
             <el-button type="primary" link @click="onEditUser(row)">修改</el-button>
-            <el-popconfirm :width="'220px'" hide-icon title="确认重置用户密码?" @confirm="onSingleResetUserPwd(row)">
+            <el-button type="primary" link @click="onSingleResetUserPwd(row)">重置密码</el-button>
+
+            <!-- <el-popconfirm :width="'220px'" hide-icon title="确认重置用户密码?" @confirm="onSingleResetUserPwd(row)">
               <template #reference>
                 <el-button type="primary" link>重置密码</el-button>
               </template>
-            </el-popconfirm>
+            </el-popconfirm> -->
             <el-popconfirm
               :width="'220px'"
               hide-icon
@@ -99,12 +101,30 @@
         <confirm-button @confirm="handleResetPwd" @cancel="() => {}"></confirm-button>
       </template>
     </base-dialog>
+    <base-dialog
+      v-model="showResetPwdDialog"
+      :width="300"
+      title="重置密码"
+      class="reset-pwd"
+      destroy-on-close
+      @close="newPwd = ''"
+    >
+      <el-input v-model="newPwd" type="password" placeholder="请输入新的密码" clearable></el-input>
+      <template #footer>
+        <confirm-button
+          :disabled="!newPwd"
+          @confirm="handleResetPwd"
+          @cancel="showResetPwdDialog = false"
+        ></confirm-button>
+      </template>
+    </base-dialog>
   </div>
 </template>
 
 <script setup lang="ts" name="UserManage">
 /** 用户管理 */
-import { ElButton, ElPopconfirm, ElCard, ElIcon, ElPagination, ElMessage } from 'element-plus'
+import { ref } from 'vue'
+import { ElButton, ElPopconfirm, ElCard, ElIcon, ElPagination, ElMessage, ElInput } from 'element-plus'
 import { ArrowRight } from '@element-plus/icons-vue'
 import { useRouter } from 'vue-router'
 import BaseDialog from '@/components/element/BaseDialog.vue'
@@ -117,7 +137,7 @@ import useUserManageTable from './hooks/useUserManageTable'
 import useVW from '@/hooks/useVW'
 
 import type { ExtractMultipleApiResponse } from '@/api/api'
-
+const newPwd = ref('')
 const { push } = useRouter()
 
 const { formRef, model, items: formItems, rows, expand, toggleExpand } = useUserManageFilter()
@@ -152,13 +172,18 @@ function onAddUser() {
 function onBulkAddUser() {
   push({ name: 'BulkAddUser' })
 }
+const showResetPwdDialog = ref(false)
+const resetPwdType = ref<any>('')
 
-function checkSelected(type: 'reset' | 'disabled') {
+function checkSelected(type: 'reset' | 'disabled', resetType?: string) {
   if (!selectedList.length) {
     return ElMessage.warning(`请选择需要${type === 'reset' ? '重置密码' : '禁用'}的用户`)
   }
+  if (type === 'reset') {
+    showResetPwdDialog.value = true
+    resetPwdType.value = resetType
+  }
 }
-
 /** 重置密码 */
 async function handleResetPwd() {
   if (!selectedList.length) {
@@ -168,10 +193,17 @@ async function handleResetPwd() {
 
   /** submit */
   try {
-    await resetPwdFetch({ userIds: selectedList.map((d) => d.id) })
+    await resetPwdFetch({ userIds: selectedList.map((d) => d.id), password: newPwd.value })
     ElMessage.success(`修改成功`)
+    if (resetPwdType.value === 'single') {
+      onSectionChange(selectionSelectedList)
+    }
+    showResetPwdDialog.value = false
   } catch (error) {
     console.error(error)
+    if (resetPwdType.value === 'single') {
+      onSectionChange(selectionSelectedList)
+    }
   }
 }
 
@@ -206,9 +238,10 @@ let selectionSelectedList = selectedList.slice(0)
 function onSingleResetUserPwd(row: ExtractMultipleApiResponse<'getUserList'>) {
   selectionSelectedList = selectedList.slice(0)
   onSectionChange([row])
-  handleResetPwd().finally(() => {
-    onSectionChange(selectionSelectedList)
-  })
+  // handleResetPwd().finally(() => {
+  //   onSectionChange(selectionSelectedList)
+  // })
+  checkSelected('reset', 'single')
 }
 
 /** table column 禁用 */

+ 1 - 1
src/modules/expert/expert/index.vue

@@ -234,7 +234,7 @@ const formItems = computed<EpFormItem[]>(() => {
       label: '角色',
       prop: 'role',
       slotType: 'select',
-      slot: { options: ROLE_OPTION },
+      slot: { options: ROLE_OPTION.value },
     }),
     { rowKey: 'row-1', slotName: 'search', labelWidth: '10px', colProp: { span: 4 } },
     span10({