123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402 |
- <template>
- <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>
- </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>
- <script lang="ts" setup>
- import { computed, ref, watch } from 'vue'
- import Modal from '@/components/Modal.vue'
- import 'katex/dist/katex.min.css'
- import katex from 'katex'
- const props = defineProps<{
- visible: boolean
- work: any
- }>()
- const emit = defineEmits<{
- (e: 'update:visible', v: boolean): void
- }>()
- const visible = computed({
- get: () => props.visible,
- set: (v: boolean) => emit('update:visible', v)
- })
- 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>
|