chenhao 2 лет назад
Родитель
Сommit
0586f25aff
35 измененных файлов с 1252 добавлено и 1213 удалено
  1. 2 0
      src/api/expert.ts
  2. 9 0
      src/api/marking.ts
  3. 2 0
      src/api/question.ts
  4. 8 0
      src/api/statistics.ts
  5. 1 0
      src/assets/styles/app.scss
  6. 34 0
      src/components/shared/ImagePreview.vue
  7. 1 0
      src/components/shared/MarkHeader.vue
  8. 8 2
      src/components/shared/Message.vue
  9. 30 33
      src/components/shared/ScoringPanel.vue
  10. 1 1
      src/components/shared/ScoringPanelItem.vue
  11. 121 0
      src/components/shared/ScoringPanelWithConfirm.vue
  12. 19 7
      src/components/shared/SendBackMark.vue
  13. 22 3
      src/components/shared/UserInfo.vue
  14. 0 7
      src/hooks/useElectron.ts
  15. 93 0
      src/hooks/useTableCheck.ts
  16. 9 0
      src/layout/main/MainHeader.vue
  17. 1 1
      src/modules/analysis/monitoring/index.vue
  18. 3 1
      src/modules/example/ImageModify.vue
  19. 22 121
      src/modules/expert/assess/index.vue
  20. 92 156
      src/modules/expert/expert/index.vue
  21. 19 97
      src/modules/expert/sample/index.vue
  22. 22 68
      src/modules/expert/standard/index.vue
  23. 13 59
      src/modules/expert/training/index.vue
  24. 21 136
      src/modules/marking/arbitration/index.vue
  25. 11 125
      src/modules/marking/mark/index.vue
  26. 20 136
      src/modules/marking/problem/index.vue
  27. 21 136
      src/modules/marking/repeat/index.vue
  28. 204 3
      src/modules/marking/similar/index.vue
  29. 13 47
      src/modules/marking/training-record/index.vue
  30. 14 50
      src/modules/marking/view-sample/index.vue
  31. 300 3
      src/modules/monitor/system-check/index.vue
  32. 8 8
      src/modules/monitor/training-monitoring/hooks/useFormFilter.ts
  33. 10 0
      src/router/marking.ts
  34. 0 10
      src/router/monitor.ts
  35. 98 3
      types/api.d.ts

+ 2 - 0
src/api/expert.ts

@@ -15,6 +15,8 @@ const ExpertApi: DefineApiModule<Expert.ApiMap> = {
   getExpertAssessList: '/api/reference/paper/force/page',
   /** 专家挑选卷 */
   getExpertPickList: '/api/reference/paper/page',
+  /** 删除 */
+  deletePaper: '/api/reference/paper/delete',
 }
 
 export default ExpertApi

+ 9 - 0
src/api/marking.ts

@@ -48,6 +48,15 @@ const MarkingApi: DefineApiModule<Marking.ApiMap> = {
   viewTrainingRecord: '/api/mark/sample/history',
   /** 查看样卷 */
   viewSamplePaper: '/api/mark/rf',
+  /** 雷同卷 */
+  getSimilarPaperList: '/api/same/paper/page',
+  /** 是否雷同 */
+  confirmIsSimilar: '/api/same/paper/mark',
+  /** 导出雷同卷 */
+  exportSimilarPaper: {
+    url: '/api/same/paper/export',
+    download: true,
+  },
 }
 
 export default MarkingApi

+ 2 - 0
src/api/question.ts

@@ -18,6 +18,8 @@ const QuestionApi: DefineApiModule<Question.ApiMap> = {
   },
   getMainQuestionInfo: '/api/question/main/info',
   getMainQuestionList: '/api/question/main/list',
+  /** 获取大题评卷结构 */
+  getQuestionStruct: '/api/mark/question',
 }
 
 export default QuestionApi

+ 8 - 0
src/api/statistics.ts

@@ -60,6 +60,14 @@ const StatisticsApi: DefineApiModule<Statistics.ApiMap> = {
       'Content-Type': 'application/json',
     },
   },
+  /** 系统抽查卷 */
+  getSystemSpotList: '/api/system/check/page',
+  /** 系统抽查卷打分 */
+  markSystemSpotPaper: '/api/system/check/mark',
+  /** 系统抽查卷打回 */
+  rejectSystemSpotPaper: '/api/system/check/reject',
+  /** 系统抽查卷浏览 */
+  viewSystemSpotPaper: '/api/system/check/view',
 }
 
 export default StatisticsApi

+ 1 - 0
src/assets/styles/app.scss

