|
@@ -7,19 +7,23 @@
|
|
|
</div>
|
|
|
<div v-if="data.property.is_enable_description" class="description">{{ data.description }}</div>
|
|
|
|
|
|
- <ul class="option-list">
|
|
|
+ <ul ref="list" 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)"
|
|
|
+ @mousedown="mousedown($event, i, j, 'pre', mark)"
|
|
|
+ @mouseup="mouseup($event, i, j, 'pre', mark)"
|
|
|
+ @click="handleClickConnection($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)"
|
|
|
+ @mousedown="mousedown($event, i, j, 'next', mark)"
|
|
|
+ @mouseup="mouseup($event, i, j, 'next', mark)"
|
|
|
+ @click="handleClickConnection($event, i, j, 'next', mark)"
|
|
|
></span>
|
|
|
</div>
|
|
|
</li>
|
|
@@ -28,6 +32,8 @@
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
+import { svgNS } from '@/views/exercise_questions/data/matching';
|
|
|
+
|
|
|
import PreviewMixin from './components/PreviewMixin';
|
|
|
|
|
|
export default {
|
|
@@ -35,8 +41,12 @@ export default {
|
|
|
mixins: [PreviewMixin],
|
|
|
data() {
|
|
|
return {
|
|
|
+ answerList: [], // 答案列表
|
|
|
curConnectionPoint: { i: -1, j: -1, position: 'pre', mark: '' }, // 当前连线点
|
|
|
connectionList: [], // 连接线列表
|
|
|
+ // 拖拽相关
|
|
|
+ drag: false,
|
|
|
+ mousePointer: { i: -1, j: -1, position: 'pre', mark: '' },
|
|
|
};
|
|
|
},
|
|
|
computed: {
|
|
@@ -55,13 +65,138 @@ export default {
|
|
|
return arr;
|
|
|
},
|
|
|
},
|
|
|
- created() {
|
|
|
+ watch: {
|
|
|
+ optionList: {
|
|
|
+ handler() {
|
|
|
+ this.clearLine();
|
|
|
+ this.initAnswerList();
|
|
|
+ },
|
|
|
+ immediate: true,
|
|
|
+ },
|
|
|
+ answerList: {
|
|
|
+ handler(list) {
|
|
|
+ let arr = [];
|
|
|
+ let column_number = this.data.property.column_number;
|
|
|
+ // 从前往后遍历,如果有 nextMark,就往后找,直到没有 nextMark
|
|
|
+ // 如果第一个选项的 nextMark 为空,则整个行都为空,等待后面的 preArr 填充
|
|
|
+ list.forEach((item, i) => {
|
|
|
+ let { mark, nextMark } = item[0];
|
|
|
+ arr[i] = nextMark ? [mark, nextMark] : new Array(column_number).fill('');
|
|
|
+ let _nextMark = nextMark;
|
|
|
+ while (_nextMark) {
|
|
|
+ let fMark = this.findMark(list, _nextMark, 'next');
|
|
|
+ if (column_number > arr[i].length) {
|
|
|
+ arr[i].push(fMark);
|
|
|
+ }
|
|
|
+ _nextMark = fMark;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ let emptyStringArray = []; // 无数据的列的下标数组
|
|
|
+ arr.forEach((item, i) => {
|
|
|
+ item.every((li) => li.length <= 0) ? emptyStringArray.push(i) : '';
|
|
|
+ });
|
|
|
+
|
|
|
+ // 从后向前遍历,如果有 preMark,就往前找,直到没有 preMark,并过滤掉无数据的列
|
|
|
+ let preArr = [];
|
|
|
+ list.forEach((item) => {
|
|
|
+ let { mark, preMark } = item[column_number - 1];
|
|
|
+ let _arr = new Array(column_number).fill('');
|
|
|
+ let back = column_number - 1;
|
|
|
+ let _preMark = preMark;
|
|
|
+ while (_preMark) {
|
|
|
+ _arr[back] = mark;
|
|
|
+ back -= 1;
|
|
|
+ _arr[back] = _preMark;
|
|
|
+ back -= 1;
|
|
|
+ let fMark = this.findMark(list, _preMark, 'pre');
|
|
|
+ if (back >= 0 && fMark) {
|
|
|
+ _arr[back] = fMark;
|
|
|
+ }
|
|
|
+ _preMark = fMark;
|
|
|
+ }
|
|
|
+ if (_arr.every((li) => li.length <= 0)) return;
|
|
|
+ preArr.push(_arr);
|
|
|
+ });
|
|
|
+ // 将 preArr 中的数据填充到 arr 无数据的位置中
|
|
|
+ if (preArr.length > 0 && emptyStringArray.length > 0) {
|
|
|
+ preArr.forEach((item, i) => {
|
|
|
+ arr[emptyStringArray[i]] = item;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ this.answer = arr;
|
|
|
+ },
|
|
|
+ deep: true,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
document.addEventListener('click', this.handleEventConnection);
|
|
|
+ document.addEventListener('mousemove', this.documentMousemouse);
|
|
|
+ document.addEventListener('mouseup', this.documentMouseup);
|
|
|
},
|
|
|
beforeDestroy() {
|
|
|
document.removeEventListener('click', this.handleEventConnection);
|
|
|
+ document.removeEventListener('mousemove', this.documentMousemouse);
|
|
|
+ document.removeEventListener('mouseup', this.documentMouseup);
|
|
|
},
|
|
|
methods: {
|
|
|
+ /* 用 mouse 事件模拟拖拽 开始*/
|
|
|
+ documentMousemouse(e) {
|
|
|
+ if (!this.drag) return;
|
|
|
+ // 使用 svg 绘制跟随鼠标移动的连接线
|
|
|
+ let svg = document.querySelector('.move-connection');
|
|
|
+ if (!svg) {
|
|
|
+ svg = document.createElementNS(svgNS, 'svg');
|
|
|
+ svg.classList.add('move-connection');
|
|
|
+ svg.setAttribute('style', `position: absolute; width: 100%; height: 100%;`);
|
|
|
+ let path = document.createElementNS(svgNS, 'path');
|
|
|
+ this.setPathAttr(path);
|
|
|
+ svg.appendChild(path);
|
|
|
+ this.$refs.list.appendChild(svg);
|
|
|
+ }
|
|
|
+ let { clientX, clientY } = e;
|
|
|
+ let { i, j, position } = this.mousePointer;
|
|
|
+ let dom = document.getElementsByClassName(`${position}-line-${i}-${j}`)[0].getBoundingClientRect(); // 连线点的位置
|
|
|
+ let list = this.$refs.list.getBoundingClientRect(); // 列表的位置
|
|
|
+ let top = dom.top - list.top + 5; // 连线点距离列表顶部的距离 + 5 是为了让线条在连线点的中间
|
|
|
+ let left = dom.left - list.left + 5; // 连线点距离列表左边的距离 + 5 是为了让线条在连线点的中间
|
|
|
+ let mouseX = clientX - list.left; // 鼠标距离列表左边的距离
|
|
|
+ let mouseY = clientY - list.top; // 鼠标距离列表顶部的距离
|
|
|
+ let path = svg.querySelector('path');
|
|
|
+ path.setAttribute('d', `M ${left} ${top} L ${mouseX} ${mouseY}`);
|
|
|
+ },
|
|
|
+ documentMouseup() {
|
|
|
+ if (!this.drag) return;
|
|
|
+ this.drag = false;
|
|
|
+ document.querySelector('.move-connection')?.remove();
|
|
|
+ document.body.style.userSelect = 'auto'; // 允许选中文本
|
|
|
+ this.mousePointer = { i: -1, j: -1, position: 'pre', mark: '' };
|
|
|
+ },
|
|
|
+ mousedown(e, i, j, position, mark) {
|
|
|
+ this.drag = true;
|
|
|
+ document.body.style.userSelect = 'none'; // 禁止选中文本
|
|
|
+ this.mousePointer = { i, j, position, mark };
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 鼠标抬起事件,如果是一个合适的连接点,则创建连接线
|
|
|
+ * @param {PointerEvent} e 事件对象
|
|
|
+ * @param {Number} i 选项列表索引
|
|
|
+ * @param {Number} j 选项索引
|
|
|
+ * @param {'pre'|'next'} position 连线点位置
|
|
|
+ * @param {String} mark 选项标识
|
|
|
+ */
|
|
|
+ mouseup(e, i, j, position, mark) {
|
|
|
+ let { i: curI, j: curJ, position: curPosition, mark: curMark } = this.mousePointer;
|
|
|
+ if (curI === -1 && curJ === -1) return;
|
|
|
+ if (Math.abs(curJ - j) > 1 || position === curPosition || mark === curMark) return;
|
|
|
+ this.changeConnectionList(mark, position, true);
|
|
|
+ this.createLine(i, j, position, mark, true);
|
|
|
+ },
|
|
|
+ /* 用 mouse 事件模拟拖拽 结束 */
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 当点击的不是连线点时,清除所有连线点的选中状态
|
|
|
+ * @param {PointerEvent} e
|
|
|
+ */
|
|
|
handleEventConnection(e) {
|
|
|
if (e.target.classList.contains('connection')) return;
|
|
|
Array.from(document.getElementsByClassName('connection')).forEach((item) => {
|
|
@@ -71,16 +206,17 @@ export default {
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
- * 处理连线
|
|
|
+ * 处理点击连线
|
|
|
* @param {PointerEvent} e 事件对象
|
|
|
* @param {Number} i 选项列表索引
|
|
|
* @param {Number} j 选项索引
|
|
|
- * @param {String} position 连线点位置
|
|
|
+ * @param {'pre'|'next'} position
|
|
|
* @param {String} mark 选项标识
|
|
|
*/
|
|
|
- handleConnection(e, i, j, position, mark) {
|
|
|
+ handleClickConnection(e, i, j, position, mark) {
|
|
|
+ let { i: curI, j: curJ, position: curPosition, mark: curMark } = this.curConnectionPoint;
|
|
|
// 如果当前连线点不存在,则设置当前连线点
|
|
|
- if (this.curConnectionPoint.i === -1 && this.curConnectionPoint.j === -1) {
|
|
|
+ if (curI === -1 && curJ === -1) {
|
|
|
this.curConnectionPoint = { i, j, mark, position };
|
|
|
e.target.classList.add('focus');
|
|
|
return;
|
|
@@ -90,80 +226,158 @@ export default {
|
|
|
item.classList.remove('focus');
|
|
|
});
|
|
|
// 如果当前连线点与上一个连线点不在相邻的位置,则设置点击的连接点为当前连线点
|
|
|
- if (Math.abs(this.curConnectionPoint.j - j) > 1 || position === this.curConnectionPoint.position) {
|
|
|
+ if (Math.abs(curJ - j) > 1 || position === curPosition || mark === curMark) {
|
|
|
this.curConnectionPoint = { i, j, mark, position };
|
|
|
e.target.classList.add('focus');
|
|
|
return;
|
|
|
}
|
|
|
- this.pushConnectionList(j, mark);
|
|
|
+ this.changeConnectionList(mark, position);
|
|
|
// 如果当前连线点与上一个连线点在相邻的位置,则创建连接线
|
|
|
- this.createLine(e, i, j, position);
|
|
|
+ this.createLine(i, j, position, mark);
|
|
|
},
|
|
|
|
|
|
- // 创建连接线
|
|
|
- createLine(e, i, j, position) {
|
|
|
+ /**
|
|
|
+ * 创建连接线
|
|
|
+ * @param {Number} i 选项列表索引
|
|
|
+ * @param {Number} j 选项索引
|
|
|
+ * @param {'pre'|'next'} position
|
|
|
+ * @param {String} mark 选项标识
|
|
|
+ * @param {Boolean} isDrag 是否是拖拽
|
|
|
+ */
|
|
|
+ createLine(i, j, position, mark, isDrag = false) {
|
|
|
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();
|
|
|
+ const { curOffsetLeft, curOffsetTop, curMark } = this.computedCurConnectionPoint(isDrag);
|
|
|
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 width = Math.abs(offsetLeft - curOffsetLeft);
|
|
|
+ let height = Math.abs(offsetTop - curOffsetTop);
|
|
|
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;`,
|
|
|
+ `position:absolute; width: 128px; height: ${Math.max(8, height) + 4}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元素添加矩形
|
|
|
+ svg.classList.add('connection-line', `svg-${mark}-${curMark}`); // 添加类名
|
|
|
+ // 向SVG元素添加 path 元素
|
|
|
+ let path = document.createElementNS(svgNS, 'path');
|
|
|
+ path.setAttribute('d', `M ${size ? 0 : width} 0 L ${size ? width : 0} ${height}`); // 设置路径数据
|
|
|
+ this.setPathAttr(path);
|
|
|
+ svg.appendChild(path);
|
|
|
+ this.$refs.list.appendChild(svg); // 将SVG元素插入到文档中
|
|
|
+
|
|
|
+ // 清除当前连线点
|
|
|
this.curConnectionPoint = { i: -1, j: -1, position: 'pre', mark: '' };
|
|
|
},
|
|
|
-
|
|
|
- computedCurConnectionPoint() {
|
|
|
- const { i, j, position } = this.curConnectionPoint;
|
|
|
+ // 设置 path 公用属性
|
|
|
+ setPathAttr(path) {
|
|
|
+ path.setAttribute('stroke-width', '2'); // 设置线条宽度
|
|
|
+ path.setAttribute('stroke-linecap', 'round'); // 设置线段的两端样式
|
|
|
+ path.setAttribute('stroke', '#306eff'); // 设置填充颜色
|
|
|
+ path.setAttribute('transform', `translate(2, 2)`); // 设置偏移量
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 计算当前连线点的位置
|
|
|
+ * @param {Boolean} isDrag 是否是拖拽
|
|
|
+ */
|
|
|
+ computedCurConnectionPoint(isDrag = false) {
|
|
|
+ const { i, j, position, mark } = isDrag ? this.mousePointer : 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 };
|
|
|
+ let dom = document.getElementsByClassName(`${position}-line-${i}-${j}`)[0];
|
|
|
+ return { curOffsetLeft: dom.offsetLeft, curOffsetTop: dom.offsetTop, curMark: mark };
|
|
|
},
|
|
|
|
|
|
// 清除连线
|
|
|
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);
|
|
|
+ // 初始化答案列表
|
|
|
+ initAnswerList() {
|
|
|
+ let list = this.optionList.map((item) => {
|
|
|
+ return item.map(({ mark }) => {
|
|
|
+ return { mark, preMark: '', nextMark: '' };
|
|
|
+ });
|
|
|
});
|
|
|
- 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;
|
|
|
- }
|
|
|
+ this.$set(this, 'answerList', list);
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 修改连接列表
|
|
|
+ * @param {String} mark 选项标识
|
|
|
+ * @param {'pre'|'next'} position
|
|
|
+ * @param {Boolean} isDrag 是否是拖拽
|
|
|
+ */
|
|
|
+ changeConnectionList(mark, position, isDrag = false) {
|
|
|
+ const { mark: curMark, position: curPosition } = isDrag ? this.mousePointer : this.curConnectionPoint;
|
|
|
+ this.changeAnswerList(curMark, mark, position);
|
|
|
+ this.changeAnswerList(mark, curMark, curPosition);
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 改变答案列表
|
|
|
+ * @param {String} curMark 当前选项标识
|
|
|
+ * @param {String} mark 选项标识
|
|
|
+ * @param {'pre'|'next'} position
|
|
|
+ */
|
|
|
+ changeAnswerList(curMark, mark, position) {
|
|
|
+ let oldPointer = { mark: '', position: '' };
|
|
|
+ this.answerList.find((item) =>
|
|
|
+ item.find((li) => {
|
|
|
+ if (li.mark === curMark) {
|
|
|
+ if (position === 'pre') {
|
|
|
+ if (li.nextMark) {
|
|
|
+ oldPointer = { mark: li.nextMark, position: 'next' };
|
|
|
+ }
|
|
|
+ li.nextMark = mark;
|
|
|
+ } else {
|
|
|
+ if (li.preMark) {
|
|
|
+ oldPointer = { mark: li.preMark, position: 'pre' };
|
|
|
+ }
|
|
|
+ li.preMark = mark;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ );
|
|
|
+ if (!oldPointer.mark) return;
|
|
|
+ document.querySelector(`svg.connection-line.svg-${curMark}-${oldPointer.mark}`)?.remove();
|
|
|
+ document.querySelector(`svg.connection-line.svg-${oldPointer.mark}-${curMark}`)?.remove();
|
|
|
+
|
|
|
+ this.answerList.find((item) =>
|
|
|
+ item.find((li) => {
|
|
|
+ if (li.mark === oldPointer.mark) {
|
|
|
+ if (oldPointer.position === 'pre') {
|
|
|
+ li.nextMark = '';
|
|
|
+ } else {
|
|
|
+ li.preMark = '';
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ );
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据 mark 查找 nextMark 或 preMark
|
|
|
+ * @param {Array} list 答案列表
|
|
|
+ * @param {String} mark 标记
|
|
|
+ * @param {'pre'|'next'} type 类型
|
|
|
+ * @returns {String} 返回 nextMark 或 preMark
|
|
|
+ */
|
|
|
+ findMark(list, mark, type) {
|
|
|
+ let fMark = '';
|
|
|
+ list.find((item) => {
|
|
|
+ return item.find((li) => {
|
|
|
+ if (mark === li.mark) {
|
|
|
+ fMark = type === 'pre' ? li.preMark : li.nextMark;
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ return fMark;
|
|
|
},
|
|
|
},
|
|
|
};
|