Procházet zdrojové kódy

1. 完成连线题预览
2. 对接复制练习题接口

dusenyao před 1 rokem
rodič
revize
e0672da192

+ 1 - 1
.vscode/settings.json

@@ -1,3 +1,3 @@
 {
-  "cSpell.words": ["cascader"]
+  "cSpell.words": ["cascader", "GCLS"]
 }

+ 7 - 0
src/api/app.js

@@ -30,6 +30,13 @@ export function GetFileStoreInfo(data) {
 }
 
 /**
+ * 得到用户能进入的子系统列表(电脑端)
+ */
+export function GetChildSysList_CanEnter_PC(data) {
+  return http.post(`${process.env.VUE_APP_FileServer}?MethodName=login_control-GetChildSysList_CanEnter_PC`, data);
+}
+
+/**
  * 上传文件
  * @param {String} SecurityLevel 保密级别
  * @param {object} file 文件对象

+ 14 - 0
src/api/exercise.js

@@ -77,3 +77,17 @@ export function DeleteQuestion(data) {
 export function CreateShareRecord(data) {
   return http.post(`/TeachingServer/ExerciseManager/CreateShareRecord`, data);
 }
+
+/**
+ * 复制练习题到公共库
+ */
+export function CopyExerciseToPublicStore(data) {
+  return http.post(`/TeachingServer/ExerciseManager/CopyExerciseToPublicStore`, data);
+}
+
+/**
+ * 复制练习题到个人库
+ */
+export function CopyExerciseToPersonalStore(data) {
+  return http.post(`/TeachingServer/ExerciseManager/CopyExerciseToPersonalStore`, data);
+}

+ 58 - 12
src/layouts/default/header/index.vue

@@ -1,9 +1,18 @@
 <template>
   <header class="header">
     <el-image class="logo" :src="$store.state.app.config.logo_image_url" />
-    <div class="header-list">
-      <div class="title">练习管理</div>
-    </div>
+
+    <ul class="header-list">
+      <li
+        v-for="({ name }, i) in projectList"
+        :key="i"
+        :class="i === LoginNavIndex ? 'active' : ''"
+        @click="handleCommand(i)"
+      >
+        {{ name }}
+      </li>
+    </ul>
+
     <div v-if="!token" class="selectLoginOrRegistration">
       <span @click="cutLoginReg">登录</span>
     </div>
@@ -27,6 +36,7 @@
 </template>
 
 <script>
+import { GetChildSysList_CanEnter_PC } from '@/api/app';
 import { getToken } from '@/utils/auth';
 
 export default {
@@ -36,9 +46,35 @@ export default {
 
     return {
       token,
+      loginNav: 'GCLS-Exercise',
+      LoginNavIndex: 0,
+      projectList: [],
     };
   },
+  created() {
+    GetChildSysList_CanEnter_PC().then(({ child_sys_list }) => {
+      if (child_sys_list && child_sys_list.length > 0) {
+        this.projectList = child_sys_list;
+        this.projectList.forEach((item, index) => {
+          if (item.key === this.loginNav) {
+            this.LoginNavIndex = index;
+          }
+        });
+      }
+    });
+  },
   methods: {
+    // 切换项目
+    handleCommand(command) {
+      this.LoginNavIndex = command;
+      if (!this.token) {
+        this.$message.warning('请先登录');
+        window.location.href = '/';
+        return;
+      }
+      const relative_path = this.projectList[command].relative_path;
+      location.href = relative_path;
+    },
     logout() {
       this.$store.dispatch('user/signOut');
       window.location.href = '/';
@@ -53,6 +89,7 @@ export default {
   top: 0;
   left: 0;
   display: flex;
+  column-gap: 24px;
   align-items: center;
   height: $header-h;
   padding: 0 24px;
@@ -61,21 +98,30 @@ export default {
   border-bottom: 1px solid #ebebeb;
 
   .logo {
-    width: 270px;
-    height: 100%;
+    height: 48px;
+    margin-right: 36px;
   }
 
   &-list {
     display: flex;
     flex: 1;
 
-    .title {
-      padding: 6px 8px;
-      margin-left: 24px;
-      font-weight: bold;
-      color: $main-color;
-      background-color: #e8f1ff;
-      border-radius: 4px;
+    li {
+      padding: 0 12px;
+      font-weight: 500;
+      line-height: $header-h;
+      color: rgba(0, 0, 0, 45%);
+      white-space: nowrap;
+      cursor: pointer;
+
+      &:hover {
+        background: #f6f6f6;
+      }
+
+      &.active {
+        color: #f90;
+        background: #ffefd6;
+      }
     }
   }
 

+ 2 - 0
src/views/exercise_questions/data/matching.js

@@ -7,6 +7,8 @@ export const columnNumberList = [
   { value: 3, label: '3列' },
 ];
 
+export const svgNS = 'http://www.w3.org/2000/svg'; // SVG命名空间
+
 export function getOption(content = '') {
   return { content, mark: getRandomNumber() };
 }

+ 265 - 51
src/views/exercise_questions/preview/MatchingPreview.vue

@@ -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;
     },
   },
 };

+ 7 - 2
src/views/home/personal_question/index.vue

@@ -27,7 +27,7 @@
           <template slot-scope="{ row }">
             <span class="link" @click="$router.push({ path: '/exercise', query: { id: row.id } })">编辑</span>
             <span class="link" @click="share(row.id)">分享</span>
-            <span class="link">公开</span>
+            <span class="link" @click="copyExerciseToPublicStore(row.id)">公开</span>
             <span class="link danger" @click="deleteExercise(row.id)">删除</span>
           </template>
         </el-table-column>
@@ -63,7 +63,7 @@
 </template>
 
 <script>
-import { PageQueryExerciseList, DeleteExercise } from '@/api/exercise';
+import { PageQueryExerciseList, DeleteExercise, CopyExerciseToPublicStore } from '@/api/exercise';
 
 import HomeCommon from '../common.vue';
 import CreateExercise from './components/CreateExercise.vue';
@@ -132,6 +132,11 @@ export default {
         })
         .catch(() => {});
     },
+    copyExerciseToPublicStore(exercise_id) {
+      CopyExerciseToPublicStore({ exercise_id }).then(() => {
+        this.$message.success('公开成功');
+      });
+    },
     share(id) {
       this.exerciseId = id;
       this.visibleShare = true;

+ 7 - 2
src/views/home/public_question/index.vue

@@ -24,7 +24,7 @@
           >
             编辑
           </span>
-          <span class="link">复制到个人题库</span>
+          <span class="link" @click="copyExerciseToPersonalStore(row.id)">复制到个人题库</span>
         </template>
       </el-table-column>
     </template>
@@ -52,7 +52,7 @@
 </template>
 
 <script>
-import { PageQueryExerciseList, GetLabelList } from '@/api/exercise';
+import { PageQueryExerciseList, GetLabelList, CopyExerciseToPersonalStore } from '@/api/exercise';
 
 import HomeCommon from '../common.vue';
 
@@ -102,6 +102,11 @@ export default {
         this.label_list = label_list;
       });
     },
+    copyExerciseToPersonalStore(exercise_id) {
+      CopyExerciseToPersonalStore({ exercise_id }).then(() => {
+        this.$message.success('复制成功');
+      });
+    },
   },
 };
 </script>