|
@@ -0,0 +1,220 @@
|
|
|
+<!-- eslint-disable vue/no-v-html -->
|
|
|
+<template>
|
|
|
+ <div class="matching-preview">
|
|
|
+ <div class="stem">
|
|
|
+ <span class="question-number">{{ data.property.question_number }}.</span>
|
|
|
+ <span v-html="sanitizeHTML(data.stem)"></span>
|
|
|
+ </div>
|
|
|
+ <div v-if="data.property.is_enable_description" class="description">{{ data.description }}</div>
|
|
|
+
|
|
|
+ <ul class="option-list">
|
|
|
+ <li v-for="(item, i) in optionList" :key="i" class="list-item">
|
|
|
+ <div v-for="({ content, mark }, j) in item" :key="mark" class="item-wrapper">
|
|
|
+ <span
|
|
|
+ v-if="j > 0 && j < item.length"
|
|
|
+ :class="['connection', `pre-line-${i}-${j}`]"
|
|
|
+ @click="handleConnection($event, i, j, 'pre', mark)"
|
|
|
+ ></span>
|
|
|
+ <span class="content" v-html="sanitizeHTML(content)"></span>
|
|
|
+ <span
|
|
|
+ v-if="j < item.length - 1"
|
|
|
+ :class="['connection', `next-line-${i}-${j}`]"
|
|
|
+ @click="handleConnection($event, i, j, 'next', mark)"
|
|
|
+ ></span>
|
|
|
+ </div>
|
|
|
+ </li>
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import PreviewMixin from './components/PreviewMixin';
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'MatchingPreview',
|
|
|
+ mixins: [PreviewMixin],
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ curConnectionPoint: { i: -1, j: -1, position: 'pre', mark: '' }, // 当前连线点
|
|
|
+ connectionList: [], // 连接线列表
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ optionList() {
|
|
|
+ // 生成一个与选项列表相同的二维数组,但没有内容
|
|
|
+ let arr = Array.from(Array(this.data.option_list.length), () => Array(this.data.option_list[0].length).fill({}));
|
|
|
+ for (let i = 0; i < this.data.property.column_number; i++) {
|
|
|
+ let rowNumList = Array.from(Array(this.data.option_list.length).keys()); // 行数列表
|
|
|
+ for (let j = 0; j < this.data.option_list.length; j++) {
|
|
|
+ let random = Math.floor(Math.random() * rowNumList.length); // 随机行数
|
|
|
+ let item = this.data.option_list[j][i]; // 当前选项
|
|
|
+ arr[rowNumList[random]][i] = item; // 将当前选项添加到随机行数的列中
|
|
|
+ rowNumList.splice(random, 1); // 删除已经添加过选项的行数
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return arr;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ document.addEventListener('click', this.handleEventConnection);
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ document.removeEventListener('click', this.handleEventConnection);
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ handleEventConnection(e) {
|
|
|
+ if (e.target.classList.contains('connection')) return;
|
|
|
+ Array.from(document.getElementsByClassName('connection')).forEach((item) => {
|
|
|
+ item.classList.remove('focus');
|
|
|
+ });
|
|
|
+ this.curConnectionPoint = { i: -1, j: -1, position: 'pre', mark: '' };
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理连线
|
|
|
+ * @param {PointerEvent} e 事件对象
|
|
|
+ * @param {Number} i 选项列表索引
|
|
|
+ * @param {Number} j 选项索引
|
|
|
+ * @param {String} position 连线点位置
|
|
|
+ * @param {String} mark 选项标识
|
|
|
+ */
|
|
|
+ handleConnection(e, i, j, position, mark) {
|
|
|
+ // 如果当前连线点不存在,则设置当前连线点
|
|
|
+ if (this.curConnectionPoint.i === -1 && this.curConnectionPoint.j === -1) {
|
|
|
+ this.curConnectionPoint = { i, j, mark, position };
|
|
|
+ e.target.classList.add('focus');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 如果当前连线点存在,清除所有连线点的选中状态
|
|
|
+ Array.from(document.getElementsByClassName('connection')).forEach((item) => {
|
|
|
+ item.classList.remove('focus');
|
|
|
+ });
|
|
|
+ // 如果当前连线点与上一个连线点不在相邻的位置,则设置点击的连接点为当前连线点
|
|
|
+ if (Math.abs(this.curConnectionPoint.j - j) > 1 || position === this.curConnectionPoint.position) {
|
|
|
+ this.curConnectionPoint = { i, j, mark, position };
|
|
|
+ e.target.classList.add('focus');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.pushConnectionList(j, mark);
|
|
|
+ // 如果当前连线点与上一个连线点在相邻的位置,则创建连接线
|
|
|
+ this.createLine(e, i, j, position);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 创建连接线
|
|
|
+ createLine(e, i, j, position) {
|
|
|
+ let offsetLeft = document.getElementsByClassName(`${position}-line-${i}-${j}`)[0].offsetLeft;
|
|
|
+ let offsetTop = document.getElementsByClassName(`${position}-line-${i}-${j}`)[0].offsetTop;
|
|
|
+ const { curOffsetLeft, curOffsetTop } = this.computedCurConnectionPoint();
|
|
|
+ let top = Math.min(offsetTop, curOffsetTop) + 5;
|
|
|
+ let left = Math.min(offsetLeft, curOffsetLeft) + 5;
|
|
|
+ let width = Math.abs(offsetLeft - curOffsetLeft) + 2;
|
|
|
+ let height = Math.abs(offsetTop - curOffsetTop) + 2;
|
|
|
+ let size = offsetLeft > curOffsetLeft ? offsetTop > curOffsetTop : offsetTop < curOffsetTop;
|
|
|
+
|
|
|
+ // 创建一个空的SVG元素
|
|
|
+ let svgNS = 'http://www.w3.org/2000/svg'; // SVG命名空间
|
|
|
+ let svg = document.createElementNS(svgNS, 'svg');
|
|
|
+ svg.setAttribute(
|
|
|
+ 'style',
|
|
|
+ `position:absolute; width: 128px; height: ${Math.max(8, height) + 2}px; top: ${top}px; left: ${left}px;`,
|
|
|
+ );
|
|
|
+ svg.classList.add('connection-line');
|
|
|
+ // 向SVG元素添加 line 元素
|
|
|
+ let line = document.createElementNS(svgNS, 'line');
|
|
|
+ line.setAttribute('x1', size ? '2' : width);
|
|
|
+ line.setAttribute('y1', '2');
|
|
|
+ line.setAttribute('x2', size ? width : '2');
|
|
|
+ line.setAttribute('y2', height);
|
|
|
+ line.setAttribute('stroke-width', '2'); // 设置线条宽度
|
|
|
+ line.setAttribute('stroke-linecap', 'round'); // 设置线段的两端样式
|
|
|
+ line.setAttribute('stroke', '#306eff'); // 设置填充颜色
|
|
|
+
|
|
|
+ // 将SVG元素插入到文档中
|
|
|
+ e.target.parentElement.parentElement.parentElement.appendChild(svg); // 在文档的<body>中插入SVG元素
|
|
|
+ svg.appendChild(line); // 向SVG元素添加矩形
|
|
|
+ this.curConnectionPoint = { i: -1, j: -1, position: 'pre', mark: '' };
|
|
|
+ },
|
|
|
+
|
|
|
+ computedCurConnectionPoint() {
|
|
|
+ const { i, j, position } = this.curConnectionPoint;
|
|
|
+ if (i === -1 && j === -1) return {};
|
|
|
+ let curOffsetLeft = document.getElementsByClassName(`${position}-line-${i}-${j}`)[0].offsetLeft;
|
|
|
+ let curOffsetTop = document.getElementsByClassName(`${position}-line-${i}-${j}`)[0].offsetTop;
|
|
|
+ return { curOffsetLeft, curOffsetTop };
|
|
|
+ },
|
|
|
+
|
|
|
+ // 清除连线
|
|
|
+ clearLine() {
|
|
|
+ document.querySelectorAll('svg.connection-line').forEach((item) => {
|
|
|
+ console.log(item);
|
|
|
+ item.remove();
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ pushConnectionList(j, mark) {
|
|
|
+ let index = this.connectionList.findIndex((item) => {
|
|
|
+ return item.includes(mark) || item.includes(this.curConnectionPoint.mark);
|
|
|
+ });
|
|
|
+ if (index === -1) {
|
|
|
+ let arr = [];
|
|
|
+ arr[j] = mark;
|
|
|
+ arr[this.curConnectionPoint.j] = this.curConnectionPoint.mark;
|
|
|
+ this.connectionList.push(arr);
|
|
|
+ } else {
|
|
|
+ this.connectionList[index][j] = mark;
|
|
|
+ this.connectionList[index][this.curConnectionPoint.j] = this.curConnectionPoint.mark;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+@use '@/styles/mixin.scss' as *;
|
|
|
+
|
|
|
+.matching-preview {
|
|
|
+ @include preview;
|
|
|
+
|
|
|
+ .option-list {
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ row-gap: 16px;
|
|
|
+
|
|
|
+ .list-item {
|
|
|
+ display: flex;
|
|
|
+ column-gap: 60px;
|
|
|
+
|
|
|
+ .item-wrapper {
|
|
|
+ display: flex;
|
|
|
+ flex: 1;
|
|
|
+ column-gap: 24px;
|
|
|
+ align-items: center;
|
|
|
+ min-height: 48px;
|
|
|
+ padding: 12px 24px;
|
|
|
+ background-color: $fill-color;
|
|
|
+ border-radius: 40px;
|
|
|
+
|
|
|
+ .content {
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .connection {
|
|
|
+ z-index: 1;
|
|
|
+ width: 14px;
|
|
|
+ height: 14px;
|
|
|
+ cursor: pointer;
|
|
|
+ border: 4px solid #306eff;
|
|
|
+ border-radius: 50%;
|
|
|
+
|
|
|
+ &.focus {
|
|
|
+ background-color: #306eff;
|
|
|
+ border-color: #fff;
|
|
|
+ outline: 2px solid #306eff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|