|
@@ -0,0 +1,516 @@
|
|
|
+<template>
|
|
|
+ <div class="flex scoring-panel" :class="[getClass('modal-panel', 'sticky'), { mini: !props.large && !props.modal }]">
|
|
|
+ <toggle-dialog-render v-if="isMarkerPage">
|
|
|
+ <svg-icon name="question"></svg-icon>
|
|
|
+ <span class="m-l-mini" style="max-width: 200px"
|
|
|
+ >{{ question.mainNumber }} - {{ question.subNumber }}{{ question.mainTitle }}</span
|
|
|
+ >
|
|
|
+ <!-- <template #callback>
|
|
|
+ <div class="grid radius-base dialog-name">
|
|
|
+ <div class="text-center">
|
|
|
+ <p>{{ question.mainNumber }} - {{ question.subNumber }}</p>
|
|
|
+ <p>{{ question.mainTitle }}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template> -->
|
|
|
+ </toggle-dialog-render>
|
|
|
+ <div class="flex flex-1 flex-wrap score-list" :class="{ 'dialog-score-list': dialogMode }">
|
|
|
+ <div
|
|
|
+ v-for="scoreItem in scoreList"
|
|
|
+ :key="scoreItem"
|
|
|
+ class="score-span"
|
|
|
+ :class="[
|
|
|
+ getClass('m-b-mini'),
|
|
|
+ { active: parseFloat(`${currentScore}`) === parseFloat(`${scoreItem}`), mini: !props.large && !props.modal },
|
|
|
+ ]"
|
|
|
+ @click="onSetScore(scoreItem)"
|
|
|
+ >
|
|
|
+ {{ scoreItem }}
|
|
|
+ </div>
|
|
|
+ <toggle-dialog-render>
|
|
|
+ <div class="flex items-center">
|
|
|
+ <div class="flex items-center score-result" :class="{ 'score-valid-fail': scoreValidFail }">
|
|
|
+ <div class="flex-1 text-center" style="font-size: 12px">给分</div>
|
|
|
+ <input
|
|
|
+ ref="refInput1"
|
|
|
+ class="flex-1 text-center score-input score-num"
|
|
|
+ :value="currentScore"
|
|
|
+ :readonly="props.loading"
|
|
|
+ @focus="onInputFocus"
|
|
|
+ @blur="onBlur"
|
|
|
+ @keydown="onValidScoreDebounce"
|
|
|
+ @input="scoreChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <el-button
|
|
|
+ v-if="!!props.showConfirmBtn"
|
|
|
+ :disabled="!props.allowSubmit || buttonDisabled"
|
|
|
+ size="small"
|
|
|
+ type="primary"
|
|
|
+ style="min-width: 44px; margin-left: 5px; margin-bottom: 8px"
|
|
|
+ @click="confirmWithBtn"
|
|
|
+ >{{ buttonText }}</el-button
|
|
|
+ >
|
|
|
+ <el-button
|
|
|
+ v-if="!!props.showAllPaperBtn"
|
|
|
+ v-permBtn="'marking-allpaper'"
|
|
|
+ type="primary"
|
|
|
+ link
|
|
|
+ style="min-width: 44px; margin-left: 20px; margin-bottom: 8px"
|
|
|
+ @click="onViewPapers"
|
|
|
+ >调全卷</el-button
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </toggle-dialog-render>
|
|
|
+ </div>
|
|
|
+ <toggle-dialog-render dialog>
|
|
|
+ <div class="grid dialog-score" :class="{ 'score-valid-fail': scoreValidFail }">
|
|
|
+ <input
|
|
|
+ ref="refInput2"
|
|
|
+ class="score-input radius-base"
|
|
|
+ :value="currentScore"
|
|
|
+ :readonly="props.loading"
|
|
|
+ @focus="onInputFocus"
|
|
|
+ @blur="onBlur"
|
|
|
+ @keydown="onValidScoreDebounce"
|
|
|
+ @input="scoreChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </toggle-dialog-render>
|
|
|
+ <toggle-dialog-render v-if="!props.cannotToggle">
|
|
|
+ <!-- <svg-icon
|
|
|
+ class="pointer toggle-icon"
|
|
|
+ :class="{ visible: props.toggleModal }"
|
|
|
+ name="toggle-panel"
|
|
|
+ @click="onToggleClick"
|
|
|
+ ></svg-icon> -->
|
|
|
+ <div class="toggle-icon-box" :class="{ visible: props.toggleModal }" @click="onToggleClick">
|
|
|
+ <img src="../../assets/images/tanchu.png" />
|
|
|
+ <p>弹出</p>
|
|
|
+ </div>
|
|
|
+ </toggle-dialog-render>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts" name="ScoringPanelItem">
|
|
|
+import { watch, computed, ref, nextTick, withDefaults, defineComponent, useSlots } from 'vue'
|
|
|
+import SvgIcon from '@/components/common/SvgIcon.vue'
|
|
|
+import useVModel from '@/hooks/useVModel'
|
|
|
+import { getNumbers } from '@/utils/common'
|
|
|
+import { debounce } from 'lodash-es'
|
|
|
+import { useRoute } from 'vue-router'
|
|
|
+import { ElButton } from 'element-plus'
|
|
|
+import useMarkStore from '@/store/mark'
|
|
|
+
|
|
|
+const markStore = useMarkStore()
|
|
|
+const route = useRoute()
|
|
|
+const isMarkerPage = computed(() => {
|
|
|
+ return route.path === '/marking/mark'
|
|
|
+})
|
|
|
+
|
|
|
+interface QuestionInfo {
|
|
|
+ mainNumber: number | string
|
|
|
+ mainTitle: string
|
|
|
+ subNumber: number
|
|
|
+ totalScore: number
|
|
|
+ intervalScore: number
|
|
|
+ // 当前试题最小评卷时间(秒)
|
|
|
+ markSpeedLimit?: number
|
|
|
+}
|
|
|
+
|
|
|
+const props = withDefaults(
|
|
|
+ defineProps<{
|
|
|
+ /** 弹窗模式? */
|
|
|
+ modal?: boolean
|
|
|
+ /** 是否可以切换显示模式 */
|
|
|
+ toggleModal?: boolean
|
|
|
+ /** 分值 */
|
|
|
+ score?: number | string | undefined
|
|
|
+ /** active 当前正在评分 */
|
|
|
+ active: boolean
|
|
|
+ scoreValidFail: boolean | undefined
|
|
|
+ question: QuestionInfo
|
|
|
+ large?: boolean
|
|
|
+ allowSubmit: boolean
|
|
|
+ showConfirmBtn?: boolean
|
|
|
+ showAllPaperBtn?: boolean
|
|
|
+ cannotToggle?: boolean
|
|
|
+ id?: any
|
|
|
+ loading?: any
|
|
|
+ }>(),
|
|
|
+ {
|
|
|
+ modal: false,
|
|
|
+ toggleModal: true,
|
|
|
+ score: void 0,
|
|
|
+ scoreValidFail: false,
|
|
|
+ showAllPaperBtn: false,
|
|
|
+ large: true,
|
|
|
+ cannotToggle: false,
|
|
|
+ id: null,
|
|
|
+ loading: false,
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
+const emit = defineEmits(['focused', 'blur', 'toggle-click', 'enter', 'update:score', 'view-papers'])
|
|
|
+
|
|
|
+const dialogMode = ref<boolean>(props.modal)
|
|
|
+
|
|
|
+const currentScore = useVModel(props, 'score')
|
|
|
+
|
|
|
+const scoreValidFail = useVModel(props, 'scoreValidFail')
|
|
|
+
|
|
|
+const ToggleDialogRender = defineComponent({
|
|
|
+ name: 'DialogHideNode',
|
|
|
+ inheritAttrs: false,
|
|
|
+ props: {
|
|
|
+ dialog: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ render() {
|
|
|
+ const slots = useSlots()
|
|
|
+ const defaultSlot = slots?.default?.()
|
|
|
+ const callBackSlot = slots?.callback?.()
|
|
|
+ return (this.dialog ? dialogMode.value : !dialogMode.value) ? defaultSlot : callBackSlot
|
|
|
+ },
|
|
|
+})
|
|
|
+
|
|
|
+const question = computed(() => {
|
|
|
+ return props.question || ({} as QuestionInfo)
|
|
|
+})
|
|
|
+
|
|
|
+const scoreList = computed<number[]>(() => {
|
|
|
+ const { totalScore, intervalScore } = question.value
|
|
|
+ return getNumbers(totalScore || 0, intervalScore || 0)
|
|
|
+})
|
|
|
+
|
|
|
+const focused = ref<boolean>(false)
|
|
|
+
|
|
|
+const refInput1 = ref<HTMLInputElement>()
|
|
|
+const refInput2 = ref<HTMLInputElement>()
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => props.active,
|
|
|
+ () => {
|
|
|
+ nextTick(() => {
|
|
|
+ if (props.active) {
|
|
|
+ inputFocus()
|
|
|
+ } else {
|
|
|
+ inputBlur()
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+)
|
|
|
+watch(
|
|
|
+ () => props.id,
|
|
|
+ (val: any) => {
|
|
|
+ !!val && inputFocus()
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
+// 倒计时相关计算属性
|
|
|
+const buttonText = computed(() => (markStore.countdownTime > 0 ? `${markStore.countdownTime}s` : '确定'))
|
|
|
+const buttonDisabled = computed(() => {
|
|
|
+ return markStore.countdownTime > 0
|
|
|
+})
|
|
|
+
|
|
|
+const getClass = (val: string, callback?: string) => {
|
|
|
+ return dialogMode.value ? val : callback || ''
|
|
|
+}
|
|
|
+
|
|
|
+const onSetScore = (v: number | string) => {
|
|
|
+ onInputFocus()
|
|
|
+ currentScore.value = v
|
|
|
+ markStore.setCurQuestionScore(currentScore.value === '' ? null : Number(currentScore.value))
|
|
|
+}
|
|
|
+
|
|
|
+const onInputFocus = () => {
|
|
|
+ focused.value = true
|
|
|
+ emit('focused')
|
|
|
+}
|
|
|
+
|
|
|
+const onBlur = (e: Event) => {
|
|
|
+ focused.value = false
|
|
|
+ const target = e.target as HTMLInputElement
|
|
|
+ target.value && scoreStrictValidFail(target.value)
|
|
|
+ emit('blur')
|
|
|
+}
|
|
|
+
|
|
|
+const inputFocus = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ refInput1?.value?.focus()
|
|
|
+ refInput2?.value?.focus()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const inputBlur = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ refInput1?.value?.blur()
|
|
|
+ refInput2?.value?.blur()
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const KEY_WHITE_LIST1 = ['ArrowLeft', 'ArrowRight', 'Backspace', 'Delete', 'Enter']
|
|
|
+const KEY_WHITE_LIST2 = ['\\.', '[0-9]', '[+-]']
|
|
|
+const KEY_VALID_REG = new RegExp(
|
|
|
+ KEY_WHITE_LIST1.map((k) => `\\b${k}\\b`)
|
|
|
+ .concat(KEY_WHITE_LIST2)
|
|
|
+ .join('|')
|
|
|
+)
|
|
|
+
|
|
|
+const validScore = (score: number | string) => {
|
|
|
+ return scoreList.value.some((s) => `${s}`.startsWith(`${score}`))
|
|
|
+}
|
|
|
+
|
|
|
+const scoreStrictValidFail = (score: number | string) => {
|
|
|
+ scoreValidFail.value = !scoreList.value.some((s) => `${s}` === `${score}`)
|
|
|
+ return scoreValidFail.value
|
|
|
+}
|
|
|
+
|
|
|
+const deleteStringChart = (str: string, index: number) => {
|
|
|
+ return str.substring(0, index) + str.substring(index + 1)
|
|
|
+}
|
|
|
+
|
|
|
+const joinStringChart = (str: string, index: number, chart: string) => {
|
|
|
+ return str.substring(0, index) + chart + str.substring(index + 1)
|
|
|
+}
|
|
|
+const confirmWithBtn = () => {
|
|
|
+ emit('enter')
|
|
|
+}
|
|
|
+const onViewPapers = () => {
|
|
|
+ emit('view-papers')
|
|
|
+}
|
|
|
+const onValidScore = (e: any) => {
|
|
|
+ const target = e.target as HTMLInputElement
|
|
|
+ const oldScore = `${currentScore.value ?? ''}`
|
|
|
+ const start = target.selectionStart || 0
|
|
|
+
|
|
|
+ if (!KEY_VALID_REG.test(e.key)) {
|
|
|
+ console.log('valid:', e.key)
|
|
|
+
|
|
|
+ e.preventDefault()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (['ArrowLeft', 'ArrowRight'].includes(e.key)) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ console.log('e.key:', e.key)
|
|
|
+
|
|
|
+ if (e.key === '+' && Number(currentScore.value || 0) < scoreList.value[scoreList.value.length - 1]) {
|
|
|
+ currentScore.value = Number(currentScore.value || 0) + 1 + ''
|
|
|
+ }
|
|
|
+ if (e.key === '-' && Number(currentScore.value || 0) > scoreList.value[0]) {
|
|
|
+ currentScore.value = Number(currentScore.value || 0) - 1 + ''
|
|
|
+ }
|
|
|
+ if ('Enter' === e.key) {
|
|
|
+ let targetInputValue = e.target?.value
|
|
|
+ if (!targetInputValue) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (oldScore && !scoreStrictValidFail(oldScore)) {
|
|
|
+ nextTick(() => {
|
|
|
+ emit('enter')
|
|
|
+ })
|
|
|
+ }
|
|
|
+ e.preventDefault()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if ('Backspace' === e.key) {
|
|
|
+ if (!validScore(deleteStringChart(oldScore, start - 1))) {
|
|
|
+ e.preventDefault()
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if ('Delete' === e.key) {
|
|
|
+ if (!validScore(deleteStringChart(oldScore, start))) {
|
|
|
+ e.preventDefault()
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!validScore(joinStringChart(oldScore, start, e.key))) {
|
|
|
+ e.preventDefault()
|
|
|
+ }
|
|
|
+}
|
|
|
+const onValidScoreDebounce = debounce(onValidScore, 50)
|
|
|
+
|
|
|
+const scoreChange = (e: Event) => {
|
|
|
+ const target = e.target as HTMLInputElement
|
|
|
+ const oldScore = `${currentScore.value || ''}`
|
|
|
+ if (validScore(target.value)) {
|
|
|
+ onSetScore(target.value)
|
|
|
+ } else {
|
|
|
+ target.value = oldScore
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onToggleClick = () => {
|
|
|
+ emit('toggle-click')
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.scoring-panel {
|
|
|
+ // background-color: $MainLayoutHeaderBg;
|
|
|
+ background-color: #fff;
|
|
|
+ font-size: $MediumFont;
|
|
|
+ // margin-bottom: 6px;
|
|
|
+ &.mini {
|
|
|
+ padding: 2px 5px !important;
|
|
|
+ }
|
|
|
+ &.modal-panel {
|
|
|
+ padding: 10px 0;
|
|
|
+ }
|
|
|
+ &.sticky {
|
|
|
+ align-items: center;
|
|
|
+ // padding: 12px 20px;
|
|
|
+ padding: 2px 20px;
|
|
|
+ // height: 50px;
|
|
|
+ }
|
|
|
+
|
|
|
+ // .dialog-name {
|
|
|
+ // width: 98px;
|
|
|
+ // place-items: center;
|
|
|
+ // background: #f5f5f5;
|
|
|
+ // }
|
|
|
+
|
|
|
+ .score-list {
|
|
|
+ font-size: $BaseFont;
|
|
|
+ margin-bottom: -8px;
|
|
|
+ // margin-left: 8px;
|
|
|
+ &.dialog-score-list {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+ .score-span {
|
|
|
+ width: 46px;
|
|
|
+ height: 32px;
|
|
|
+ line-height: 32px;
|
|
|
+ text-align: center;
|
|
|
+ border-radius: 4px;
|
|
|
+ border: 1px solid #e5e5e5;
|
|
|
+ color: #666666;
|
|
|
+ margin-left: 8px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ &.mini {
|
|
|
+ width: 32px;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+ &.active,
|
|
|
+ &:hover {
|
|
|
+ background-color: $color--primary;
|
|
|
+ color: $color--white;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .score-result {
|
|
|
+ background-color: $color--primary;
|
|
|
+ color: $color--white;
|
|
|
+ margin-left: 8px;
|
|
|
+ border-radius: 6px;
|
|
|
+ padding: 2px;
|
|
|
+ height: 32px;
|
|
|
+ line-height: 32px;
|
|
|
+ min-width: 84px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+
|
|
|
+ .score-num {
|
|
|
+ width: 40px;
|
|
|
+ height: 100%;
|
|
|
+ line-height: 28px;
|
|
|
+ background-color: $color--white;
|
|
|
+ color: #e02020;
|
|
|
+ border-radius: 0 6px 6px 0;
|
|
|
+ outline: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .dialog-score {
|
|
|
+ // width: 80px;
|
|
|
+ width: 70px;
|
|
|
+ // place-items: center;
|
|
|
+ // background: $color--primary;
|
|
|
+ color: $color--white;
|
|
|
+ // font-size: 32px;
|
|
|
+ outline: none;
|
|
|
+ margin-left: 6px;
|
|
|
+ overflow: hidden;
|
|
|
+ position: relative;
|
|
|
+ top: -2px;
|
|
|
+ .score-input {
|
|
|
+ background: inherit;
|
|
|
+ color: inherit;
|
|
|
+ font-size: inherit;
|
|
|
+ width: 100% !important;
|
|
|
+ // height: 100%;
|
|
|
+ height: calc(100% - 4px);
|
|
|
+ background: $color--primary;
|
|
|
+ font-size: 24px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .score-input {
|
|
|
+ border: none;
|
|
|
+ outline: none;
|
|
|
+ font-weight: bold;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .score-valid-fail {
|
|
|
+ position: relative;
|
|
|
+ // background-color: $DangerColor !important;
|
|
|
+ border: 1px solid $DangerColor;
|
|
|
+ box-shadow: 0 0 4px $DangerColor;
|
|
|
+ &:before {
|
|
|
+ content: '无效分值';
|
|
|
+ position: absolute;
|
|
|
+ width: 100%;
|
|
|
+ height: 1em;
|
|
|
+ line-height: 1;
|
|
|
+ text-align: center;
|
|
|
+ top: -1.2em;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ font-size: 10px;
|
|
|
+ color: $DangerColor;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // .toggle-icon {
|
|
|
+ // align-self: flex-start;
|
|
|
+ // margin-top: 6px;
|
|
|
+ // margin-left: 6px;
|
|
|
+ // opacity: 0;
|
|
|
+ // &.visible {
|
|
|
+ // opacity: 1;
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+ .toggle-icon-box {
|
|
|
+ cursor: pointer;
|
|
|
+ background-color: #0091ff;
|
|
|
+ width: 70px;
|
|
|
+ border-radius: 6px;
|
|
|
+ text-align: center;
|
|
|
+ color: #fff;
|
|
|
+ opacity: 0;
|
|
|
+ height: 100%;
|
|
|
+ transition: all 0.3s;
|
|
|
+ &:hover {
|
|
|
+ background-color: rgba(0, 145, 255, 0.8);
|
|
|
+ }
|
|
|
+ &.visible {
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+ img {
|
|
|
+ height: 20px;
|
|
|
+ margin-top: 3px;
|
|
|
+ }
|
|
|
+ p {
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|