|
@@ -1,15 +1,101 @@
|
|
|
<template>
|
|
<template>
|
|
|
- <Modal :visible="visible" :width="720" :closeButton="true" @update:visible="val => emit('update:visible', val)">
|
|
|
|
|
- <div>
|
|
|
|
|
- <h3>选择题作业</h3>
|
|
|
|
|
- <pre>{{ work }}</pre>
|
|
|
|
|
|
|
+ <Modal :visible="visible" :width="1024" :closeButton="true" @update:visible="val => emit('update:visible', val)">
|
|
|
|
|
+ <div class="wp_tool wp_tool45" v-if="workData">
|
|
|
|
|
+ <div class="wp_t45_title">题目内容</div>
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="s_b_m_toolItem"
|
|
|
|
|
+ v-for="(item, index) in workData.testJson"
|
|
|
|
|
+ :key="index + '_' + workData.id"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="s_b_m_ti_title">
|
|
|
|
|
+ <span>{{ index + 1 }}</span>
|
|
|
|
|
+ <svg
|
|
|
|
|
+ width="16"
|
|
|
|
|
+ height="16"
|
|
|
|
|
+ viewBox="0 0 16 16"
|
|
|
|
|
+ fill="none"
|
|
|
|
|
+ xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
+ >
|
|
|
|
|
+ <path
|
|
|
|
|
+ d="M15.3536 8.35355C15.5488 8.15829 15.5488 7.84171 15.3536 7.64645L12.1716 4.46447C11.9763 4.2692 11.6597 4.2692 11.4645 4.46447C11.2692 4.65973 11.2692 4.97631 11.4645 5.17157L14.2929 8L11.4645 10.8284C11.2692 11.0237 11.2692 11.3403 11.4645 11.5355C11.6597 11.7308 11.9763 11.7308 12.1716 11.5355L15.3536 8.35355ZM1 8.5H15V7.5H1V8.5Z"
|
|
|
|
|
+ fill="#3681FC"
|
|
|
|
|
+ />
|
|
|
|
|
+ </svg>
|
|
|
|
|
+
|
|
|
|
|
+ <span style="display: flex;align-items: center;"
|
|
|
|
|
+ >{{ item.type == 1 ? "单选题:" : "多选题:"
|
|
|
|
|
+ }}<span v-html="renderedFormula(item.teststitle)"></span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="s_b_m_ti_option"
|
|
|
|
|
+ v-for="(item2, index2) in item.checkList"
|
|
|
|
|
+ :key="index + '_' + index2 + 'index2T'"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ s_b_m_ti_o_choice:
|
|
|
|
|
+ item.type == '1'
|
|
|
|
|
+ ? workData.testJson[index].userAnswer === index2
|
|
|
|
|
+ : workData.testJson[index].userAnswer.includes(index2)
|
|
|
|
|
+ }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="s_b_m_ti_o_btn">
|
|
|
|
|
+ <span class="s_b_m_ti_o_btn1" v-if="item.type == 1">
|
|
|
|
|
+ <span
|
|
|
|
|
+ v-if="workData.testJson[index].userAnswer === index2"
|
|
|
|
|
+ ></span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="s_b_m_ti_o_btn2" v-else>
|
|
|
|
|
+ <span
|
|
|
|
|
+ v-if="workData.testJson[index].userAnswer.includes(index2)"
|
|
|
|
|
+ >
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <span>
|
|
|
|
|
+ <img
|
|
|
|
|
+ v-if="item2.imgType && item2.imgType === 1"
|
|
|
|
|
+ :src="item2.src"
|
|
|
|
|
+ alt=""
|
|
|
|
|
+ @click.stop="openPreview(item2)"
|
|
|
|
|
+ />
|
|
|
|
|
+ <span v-else>{{ item2 }}</span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</Modal>
|
|
</Modal>
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 预览放大(带缩放/拖拽/旋转/工具栏) -->
|
|
|
|
|
+ <Teleport to="body">
|
|
|
|
|
+ <div v-if="previewVisible" class="image-preview" @click.self="closePreview" @wheel.prevent="onWheel">
|
|
|
|
|
+ <div class="image-preview__toolbar">
|
|
|
|
|
+ <button @click.stop="zoomOut">-</button>
|
|
|
|
|
+ <button @click.stop="zoomIn">+</button>
|
|
|
|
|
+ <button @click.stop="resetTransform">重置</button>
|
|
|
|
|
+ <button @click.stop="rotateLeft">⟲</button>
|
|
|
|
|
+ <button @click.stop="rotateRight">⟳</button>
|
|
|
|
|
+ <button @click.stop="toggleFit">{{ fitMode ? '实际大小' : '适应屏幕' }}</button>
|
|
|
|
|
+ <button @click.stop="closePreview">关闭</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="image-preview__stage"
|
|
|
|
|
+ @mousedown="onDragStart"
|
|
|
|
|
+ @mousemove="onDragMove"
|
|
|
|
|
+ @mouseup="onDragEnd"
|
|
|
|
|
+ @mouseleave="onDragEnd"
|
|
|
|
|
+ @dblclick.stop="toggleZoom">
|
|
|
|
|
+ <img :src="imageUrl" alt="预览" class="image-preview__img"
|
|
|
|
|
+ :style="imgStyle" draggable="false" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Teleport>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
|
<script lang="ts" setup>
|
|
|
-import { computed } from 'vue'
|
|
|
|
|
|
|
+import { computed, ref, watch } from 'vue'
|
|
|
import Modal from '@/components/Modal.vue'
|
|
import Modal from '@/components/Modal.vue'
|
|
|
|
|
+import 'katex/dist/katex.min.css'
|
|
|
|
|
+import katex from 'katex'
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
const props = defineProps<{
|
|
|
visible: boolean
|
|
visible: boolean
|
|
@@ -24,4 +110,293 @@ const visible = computed({
|
|
|
get: () => props.visible,
|
|
get: () => props.visible,
|
|
|
set: (v: boolean) => emit('update:visible', v)
|
|
set: (v: boolean) => emit('update:visible', v)
|
|
|
})
|
|
})
|
|
|
-</script>
|
|
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+const renderedFormula = computed(() => {
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 渲染公式文本,支持带HTML标签的内容
|
|
|
|
|
+ * @param {string} val
|
|
|
|
|
+ * @returns {string}
|
|
|
|
|
+ */
|
|
|
|
|
+ const render = (val: string): string => {
|
|
|
|
|
+ if (typeof val !== 'string') return ''
|
|
|
|
|
+ const input: string = val.trim().replace(/[\u200B-\u200D\uFEFF]/g, '')
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 检查是否包含HTML标签
|
|
|
|
|
+ const tagReg = /<([a-zA-Z][\w\-]*)([^>]*)>([\s\S]*?)<\/\1>/g
|
|
|
|
|
+ if (!tagReg.test(input)) {
|
|
|
|
|
+ // 纯文本,整体渲染
|
|
|
|
|
+ try {
|
|
|
|
|
+ return katex.renderToString(input, {
|
|
|
|
|
+ throwOnError: false,
|
|
|
|
|
+ strict: false,
|
|
|
|
|
+ output: 'htmlAndMathml'
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ catch {
|
|
|
|
|
+ return input // 渲染失败原样输出
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
|
|
+ // 有标签,对每个标签内容渲染
|
|
|
|
|
+ return input.replace(tagReg, (match, tag, attrs, inner) => {
|
|
|
|
|
+ let html = inner
|
|
|
|
|
+ try {
|
|
|
|
|
+ html = katex.renderToString(inner.trim(), {
|
|
|
|
|
+ throwOnError: false,
|
|
|
|
|
+ strict: false,
|
|
|
|
|
+ output: 'htmlAndMathml'
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ catch {
|
|
|
|
|
+ // 渲染失败,保留原内容
|
|
|
|
|
+ }
|
|
|
|
|
+ return `<${tag}${attrs}>${html}</${tag}>`
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (e) {
|
|
|
|
|
+ // console.error('KaTeX渲染错误:', e)
|
|
|
|
|
+ return input
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return render
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+const workData = ref<any>(null)
|
|
|
|
|
+const previewVisible = ref(false)
|
|
|
|
|
+const scale = ref(1)
|
|
|
|
|
+const rotate = ref(0)
|
|
|
|
|
+const offsetX = ref(0)
|
|
|
|
|
+const offsetY = ref(0)
|
|
|
|
|
+const dragging = ref(false)
|
|
|
|
|
+const lastX = ref(0)
|
|
|
|
|
+const lastY = ref(0)
|
|
|
|
|
+const fitMode = ref(true)
|
|
|
|
|
+const imageUrl = ref('')
|
|
|
|
|
+
|
|
|
|
|
+watch(() => props.visible, (newVal) => {
|
|
|
|
|
+ if (props.work && newVal) {
|
|
|
|
|
+ workData.value = JSON.parse(decodeURIComponent(props.work.content))
|
|
|
|
|
+ }
|
|
|
|
|
+}, {
|
|
|
|
|
+ immediate: true,
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+const imgStyle = computed(() => {
|
|
|
|
|
+ const t = `translate(${offsetX.value}px, ${offsetY.value}px) rotate(${rotate.value}deg) scale(${scale.value})`
|
|
|
|
|
+ return {
|
|
|
|
|
+ transform: t
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const openPreview = (item: any) => {
|
|
|
|
|
+ imageUrl.value = item.src
|
|
|
|
|
+ previewVisible.value = true
|
|
|
|
|
+ nextTickFit()
|
|
|
|
|
+}
|
|
|
|
|
+const closePreview = () => {
|
|
|
|
|
+ previewVisible.value = false
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const zoomStep = 0.2
|
|
|
|
|
+const minScale = 0.2
|
|
|
|
|
+const maxScale = 6
|
|
|
|
|
+
|
|
|
|
|
+const zoomIn = () => {
|
|
|
|
|
+ fitMode.value = false; scale.value = Math.min(maxScale, +(scale.value + zoomStep).toFixed(2))
|
|
|
|
|
+}
|
|
|
|
|
+const zoomOut = () => {
|
|
|
|
|
+ fitMode.value = false; scale.value = Math.max(minScale, +(scale.value - zoomStep).toFixed(2))
|
|
|
|
|
+}
|
|
|
|
|
+const resetTransform = () => {
|
|
|
|
|
+ scale.value = 1; rotate.value = 0; offsetX.value = 0; offsetY.value = 0; fitMode.value = true; nextTickFit()
|
|
|
|
|
+}
|
|
|
|
|
+const rotateLeft = () => {
|
|
|
|
|
+ rotate.value = (rotate.value - 90) % 360
|
|
|
|
|
+}
|
|
|
|
|
+const rotateRight = () => {
|
|
|
|
|
+ rotate.value = (rotate.value + 90) % 360
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const toggleZoom = () => {
|
|
|
|
|
+ fitMode.value = false
|
|
|
|
|
+ scale.value = scale.value >= 1.8 ? 1 : 2
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const toggleFit = () => {
|
|
|
|
|
+ fitMode.value = !fitMode.value
|
|
|
|
|
+ nextTickFit()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+const nextTickFit = () => {
|
|
|
|
|
+ // 适应屏幕时复位位置与缩放
|
|
|
|
|
+ if (fitMode.value) {
|
|
|
|
|
+ scale.value = 1
|
|
|
|
|
+ offsetX.value = 0
|
|
|
|
|
+ offsetY.value = 0
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const onWheel = (e: WheelEvent) => {
|
|
|
|
|
+ if (e.deltaY > 0) zoomOut()
|
|
|
|
|
+ else zoomIn()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const onDragStart = (e: MouseEvent) => {
|
|
|
|
|
+ dragging.value = true
|
|
|
|
|
+ lastX.value = e.clientX
|
|
|
|
|
+ lastY.value = e.clientY
|
|
|
|
|
+}
|
|
|
|
|
+const onDragMove = (e: MouseEvent) => {
|
|
|
|
|
+ if (!dragging.value) return
|
|
|
|
|
+ const dx = e.clientX - lastX.value
|
|
|
|
|
+ const dy = e.clientY - lastY.value
|
|
|
|
|
+ lastX.value = e.clientX
|
|
|
|
|
+ lastY.value = e.clientY
|
|
|
|
|
+ offsetX.value += dx
|
|
|
|
|
+ offsetY.value += dy
|
|
|
|
|
+}
|
|
|
|
|
+const onDragEnd = () => {
|
|
|
|
|
+ dragging.value = false
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+
|
|
|
|
|
+.wp_tool {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: auto;
|
|
|
|
|
+ max-height: 80vh;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 40px 0;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.wp_tool45 {
|
|
|
|
|
+ height: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.wp_t45_title {
|
|
|
|
|
+ font-size: 3em;
|
|
|
|
|
+ font-weight: 300;
|
|
|
|
|
+ margin: 20px 0 40px 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.s_b_m_toolItem {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: auto;
|
|
|
|
|
+ margin-bottom: 40px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.s_b_m_ti_option {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: auto;
|
|
|
|
|
+ padding: 15px 15px 15px 15px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ background-color: #f3f7fd;
|
|
|
|
|
+ border-radius: 30px;
|
|
|
|
|
+ margin: 10px 0 10px 0px;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.s_b_m_ti_option > span > img {
|
|
|
|
|
+ max-height: 150px;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.s_b_m_ti_o_btn {
|
|
|
|
|
+ width: 20px;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ min-height: 20px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-right: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.s_b_m_ti_o_btn > span {
|
|
|
|
|
+ width: 20px;
|
|
|
|
|
+ height: 20px;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ border: solid 1px #3681fc;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.s_b_m_ti_o_btn > span > span {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ background-color: #3681fc;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.s_b_m_ti_o_btn1 {
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.s_b_m_ti_o_btn1 > span::after {
|
|
|
|
|
+ content: "";
|
|
|
|
|
+ width: 8px;
|
|
|
|
|
+ height: 8px;
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ background-color: #fff;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.s_b_m_ti_o_btn2 {
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.s_b_m_ti_o_btn2 > span::after {
|
|
|
|
|
+ content: "";
|
|
|
|
|
+ width: 8px;
|
|
|
|
|
+ height: 8px;
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ background-color: #fff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.s_b_m_ti_o_choice {
|
|
|
|
|
+ border: solid 1px #3681fc;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.s_b_m_ti_title {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.s_b_m_ti_title > span:nth-of-type(1) {
|
|
|
|
|
+ font-size: 30px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ color: #3681fc;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.s_b_m_ti_title > svg {
|
|
|
|
|
+ width: 30px;
|
|
|
|
|
+ height: 30px;
|
|
|
|
|
+ margin: 0 20px 0 5px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.s_b_m_ti_title > span:nth-of-type(2) {
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.s_b_m_ti_title > div {
|
|
|
|
|
+ font-size: 30px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|