@@ -43,6 +43,7 @@ input {
   margin: 0;
   padding: 0;
   box-sizing: border-box;
+  outline: none;
 }
 
 ul {

+ 34 - 0
src/components/shared/ImagePreview.vue

@@ -0,0 +1,34 @@
+<template>
+  <base-dialog v-model="visible" title="试卷预览" :footer="false" :modal="false" draggable class="preview-dialog">
+    <div class="preview-content">
+      <img :src="url" alt="" />
+    </div>
+  </base-dialog>
+</template>
+
+<script setup lang="ts" name="ImagePreview">
+import BaseDialog from '../element/BaseDialog.vue'
+import useVModel from '@/hooks/useVModel'
+
+const props = defineProps<{
+  modelValue: boolean
+  url: string
+}>()
+const visible = useVModel(props)
+</script>
+
+<style scoped lang="scss">
+.preview-dialog {
+  margin-left: 70%;
+  .preview-content {
+    width: 100%;
+    max-width: 300px;
+    max-height: 600px;
+    img {
+      width: 100%;
+      height: 100%;
+      object-fit: contain;
+    }
+  }
+}
+</style>

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

@@ -176,6 +176,7 @@ const emitEvent = (type: ButtonType, val?: string | number | number[]) => {
   .mark-header {
     margin-left: auto;
     color: $RegularFontColor;
+    font-size: $SmallFont;
     ::v-deep(.data-item) {
       padding-left: 20px;
       display: inline-flex;

+ 8 - 2
src/components/shared/Message.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="">
+  <div class="message-icon">
     <svg-icon name="message"></svg-icon>
   </div>
 </template>
@@ -9,4 +9,10 @@ import { reactive, ref } from 'vue'
 import SvgIcon from '../common/SvgIcon.vue'
 </script>
 
-<style scoped lang="scss"></style>
+<style scoped lang="scss">
+.message-icon {
+  display: grid;
+  place-items: center;
+  font-size: $MediumFont;
+}
+</style>

+ 30 - 33
src/components/shared/ScoringPanel.vue

@@ -9,13 +9,13 @@
     @close="onToggleClick"
   >
     <div class="scoring-panel-box" :class="getClass('modal-box')">
-      <template v-for="(question, index) in task?.questionList || []" :key="task.mainNumber + question.subNumber">
+      <template v-for="(question, index) in questionList" :key="question.mainNumber + question.subNumber">
         <scoring-panel-item
           v-model:score="scoreValues[index]"
           :active="activeIndex === index"
           :modal="dialogMode"
           :toggle-modal="props.toggleModal && index === 0"
-          :question="getQuestion(index)"
+          :question="question"
           @enter="() => onEnter(index)"
           @focused="() => onFocused(index)"
           @toggle-click="onToggleClick"
@@ -36,8 +36,7 @@ import BaseDialog from '@/components/element/BaseDialog.vue'
 import ScoringPanelItem from './ScoringPanelItem.vue'
 import useVModel from '@/hooks/useVModel'
 import useVW from '@/hooks/useVW'
-
-import type { ExtractApiResponse } from 'api-type'
+import useFetch from '@/hooks/useFetch'
 
 const props = withDefaults(
   defineProps<{
@@ -49,7 +48,7 @@ const props = withDefaults(
     visible?: boolean
     /** 分值 */
     score: (number | string)[]
-    task: ExtractApiResponse<'getMarkingTask'>
+    mainNumber: number | undefined
   }>(),
   { modal: false, toggleModal: true, score: void 0 }
 )
@@ -76,6 +75,27 @@ const ScoringPanelContainer = computed(() => {
   return dialogMode.value ? BaseDialog : LessRenderComponent
 })
 
+const { fetch: getQuestionStruct, reset, result: questionStruct } = useFetch('getQuestionStruct')
+
+watch(
+  () => props.mainNumber,
+  () => {
+    if (props.mainNumber) {
+      reset()
+      getQuestionStruct({ mainNumber: props.mainNumber })
+    }
+  },
+  { immediate: true }
+)
+
+const questionList = computed(() => {
+  if (!questionStruct.value) {
+    return []
+  }
+  const { mainNumber, mainTitle, questionList = [] } = questionStruct.value
+  return questionList.map((q) => ({ ...q, mainNumber, mainTitle }))
+})
+
 const modalVisible = useVModel(props, 'visible')
 
 watch(modalVisible, () => {
@@ -84,40 +104,21 @@ watch(modalVisible, () => {
   }
 })
 
-// watch(modalVisible, () => {
-//   if (modalVisible.value && !dialogMode.value) {
-//     modalVisible.value = false
-//   }
-// })
-
-// watch(dialogMode, () => {
-//   if (!modalVisible.value && dialogMode.value) {
-//     modalVisible.value = true
-//   }
-// })
-
 const getClass = (val: string, callback?: string) => {
   return dialogMode.value ? val : callback || ''
 }
 
-const getQuestion = (index: number) => {
-  const question = props.task.questionList[index]
-  return {
-    mainNumber: props.task.mainNumber,
-    mainTitle: props.task.mainTitle,
-    subNumber: question.subNumber,
-    totalScore: question.totalScore,
-    intervalScore: question.intervalScore,
-  }
-}
-
 const scoreValues = useVModel(props, 'score')
 
 const activeIndex = ref<number>(0)
 
+const onSubmit = () => {
+  emits('submit', questionStruct.value)
+}
+
 const onEnter = (index: number) => {
   activeIndex.value = index + 1
-  if (activeIndex.value >= props?.task?.questionList?.length) {
+  if (activeIndex.value >= questionList.value?.length) {
     onSubmit()
   }
 }
@@ -132,10 +133,6 @@ const onToggleClick = () => {
     modalVisible.value = false
   }
 }
-
-const onSubmit = () => {
-  emits('submit')
-}
 </script>
 
 <style scoped lang="scss">

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

@@ -63,7 +63,7 @@ import { getNumbers } from '@/utils/common'
 interface QuestionInfo {
   mainNumber: number
   mainTitle: string
-  subNumber: string
+  subNumber: number
   totalScore: number
   intervalScore: number
 }

+ 121 - 0
src/components/shared/ScoringPanelWithConfirm.vue

@@ -0,0 +1,121 @@
+<template>
+  <scoring-panel
+    v-bind="attrs"
+    v-model:score="modelScore"
+    v-model:visible="modalVisible"
+    :main-number="props.mainNumber"
+    @submit="onSubmit"
+  ></scoring-panel>
+  <base-dialog v-model="submitModalVisible" unless :width="useVW(260)" center>
+    <div class="text-center question-name">{{ questionInfo?.mainNumber }} {{ questionInfo?.mainTitle }}</div>
+    <div class="fill-lighter p-t-base text-center">
+      <div class="total-score-title">— 总分 —</div>
+      <div class="m-t-extra-small">
+        <span class="score-value">{{ totalScore }}</span>
+        <span class="score-unit">分</span>
+      </div>
+    </div>
+    <div class="text-center m-t-base confirm-text">确认提交?</div>
+    <template #footer>
+      <div class="flex items-center justify-between">
+        <el-button class="confirm-button" type="primary" @click="onConfirmSubmit">是(Y)</el-button>
+        <el-button class="confirm-button" plain @click="onCancelSubmit">否(N)</el-button>
+      </div>
+    </template>
+  </base-dialog>
+</template>
+
+<script setup lang="ts" name="ScoringPanelWithConfirm">
+import { computed, ref, useAttrs } from 'vue'
+import { add } from '@/utils/common'
+import { ElButton } from 'element-plus'
+import ScoringPanel from '@/components/shared/ScoringPanel.vue'
+import BaseDialog from '@/components/element/BaseDialog.vue'
+import useVModel from '@/hooks/useVModel'
+import useVW from '@/hooks/useVW'
+
+import type { ExtractApiResponse } from 'api-type'
+
+const props = defineProps<{
+  /** 显示隐藏 */
+  visible?: boolean
+  /** 分值 */
+  score: number[]
+  /** 大题号 */
+  mainNumber?: number
+}>()
+
+const attrs = useAttrs()
+
+const emit = defineEmits<{
+  (e: 'submit', data: { question: ExtractApiResponse<'getQuestionStruct'>; scores: number[]; totalScore: number }): void
+  (e: 'update:visible', visible: boolean): void
+  (e: 'update:score', scores: number[]): void
+}>()
+
+/** 给分面板显示隐藏 */
+const modalVisible = useVModel(props, 'visible')
+
+/** 给分列表 */
+const modelScore = useVModel(props, 'score')
+
+/** 确认给分 */
+const submitModalVisible = ref<boolean>(false)
+
+/** 总分 */
+const totalScore = computed(() => {
+  return add(...modelScore.value)
+})
+
+const questionInfo = ref<ExtractApiResponse<'getQuestionStruct'>>()
+
+/** 提交 */
+const onSubmit = (data: ExtractApiResponse<'getQuestionStruct'>) => {
+  questionInfo.value = data
+  modalVisible.value = false
+  submitModalVisible.value = true
+}
+
+/** 取消提交 */
+const onCancelSubmit = () => {
+  modalVisible.value = true
+  submitModalVisible.value = false
+}
+
+/** 确认提交 */
+const onConfirmSubmit = () => {
+  if (!questionInfo.value) return
+  submitModalVisible.value = false
+  emit('submit', { question: questionInfo.value, scores: modelScore.value, totalScore: totalScore.value })
+}
+</script>
+
+<style scoped lang="scss">
+.question-name {
+  font-size: $BaseFont;
+  color: $NormalColor;
+  margin-bottom: 10px;
+}
+.total-score-title {
+  font-size: $MediumFont;
+  font-weight: bold;
+  color: $NormalColor;
+}
+.confirm-text {
+  font-size: $BaseFont;
+  color: $NormalColor;
+}
+.score-value {
+  font-size: 48px;
+  font-weight: 900;
+  letter-spacing: 4px;
+  color: $NormalColor;
+}
+.score-unit {
+  font-size: $BaseFont;
+  color: $NormalColor;
+}
+.confirm-button {
+  width: 90px;
+}
+</style>

+ 19 - 7
src/components/shared/SendBackMark.vue

@@ -10,7 +10,7 @@
 
 <script setup lang="ts" name="SendBackMark">
 /** 打回弹窗 */
-import { reactive, ref } from 'vue'
+import { reactive, withDefaults } from 'vue'
 import { ElFormItem } from 'element-plus'
 import BaseDialog from '@/components/element/BaseDialog.vue'
 import BaseForm from '../element/BaseForm.vue'
@@ -26,10 +26,14 @@ const emits = defineEmits<{
   (e: 'rejected'): void
 }>()
 
-const props = defineProps<{
-  modelValue: boolean
-  id: number | undefined
-}>()
+const props = withDefaults(
+  defineProps<{
+    modelValue: boolean
+    id: number | undefined
+    type?: 'problem' | 'system-check'
+  }>(),
+  { type: 'problem' }
+)
 
 const visible = useVModel(props)
 
@@ -44,13 +48,20 @@ const rules = {
   reason: [{ required: true, message: '请选择打回原因' }],
 }
 
+const reason = ['给分太高', '给分太低', '注意,评分偏紧', '注意,评分偏松', '评分不稳定,请认真评卷'].map((v) => ({
+  label: v,
+  value: v,
+}))
+
 const items: EpFormItem[] = [
-  { label: '打回原因', prop: 'reason', slotType: 'select', slot: { options: [] } },
+  { label: '打回原因', prop: 'reason', slotType: 'select', slot: { options: reason } },
   { label: '说明', prop: 'description', slotType: 'input', slot: { type: 'textarea' } },
 ]
 
 const { fetch: rejectMarkHistory } = useFetch('rejectMarkHistory')
 
+const { fetch: rejectSystemSpotPaper } = useFetch('rejectSystemSpotPaper')
+
 const onSendBack = async () => {
   try {
     if (!props.id) {
@@ -58,7 +69,8 @@ const onSendBack = async () => {
     }
     const valid = await elFormRef?.value?.validate()
     if (valid) {
-      await rejectMarkHistory({ description: model.description, reason: model.reason, id: props.id })
+      const api = props.type === 'system-check' ? rejectSystemSpotPaper : rejectMarkHistory
+      await api({ description: model.description, reason: model.reason, id: props.id })
     }
     visible.value = false
     emits('rejected')

+ 22 - 3
src/components/shared/UserInfo.vue

@@ -1,9 +1,28 @@
 <template>
-  <div class="">user-info</div>
+  <div class="user-info">
+    <span>{{ info.userName }}</span>
+    <span class="m-l-base">{{ info.roleName }}</span>
+  </div>
 </template>
 
 <script setup lang="ts" name="UserInfo">
-import { reactive, ref } from 'vue'
+import { reactive, computed } from 'vue'
+import useMainStore from '@/store/main'
+
+const mainStore = useMainStore()
+
+const info = computed(() => {
+  const { loginName, name, roleName } = mainStore.myUserInfo || {}
+  return {
+    userName: [loginName, name].filter(Boolean).join('-'),
+    roleName,
+  }
+})
 </script>
 
-<style scoped lang="scss"></style>
+<style scoped lang="scss">
+.user-info {
+  color: $RegularFontColor;
+  font-size: $SmallFont;
+}
+</style>

+ 0 - 7
src/hooks/useElectron.ts

@@ -1,7 +0,0 @@
-import { reactive, ref, computed, watch } from 'vue'
-
-const useElectron = () => {
-  return {}
-}
-
-export default useElectron

+ 93 - 0
src/hooks/useTableCheck.ts

@@ -0,0 +1,93 @@
+import { ref, computed, watch, nextTick, unref } from 'vue'
+import { isDefine } from '@/utils/common'
+
+import type { Ref, ShallowRef, UnwrapRef } from 'vue'
+import type { InstanceTable } from 'global-type'
+import type { MultipleResult } from 'api-type'
+
+type ArrayObjectType = Array<Record<string, any>>
+
+type MultipleResultType<T = Record<string, any>> = MultipleResult<T>
+
+type InputDataType = MultipleResultType | ArrayObjectType
+
+type TableDataType<T> = Ref<T> | ShallowRef<T>
+
+type ArrayData<T extends InputDataType> = T extends MultipleResultType<infer D> ? D : ExtractArrayValue<T>
+
+type RowType<T extends TableDataType<InputDataType>> = ArrayData<UnwrapRef<T>> & {
+  index: number
+}
+
+function isMultipleData(data: any): data is MultipleResultType {
+  return isDefine(data.value.result) && isDefine(data.value.totalCount)
+}
+
+const useTableCheck = <T extends TableDataType<InputDataType>>(data: T, auto = true) => {
+  const tableRef = ref<InstanceTable>()
+
+  const elTableRef = computed(() => {
+    return tableRef?.value?.tableRef
+  })
+
+  const tableData = computed(() => {
+    const d = unref(data)
+    let result: RowType<T>[] = []
+    if (d) {
+      if (isMultipleData(d)) {
+        result = d?.result?.map((d, index) => ({ ...d, index: d.index ?? index })) as RowType<T>[]
+      } else {
+        result = d?.map((d, index) => ({ ...d, index: d.index ?? index })) as RowType<T>[]
+      }
+    }
+    return result
+  })
+
+  const current = ref<RowType<T>>()
+
+  const currentView = ref<RowType<T>>()
+
+  const visibleHistory = ref<boolean>(false)
+
+  watch(
+    tableData,
+    () => {
+      if (tableData?.value?.length && auto) {
+        nextTick(() => {
+          elTableRef?.value?.setCurrentRow(tableData.value[0])
+        })
+      }
+    },
+    { immediate: true }
+  )
+
+  /** 表格选中 */
+  const onCurrentChange = (row: RowType<T>) => {
+    current.value = row
+  }
+
+  /** 表格行双击 */
+  const onDbClick = (row: RowType<T>) => {
+    currentView.value = row
+    visibleHistory.value = true
+  }
+
+  /** 下一份 */
+  const next = () => {
+    elTableRef?.value?.setCurrentRow(tableData.value[((current.value?.index || 0) + 1) % tableData.value.length])
+  }
+
+  return {
+    tableRef,
+    elTableRef,
+    tableData,
+    current,
+    currentView,
+    visibleHistory,
+    onCurrentChange,
+    onDbClick,
+    next,
+  }
+}
+
+export default useTableCheck

+ 9 - 0
src/layout/main/MainHeader.vue

@@ -7,6 +7,12 @@
       </el-icon>
     </div>
     <div class="flex items-center header-info-view">
+      <div class="m-r-base">
+        <message></message>
+      </div>
+      <div class="m-r-base">
+        <user-info></user-info>
+      </div>
       <div class="grid fill-light-gray pointer close-icon" @click="closeApp">
         <el-icon><close /></el-icon>
       </div>
@@ -20,6 +26,9 @@ import { ElIcon } from 'element-plus'
 import { Fold, Expand, Close } from '@element-plus/icons-vue'
 import { closeApp } from '@/utils/shared'
 import useMainLayoutStore from '@/store/layout'
+import Message from '@/components/shared/Message.vue'
+import UserInfo from '@/components/shared/UserInfo.vue'
+
 const mainLayoutStore = useMainLayoutStore()
 </script>
 

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

@@ -252,7 +252,7 @@ const toggleSetting = (visible: boolean) => {
   visibleSetting.value = visible
 }
 
-const interval = computed(() => fetchModel.refresh * 500 * 2)
+const interval = computed(() => fetchModel.refresh * 60 * 1000)
 
 const { fetch, result } = useFetch('getStatistics')
 

+ 3 - 1
src/modules/example/ImageModify.vue

@@ -42,6 +42,8 @@ import { convertColor, BLACK, DISTANCE, useSetImgBg } from '@/hooks/useSetImgBg'
 import { Minus, Plus } from '@element-plus/icons-vue'
 import { useResizeObserver } from '@vueuse/core'
 
+import type { SetImgBgOption } from '@/hooks/useSetImgBg'
+
 const canvas = ref<HTMLCanvasElement | null>(null)
 const image = ref<HTMLImageElement | null>(null)
 const color = ref('rgba(95, 228, 83, 255)')
@@ -49,7 +51,7 @@ const imageList = ref<UploadFiles>([])
 
 let distance = ref(DISTANCE)
 
-const P = computed(() => ({
+const P = computed<SetImgBgOption>(() => ({
   image: image.value,
   canvas: canvas.value,
   useNaturalSize: false,

+ 22 - 121
src/modules/expert/assess/index.vue

@@ -38,43 +38,15 @@
       </div>
     </div>
   </div>
-  <base-dialog
-    v-model="previewModalVisible"
-    title="试卷预览"
-    :footer="false"
-    :modal="false"
-    draggable
-    class="preview-dialog"
-  >
-    <div class="preview-content">
-      <img :src="MockImg" alt="" />
-    </div>
-  </base-dialog>
-  <base-dialog v-model="submitModalVisible" unless :width="useVW(260)" center>
-    <div class="text-center">{{ currentAssessPaper?.mainNumber }} {{ currentAssessPaper?.mainName }}</div>
-    <div class="fill-lighter p-t-base text-center">
-      <div class="total-score-title">— 总分 —</div>
-      <div class="m-t-extra-small">
-        <span class="score-value">{{ totalScore || 15 }}</span>
-        <span class="score-unit">分</span>
-      </div>
-    </div>
-    <div class="text-center m-t-base confirm-text">确认提交?</div>
-    <template #footer>
-      <div class="flex items-center justify-between">
-        <el-button class="confirm-button" type="primary" @click="onConfirmEditScore">是(Y)</el-button>
-        <el-button class="confirm-button" plain @click="onCancelSubmit">否(N)</el-button>
-      </div>
-    </template>
-  </base-dialog>
-  <scoring-panel
+  <image-preview v-model="previewModalVisible" :url="MockImg"></image-preview>
+  <scoring-panel-with-confirm
     v-model:visible="editScoreVisible"
     v-model:score="modelScore"
+    :main-number="mockTask.mainNumber"
     modal
     :toggle-modal="false"
-    :task="mockTask"
     @submit="onSubmit"
-  ></scoring-panel>
+  ></scoring-panel-with-confirm>
   <mark-history-list :id="currentViewHistory?.secretNumber" v-model="visibleHistory" type="secret"></mark-history-list>
 </template>
 
@@ -82,7 +54,6 @@
 /** 专家卷浏览-强制考核卷 */
 import { reactive, ref, computed, watch, nextTick } from 'vue'
 import { ElButton } from 'element-plus'
-import { add } from '@/utils/common'
 import { useSetImgBg } from '@/hooks/useSetImgBg'
 import useFetch from '@/hooks/useFetch'
 import useVW from '@/hooks/useVW'
@@ -90,14 +61,15 @@ import useForm from '@/hooks/useForm'
 import useOptions from '@/hooks/useOptions'
 import useMarkHeader from '@/hooks/useMarkHeader'
 import useMainStore from '@/store/main'
+import useTableCheck from '@/hooks/useTableCheck'
 import MarkHeader from '@/components/shared/MarkHeader.vue'
 import BaseForm from '@/components/element/BaseForm.vue'
 import BaseTable from '@/components/element/BaseTable.vue'
 import MarkHistoryList from '@/components/shared/MarkHistoryList.vue'
 import RightButton from '@/components/shared/RightButton.vue'
-import BaseDialog from '@/components/element/BaseDialog.vue'
 import SvgIcon from '@/components/common/SvgIcon.vue'
-import ScoringPanel from '@/components/shared/ScoringPanel.vue'
+import ScoringPanelWithConfirm from '@/components/shared/ScoringPanelWithConfirm.vue'
+import ImagePreview from '@/components/shared/ImagePreview.vue'
 
 import MockImg from '@/assets/mock/SAMPA-1.jpg'
 import type { SetImgBgOption } from '@/hooks/useSetImgBg'
@@ -109,38 +81,14 @@ type RowType = ExtractMultipleApiResponse<'getExpertAssessList'> & { index: numb
 /** 给分板 */
 const editScoreVisible = ref<boolean>(false)
 
-/** 提交确认 */
-const submitModalVisible = ref<boolean>(false)
-
 /** 图片预览 */
 const previewModalVisible = ref<boolean>(false)
 
+/** 分数 */
 const modelScore = ref<number[]>([])
 
-const totalScore = computed(() => {
-  return add(...modelScore.value)
-})
-
-const mockTask = ref<ExtractApiResponse<'getMarkingTask'>>({
+const mockTask = ref<{ mainNumber: number }>({
   mainNumber: 1,
-  secretNumber: 'xxx',
-  mainTitle: '第一大题',
-  questionList: [
-    {
-      intervalScore: 0.5,
-      subNumber: '1',
-      totalScore: 8,
-    },
-    {
-      intervalScore: 1,
-      subNumber: '2',
-      totalScore: 20,
-    },
-  ],
-  subjectCode: 'C0001',
-  taskId: 1,
-  taskType: 'FORMAL',
-  url: '',
 })
 
 const {
@@ -170,18 +118,6 @@ const imgOption = computed<SetImgBgOption>(() => {
 
 const { drawing, dataUrl } = useSetImgBg(imgOption)
 
-/** 提交 */
-const onSubmit = () => {
-  editScoreVisible.value = false
-  submitModalVisible.value = true
-}
-
-/** 取消提交 */
-const onCancelSubmit = () => {
-  editScoreVisible.value = true
-  submitModalVisible.value = false
-}
-
 /** 刷新 */
 const onRefresh = () => {
   onSearch()
@@ -258,7 +194,7 @@ const formItems = computed<EpFormItem[]>(() => [
     prop: 'forceGroupNumber',
     slotType: 'select',
     slot: {
-      options: forceCheckGroup.value.map((d) => ({ value: d.forceGroupNumber, label: `第${d.forceGroupNumber}组` })),
+      options: forceCheckGroup.value?.map((d) => ({ value: d.forceGroupNumber, label: `第${d.forceGroupNumber}组` })),
       onChange: changeModelValue('question'),
     },
   }),
@@ -266,9 +202,6 @@ const formItems = computed<EpFormItem[]>(() => [
 ])
 
 /** 强制考核卷 */
-const { fetch: getExpertAssessList, result: rfSampleList } = useFetch('getExpertAssessList')
-
-const tableRef = ref<InstanceType<typeof BaseTable>>()
 
 const columns: EpTableColumn<RowType>[] = [
   { label: '序号', type: 'index' },
@@ -279,37 +212,18 @@ const columns: EpTableColumn<RowType>[] = [
   { label: '提交时间', prop: 'createTime' },
 ]
 
-const tableData = computed<RowType[]>(() => {
-  return rfSampleList?.value?.result?.map((d, index) => ({ ...d, index })) || []
-})
-
-const currentAssessPaper = ref<RowType>()
-
-watch(tableData, () => {
-  nextTick(() => {
-    tableRef?.value?.tableRef?.setCurrentRow(tableData.value[0])
-  })
-})
-
-const checkNext = () => {
-  tableRef?.value?.tableRef?.setCurrentRow(
-    tableData.value[((currentAssessPaper.value?.index || 0) + 1) % tableData.value.length]
-  )
-}
-
-const visibleHistory = ref<boolean>(false)
-const currentViewHistory = ref<RowType>()
-
-/** 表格行双击 */
-const onDbClick = (row: RowType) => {
-  currentViewHistory.value = row
-  visibleHistory.value = true
-}
+const { fetch: getExpertAssessList, result: rfSampleList } = useFetch('getExpertAssessList')
 
-/** 表格选中 */
-const onCurrentChange = (row: RowType) => {
-  currentAssessPaper.value = row
-}
+const {
+  tableRef,
+  tableData,
+  current: currentAssessPaper,
+  currentView: currentViewHistory,
+  next: checkNext,
+  visibleHistory,
+  onDbClick,
+  onCurrentChange,
+} = useTableCheck(rfSampleList)
 
 const onSearch = async () => {
   getExpertAssessList(formModel)
@@ -319,7 +233,7 @@ const onSearch = async () => {
 
 const { fetch: updateMarkScore } = useFetch('updateMarkScore')
 
-const onConfirmEditScore = () => {
+const onSubmit = () => {
   if (currentAssessPaper.value) {
     updateMarkScore({ id: currentAssessPaper.value.id, scores: modelScore.value })
   }
@@ -354,17 +268,4 @@ onOptionInit(onSearch)
     width: 580px;
   }
 }
-.preview-dialog {
-  margin-left: 70%;
-  .preview-content {
-    width: 100%;
-    max-width: 300px;
-    max-height: 600px;
-    img {
-      width: 100%;
-      height: 100%;
-      object-fit: contain;
-    }
-  }
-}
 </style>

+ 92 - 156
src/modules/expert/expert/index.vue

@@ -18,12 +18,12 @@
         </div>
       </div>
       <div class="p-base radius-base fill-blank scroll-auto m-l-base problem-list">
-        <base-form size="small" :model="formModel" :items="formItems" :label-width="useVW(40)">
+        <base-form size="small" :model="formModel" :items="formItems" :label-width="useVW(60)">
           <template #form-item-search>
             <el-button type="primary" @click="onSearch">查询</el-button>
           </template>
         </base-form>
-        <div class="flex items-center p-l-base">
+        <div class="flex items-center">
           <span>考核卷</span>
           <span>: 共{{ tableData.length }}</span>
         </div>
@@ -38,43 +38,15 @@
       </div>
     </div>
   </div>
-  <base-dialog
-    v-model="previewModalVisible"
-    title="试卷预览"
-    :footer="false"
-    :modal="false"
-    draggable
-    class="preview-dialog"
-  >
-    <div class="preview-content">
-      <img :src="MockImg" alt="" />
-    </div>
-  </base-dialog>
-  <base-dialog v-model="submitModalVisible" unless :width="useVW(260)" center>
-    <div class="text-center">{{ currentRfPaper?.mainNumber }} {{ currentRfPaper?.mainName }}</div>
-    <div class="fill-lighter p-t-base text-center">
-      <div class="total-score-title">— 总分 —</div>
-      <div class="m-t-extra-small">
-        <span class="score-value">{{ totalScore || 15 }}</span>
-        <span class="score-unit">分</span>
-      </div>
-    </div>
-    <div class="text-center m-t-base confirm-text">确认提交?</div>
-    <template #footer>
-      <div class="flex items-center justify-between">
-        <el-button class="confirm-button" type="primary" @click="onConfirmEditScore">是(Y)</el-button>
-        <el-button class="confirm-button" plain @click="onCancelSubmit">否(N)</el-button>
-      </div>
-    </template>
-  </base-dialog>
-  <scoring-panel
+  <image-preview v-model="previewModalVisible" :url="MockImg"></image-preview>
+  <scoring-panel-with-confirm
     v-model:visible="editScoreVisible"
     v-model:score="modelScore"
     modal
     :toggle-modal="false"
-    :task="mockTask"
+    :main-number="mockTask.mainNumber"
     @submit="onSubmit"
-  ></scoring-panel>
+  ></scoring-panel-with-confirm>
   <mark-history-list :id="currentViewHistory?.secretNumber" v-model="visibleHistory" type="secret"></mark-history-list>
 </template>
 
@@ -82,7 +54,6 @@
 /** 专家卷浏览-专家挑选卷 */
 import { reactive, ref, computed, watch, nextTick } from 'vue'
 import { ElButton } from 'element-plus'
-import { add } from '@/utils/common'
 import { ROLE_OPTION } from '@/constants/dicts'
 import { useSetImgBg } from '@/hooks/useSetImgBg'
 import useFetch from '@/hooks/useFetch'
@@ -91,14 +62,15 @@ import useForm from '@/hooks/useForm'
 import useOptions from '@/hooks/useOptions'
 import useMarkHeader from '@/hooks/useMarkHeader'
 import useMainStore from '@/store/main'
+import useTableCheck from '@/hooks/useTableCheck'
 import MarkHeader from '@/components/shared/MarkHeader.vue'
 import BaseForm from '@/components/element/BaseForm.vue'
 import BaseTable from '@/components/element/BaseTable.vue'
 import MarkHistoryList from '@/components/shared/MarkHistoryList.vue'
 import RightButton from '@/components/shared/RightButton.vue'
-import BaseDialog from '@/components/element/BaseDialog.vue'
 import SvgIcon from '@/components/common/SvgIcon.vue'
-import ScoringPanel from '@/components/shared/ScoringPanel.vue'
+import ScoringPanelWithConfirm from '@/components/shared/ScoringPanelWithConfirm.vue'
+import ImagePreview from '@/components/shared/ImagePreview.vue'
 
 import MockImg from '@/assets/mock/SAMPA-1.jpg'
 import type { SetImgBgOption } from '@/hooks/useSetImgBg'
@@ -110,38 +82,13 @@ type RowType = ExtractMultipleApiResponse<'getExpertPickList'> & { index: number
 /** 给分板 */
 const editScoreVisible = ref<boolean>(false)
 
-/** 提交确认 */
-const submitModalVisible = ref<boolean>(false)
-
 /** 图片预览 */
 const previewModalVisible = ref<boolean>(false)
 
 const modelScore = ref<number[]>([])
 
-const totalScore = computed(() => {
-  return add(...modelScore.value)
-})
-
-const mockTask = ref<ExtractApiResponse<'getMarkingTask'>>({
+const mockTask = ref<{ mainNumber: number }>({
   mainNumber: 1,
-  secretNumber: 'xxx',
-  mainTitle: '第一大题',
-  questionList: [
-    {
-      intervalScore: 0.5,
-      subNumber: '1',
-      totalScore: 8,
-    },
-    {
-      intervalScore: 1,
-      subNumber: '2',
-      totalScore: 20,
-    },
-  ],
-  subjectCode: 'C0001',
-  taskId: 1,
-  taskType: 'FORMAL',
-  url: '',
 })
 
 const {
@@ -171,18 +118,6 @@ const imgOption = computed<SetImgBgOption>(() => {
 
 const { drawing, dataUrl } = useSetImgBg(imgOption)
 
-/** 提交 */
-const onSubmit = () => {
-  editScoreVisible.value = false
-  submitModalVisible.value = true
-}
-
-/** 取消提交 */
-const onCancelSubmit = () => {
-  editScoreVisible.value = true
-  submitModalVisible.value = false
-}
-
 /** 刷新 */
 const onRefresh = () => {
   onSearch()
@@ -190,7 +125,9 @@ const onRefresh = () => {
 
 /** 删除 */
 const onDelete = () => {
-  console.log('删除')
+  if (currentExpertPaper.value) {
+    useFetch('deletePaper').fetch({ id: currentExpertPaper.value.id })
+  }
 }
 
 /** 预览试卷 */
@@ -232,6 +169,15 @@ const formModel = reactive<ExtractApiParams<'getExpertPickList'>>({
   pageSize: 9999999,
 })
 
+watch(
+  () => formModel.paperType,
+  () => {
+    if (formModel.paperType !== 'FORCE') {
+      formModel.forceGroupNumber = void 0
+    }
+  }
+)
+
 const mainStore = useMainStore()
 
 const { mainQuestionList, dataModel, changeModelValue, onOptionInit } = useOptions(['question'], {
@@ -254,79 +200,82 @@ const { defineColumn, _ } = useForm()
 
 const span10 = defineColumn(_, '', { span: 10 })
 
-const formItems = computed<EpFormItem[]>(() => [
-  span10({
-    rowKey: 'row-1',
-    label: '大题',
-    prop: 'mainNumber',
-    slotType: 'select',
-    slot: { options: mainQuestionList.value, onChange: changeModelValue('question') },
-  }),
-  span10({
-    rowKey: 'row-1',
-    label: '角色',
-    prop: 'role',
-    slotType: 'select',
-    slot: { options: ROLE_OPTION },
-  }),
-  { rowKey: 'row-1', slotName: 'search', labelWidth: '10px', colProp: { span: 4 } },
-  span10({
-    rowKey: 'row-2',
-    label: '组号',
-    prop: 'forceGroupNumber',
-    slotType: 'select',
-    slot: {
-      options: forceCheckGroup.value.map((d) => ({ value: d.forceGroupNumber, label: `第${d.forceGroupNumber}组` })),
-      onChange: changeModelValue('question'),
-    },
-  }),
-])
-
-/** 强制考核卷 */
-const { fetch: getExpertPickList, result: rfSampleList } = useFetch('getExpertPickList')
-
-const tableRef = ref<InstanceType<typeof BaseTable>>()
+const formItems = computed<EpFormItem[]>(() => {
+  const items = [
+    span10({
+      rowKey: 'row-1',
+      label: '大题',
+      prop: 'mainNumber',
+      slotType: 'select',
+      slot: { options: mainQuestionList.value, onChange: changeModelValue('question') },
+    }),
+    span10({
+      rowKey: 'row-1',
+      label: '角色',
+      prop: 'role',
+      slotType: 'select',
+      slot: { options: ROLE_OPTION },
+    }),
+    { rowKey: 'row-1', slotName: 'search', labelWidth: '10px', colProp: { span: 4 } },
+    span10({
+      rowKey: 'row-2',
+      label: '试卷类型',
+      prop: 'paperType',
+      slotType: 'select',
+      slot: {
+        options: [
+          { label: 'RF卷', value: 'RF' },
+          { label: '强制考核卷', value: 'FORCE' },
+          { label: 'SAMPLE_A', value: 'SAMPLE_A' },
+          { label: 'SAMPLE_B', value: 'SAMPLE_B' },
+        ],
+      },
+    }),
+  ]
+
+  if (formModel.paperType === 'FORCE') {
+    items.push(
+      span10({
+        rowKey: 'row-2',
+        label: '组号',
+        prop: 'forceGroupNumber',
+        slotType: 'select',
+        slot: {
+          options: forceCheckGroup.value.map((d) => ({
+            value: d.forceGroupNumber,
+            label: `第${d.forceGroupNumber}组`,
+          })),
+          onChange: changeModelValue('question'),
+        },
+      })
+    )
+  }
+  return items
+})
+
+/** 专家挑选卷 */
 
 const columns: EpTableColumn<RowType>[] = [
   { label: '序号', type: 'index' },
   { label: '密号', prop: 'secretNumber' },
-  { label: '分数', prop: 'score' },
-  { label: '档次', prop: 'scoreLevel' },
+  { label: '标准分', prop: 'score' },
   { label: '提交人', prop: 'createName' },
+  { label: '角色', prop: 'roleName' },
   { label: '提交时间', prop: 'createTime' },
 ]
 
-const tableData = computed<RowType[]>(() => {
-  return rfSampleList?.value?.result?.map((d, index) => ({ ...d, index })) || []
-})
-
-const currentRfPaper = ref<RowType>()
-
-watch(tableData, () => {
-  nextTick(() => {
-    tableRef?.value?.tableRef?.setCurrentRow(tableData.value[0])
-  })
-})
-
-const checkNext = () => {
-  tableRef?.value?.tableRef?.setCurrentRow(
-    tableData.value[((currentRfPaper.value?.index || 0) + 1) % tableData.value.length]
-  )
-}
-
-const visibleHistory = ref<boolean>(false)
-const currentViewHistory = ref<RowType>()
+const { fetch: getExpertPickList, result: expertPaperList } = useFetch('getExpertPickList')
 
-/** 表格行双击 */
-const onDbClick = (row: RowType) => {
-  currentViewHistory.value = row
-  visibleHistory.value = true
-}
-
-/** 表格选中 */
-const onCurrentChange = (row: RowType) => {
-  currentRfPaper.value = row
-}
+const {
+  tableRef,
+  tableData,
+  current: currentExpertPaper,
+  currentView: currentViewHistory,
+  next: checkNext,
+  visibleHistory,
+  onDbClick,
+  onCurrentChange,
+} = useTableCheck(expertPaperList)
 
 const onSearch = async () => {
   getExpertPickList(formModel)
@@ -336,9 +285,9 @@ const onSearch = async () => {
 
 const { fetch: updateMarkScore } = useFetch('updateMarkScore')
 
-const onConfirmEditScore = () => {
-  if (currentRfPaper.value) {
-    updateMarkScore({ id: currentRfPaper.value.id, scores: modelScore.value })
+const onSubmit = () => {
+  if (currentExpertPaper.value) {
+    updateMarkScore({ id: currentExpertPaper.value.id, scores: modelScore.value })
   }
 }
 
@@ -371,17 +320,4 @@ onOptionInit(onSearch)
     width: 580px;
   }
 }
-.preview-dialog {
-  margin-left: 70%;
-  .preview-content {
-    width: 100%;
-    max-width: 300px;
-    max-height: 600px;
-    img {
-      width: 100%;
-      height: 100%;
-      object-fit: contain;
-    }
-  }
-}
 </style>

+ 19 - 97
src/modules/expert/sample/index.vue

@@ -38,43 +38,15 @@
       </div>
     </div>
   </div>
-  <base-dialog
-    v-model="previewModalVisible"
-    title="试卷预览"
-    :footer="false"
-    :modal="false"
-    draggable
-    class="preview-dialog"
-  >
-    <div class="preview-content">
-      <img :src="MockImg" alt="" />
-    </div>
-  </base-dialog>
-  <base-dialog v-model="submitModalVisible" unless :width="useVW(260)" center>
-    <div class="text-center">{{ currentRfPaper?.mainNumber }} {{ currentRfPaper?.mainName }}</div>
-    <div class="fill-lighter p-t-base text-center">
-      <div class="total-score-title">— 总分 —</div>
-      <div class="m-t-extra-small">
-        <span class="score-value">{{ totalScore || 15 }}</span>
-        <span class="score-unit">分</span>
-      </div>
-    </div>
-    <div class="text-center m-t-base confirm-text">确认提交?</div>
-    <template #footer>
-      <div class="flex items-center justify-between">
-        <el-button class="confirm-button" type="primary" @click="onConfirmEditScore">是(Y)</el-button>
-        <el-button class="confirm-button" plain @click="onCancelSubmit">否(N)</el-button>
-      </div>
-    </template>
-  </base-dialog>
-  <scoring-panel
+  <image-preview v-model="previewModalVisible" :url="MockImg"></image-preview>
+  <scoring-panel-with-confirm
     v-model:visible="editScoreVisible"
     v-model:score="modelScore"
     modal
     :toggle-modal="false"
-    :task="mockTask"
+    :main-number="mockTask.mainNumber"
     @submit="onSubmit"
-  ></scoring-panel>
+  ></scoring-panel-with-confirm>
   <mark-history-list :id="currentViewHistory?.secretNumber" v-model="visibleHistory" type="secret"></mark-history-list>
 </template>
 
@@ -90,14 +62,15 @@ import useForm from '@/hooks/useForm'
 import useOptions from '@/hooks/useOptions'
 import useMarkHeader from '@/hooks/useMarkHeader'
 import useMainStore from '@/store/main'
+import useTableCheck from '@/hooks/useTableCheck'
 import MarkHeader from '@/components/shared/MarkHeader.vue'
 import BaseForm from '@/components/element/BaseForm.vue'
 import BaseTable from '@/components/element/BaseTable.vue'
 import MarkHistoryList from '@/components/shared/MarkHistoryList.vue'
 import RightButton from '@/components/shared/RightButton.vue'
-import BaseDialog from '@/components/element/BaseDialog.vue'
 import SvgIcon from '@/components/common/SvgIcon.vue'
-import ScoringPanel from '@/components/shared/ScoringPanel.vue'
+import ScoringPanelWithConfirm from '@/components/shared/ScoringPanelWithConfirm.vue'
+import ImagePreview from '@/components/shared/ImagePreview.vue'
 
 import MockImg from '@/assets/mock/SAMPA-1.jpg'
 import type { SetImgBgOption } from '@/hooks/useSetImgBg'
@@ -109,9 +82,6 @@ type RowType = ExtractMultipleApiResponse<'getRfSampleList'> & { index: number }
 /** 给分板 */
 const editScoreVisible = ref<boolean>(false)
 
-/** 提交确认 */
-const submitModalVisible = ref<boolean>(false)
-
 /** 图片预览 */
 const previewModalVisible = ref<boolean>(false)
 
@@ -170,18 +140,6 @@ const imgOption = computed<SetImgBgOption>(() => {
 
 const { drawing, dataUrl } = useSetImgBg(imgOption)
 
-/** 提交 */
-const onSubmit = () => {
-  editScoreVisible.value = false
-  submitModalVisible.value = true
-}
-
-/** 取消提交 */
-const onCancelSubmit = () => {
-  editScoreVisible.value = true
-  submitModalVisible.value = false
-}
-
 /** 刷新 */
 const onRefresh = () => {
   onSearch()
@@ -248,10 +206,6 @@ const formItems = computed<EpFormItem[]>(() => [
 ])
 
 /** RF卷 */
-const { fetch: getRfSampleList, result: rfSampleList } = useFetch('getRfSampleList')
-
-const tableRef = ref<InstanceType<typeof BaseTable>>()
-
 const columns: EpTableColumn<RowType>[] = [
   { label: '密号', prop: 'secretNumber' },
   { label: '分数', prop: 'score' },
@@ -260,37 +214,18 @@ const columns: EpTableColumn<RowType>[] = [
   { label: '提交时间', prop: 'createTime' },
 ]
 
-const tableData = computed<RowType[]>(() => {
-  return rfSampleList?.value?.result?.map((d, index) => ({ ...d, index })) || []
-})
-
-const currentRfPaper = ref<RowType>()
-
-watch(tableData, () => {
-  nextTick(() => {
-    tableRef?.value?.tableRef?.setCurrentRow(tableData.value[0])
-  })
-})
-
-const checkNext = () => {
-  tableRef?.value?.tableRef?.setCurrentRow(
-    tableData.value[((currentRfPaper.value?.index || 0) + 1) % tableData.value.length]
-  )
-}
-
-const visibleHistory = ref<boolean>(false)
-const currentViewHistory = ref<RowType>()
-
-/** 表格行双击 */
-const onDbClick = (row: RowType) => {
-  currentViewHistory.value = row
-  visibleHistory.value = true
-}
+const { fetch: getRfSampleList, result: rfSampleList } = useFetch('getRfSampleList')
 
-/** 表格选中 */
-const onCurrentChange = (row: RowType) => {
-  currentRfPaper.value = row
-}
+const {
+  tableRef,
+  tableData,
+  current: currentRfPaper,
+  currentView: currentViewHistory,
+  next: checkNext,
+  visibleHistory,
+  onDbClick,
+  onCurrentChange,
+} = useTableCheck(rfSampleList)
 
 const onSearch = async () => {
   getRfSampleList(formModel)
@@ -300,7 +235,7 @@ const onSearch = async () => {
 
 const { fetch: updateMarkScore } = useFetch('updateMarkScore')
 
-const onConfirmEditScore = () => {
+const onSubmit = () => {
   if (currentRfPaper.value) {
     updateMarkScore({ id: currentRfPaper.value.id, scores: modelScore.value })
   }
@@ -335,17 +270,4 @@ onOptionInit(onSearch)
     width: 580px;
   }
 }
-.preview-dialog {
-  margin-left: 70%;
-  .preview-content {
-    width: 100%;
-    max-width: 300px;
-    max-height: 600px;
-    img {
-      width: 100%;
-      height: 100%;
-      object-fit: contain;
-    }
-  }
-}
 </style>

+ 22 - 68
src/modules/expert/standard/index.vue

@@ -34,18 +34,7 @@
       </div>
     </div>
   </div>
-  <base-dialog
-    v-model="previewModalVisible"
-    title="试卷预览"
-    :footer="false"
-    :modal="false"
-    draggable
-    class="preview-dialog"
-  >
-    <div class="preview-content">
-      <img :src="MockImg" alt="" />
-    </div>
-  </base-dialog>
+  <image-preview v-model="previewModalVisible" :url="MockImg"></image-preview>
   <mark-history-list :id="currentViewHistory?.secretNumber" v-model="visibleHistory" type="secret"></mark-history-list>
 </template>
 
@@ -60,13 +49,14 @@ import useForm from '@/hooks/useForm'
 import useOptions from '@/hooks/useOptions'
 import useMarkHeader from '@/hooks/useMarkHeader'
 import useMainStore from '@/store/main'
+import useTableCheck from '@/hooks/useTableCheck'
 import BaseForm from '@/components/element/BaseForm.vue'
 import BaseTable from '@/components/element/BaseTable.vue'
 import MarkHistoryList from '@/components/shared/MarkHistoryList.vue'
 import RightButton from '@/components/shared/RightButton.vue'
-import BaseDialog from '@/components/element/BaseDialog.vue'
 import SvgIcon from '@/components/common/SvgIcon.vue'
 import MarkHeader from '@/components/shared/MarkHeader.vue'
+import ImagePreview from '@/components/shared/ImagePreview.vue'
 
 import MockImg from '@/assets/mock/SAMPA-1.jpg'
 
@@ -95,15 +85,6 @@ const onPreview = () => {
   previewModalVisible.value = true
 }
 
-/** 转为样卷 */
-const { fetch: transformStandardToSample } = useFetch('transformStandardToSample')
-
-const onTransformToSample = () => {
-  if (currentStandardPaper.value) {
-    transformStandardToSample({ id: currentStandardPaper.value.id })
-  }
-}
-
 type OperationClick = MarkHeaderInstance['onClick']
 
 type OperationType = Parameters<Exclude<OperationClick, undefined>>[0]['type']
@@ -177,10 +158,6 @@ const formItems = computed<EpFormItem[]>(() => [
 ])
 
 /** 标准卷列表 */
-const { fetch: getStandardList, result: standardList } = useFetch('getStandardList')
-
-const tableRef = ref<InstanceType<typeof BaseTable>>()
-
 const columns: EpTableColumn<RowType>[] = [
   { label: '密号', prop: 'secretNumber' },
   { label: '分数', prop: 'score' },
@@ -188,40 +165,30 @@ const columns: EpTableColumn<RowType>[] = [
   { label: '提交时间', prop: 'createTime' },
 ]
 
-const tableData = computed<RowType[]>(() => {
-  return standardList?.value?.result?.map((d, index) => ({ ...d, index })) || []
-})
-
-const currentStandardPaper = ref<RowType>()
-
-watch(tableData, () => {
-  nextTick(() => {
-    tableRef?.value?.tableRef?.setCurrentRow(tableData.value[0])
-  })
-})
-
-const checkNext = () => {
-  tableRef?.value?.tableRef?.setCurrentRow(
-    tableData.value[((currentStandardPaper.value?.index || 0) + 1) % tableData.value.length]
-  )
-}
+const { fetch: getStandardList, result: standardList } = useFetch('getStandardList')
 
-const visibleHistory = ref<boolean>(false)
-const currentViewHistory = ref<RowType>()
+const {
+  tableRef,
+  tableData,
+  current: currentStandardPaper,
+  currentView: currentViewHistory,
+  next: checkNext,
+  visibleHistory,
+  onDbClick,
+  onCurrentChange,
+} = useTableCheck(standardList)
 
-/** 表格行双击 */
-const onDbClick = (row: RowType) => {
-  currentViewHistory.value = row
-  visibleHistory.value = true
+const onSearch = async () => {
+  getStandardList(formModel)
 }
 
-/** 表格选中 */
-const onCurrentChange = (row: RowType) => {
-  currentStandardPaper.value = row
-}
+/** 转为样卷 */
+const { fetch: transformStandardToSample } = useFetch('transformStandardToSample')
 
-const onSearch = async () => {
-  getStandardList(formModel)
+const onTransformToSample = () => {
+  if (currentStandardPaper.value) {
+    transformStandardToSample({ id: currentStandardPaper.value.id })
+  }
 }
 
 onOptionInit(onSearch)
@@ -253,17 +220,4 @@ onOptionInit(onSearch)
     width: 580px;
   }
 }
-.preview-dialog {
-  margin-left: 70%;
-  .preview-content {
-    width: 100%;
-    max-width: 300px;
-    max-height: 600px;
-    img {
-      width: 100%;
-      height: 100%;
-      object-fit: contain;
-    }
-  }
-}
 </style>

+ 13 - 59
src/modules/expert/training/index.vue

@@ -39,18 +39,7 @@
       </div>
     </div>
   </div>
-  <base-dialog
-    v-model="previewModalVisible"
-    title="试卷预览"
-    :footer="false"
-    :modal="false"
-    draggable
-    class="preview-dialog"
-  >
-    <div class="preview-content">
-      <img :src="MockImg" alt="" />
-    </div>
-  </base-dialog>
+  <image-preview v-model="previewModalVisible" :url="MockImg"></image-preview>
   <mark-history-list :id="currentViewHistory?.secretNumber" v-model="visibleHistory" type="secret"></mark-history-list>
 </template>
 
@@ -65,12 +54,12 @@ import useForm from '@/hooks/useForm'
 import useOptions from '@/hooks/useOptions'
 import useMarkHeader from '@/hooks/useMarkHeader'
 import useMainStore from '@/store/main'
+import useTableCheck from '@/hooks/useTableCheck'
 import MarkHeader from '@/components/shared/MarkHeader.vue'
 import BaseForm from '@/components/element/BaseForm.vue'
 import BaseTable from '@/components/element/BaseTable.vue'
 import MarkHistoryList from '@/components/shared/MarkHistoryList.vue'
 import RightButton from '@/components/shared/RightButton.vue'
-import BaseDialog from '@/components/element/BaseDialog.vue'
 import SvgIcon from '@/components/common/SvgIcon.vue'
 
 import MockImg from '@/assets/mock/SAMPA-1.jpg'
@@ -185,9 +174,6 @@ const formItems = computed<EpFormItem[]>(() => [
 ])
 
 /** AB培训卷 */
-const { fetch: getSampleList, result: sampleList } = useFetch('getSampleList')
-
-const tableRef = ref<InstanceType<typeof BaseTable>>()
 
 const columns: EpTableColumn<RowType>[] = [
   { label: '序号', type: 'index' },
@@ -197,37 +183,18 @@ const columns: EpTableColumn<RowType>[] = [
   { label: '提交时间', prop: 'createTime' },
 ]
 
-const tableData = computed<RowType[]>(() => {
-  return sampleList?.value?.result?.map((d, index) => ({ ...d, index })) || []
-})
-
-const currentTrainingRecord = ref<RowType>()
-
-watch(tableData, () => {
-  nextTick(() => {
-    tableRef?.value?.tableRef?.setCurrentRow(tableData.value[0])
-  })
-})
-
-const checkNext = () => {
-  tableRef?.value?.tableRef?.setCurrentRow(
-    tableData.value[((currentTrainingRecord.value?.index || 0) + 1) % tableData.value.length]
-  )
-}
-
-const visibleHistory = ref<boolean>(false)
-const currentViewHistory = ref<RowType>()
-
-/** 表格行双击 */
-const onDbClick = (row: RowType) => {
-  currentViewHistory.value = row
-  visibleHistory.value = true
-}
+const { fetch: getSampleList, result: sampleList } = useFetch('getSampleList')
 
-/** 表格选中 */
-const onCurrentChange = (row: RowType) => {
-  currentTrainingRecord.value = row
-}
+const {
+  tableRef,
+  tableData,
+  current,
+  currentView: currentViewHistory,
+  next: checkNext,
+  visibleHistory,
+  onDbClick,
+  onCurrentChange,
+} = useTableCheck(sampleList)
 
 const onSearch = async () => {
   getSampleList(formModel)
@@ -262,17 +229,4 @@ onOptionInit(onSearch)
     width: 580px;
   }
 }
-.preview-dialog {
-  margin-left: 70%;
-  .preview-content {
-    width: 100%;
-    max-width: 300px;
-    max-height: 600px;
-    img {
-      width: 100%;
-      height: 100%;
-      object-fit: contain;
-    }
-  }
-}
 </style>

+ 21 - 136
src/modules/marking/arbitration/index.vue

@@ -17,12 +17,12 @@
         <div class="flex-1 p-base scroll-auto mark-content-paper">
           <img :src="dataUrl" alt="" class="paper-img" :style="{ 'background-color': frontColor }" />
         </div>
-        <scoring-panel
+        <scoring-panel-with-confirm
           v-model:visible="scoringPanelVisible"
           v-model:score="modelScore"
-          :task="mockTask"
+          :main-number="mockTask.mainNumber"
           @submit="onSubmit"
-        ></scoring-panel>
+        ></scoring-panel-with-confirm>
       </div>
       <div class="p-base radius-base fill-blank scroll-auto m-l-base problem-list">
         <base-form size="small" :model="formModel" :items="formItems" :label-width="useVW(62)">
@@ -45,35 +45,7 @@
       </div>
     </div>
   </div>
-  <base-dialog v-model="submitModalVisible" unless :width="useVW(260)" center>
-    <div class="text-center">{{ currentArbitration?.mainNumber }} {{ currentArbitration?.mainName }}</div>
-    <div class="fill-lighter p-t-base text-center">
-      <div class="total-score-title">— 总分 —</div>
-      <div class="m-t-extra-small">
-        <span class="score-value">{{ totalScore || 15 }}</span>
-        <span class="score-unit">分</span>
-      </div>
-    </div>
-    <div class="text-center m-t-base confirm-text">确认提交?</div>
-    <template #footer>
-      <div class="flex items-center justify-between">
-        <el-button class="confirm-button" type="primary" @click="onMarkArbitrationPaper">是(Y)</el-button>
-        <el-button class="confirm-button" plain @click="onCancelSubmit">否(N)</el-button>
-      </div>
-    </template>
-  </base-dialog>
-  <base-dialog
-    v-model="previewModalVisible"
-    title="试卷预览"
-    :footer="false"
-    :modal="false"
-    draggable
-    class="preview-dialog"
-  >
-    <div class="preview-content">
-      <img :src="MockImg" alt="" />
-    </div>
-  </base-dialog>
+  <image-preview v-model="previewModalVisible" :url="MockImg"></image-preview>
   <mark-history-list :id="currentViewHistory?.taskId" v-model="visibleHistory"></mark-history-list>
 </template>
 
@@ -81,7 +53,6 @@
 /** 仲裁卷查看 */
 import { reactive, ref, computed, watch, nextTick } from 'vue'
 import { ElButton } from 'element-plus'
-import { add } from '@/utils/common'
 import { useSetImgBg } from '@/hooks/useSetImgBg'
 import useFetch from '@/hooks/useFetch'
 import useVW from '@/hooks/useVW'
@@ -89,14 +60,15 @@ import useForm from '@/hooks/useForm'
 import useOptions from '@/hooks/useOptions'
 import useMarkHeader from '@/hooks/useMarkHeader'
 import useMainStore from '@/store/main'
+import useTableCheck from '@/hooks/useTableCheck'
 import MarkHeader from '@/components/shared/MarkHeader.vue'
-import ScoringPanel from '@/components/shared/ScoringPanel.vue'
-import BaseDialog from '@/components/element/BaseDialog.vue'
+import ScoringPanelWithConfirm from '@/components/shared/ScoringPanelWithConfirm.vue'
 import SvgIcon from '@/components/common/SvgIcon.vue'
 import BaseForm from '@/components/element/BaseForm.vue'
 import BaseTable from '@/components/element/BaseTable.vue'
 import MarkHistoryList from '@/components/shared/MarkHistoryList.vue'
 import RightButton from '@/components/shared/RightButton.vue'
+import ImagePreview from '@/components/shared/ImagePreview.vue'
 
 import MockImg from '@/assets/mock/SAMPA-1.jpg'
 
@@ -125,18 +97,11 @@ const {
 /** 给分板 */
 const scoringPanelVisible = ref<boolean>(true)
 
-/** 提交确认 */
-const submitModalVisible = ref<boolean>(false)
-
 /** 图片预览 */
 const previewModalVisible = ref<boolean>(false)
 
 const modelScore = ref<number[]>([])
 
-const totalScore = computed(() => {
-  return add(...modelScore.value)
-})
-
 const imgOption = computed<SetImgBgOption>(() => {
   return {
     image: MockImg,
@@ -148,55 +113,10 @@ const imgOption = computed<SetImgBgOption>(() => {
 
 const { drawing, dataUrl } = useSetImgBg(imgOption)
 
-const mockTask = ref<ExtractApiResponse<'getMarkingTask'>>({
+const mockTask = ref<{ mainNumber: number }>({
   mainNumber: 1,
-  mainTitle: '第一大题',
-  questionList: [
-    {
-      intervalScore: 0.5,
-      subNumber: '1',
-      totalScore: 8,
-    },
-    {
-      intervalScore: 1,
-      subNumber: '2',
-      totalScore: 20,
-    },
-    {
-      intervalScore: 0.3,
-      subNumber: '3',
-      totalScore: 5,
-    },
-    // {
-    //   intervalScore: 0.4,
-    //   subNumber: '4',
-    //   totalScore: 8,
-    // },
-    // {
-    //   intervalScore: 0.6,
-    //   subNumber: '5',
-    //   totalScore: 6,
-    // },
-  ],
-  secretNumber: '100000001',
-  subjectCode: 'C0001',
-  taskId: 1,
-  taskType: 'FORMAL',
-  url: '',
 })
 
-/** 提交 */
-const onSubmit = () => {
-  scoringPanelVisible.value = false
-  submitModalVisible.value = true
-}
-
-/** 取消提交 */
-const onCancelSubmit = () => {
-  scoringPanelVisible.value = true
-  submitModalVisible.value = false
-}
-
 /** 刷新 */
 const onRefresh = () => {
   onSearch()
@@ -275,9 +195,6 @@ const formItems = computed<EpFormItem[]>(() => [
 ])
 
 /** 查询仲裁卷列表 */
-const { fetch: getArbitrationList, result: problemHistory } = useFetch('getArbitrationList')
-
-const tableRef = ref<InstanceType<typeof BaseTable>>()
 
 const columns: EpTableColumn[] = [
   { label: '密号', prop: 'secretNumber', width: 45 },
@@ -293,19 +210,20 @@ const columns: EpTableColumn[] = [
   { label: '大组长客主比', prop: 'chiefRatio', width: 45 },
 ]
 
-const tableData = computed<RowType[]>(() => {
-  return problemHistory?.value?.result?.map((d, index) => ({ ...d, index })) || []
-})
+const { fetch: getArbitrationList, result: arbitrationList } = useFetch('getArbitrationList')
 
-const { fetch: viewArbitrationPaper } = useFetch('viewArbitrationPaper')
-
-const currentArbitration = ref<RowType>()
+const {
+  tableRef,
+  tableData,
+  current: currentArbitration,
+  currentView: currentViewHistory,
+  next: checkNext,
+  visibleHistory,
+  onDbClick,
+  onCurrentChange,
+} = useTableCheck(arbitrationList)
 
-watch(tableData, () => {
-  nextTick(() => {
-    tableRef?.value?.tableRef?.setCurrentRow(tableData.value[0])
-  })
-})
+const { fetch: viewArbitrationPaper } = useFetch('viewArbitrationPaper')
 
 watch(currentArbitration, () => {
   if (currentArbitration.value?.id) {
@@ -313,26 +231,6 @@ watch(currentArbitration, () => {
   }
 })
 
-const checkNext = () => {
-  tableRef?.value?.tableRef?.setCurrentRow(
-    tableData.value[((currentArbitration.value?.index || 0) + 1) % tableData.value.length]
-  )
-}
-
-const visibleHistory = ref<boolean>(false)
-const currentViewHistory = ref<RowType>()
-
-/** 表格行双击 */
-const onDbClick = (row: RowType) => {
-  currentViewHistory.value = row
-  visibleHistory.value = true
-}
-
-/** 表格选中 */
-const onCurrentChange = (row: RowType) => {
-  currentArbitration.value = row
-}
-
 const onSearch = async () => {
   getArbitrationList(formModel)
 }
@@ -340,7 +238,7 @@ const onSearch = async () => {
 /** 仲裁卷打分 */
 const { fetch: markArbitrationPaper } = useFetch('markArbitrationPaper')
 
-const onMarkArbitrationPaper = () => {
+const onSubmit = () => {
   if (currentArbitration.value) {
     markArbitrationPaper({ id: currentArbitration.value.id, scores: modelScore.value })
   }
@@ -375,17 +273,4 @@ onOptionInit(onSearch)
     width: 580px;
   }
 }
-.preview-dialog {
-  margin-left: 70%;
-  .preview-content {
-    width: 100%;
-    max-width: 300px;
-    max-height: 600px;
-    img {
-      width: 100%;
-      height: 100%;
-      object-fit: contain;
-    }
-  }
-}
 </style>

+ 11 - 125
src/modules/marking/mark/index.vue

@@ -21,30 +21,13 @@
         <img :src="dataUrl" alt="" class="paper-img" :style="{ 'background-color': frontColor }" />
       </div>
     </div>
-    <scoring-panel
+    <scoring-panel-with-confirm
       v-model:visible="scoringPanelVisible"
       v-model:score="modelScore"
-      :task="mockTask"
+      :main-number="mockTask.mainNumber"
       @submit="onSubmit"
-    ></scoring-panel>
+    ></scoring-panel-with-confirm>
   </div>
-  <base-dialog v-model="submitModalVisible" unless :width="useVW(260)" center>
-    <div class="text-center">{{ task?.mainNumber }} {{ task?.mainTitle }}</div>
-    <div class="fill-lighter p-t-base text-center">
-      <div class="total-score-title">— 总分 —</div>
-      <div class="m-t-extra-small">
-        <span class="score-value">{{ totalScore || 15 }}</span>
-        <span class="score-unit">分</span>
-      </div>
-    </div>
-    <div class="text-center m-t-base confirm-text">确认提交?</div>
-    <template #footer>
-      <div class="flex items-center justify-between">
-        <el-button class="confirm-button" type="primary" @click="onFinalSubmit">是(Y)</el-button>
-        <el-button class="confirm-button" plain @click="onCancelSubmit">否(N)</el-button>
-      </div>
-    </template>
-  </base-dialog>
   <base-dialog v-model="problemVisible" unless :width="useVW(260)" center>
     <el-radio-group v-model="problemClass">
       <el-radio-button label="problem">问题卷</el-radio-button>
@@ -70,18 +53,7 @@
     </template>
   </base-dialog>
   <remark-list-modal v-model="remarkModalVisible"></remark-list-modal>
-  <base-dialog
-    v-model="previewModalVisible"
-    title="试卷预览"
-    :footer="false"
-    :modal="false"
-    draggable
-    class="preview-dialog"
-  >
-    <div class="preview-content">
-      <img :src="MockImg" alt="" />
-    </div>
-  </base-dialog>
+  <image-preview v-model="previewModalVisible" :url="MockImg"></image-preview>
 </template>
 
 <script setup lang="ts" name="MarkingMark">
@@ -89,14 +61,14 @@
 import { computed, reactive, ref, watch } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElButton, ElRadioGroup, ElRadioButton, ElRadio } from 'element-plus'
-import { add } from '@/utils/common'
 import { useSetImgBg } from '@/hooks/useSetImgBg'
 import useFetch from '@/hooks/useFetch'
 import useVW from '@/hooks/useVW'
 import useTime from '@/hooks/useTime'
 import BaseDialog from '@/components/element/BaseDialog.vue'
 import MarkHeader from '@/components/shared/MarkHeader.vue'
-import ScoringPanel from '@/components/shared/ScoringPanel.vue'
+import ScoringPanelWithConfirm from '@/components/shared/ScoringPanelWithConfirm.vue'
+import ImagePreview from '@/components/shared/ImagePreview.vue'
 import RemarkListModal from '@/components/shared/RemarkListModal.vue'
 import SvgIcon from '@/components/common/SvgIcon.vue'
 
@@ -138,41 +110,8 @@ const imgOption = computed<SetImgBgOption>(() => {
 
 const { drawing, dataUrl } = useSetImgBg(imgOption)
 
-const mockTask = ref<ExtractApiResponse<'getMarkingTask'>>({
+const mockTask = ref<{ mainNumber: number }>({
   mainNumber: 1,
-  mainTitle: '第一大题',
-  questionList: [
-    {
-      intervalScore: 0.5,
-      subNumber: '1',
-      totalScore: 10,
-    },
-    {
-      intervalScore: 1,
-      subNumber: '2',
-      totalScore: 20,
-    },
-    {
-      intervalScore: 0.2,
-      subNumber: '3',
-      totalScore: 5,
-    },
-    // {
-    //   intervalScore: 0.4,
-    //   subNumber: '4',
-    //   totalScore: 8,
-    // },
-    // {
-    //   intervalScore: 0.6,
-    //   subNumber: '5',
-    //   totalScore: 6,
-    // },
-  ],
-  secretNumber: '100000001',
-  subjectCode: 'C0001',
-  taskId: 1,
-  taskType: 'FORMAL',
-  url: '',
 })
 
 const { fetch: getMarkingTask, loading, result: task } = useFetch('getMarkingTask')
@@ -186,9 +125,6 @@ watch(task, resume)
 /** 给分板 */
 const scoringPanelVisible = ref<boolean>(true)
 
-/** 提交确认 */
-const submitModalVisible = ref<boolean>(false)
-
 /** 回评弹窗 */
 const remarkModalVisible = ref<boolean>(false)
 
@@ -202,34 +138,18 @@ const problemClass = ref<'problem' | 'similar'>('problem')
 /** 问题卷类型 */
 const problemType = ref<ProblemType>('OVERSTEP')
 
+/** 分数列表 */
 const modelScore = ref<number[]>([])
 
-const totalScore = computed(() => {
-  return add(...modelScore.value)
-})
-
-/** 提交 */
-const onSubmit = () => {
-  scoringPanelVisible.value = false
-  submitModalVisible.value = true
-}
-
-/** 取消提交 */
-const onCancelSubmit = () => {
-  scoringPanelVisible.value = true
-  submitModalVisible.value = false
-}
-
 /** 提交评卷 */
-const onFinalSubmit = async () => {
+const onSubmit: InstanceType<typeof ScoringPanelWithConfirm>['onSubmit'] = async ({ question, scores, totalScore }) => {
   try {
     if (!task.value?.taskId) {
       return
     }
-    submitModalVisible.value = false
     await submitMarkTask({
-      markScore: totalScore.value,
-      markScores: modelScore.value,
+      markScore: totalScore,
+      markScores: scores,
       spentTime: getSpentTime(),
       problem: false,
       taskId: task.value.taskId,
@@ -321,38 +241,4 @@ const onOperationClick: OperationClick = ({ type, value }) => {
 }
 .paper-img {
 }
-.total-score-title {
-  font-size: $MediumFont;
-  font-weight: bold;
-  color: $NormalColor;
-}
-.confirm-text {
-  color: $PrimaryPlusFontColor;
-}
-.score-value {
-  font-size: 56px;
-  font-weight: 900;
-  color: $NormalColor;
-}
-.score-unit {
-  font-size: $BaseFont;
-  color: $NormalColor;
-}
-.confirm-button {
-  width: 90px;
-}
-
-.preview-dialog {
-  margin-left: 70%;
-  .preview-content {
-    width: 100%;
-    max-width: 300px;
-    max-height: 600px;
-    img {
-      width: 100%;
-      height: 100%;
-      object-fit: contain;
-    }
-  }
-}
 </style>

+ 20 - 136
src/modules/marking/problem/index.vue

@@ -16,12 +16,12 @@
         <div class="flex-1 p-base scroll-auto mark-content-paper">
           <img :src="dataUrl" alt="" class="paper-img" :style="{ 'background-color': frontColor }" />
         </div>
-        <scoring-panel
+        <scoring-panel-with-confirm
           v-model:visible="scoringPanelVisible"
           v-model:score="modelScore"
-          :task="mockTask"
+          :main-number="mockTask.mainNumber"
           @submit="onSubmit"
-        ></scoring-panel>
+        ></scoring-panel-with-confirm>
       </div>
       <div class="p-base radius-base fill-blank scroll-auto m-l-base problem-list">
         <base-form size="small" :model="formModel" :items="formItems" :label-width="useVW(62)">
@@ -44,35 +44,7 @@
       </div>
     </div>
   </div>
-  <base-dialog v-model="submitModalVisible" unless :width="useVW(260)" center>
-    <div class="text-center">{{ currentProblem?.mainNumber }} {{ currentProblem?.mainName }}</div>
-    <div class="fill-lighter p-t-base text-center">
-      <div class="total-score-title">— 总分 —</div>
-      <div class="m-t-extra-small">
-        <span class="score-value">{{ totalScore || 15 }}</span>
-        <span class="score-unit">分</span>
-      </div>
-    </div>
-    <div class="text-center m-t-base confirm-text">确认提交?</div>
-    <template #footer>
-      <div class="flex items-center justify-between">
-        <el-button class="confirm-button" type="primary" @click="onMarkProblemPaper">是(Y)</el-button>
-        <el-button class="confirm-button" plain @click="onCancelSubmit">否(N)</el-button>
-      </div>
-    </template>
-  </base-dialog>
-  <base-dialog
-    v-model="previewModalVisible"
-    title="试卷预览"
-    :footer="false"
-    :modal="false"
-    draggable
-    class="preview-dialog"
-  >
-    <div class="preview-content">
-      <img :src="MockImg" alt="" />
-    </div>
-  </base-dialog>
+  <image-preview v-model="previewModalVisible" :url="MockImg"></image-preview>
   <mark-history-list :id="currentViewHistory?.taskId" v-model="visibleHistory"></mark-history-list>
   <send-back-mark :id="currentProblem?.id" v-model="sendBackVisible" @rejected="onRejected"></send-back-mark>
 </template>
@@ -81,7 +53,6 @@
 /** 问题卷查看 */
 import { reactive, ref, computed, watch, nextTick } from 'vue'
 import { ElButton } from 'element-plus'
-import { add } from '@/utils/common'
 import { useSetImgBg } from '@/hooks/useSetImgBg'
 import useVW from '@/hooks/useVW'
 import useFetch from '@/hooks/useFetch'
@@ -89,15 +60,16 @@ import useForm from '@/hooks/useForm'
 import useOptions from '@/hooks/useOptions'
 import useMarkHeader from '@/hooks/useMarkHeader'
 import useMainStore from '@/store/main'
+import useTableCheck from '@/hooks/useTableCheck'
 import MarkHeader from '@/components/shared/MarkHeader.vue'
-import ScoringPanel from '@/components/shared/ScoringPanel.vue'
-import BaseDialog from '@/components/element/BaseDialog.vue'
+import ScoringPanelWithConfirm from '@/components/shared/ScoringPanelWithConfirm.vue'
 import SvgIcon from '@/components/common/SvgIcon.vue'
 import BaseForm from '@/components/element/BaseForm.vue'
 import BaseTable from '@/components/element/BaseTable.vue'
 import MarkHistoryList from '@/components/shared/MarkHistoryList.vue'
 import SendBackMark from '@/components/shared/SendBackMark.vue'
 import RightButton from '@/components/shared/RightButton.vue'
+import ImagePreview from '@/components/shared/ImagePreview.vue'
 
 import MockImg from '@/assets/mock/SAMPA-1.jpg'
 import type { SetImgBgOption } from '@/hooks/useSetImgBg'
@@ -125,9 +97,6 @@ const {
 /** 给分板 */
 const scoringPanelVisible = ref<boolean>(true)
 
-/** 提交确认 */
-const submitModalVisible = ref<boolean>(false)
-
 /** 图片预览 */
 const previewModalVisible = ref<boolean>(false)
 
@@ -136,10 +105,6 @@ const sendBackVisible = ref<boolean>(false)
 
 const modelScore = ref<number[]>([])
 
-const totalScore = computed(() => {
-  return add(...modelScore.value)
-})
-
 const imgOption = computed<SetImgBgOption>(() => {
   return {
     image: MockImg,
@@ -151,55 +116,10 @@ const imgOption = computed<SetImgBgOption>(() => {
 
 const { drawing, dataUrl } = useSetImgBg(imgOption)
 
-const mockTask = ref<ExtractApiResponse<'getMarkingTask'>>({
+const mockTask = ref<{ mainNumber: number }>({
   mainNumber: 1,
-  mainTitle: '第一大题',
-  questionList: [
-    {
-      intervalScore: 0.5,
-      subNumber: '1',
-      totalScore: 8,
-    },
-    {
-      intervalScore: 1,
-      subNumber: '2',
-      totalScore: 20,
-    },
-    {
-      intervalScore: 0.3,
-      subNumber: '3',
-      totalScore: 5,
-    },
-    // {
-    //   intervalScore: 0.4,
-    //   subNumber: '4',
-    //   totalScore: 8,
-    // },
-    // {
-    //   intervalScore: 0.6,
-    //   subNumber: '5',
-    //   totalScore: 6,
-    // },
-  ],
-  secretNumber: '100000001',
-  subjectCode: 'C0001',
-  taskId: 1,
-  taskType: 'FORMAL',
-  url: '',
 })
 
-/** 提交 */
-const onSubmit = () => {
-  scoringPanelVisible.value = false
-  submitModalVisible.value = true
-}
-
-/** 取消提交 */
-const onCancelSubmit = () => {
-  scoringPanelVisible.value = true
-  submitModalVisible.value = false
-}
-
 /** 刷新 */
 const onRefresh = () => {
   onSearch()
@@ -301,10 +221,6 @@ const formItems = computed<EpFormItem[]>(() => [
 ])
 
 /** 查询问题卷列表 */
-const { fetch: getProblemHistory, result: problemHistory } = useFetch('getProblemHistory')
-
-const tableRef = ref<InstanceType<typeof BaseTable>>()
-
 const columns: EpTableColumn[] = [
   { label: '密号', prop: 'secretNumber', width: 45 },
   { label: '提交人', prop: 'markerName', width: 60 },
@@ -317,37 +233,18 @@ const columns: EpTableColumn[] = [
   { label: '处理时间', prop: 'solveTime', width: 45 },
 ]
 
-const tableData = computed<RowType[]>(() => {
-  return problemHistory?.value?.result?.map((d, index) => ({ ...d, index })) || []
-})
-
-const currentProblem = ref<RowType>()
-
-watch(tableData, () => {
-  nextTick(() => {
-    tableRef?.value?.tableRef?.setCurrentRow(tableData.value[0])
-  })
-})
-
-const checkNext = () => {
-  tableRef?.value?.tableRef?.setCurrentRow(
-    tableData.value[((currentProblem.value?.index || 0) + 1) % tableData.value.length]
-  )
-}
-
-const visibleHistory = ref<boolean>(false)
-const currentViewHistory = ref<RowType>()
-
-/** 表格行双击 */
-const onDbClick = (row: RowType) => {
-  currentViewHistory.value = row
-  visibleHistory.value = true
-}
+const { fetch: getProblemHistory, result: problemHistory } = useFetch('getProblemHistory')
 
-/** 表格选中 */
-const onCurrentChange = (row: RowType) => {
-  currentProblem.value = row
-}
+const {
+  tableRef,
+  tableData,
+  current: currentProblem,
+  currentView: currentViewHistory,
+  next: checkNext,
+  visibleHistory,
+  onDbClick,
+  onCurrentChange,
+} = useTableCheck(problemHistory)
 
 const onSearch = async () => {
   getProblemHistory(formModel)
@@ -356,7 +253,7 @@ const onSearch = async () => {
 /** 问题卷打分 */
 const { fetch: markProblemPaper } = useFetch('markReMarkPaper')
 
-const onMarkProblemPaper = () => {
+const onSubmit = () => {
   if (currentProblem.value) {
     markProblemPaper({ id: currentProblem.value.id, scores: modelScore.value })
   }
@@ -391,17 +288,4 @@ onOptionInit(onSearch)
     width: 480px;
   }
 }
-.preview-dialog {
-  margin-left: 70%;
-  .preview-content {
-    width: 100%;
-    max-width: 300px;
-    max-height: 600px;
-    img {
-      width: 100%;
-      height: 100%;
-      object-fit: contain;
-    }
-  }
-}
 </style>

+ 21 - 136
src/modules/marking/repeat/index.vue

@@ -16,12 +16,12 @@
         <div class="flex-1 p-base scroll-auto mark-content-paper">
           <img :src="dataUrl" alt="" class="paper-img" :style="{ 'background-color': frontColor }" />
         </div>
-        <scoring-panel
+        <scoring-panel-with-confirm
           v-model:visible="scoringPanelVisible"
           v-model:score="modelScore"
-          :task="mockTask"
+          :main-number="mockTask.mainNumber"
           @submit="onSubmit"
-        ></scoring-panel>
+        ></scoring-panel-with-confirm>
       </div>
       <div class="p-base radius-base fill-blank scroll-auto m-l-base problem-list">
         <base-form size="small" :model="formModel" :items="formItems" :label-width="useVW(62)">
@@ -44,35 +44,7 @@
       </div>
     </div>
   </div>
-  <base-dialog v-model="submitModalVisible" unless :width="useVW(260)" center>
-    <div class="text-center">{{ currentReMarkPaper?.mainNumber }} {{ currentReMarkPaper?.mainName }}</div>
-    <div class="fill-lighter p-t-base text-center">
-      <div class="total-score-title">— 总分 —</div>
-      <div class="m-t-extra-small">
-        <span class="score-value">{{ totalScore || 15 }}</span>
-        <span class="score-unit">分</span>
-      </div>
-    </div>
-    <div class="text-center m-t-base confirm-text">确认提交?</div>
-    <template #footer>
-      <div class="flex items-center justify-between">
-        <el-button class="confirm-button" type="primary" @click="onMarkReMarkPaper">是(Y)</el-button>
-        <el-button class="confirm-button" plain @click="onCancelSubmit">否(N)</el-button>
-      </div>
-    </template>
-  </base-dialog>
-  <base-dialog
-    v-model="previewModalVisible"
-    title="试卷预览"
-    :footer="false"
-    :modal="false"
-    draggable
-    class="preview-dialog"
-  >
-    <div class="preview-content">
-      <img :src="MockImg" alt="" />
-    </div>
-  </base-dialog>
+  <image-preview v-model="previewModalVisible" :url="MockImg"></image-preview>
   <mark-history-list :id="currentViewHistory?.taskId" v-model="visibleHistory"></mark-history-list>
 </template>
 
@@ -88,14 +60,15 @@ import useForm from '@/hooks/useForm'
 import useOptions from '@/hooks/useOptions'
 import useMarkHeader from '@/hooks/useMarkHeader'
 import useMainStore from '@/store/main'
+import useTableCheck from '@/hooks/useTableCheck'
 import MarkHeader from '@/components/shared/MarkHeader.vue'
-import ScoringPanel from '@/components/shared/ScoringPanel.vue'
-import BaseDialog from '@/components/element/BaseDialog.vue'
+import ScoringPanelWithConfirm from '@/components/shared/ScoringPanelWithConfirm.vue'
 import SvgIcon from '@/components/common/SvgIcon.vue'
 import BaseForm from '@/components/element/BaseForm.vue'
 import BaseTable from '@/components/element/BaseTable.vue'
 import MarkHistoryList from '@/components/shared/MarkHistoryList.vue'
 import RightButton from '@/components/shared/RightButton.vue'
+import ImagePreview from '@/components/shared/ImagePreview.vue'
 
 import MockImg from '@/assets/mock/SAMPA-1.jpg'
 import type { SetImgBgOption } from '@/hooks/useSetImgBg'
@@ -123,18 +96,11 @@ const {
 /** 给分板 */
 const scoringPanelVisible = ref<boolean>(true)
 
-/** 提交确认 */
-const submitModalVisible = ref<boolean>(false)
-
 /** 图片预览 */
 const previewModalVisible = ref<boolean>(false)
 
 const modelScore = ref<number[]>([])
 
-const totalScore = computed(() => {
-  return add(...modelScore.value)
-})
-
 const imgOption = computed<SetImgBgOption>(() => {
   return {
     image: MockImg,
@@ -146,55 +112,10 @@ const imgOption = computed<SetImgBgOption>(() => {
 
 const { drawing, dataUrl } = useSetImgBg(imgOption)
 
-const mockTask = ref<ExtractApiResponse<'getMarkingTask'>>({
+const mockTask = ref<{ mainNumber: number }>({
   mainNumber: 1,
-  mainTitle: '第一大题',
-  questionList: [
-    {
-      intervalScore: 0.5,
-      subNumber: '1',
-      totalScore: 8,
-    },
-    {
-      intervalScore: 1,
-      subNumber: '2',
-      totalScore: 20,
-    },
-    {
-      intervalScore: 0.3,
-      subNumber: '3',
-      totalScore: 5,
-    },
-    // {
-    //   intervalScore: 0.4,
-    //   subNumber: '4',
-    //   totalScore: 8,
-    // },
-    // {
-    //   intervalScore: 0.6,
-    //   subNumber: '5',
-    //   totalScore: 6,
-    // },
-  ],
-  secretNumber: '100000001',
-  subjectCode: 'C0001',
-  taskId: 1,
-  taskType: 'FORMAL',
-  url: '',
 })
 
-/** 提交 */
-const onSubmit = () => {
-  scoringPanelVisible.value = false
-  submitModalVisible.value = true
-}
-
-/** 取消提交 */
-const onCancelSubmit = () => {
-  scoringPanelVisible.value = true
-  submitModalVisible.value = false
-}
-
 /** 刷新 */
 const onRefresh = () => {
   onSearch()
@@ -279,11 +200,7 @@ const formItems = computed<EpFormItem[]>(() => [
   { rowKey: 'row-1', slotName: 'search', labelWidth: '10px', colProp: { span: 4 } },
 ])
 
-/** 查询问题卷列表 */
-const { fetch: getReMarkPaperList, result: reMarkPaperList } = useFetch('getReMarkPaperList')
-
-const tableRef = ref<InstanceType<typeof BaseTable>>()
-
+/** 查询重评卷列表 */
 const columns: EpTableColumn[] = [
   { label: '密号', prop: 'secretNumber', width: 70 },
   { label: '评卷员', prop: 'markerName', width: 70 },
@@ -293,37 +210,18 @@ const columns: EpTableColumn[] = [
   { label: '确认给分', prop: 'confirmScore', width: 72 },
 ]
 
-const tableData = computed<RowType[]>(() => {
-  return reMarkPaperList?.value?.result?.map((d, index) => ({ ...d, index })) || []
-})
-
-const currentReMarkPaper = ref<RowType>()
-
-watch(tableData, () => {
-  nextTick(() => {
-    tableRef?.value?.tableRef?.setCurrentRow(tableData.value[0])
-  })
-})
-
-const checkNext = () => {
-  tableRef?.value?.tableRef?.setCurrentRow(
-    tableData.value[((currentReMarkPaper.value?.index || 0) + 1) % tableData.value.length]
-  )
-}
-
-const visibleHistory = ref<boolean>(false)
-const currentViewHistory = ref<RowType>()
-
-/** 表格行双击 */
-const onDbClick = (row: RowType) => {
-  currentViewHistory.value = row
-  visibleHistory.value = true
-}
+const { fetch: getReMarkPaperList, result: reMarkPaperList } = useFetch('getReMarkPaperList')
 
-/** 表格选中 */
-const onCurrentChange = (row: RowType) => {
-  currentReMarkPaper.value = row
-}
+const {
+  tableRef,
+  tableData,
+  current: currentReMarkPaper,
+  currentView: currentViewHistory,
+  next: checkNext,
+  visibleHistory,
+  onDbClick,
+  onCurrentChange,
+} = useTableCheck(reMarkPaperList)
 
 const onSearch = async () => {
   getReMarkPaperList(formModel)
@@ -332,7 +230,7 @@ const onSearch = async () => {
 /** 重评卷打分 */
 const { fetch: markReMarkPaper } = useFetch('markReMarkPaper')
 
-const onMarkReMarkPaper = () => {
+const onSubmit = () => {
   if (currentReMarkPaper.value) {
     markReMarkPaper({ id: currentReMarkPaper.value.id, scores: modelScore.value })
   }
@@ -367,17 +265,4 @@ onOptionInit(onSearch)
     width: 480px;
   }
 }
-.preview-dialog {
-  margin-left: 70%;
-  .preview-content {
-    width: 100%;
-    max-width: 300px;
-    max-height: 600px;
-    img {
-      width: 100%;
-      height: 100%;
-      object-fit: contain;
-    }
-  }
-}
 </style>

+ 204 - 3
src/modules/marking/similar/index.vue

@@ -1,10 +1,211 @@
 <template>
-  <div class="">雷同卷</div>
+  <div class="flex direction-column full">
+    <mark-header :include-operations="['back']" @click="onOperationClick">
+      <el-button class="m-l-base" size="small" type="primary" @click="onConfirmSame(true)">确定雷同</el-button>
+      <el-button class="m-l-base" size="small" type="primary" plain @click="onConfirmSame(false)">否定雷同</el-button>
+      <el-button class="m-l-base m-r-auto" size="small" type="primary" custom-1 @click="onExport"> 导出 </el-button>
+    </mark-header>
+    <div class="flex flex-1 overflow-hidden p-base mark-container">
+      <div class="flex flex-1 direction-column mark-content">
+        <right-button class="next-button" @click="checkNext" />
+        <div class="flex-1 p-base scroll-auto radius-base fill-blank mark-content-paper">
+          <img :src="dataUrl" alt="" class="paper-img" />
+        </div>
+        <div class="flex-1 p-base m-t-base scroll-auto radius-base fill-blank mark-content-paper">
+          <img :src="dataUrl" alt="" class="paper-img" />
+        </div>
+      </div>
+      <div class="p-base radius-base fill-blank scroll-auto m-l-base problem-list">
+        <base-form size="small" :model="formModel" :items="formItems">
+          <template #form-item-search>
+            <el-button type="primary" @click="onSearch">查询</el-button>
+          </template>
+        </base-form>
+        <div class="flex items-center">
+          <span>雷同卷</span>
+          <span>: 共{{ tableData.length }}</span>
+        </div>
+        <base-table
+          ref="tableRef"
+          size="small"
+          :data="tableData"
+          :columns="columns"
+          @current-change="onCurrentChange"
+          @row-dblclick="onDbClick"
+        ></base-table>
+      </div>
+    </div>
+  </div>
+  <mark-history-list :id="currentViewHistory?.secretNumber" v-model="visibleHistory" type="secret"></mark-history-list>
 </template>
 
 <script setup lang="ts" name="MarkingSimilar">
 /** 雷同卷查看 */
-import { reactive, ref } from 'vue'
+import { reactive, ref, computed, watch, nextTick } from 'vue'
+import { ElButton } from 'element-plus'
+import { useSetImgBg } from '@/hooks/useSetImgBg'
+import useFetch from '@/hooks/useFetch'
+import useVW from '@/hooks/useVW'
+import useForm from '@/hooks/useForm'
+import useOptions from '@/hooks/useOptions'
+import useMarkHeader from '@/hooks/useMarkHeader'
+import useMainStore from '@/store/main'
+import useTableCheck from '@/hooks/useTableCheck'
+import BaseForm from '@/components/element/BaseForm.vue'
+import BaseTable from '@/components/element/BaseTable.vue'
+import MarkHistoryList from '@/components/shared/MarkHistoryList.vue'
+import RightButton from '@/components/shared/RightButton.vue'
+import MarkHeader from '@/components/shared/MarkHeader.vue'
+
+import MockImg from '@/assets/mock/SAMPA-1.jpg'
+
+import type { SetImgBgOption } from '@/hooks/useSetImgBg'
+import type { ExtractMultipleApiResponse, ExtractApiParams } from 'api-type'
+import type { MarkHeaderInstance, EpFormItem, EpTableColumn } from 'global-type'
+
+type RowType = ExtractMultipleApiResponse<'getSimilarPaperList'> & { index: number }
+
+const { onBack } = useMarkHeader()
+
+const imgOption = computed<SetImgBgOption>(() => {
+  return {
+    image: MockImg,
+    immediate: true,
+  }
+})
+
+const { drawing, dataUrl } = useSetImgBg(imgOption)
+
+type OperationClick = MarkHeaderInstance['onClick']
+
+type OperationType = Parameters<Exclude<OperationClick, undefined>>[0]['type']
+
+const operationHandles: Partial<Record<OperationType, (...args: any) => void>> = {
+  back: onBack,
+}
+
+const onOperationClick: OperationClick = ({ type, value }) => {
+  operationHandles[type]?.(value)
+}
+
+const formModel = reactive<ExtractApiParams<'getSimilarPaperList'>>({
+  mainNumber: void 0,
+  status: 'INITIAL',
+  pageNumber: 1,
+  pageSize: 9999999,
+})
+
+const mainStore = useMainStore()
+
+const { mainQuestionList, dataModel, changeModelValue, onOptionInit } = useOptions(['question'], {
+  subject: mainStore.myUserInfo?.subjectCode,
+})
+
+watch(dataModel, () => {
+  formModel.mainNumber = dataModel.question
+})
+
+const { defineColumn, _ } = useForm()
+
+const span10 = defineColumn(_, '', { span: 10 })
+
+const formItems = computed<EpFormItem[]>(() => [
+  span10({
+    rowKey: 'row-1',
+    label: '大题',
+    prop: 'mainNumber',
+    slotType: 'select',
+    slot: { options: mainQuestionList.value, onChange: changeModelValue('question') },
+  }),
+  span10({
+    rowKey: 'row-1',
+    label: '雷同判定',
+    labelWidth: useVW(72),
+    prop: 'status',
+    slotType: 'select',
+    slot: {
+      options: [
+        { label: '全部', value: 'INITIAL' },
+        { label: '是', value: 'SAME' },
+        { label: '否', value: 'NOT_SAME' },
+      ],
+    },
+  }),
+  { rowKey: 'row-1', slotName: 'search', labelWidth: '10px', colProp: { span: 4 } },
+])
+
+/** 雷同卷卷列表 */
+const columns: EpTableColumn<RowType>[] = [
+  { label: '密号', prop: 'secretNumber' },
+  { label: '雷同密号', prop: 'sameSecretNumber' },
+  { label: '评卷员', prop: 'markerName' },
+  {
+    label: '雷同判定',
+    prop: 'status',
+    formatter(row) {
+      return { INITIAL: '', SAME: '雷同', NOT_SAME: '不雷同' }[row.status]
+    },
+  },
+]
+
+const { fetch: getSimilarPaperList, result: similarPaperList } = useFetch('getSimilarPaperList')
+
+const {
+  tableRef,
+  tableData,
+  current: currentSamePaper,
+  currentView: currentViewHistory,
+  next: checkNext,
+  visibleHistory,
+  onDbClick,
+  onCurrentChange,
+} = useTableCheck(similarPaperList)
+
+const onSearch = async () => {
+  getSimilarPaperList(formModel)
+}
+
+/** 是否雷同 */
+const { fetch: confirmIsSimilar } = useFetch('confirmIsSimilar')
+
+const onConfirmSame = (same: boolean) => {
+  if (currentSamePaper.value) {
+    confirmIsSimilar({ id: currentSamePaper.value.id, same })
+  }
+}
+
+/** 导出 */
+const onExport = () => {
+  useFetch('exportSimilarPaper').fetch(formModel)
+}
+
+onOptionInit(onSearch)
 </script>
 
-<style scoped lang="scss"></style>
+<style scoped lang="scss">
+.mark-container {
+  .mark-content {
+    position: relative;
+    .preview {
+      position: absolute;
+      cursor: pointer;
+      top: 10px;
+      right: 20px;
+      font-size: 24px;
+    }
+    .next-button {
+      position: absolute;
+      right: -20px;
+      top: 300px;
+    }
+    .mark-content-paper {
+      img {
+        max-width: 100%;
+      }
+    }
+  }
+  .problem-list {
+    width: 580px;
+  }
+}
+</style>

+ 13 - 47
src/modules/marking/training-record/index.vue

@@ -35,11 +35,12 @@
 import { reactive, ref, computed, watch, nextTick } from 'vue'
 import { useSetImgBg } from '@/hooks/useSetImgBg'
 import useFetch from '@/hooks/useFetch'
+import useMarkHeader from '@/hooks/useMarkHeader'
+import useTableCheck from '@/hooks/useTableCheck'
 import MarkHeader from '@/components/shared/MarkHeader.vue'
 import BaseTable from '@/components/element/BaseTable.vue'
 import MarkHistoryList from '@/components/shared/MarkHistoryList.vue'
 import RightButton from '@/components/shared/RightButton.vue'
-import useMarkHeader from '@/hooks/useMarkHeader'
 
 import MockImg from '@/assets/mock/SAMPA-1.jpg'
 import type { SetImgBgOption } from '@/hooks/useSetImgBg'
@@ -101,9 +102,6 @@ const onOperationClick: OperationClick = ({ type, value }) => {
 }
 
 /** 查询培训记录 */
-const { fetch: viewTrainingRecord, result: trainingRecordList } = useFetch('viewTrainingRecord')
-
-const tableRef = ref<InstanceType<typeof BaseTable>>()
 
 const taskTypeMap: Record<string, string> = {
   SAMPLE_A: 'A',
@@ -123,37 +121,18 @@ const columns: EpTableColumn<RowType>[] = [
   },
 ]
 
-const tableData = computed<RowType[]>(() => {
-  return trainingRecordList?.value?.map((d, index) => ({ ...d, index })) || []
-})
-
-const currentTrainingRecord = ref<RowType>()
-
-watch(tableData, () => {
-  nextTick(() => {
-    tableRef?.value?.tableRef?.setCurrentRow(tableData.value[0])
-  })
-})
-
-const checkNext = () => {
-  tableRef?.value?.tableRef?.setCurrentRow(
-    tableData.value[((currentTrainingRecord.value?.index || 0) + 1) % tableData.value.length]
-  )
-}
-
-const visibleHistory = ref<boolean>(false)
-const currentViewHistory = ref<RowType>()
-
-/** 表格行双击 */
-const onDbClick = (row: RowType) => {
-  currentViewHistory.value = row
-  visibleHistory.value = true
-}
+const { fetch: viewTrainingRecord, result: trainingRecordList } = useFetch('viewTrainingRecord')
 
-/** 表格选中 */
-const onCurrentChange = (row: RowType) => {
-  currentTrainingRecord.value = row
-}
+const {
+  tableRef,
+  tableData,
+  current,
+  currentView: currentViewHistory,
+  next: checkNext,
+  visibleHistory,
+  onDbClick,
+  onCurrentChange,
+} = useTableCheck(trainingRecordList)
 
 viewTrainingRecord()
 </script>
@@ -184,17 +163,4 @@ viewTrainingRecord()
     width: 580px;
   }
 }
-.preview-dialog {
-  margin-left: 70%;
-  .preview-content {
-    width: 100%;
-    max-width: 300px;
-    max-height: 600px;
-    img {
-      width: 100%;
-      height: 100%;
-      object-fit: contain;
-    }
-  }
-}
 </style>

+ 14 - 50
src/modules/marking/view-sample/index.vue

@@ -35,11 +35,12 @@
 import { reactive, ref, computed, watch, nextTick } from 'vue'
 import { useSetImgBg } from '@/hooks/useSetImgBg'
 import useFetch from '@/hooks/useFetch'
+import useMarkHeader from '@/hooks/useMarkHeader'
+import useTableCheck from '@/hooks/useTableCheck'
 import MarkHeader from '@/components/shared/MarkHeader.vue'
 import BaseTable from '@/components/element/BaseTable.vue'
 import MarkHistoryList from '@/components/shared/MarkHistoryList.vue'
 import RightButton from '@/components/shared/RightButton.vue'
-import useMarkHeader from '@/hooks/useMarkHeader'
 
 import MockImg from '@/assets/mock/SAMPA-1.jpg'
 import type { SetImgBgOption } from '@/hooks/useSetImgBg'
@@ -100,11 +101,7 @@ const onOperationClick: OperationClick = ({ type, value }) => {
   operationHandles[type]?.(value)
 }
 
-/** 查询培训记录 */
-const { fetch: viewSamplePaper, result: samplePaperList } = useFetch('viewSamplePaper')
-
-const tableRef = ref<InstanceType<typeof BaseTable>>()
-
+/** 样卷 */
 const columns: EpTableColumn<RowType>[] = [
   { label: '密号', prop: 'secretNumber' },
   { label: '题目', prop: 'mainTitle' },
@@ -113,38 +110,18 @@ const columns: EpTableColumn<RowType>[] = [
   { label: '提交时间', prop: 'markTime' },
   { label: '给分说明', prop: 'description' },
 ]
+const { fetch: viewSamplePaper, result: samplePaperList } = useFetch('viewSamplePaper')
 
-const tableData = computed<RowType[]>(() => {
-  return samplePaperList?.value?.map((d, index) => ({ ...d, index })) || []
-})
-
-const currentSamplePaper = ref<RowType>()
-
-watch(tableData, () => {
-  nextTick(() => {
-    tableRef?.value?.tableRef?.setCurrentRow(tableData.value[0])
-  })
-})
-
-const checkNext = () => {
-  tableRef?.value?.tableRef?.setCurrentRow(
-    tableData.value[((currentSamplePaper.value?.index || 0) + 1) % tableData.value.length]
-  )
-}
-
-const visibleHistory = ref<boolean>(false)
-const currentViewHistory = ref<RowType>()
-
-/** 表格行双击 */
-const onDbClick = (row: RowType) => {
-  currentViewHistory.value = row
-  visibleHistory.value = true
-}
-
-/** 表格选中 */
-const onCurrentChange = (row: RowType) => {
-  currentSamplePaper.value = row
-}
+const {
+  tableRef,
+  tableData,
+  current,
+  currentView: currentViewHistory,
+  next: checkNext,
+  visibleHistory,
+  onDbClick,
+  onCurrentChange,
+} = useTableCheck(samplePaperList)
 
 viewSamplePaper()
 </script>
@@ -175,17 +152,4 @@ viewSamplePaper()
     width: 580px;
   }
 }
-.preview-dialog {
-  margin-left: 70%;
-  .preview-content {
-    width: 100%;
-    max-width: 300px;
-    max-height: 600px;
-    img {
-      width: 100%;
-      height: 100%;
-      object-fit: contain;
-    }
-  }
-}
 </style>

+ 300 - 3
src/modules/monitor/system-check/index.vue

@@ -1,10 +1,307 @@
 <template>
-  <div class="">系统抽查卷</div>
+  <div class="flex direction-column full">
+    <mark-header :exclude-operations="['remark', 'problem', 'example', 'delete', 'bookmark']" @click="onOperationClick">
+      <template v-if="!hideHeaderButtons">
+        <el-button class="m-l-base" size="small" type="primary" plain @click="onEditScore">修改给分</el-button>
+        <el-button class="m-l-base m-r-auto" size="small" type="primary" @click="onSendBack">打回</el-button>
+      </template>
+    </mark-header>
+    <div class="flex flex-1 overflow-hidden p-base mark-container">
+      <div
+        class="flex flex-1 direction-column radius-base fill-blank mark-content"
+        :class="{ 'text-center': center }"
+        :style="{ 'background-color': backgroundColor }"
+      >
+        <span class="preview" @click="onPreview">
+          <svg-icon name="preview"></svg-icon>
+        </span>
+        <right-button class="next-button" @click="checkNext" />
+        <div class="flex-1 p-base scroll-auto mark-content-paper">
+          <img :src="dataUrl" alt="" class="paper-img" :style="{ 'background-color': frontColor }" />
+        </div>
+      </div>
+      <div class="p-base radius-base fill-blank scroll-auto m-l-base problem-list">
+        <base-form size="small" :model="formModel" :items="formItems" :label-width="useVW(40)">
+          <template #form-item-search>
+            <el-button type="primary" @click="onSearch">查询</el-button>
+          </template>
+        </base-form>
+        <div class="flex items-center p-l-base">
+          <span>系统抽查卷</span>
+          <span>: 共{{ tableData.length }}</span>
+        </div>
+        <base-table
+          ref="tableRef"
+          size="small"
+          :data="tableData"
+          :columns="columns"
+          @current-change="onCurrentChange"
+          @row-dblclick="onDbClick"
+        ></base-table>
+      </div>
+    </div>
+  </div>
+  <image-preview v-model="previewModalVisible" :url="MockImg"></image-preview>
+  <scoring-panel-with-confirm
+    v-model:visible="scoringPanelVisible"
+    v-model:score="modelScore"
+    modal
+    :toggle-modal="false"
+    :main-number="mockTask.mainNumber"
+    @submit="onSubmit"
+  ></scoring-panel-with-confirm>
+  <mark-history-list :id="currentViewHistory?.taskId" v-model="visibleHistory"></mark-history-list>
+  <send-back-mark :id="currentSystemCheckPaper?.id" v-model="sendBackVisible" @rejected="onRejected"></send-back-mark>
 </template>
 
 <script setup lang="ts" name="SystemCheck">
 /** 系统抽查卷 */
-import { reactive, ref } from 'vue'
+import { reactive, ref, computed, watch, nextTick } from 'vue'
+import { ElButton } from 'element-plus'
+import { add } from '@/utils/common'
+import { useSetImgBg } from '@/hooks/useSetImgBg'
+import useVW from '@/hooks/useVW'
+import useFetch from '@/hooks/useFetch'
+import useForm from '@/hooks/useForm'
+import useOptions from '@/hooks/useOptions'
+import useMarkHeader from '@/hooks/useMarkHeader'
+import useMainStore from '@/store/main'
+import useTableCheck from '@/hooks/useTableCheck'
+import MarkHeader from '@/components/shared/MarkHeader.vue'
+import SvgIcon from '@/components/common/SvgIcon.vue'
+import BaseForm from '@/components/element/BaseForm.vue'
+import BaseTable from '@/components/element/BaseTable.vue'
+import MarkHistoryList from '@/components/shared/MarkHistoryList.vue'
+import SendBackMark from '@/components/shared/SendBackMark.vue'
+import RightButton from '@/components/shared/RightButton.vue'
+import ScoringPanelWithConfirm from '@/components/shared/ScoringPanelWithConfirm.vue'
+import ImagePreview from '@/components/shared/ImagePreview.vue'
+
+import MockImg from '@/assets/mock/SAMPA-1.jpg'
+import type { SetImgBgOption } from '@/hooks/useSetImgBg'
+import type { ExtractApiParams, ExtractApiResponse, ExtractMultipleApiResponse } from 'api-type'
+import type { MarkHeaderInstance, EpFormItem, EpTableColumn } from 'global-type'
+
+type RowType = ExtractMultipleApiResponse<'getSystemSpotList'> & { index: number }
+
+const {
+  rotate,
+  scale,
+  center,
+  frontColor,
+  backgroundColor,
+  onBack,
+  onScaleUp,
+  onScaleDown,
+  onCenter,
+  onRotate,
+  setBackgroundColor,
+  setFrontColor,
+  onViewStandard,
+} = useMarkHeader()
+
+/** 给分板 */
+const scoringPanelVisible = ref<boolean>(false)
+
+/** 图片预览 */
+const previewModalVisible = ref<boolean>(false)
+
+/** 打回弹窗 */
+const sendBackVisible = ref<boolean>(false)
+
+const modelScore = ref<number[]>([])
+
+const imgOption = computed<SetImgBgOption>(() => {
+  return {
+    image: MockImg,
+    immediate: true,
+    rotate: rotate.value,
+    scale: scale.value,
+  }
+})
+
+const { drawing, dataUrl } = useSetImgBg(imgOption)
+
+const mockTask = ref<{ mainNumber: number }>({
+  mainNumber: 1,
+})
+
+/** 刷新 */
+const onRefresh = () => {
+  onSearch()
+}
+
+/** 预览试卷 */
+const onPreview = () => {
+  previewModalVisible.value = true
+}
+
+/** 修改给分 */
+const onEditScore = () => {
+  scoringPanelVisible.value = true
+}
+
+/** 打回 */
+const onSendBack = () => {
+  sendBackVisible.value = true
+}
+
+/** 打回成功 */
+const onRejected = () => {
+  checkNext()
+}
+
+type OperationClick = MarkHeaderInstance['onClick']
+
+type OperationType = Parameters<Exclude<OperationClick, undefined>>[0]['type']
+
+const operationHandles: Partial<Record<OperationType, (...args: any) => void>> = {
+  back: onBack,
+  'scale-up': onScaleUp,
+  'scale-down': onScaleDown,
+  center: onCenter,
+  rotate: onRotate,
+  'front-color': setFrontColor,
+  'background-color': setBackgroundColor,
+  refresh: onRefresh,
+  standard: onViewStandard,
+}
+
+const onOperationClick: OperationClick = ({ type, value }) => {
+  operationHandles[type]?.(value)
+}
+
+const formModel = reactive<ExtractApiParams<'getSystemSpotList'>>({
+  mainNumber: void 0,
+  status: 'INITIAL',
+  level: '',
+  pageNumber: 1,
+  pageSize: 9999999,
+})
+
+const hideHeaderButtons = computed(() => {
+  return formModel.status === 'REJECT'
+})
+
+const mainStore = useMainStore()
+
+const { mainQuestionList, dataModel, changeModelValue, onOptionInit } = useOptions(['question'], {
+  subject: mainStore.myUserInfo?.subjectCode,
+})
+
+watch(dataModel, () => {
+  formModel.mainNumber = dataModel.question
+})
+
+const { defineColumn, _ } = useForm()
+
+const span10 = defineColumn(_, '', { span: 10 })
+
+const formItems = computed<EpFormItem[]>(() => [
+  span10({
+    rowKey: 'row-1',
+    label: '大题',
+    prop: 'mainNumber',
+    slotType: 'select',
+    slot: { options: mainQuestionList.value, onChange: changeModelValue('question') },
+  }),
+  span10({
+    rowKey: 'row-1',
+    label: '档次',
+    labelWidth: useVW(40),
+    prop: 'level',
+    slotType: 'select',
+    slot: {
+      options: [{ label: '全部', value: '' }, ...[1, 2, 3, 4, 5].map((n) => ({ label: n, value: `LEVEL_${n}` }))],
+    },
+  }),
+  { rowKey: 'row-1', slotName: 'search', labelWidth: '10px', colProp: { span: 4 } },
+  span10({
+    rowKey: 'row-2',
+    label: '类型',
+    prop: 'status',
+    slotType: 'select',
+    slot: {
+      options: [
+        { label: '未浏览', value: 'INITIAL' },
+        { label: '已浏览', value: 'VIEWED' },
+        { label: '已给分', value: 'GRADED' },
+        { label: '已打回', value: 'REJECT' },
+      ],
+    },
+  }),
+])
+
+/** 系统抽查卷列表 */
+
+const columns: EpTableColumn[] = [
+  { label: '密号', prop: 'secretNumber' },
+  { label: '评卷员', prop: 'markerName' },
+  { label: '评卷员给分', prop: 'markScore' },
+  { label: '组长给分', prop: 'unknown', width: 70 },
+  { label: '评卷时间', prop: 'markTime', width: 45 },
+  { label: '抽查次数', prop: 'unknown', width: 45 },
+]
+
+const { fetch: getSystemSpotList, result: systemSpotList } = useFetch('getSystemSpotList')
+
+const {
+  tableRef,
+  tableData,
+  current: currentSystemCheckPaper,
+  currentView: currentViewHistory,
+  next: checkNext,
+  visibleHistory,
+  onDbClick,
+  onCurrentChange,
+} = useTableCheck(systemSpotList)
+
+watch(currentSystemCheckPaper, () => {
+  if (currentSystemCheckPaper.value) {
+    useFetch('viewSystemSpotPaper').fetch({ id: currentSystemCheckPaper.value.id })
+  }
+})
+
+const onSearch = async () => {
+  getSystemSpotList(formModel)
+}
+
+/** 系统抽查卷打分 */
+const { fetch: markSystemSpotPaper } = useFetch('markSystemSpotPaper')
+
+const onSubmit = () => {
+  if (currentSystemCheckPaper.value) {
+    markSystemSpotPaper({ id: currentSystemCheckPaper.value.id, scores: modelScore.value })
+  }
+}
+
+onOptionInit(onSearch)
 </script>
 
-<style scoped lang="scss"></style>
+<style scoped lang="scss">
+.mark-container {
+  .mark-content {
+    position: relative;
+    .preview {
+      position: absolute;
+      cursor: pointer;
+      top: 10px;
+      right: 20px;
+      font-size: 24px;
+    }
+    .next-button {
+      position: absolute;
+      right: -20px;
+      top: 300px;
+    }
+    .mark-content-paper {
+      img {
+        max-width: 100%;
+      }
+    }
+  }
+  .problem-list {
+    width: 480px;
+  }
+}
+</style>

+ 8 - 8
src/modules/monitor/training-monitoring/hooks/useFormFilter.ts

@@ -12,17 +12,17 @@ const useFormFilter = () => {
 
   const model = reactive<ExtractApiParams<'getTrainingMonitor'> & { diffShow: string[] }>({
     subjectCode: '',
-    questionMainNumber: void 0,
+    mainNumber: void 0,
     markingGroupNumber: void 0,
-    paperType: 'SAMPLE_A',
-    forceExamineGroup: void 0,
+    markStage: 'SAMPLE_A',
+    forceGroupNumber: void 0,
     diffShow: [],
   })
 
   const forceGroupListParams = computed(() => {
     return {
       subjectCode: model.subjectCode,
-      questionMainNumber: model.questionMainNumber,
+      questionMainNumber: model.mainNumber,
     }
   })
 
@@ -37,8 +37,8 @@ const useFormFilter = () => {
   watch(
     forceGroupListParams,
     () => {
-      if (model.subjectCode && model.questionMainNumber) {
-        getForceCheckGroupList({ subjectCode: model.subjectCode, mainNumber: model.questionMainNumber })
+      if (model.subjectCode && model.mainNumber) {
+        getForceCheckGroupList({ subjectCode: model.subjectCode, mainNumber: model.mainNumber })
       }
     },
     { deep: true }
@@ -54,7 +54,7 @@ const useFormFilter = () => {
     dataModel,
     () => {
       model.subjectCode = dataModel.subject || ''
-      model.questionMainNumber = dataModel.question
+      model.mainNumber = dataModel.question
       model.markingGroupNumber = dataModel.group
     },
     { deep: true }
@@ -108,7 +108,7 @@ const useFormFilter = () => {
           ],
         },
       }),
-      model.paperType === 'FORCE'
+      model.markStage === 'FORCE'
         ? TwoRowSpan5({
             label: '强制考核组',
             slotType: 'select',

+ 10 - 0
src/router/marking.ts

@@ -172,6 +172,16 @@ const markingRoutes: RouteRecordRaw[] = [
       sort: 1,
     },
   },
+  {
+    name: 'SystemCheck',
+    path: '/system-check',
+    component: () => import('@/modules/monitor/system-check/index.vue'),
+    meta: {
+      label: '系统抽查卷',
+      menu: true,
+      menuId: 'marking-system_check',
+    },
+  },
 ]
 
 export default markingRoutes

+ 0 - 10
src/router/monitor.ts

@@ -37,16 +37,6 @@ const routes: RouteRecordRaw[] = [
       menuId: 'training-detail',
     },
   },
-  {
-    name: 'SystemCheck',
-    path: '/system-check',
-    component: () => import('@/modules/monitor/system-check/index.vue'),
-    meta: {
-      label: '系统抽查卷',
-      menu: false,
-      menuId: 'system_check',
-    },
-  },
 ]
 
 export default routes

+ 98 - 3
types/api.d.ts

@@ -150,6 +150,8 @@ declare module 'api-type' {
       MultipleResult<ExpertPickListItem>
     >
 
+    type DeletePaper = BaseDefine<{ id?: number }>
+
     export interface ApiMap {
       /** AB培训卷 */
       getSampleList: GetSampleList
@@ -165,6 +167,8 @@ declare module 'api-type' {
       getExpertAssessList: GetExpertAssessList
       /** 专家挑选卷 */
       getExpertPickList: GetExpertPickList
+      /** 删除 */
+      deletePaper: DeletePaper
     }
   }
 
@@ -567,6 +571,37 @@ declare module 'api-type' {
 
     type ViewSamplePaper = BaseDefine<null, SamplePaper[]>
 
+    interface SamePaperListItem {
+      examNumber: string
+      filePath: string
+      id: number
+      mainNumber: number
+      markerName: string
+      sameExamNumber: string
+      sameFilePath: string
+      sameSecretNumber: string
+      secretNumber: number
+      status: 'INITIAL' | 'NOT_SAME' | 'SAME'
+      taskId: number
+    }
+
+    type GetSimilarPaperList = BaseDefine<
+      MultipleQuery<{
+        mainNumber?: number
+        status: 'INITIAL' | 'NOT_SAME' | 'SAME'
+      }>,
+      MultipleResult<SamePaperListItem>
+    >
+
+    type ConfirmIsSimilar = BaseDefine<{ id?: number; same: boolean }>
+
+    type ExportSimilarPaper = BaseDefine<
+      MultipleQuery<{
+        mainNumber?: number
+        status: 'INITIAL' | 'NOT_SAME' | 'SAME'
+      }>
+    >
+
     export interface ApiMap {
       /** 自定义查询 - 抽查 */
       getCustomQueryTasks: GetCustomQueryTasks
@@ -610,6 +645,12 @@ declare module 'api-type' {
       viewTrainingRecord: ViewTrainingRecord
       /** 查看样卷 */
       viewSamplePaper: ViewSamplePaper
+      /** 雷同卷列表 */
+      getSimilarPaperList: GetSimilarPaperList
+      /** 是否雷同 */
+      confirmIsSimilar: ConfirmIsSimilar
+      /** 导出雷同 */
+      exportSimilarPaper: ExportSimilarPaper
     }
   }
 
@@ -664,6 +705,18 @@ declare module 'api-type' {
     /** 获取大题列表 */
     type GetMainQuestionList = BaseDefine<{ subjectCode: string }, MainQuestionListItem[]>
 
+    interface SubQuestionStruct {
+      intervalScore: number
+      score: number
+      subNumber: number
+      totalScore: number
+    }
+    /** 获取大题评卷结构 */
+    type GetQuestionStruct = BaseDefine<
+      { mainNumber?: number },
+      { mainNumber: number; mainTitle: string; questionList: SubQuestionStruct[] }
+    >
+
     /** 大题设置 (新增/编辑) */
     interface MainQuestionMeta {
       /** 大题号 */
@@ -720,6 +773,8 @@ declare module 'api-type' {
       editMainQuestion: EditMainQuestion
       getMainQuestionInfo: GetMainQuestionInfo
       getMainQuestionList: GetMainQuestionList
+      /** 获取大题评卷结构 */
+      getQuestionStruct: GetQuestionStruct
     }
   }
 
@@ -1269,10 +1324,10 @@ declare module 'api-type' {
     /** 培训监控 */
     interface TrainingMonitorParams {
       subjectCode: string
-      questionMainNumber?: number
+      mainNumber?: number
       markingGroupNumber?: number
-      paperType: 'SAMPLE_A' | 'SAMPLE_B' | 'FORCE'
-      forceExamineGroup?: number
+      markStage: 'SAMPLE_A' | 'SAMPLE_B' | 'FORCE'
+      forceGroupNumber?: number
     }
     interface TrainingMonitorResponse {
       /** 平均分 */
@@ -1305,6 +1360,38 @@ declare module 'api-type' {
       questionMainNumber: number
     }
     type GetPersonalStatistic = BaseDefine<{ startTime: string; endTime: string; markerId?: number }, PersonalStatistic>
+
+    interface SystemSpotListItem {
+      examNumber: string
+      filePath: string
+      id: number
+      mainNumber: number
+      markScore: number
+      markTime: string
+      markerName: string
+      secretNumber: number
+      status: string
+      taskId: number
+    }
+
+    type GetSystemSpotList = BaseDefine<
+      MultipleQuery<{
+        level: 'LEVEL_1' | 'LEVEL_2' | 'LEVEL_3' | 'LEVEL_4' | 'LEVEL_5' | ''
+        status: 'INITIAL' | 'VIEWED' | 'GRADED' | 'REJECT'
+        mainNumber?: number
+      }>,
+      MultipleResult<SystemSpotListItem>
+    >
+
+    /** 系统抽查卷打分 */
+    type MarkSystemSpotPaper = BaseDefine<{ id?: number; scores: number[] }>
+
+    /** 系统抽查卷打回 */
+    type RejectSystemSpotPaper = BaseDefine<{ description: string; id: number; reason: string }>
+
+    /** 系统抽查卷浏览 */
+    type ViewSystemSpotPaper = BaseDefine<{ id?: number }>
+
     interface ApiMap {
       /** 质量统计-自查一致性分析 */
       selfCheckAnalysis: SelfCheckAnalysis
@@ -1334,6 +1421,14 @@ declare module 'api-type' {
       getTrainingMonitor: GetTrainingMonitor
       /** 个人统计 */
       getPersonalStatistic: GetPersonalStatistic
+      /** 系统抽查卷 */
+      getSystemSpotList: GetSystemSpotList
+      /** 系统抽查卷打分 */
+      markSystemSpotPaper: MarkSystemSpotPaper
+      /** 系统抽查卷打回 */
+      rejectSystemSpotPaper: RejectSystemSpotPaper
+      /** 系统抽查卷浏览 */
+      viewSystemSpotPaper: ViewSystemSpotPaper
     }
   }
 }