Browse Source

Merge branch 'master' of http://60.205.254.193:3000/GCLS/eep_page

dsy 1 week ago
parent
commit
8a36c81b88

File diff suppressed because it is too large
+ 2 - 16530
package-lock.json


+ 2 - 0
package.json

@@ -38,6 +38,8 @@
     "three": "^0.181.2",
     "tinymce": "^5.10.9",
     "v-fit-columns": "^0.2.0",
+    "vis-data": "^8.0.3",
+    "vis-network": "^10.0.2",
     "vue": "^2.6.14",
     "vue-esign": "^1.1.4",
     "vue-router": "^3.6.5",

+ 31 - 7
src/api/book.js

@@ -285,50 +285,74 @@ export function GetBookUnifiedAttrib(data) {
 
 /**
  *@description 添加我的笔记
- * @param {object} data
+ * @param {object} data  
  */
 export function AddMyNote(data) {
   return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_preview_manager-AddMyNote`, data);
 }
 /**
  *@description 更新我的笔记
- * @param {object} data
+ * @param {object} data  
  */
 export function UpdateMyNote(data) {
   return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_preview_manager-UpdateMyNote`, data);
 }
 /**
  * @description 得到我的笔记列表
- * @param {object} data
+ * @param {object} data 
  */
 export function GetMyNoteList(data) {
   return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_preview_manager-GetMyNoteList`, data);
 }
 /**
  * @description 删除我的笔记
- * @param {object} data
+ * @param {object} data 
  */
 export function DeleteMyNote(data) {
   return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_preview_manager-DeleteMyNote`, data);
 }
 /**
  *@description 添加我的收藏
- * @param {object} data
+ * @param {object} data  
  */
 export function AddMyCollect(data) {
   return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_preview_manager-AddMyCollect`, data);
 }
 /**
  * @description 得到我的收藏列表
- * @param {object} data
+ * @param {object} data 
  */
 export function GetMyCollectList(data) {
   return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_preview_manager-GetMyCollectList`, data);
 }
 /**
  * @description 删除我的收藏
- * @param {object} data
+ * @param {object} data 
  */
 export function DeleteMyCollect(data) {
   return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_preview_manager-DeleteMyCollect`, data);
 }
+
+/**
+ * @description 根据教材内容生成知识图谱
+ * @param {object} data
+ */
+export function MangerGenerateKnowledgeGraphByBookContent(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_content_manager-GenerateKnowledgeGraphByBookContent`,data);
+}
+
+/**
+ * @description 得到教材知识图谱
+ * @param {object} data
+ */
+export function GetBookKnowledgeGraph(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_content_manager-GetBookKnowledgeGraph`, data);
+}
+
+/**
+ * @description 保存教材知识图谱
+ * @param {object} data
+ */
+export function SaveBookKnowledgeGraph(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_content_manager-SaveBookKnowledgeGraph`, data);
+}

+ 23 - 7
src/components/CommonPreview.vue

@@ -252,6 +252,16 @@
         @child-click="handleNodeClick"
       />
     </el-dialog>
+    <el-dialog
+      title=""
+      :visible="visibleVisNetwork"
+      width="1100px"
+      class="audit-dialog"
+      :close-on-click-modal="false"
+      @close="dialogClose('VisNetwork')"
+    >
+      <VisNetwork ref="visNetworkRef" :book-id="projectId" @child-click="handleNodeClick" />
+    </el-dialog>
 
     <ExplanatoryNoteDialog
       ref="explanatoryNote"
@@ -269,6 +279,7 @@ import CoursewarePreview from '@/views/book/courseware/preview/CoursewarePreview
 import RichText from '@/components/RichText.vue';
 import { isTrue } from '@/utils/validate';
 import MindMap from '@/components/MindMap.vue';
+import VisNetwork from '@/components/VisNetwork.vue';
 import VideoPlay from '@/views/book/courseware/preview/components/common/VideoPlay.vue';
 import AudioPlay from '@/views/book/courseware/preview/components/common/AudioPlay.vue';
 import AuditRemark from '@/components/AuditRemark.vue';
@@ -310,6 +321,7 @@ export default {
     VideoPlay,
     AuditRemark,
     ExplanatoryNoteDialog,
+    VisNetwork,
   },
   provide() {
     return {
@@ -346,7 +358,7 @@ export default {
     const sidebarIconList = [
       // { icon: 'search', title: '搜索', handle: '', param: {} },
       { icon: 'mindmap', title: '思维导图', handle: 'openMindMap', param: {} },
-      // { icon: 'knowledge', title: '知识图谱', handle: '', param: {} },
+      { icon: 'knowledge', title: '知识图谱', handle: 'openVisNetwork', param: {} },
       // { icon: 'totalResources', title: '总资源', handle: '', param: {} },
       { icon: 'collect', title: '收藏', handle: 'getCollect', param: { type: '3' } },
       { icon: 'audio', title: '音频', handle: 'openDrawer', param: { type: '1' } },
@@ -401,6 +413,7 @@ export default {
       curToolbarIcon: this.isShowAudit ? 'audit' : '',
       sidebarIconList,
       visibleMindMap: false,
+      visibleVisNetwork: false,
       isChildDataLoad: false,
       mindMapJsonData: {}, // 思维导图json数据
       drawerType: '', // 抽屉类型
@@ -433,6 +446,7 @@ export default {
       oldRichData: {},
       newSelectedInfo: null,
       allCottectList: [],
+      book_id: '',
     };
   },
   computed: {
@@ -679,7 +693,6 @@ export default {
       }
       this.curToolbarIcon = icon;
     },
-
     openMindMap() {
       MangerGetBookMindMap({ book_id: this.projectId }).then(({ content }) => {
         if (content) {
@@ -689,7 +702,6 @@ export default {
       });
       this.visibleMindMap = true;
     },
-
     async handleNodeClick(data) {
       let [nodeId, componentId] = data.split('#');
       if (nodeId) this.selectNode(nodeId);
@@ -705,6 +717,10 @@ export default {
         }
       }
       this.visibleMindMap = false;
+      this.visibleVisNetwork = false;
+    },
+    async openVisNetwork() {
+      this.visibleVisNetwork = true;
     },
 
     /**
@@ -864,8 +880,8 @@ export default {
         this.$refs.courserware.handLocation(item);
       }
     },
-    async getNote() {
-      this.drawerType = 4;
+    async getNote(params) {
+      if (params && params.type) this.drawerType = Number(params.type);
       this.allNoteList = [];
       await GetMyNoteList({ courseware_id: this.curSelectId }).then((res) => {
         if (res.status === 1) {
@@ -959,8 +975,8 @@ export default {
       });
     },
 
-    async getCollect() {
-      this.drawerType = 3;
+    async getCollect(params) {
+      if (params && params.type) this.drawerType = Number(params.type);
       this.allCottectList = [];
       await GetMyCollectList({ courseware_id: this.curSelectId }).then((res) => {
         if (res.status === 1) {

+ 417 - 0
src/components/VisNetwork.vue

@@ -0,0 +1,417 @@
+<template>
+  <div class="vis-network-container">
+    <div v-if="isEdit" class="toolbar">
+      说明:双击节点可以进行操作
+      <el-button type="primary" @click="addNode">添加子节点</el-button>
+      <el-button type="danger" @click="deleteNode">删除</el-button>
+    </div>
+    <div ref="network" class="network">
+      <p class="noData">{{ loadingTitle }}</p>
+    </div>
+    <el-dialog
+      title="编辑"
+      :visible.sync="showEditModal"
+      width="30%"
+      :close-on-click-modal="false"
+      :append-to-body="true"
+    >
+      <el-form :model="editForm" label-width="80px">
+        <el-form-item label="所属层级">
+          <el-input v-model="editForm.fullPath" type="textarea" disabled />
+        </el-form-item>
+        <el-form-item label="节点名称">
+          <el-input v-model="editForm.label" type="textarea" placeholder="请输入节点名称" />
+        </el-form-item>
+
+        <el-form-item v-if="false" label="节点类型">
+          <select v-model="editForm.type" class="form-select">
+            <option value="0">章节</option>
+            <option value="3">资源类型</option>
+            <option value="4">具体文件</option>
+          </select>
+        </el-form-item>
+      </el-form>
+
+      <!-- 按钮区域 -->
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="cancelEdit">取消</el-button>
+        <el-button type="success" @click="saveEdit">保存</el-button>
+        <el-button type="danger" @click="deleteNode">删除</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { DataSet } from 'vis-data';
+import { Network } from 'vis-network';
+import { GetBookKnowledgeGraph } from '@/api/book';
+export default {
+  name: 'VisNetwork',
+  props: {
+    isEdit: {
+      type: Boolean,
+      default: false,
+    },
+    bookId: {
+      type: String,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      loadingTitle: '数据加载中...',
+      network: null,
+      nodes: null,
+      edges: null,
+      selectedNode: null,
+      showEditModal: false,
+      editingNode: null,
+      positions: {},
+      jsonData: {},
+      hasData: false,
+      editForm: {
+        label: '',
+        type: '0',
+        fullPath: '',
+      },
+    };
+  },
+  computed: {},
+  created() {
+    this.hasData = false;
+    this.getBookVisNetWork();
+  },
+  mounted() {},
+  beforeDestroy() {
+    this.destroyNetwork();
+  },
+  methods: {
+    /**
+     * 得到教材知识图谱
+     * @param {string} id - 教材ID
+     */
+    getBookVisNetWork() {
+      GetBookKnowledgeGraph({ book_id: this.bookId })
+        .then((res) => {
+          this.convertToVisNetworkData(res);
+        })
+        .catch(() => {
+          this.loadingTitle = '数据未生成';
+        });
+    },
+    init() {
+      this.destroyNetwork();
+      this.initNetwork(this.jsonData.nodes, this.jsonData.edges);
+    },
+    destroyNetwork() {
+      if (this.network) {
+        this.network.destroy();
+      }
+      this.positions = {};
+    },
+    initNetwork(nodeData, edgeData) {
+      this.nodes = new DataSet(nodeData);
+      this.edges = new DataSet(edgeData);
+      const data = {
+        nodes: this.nodes,
+        edges: this.edges,
+      };
+
+      // 创建网络图
+      const container = this.$refs.network;
+
+      let options = {
+        nodes: {
+          shape: 'box',
+          margin: 10,
+          size: 20,
+          font: {
+            size: 14,
+          },
+          borderWidth: 2,
+        },
+        edges: {
+          width: 2,
+          color: { color: '#848484' },
+          smooth: {
+            type: 'continuous',
+          },
+        },
+        groups: {
+          default: {
+            color: { background: '#97C2FC', border: '#6AA6F8' },
+            borderWidth: 2,
+            font: { color: '#FFFFFF' },
+          },
+          type0: {
+            color: { background: '#97C2FC', border: '#84B7FD' },
+            borderWidth: 3,
+          },
+          type1: {
+            color: { background: '#FFA807', border: '#FDA400' },
+            borderWidth: 2,
+          },
+          type2: {
+            color: { background: '#2ECC71', border: '#0BCC5D' },
+            borderWidth: 2,
+          },
+          type3: {
+            color: { background: '#FB7E81', border: '#F85C5F' },
+            borderWidth: 2,
+          },
+        },
+        physics: {
+          enabled: true,
+          stabilization: {
+            iterations: 100,
+          },
+        },
+        interaction: {
+          hover: true,
+          tooltipDelay: 200,
+        },
+      };
+
+      this.network = new Network(container, data, options);
+
+      // 添加事件监听
+      this.network.on('click', (params) => {
+        if (params.nodes.length > 0) {
+          const nodeId = params.nodes[0];
+          if (!this.isEdit) {
+            this.$emit('child-click', nodeId);
+            return;
+          }
+          this.selectedNode = this.nodes.get(nodeId);
+          this.editingNode = nodeId;
+        } else {
+          this.selectedNode = null;
+        }
+      });
+
+      this.network.on('doubleClick', (params) => {
+        if (!this.isEdit) return;
+        if (params.nodes.length > 0) {
+          const nodeId = params.nodes[0];
+          this.handleNodeDoubleClick(nodeId);
+        }
+      });
+
+      this.network.on('oncontext', (params) => {
+        params.event.preventDefault();
+      });
+      this.network.on('hoverNode', (params) => {});
+    },
+
+    // 树形数据转换工具
+    convertToVisData(treeData) {
+      const nodes = [];
+      const edges = [];
+      let x = 0;
+      let y = 0;
+
+      const traverse = (node, parentId = null, level = 0, parentPath = '') => {
+        const nodeId = node.data.uid;
+
+        // 创建节点
+        nodes.push({
+          id: nodeId,
+          label: node.data.text,
+          type: node.data.type,
+          level,
+          fullPath: parentPath, // 保存所有父级路径,用->分隔
+          group: `type${node.data.type || 0}`,
+        });
+        // 创建边(如果有父节点)
+        if (parentId) {
+          edges.push({
+            from: parentId,
+            to: nodeId,
+            arrows: 'to',
+            smooth: { type: 'cubicBezier' },
+          });
+        }
+
+        // 递归处理子节点
+        if (node.children && node.children.length > 0) {
+          // 构建当前节点的完整路径
+          const currentPath = parentPath ? `${parentPath}->${node.data.text}` : node.data.text;
+
+          node.children.forEach((child, index) => {
+            if (level < 3) traverse(child, nodeId, level + 1, currentPath);
+          });
+        }
+      };
+
+      // 调用时传入空字符串作为初始路径
+      traverse(treeData.root, null, 0, '');
+      return { nodes, edges };
+    },
+
+    handleNodeDoubleClick(nodeId) {
+      const node = this.nodes.get(nodeId);
+      console.info(node);
+      if (node) {
+        this.editingNode = nodeId;
+        this.editForm = {
+          label: node.label,
+          type: node.type,
+          fullPath: node.fullPath,
+        };
+        this.showEditModal = true;
+      }
+    },
+    addOrUpdate(data) {
+      if (!data) return;
+      let oldNodes = this.nodes.get();
+      let oldEdges = this.edges.get();
+      const node = this.nodes.get(data.id);
+      if (node) {
+        oldNodes = oldNodes.filter((x) => x.id !== node.id);
+        let newData = Object.assign({}, node, data);
+        oldNodes.push(newData);
+      } else {
+        oldNodes.push(data);
+      }
+
+      this.positions = this.network.getPositions() || {};
+      oldNodes.map((x) => {
+        if (x.id && this.positions[x.id]) {
+          let tmpPosition = this.positions[x.id];
+          x.x = tmpPosition.x;
+          x.y = tmpPosition.y;
+        }
+      });
+      console.info('oldNodes', oldNodes);
+      this.initNetwork(oldNodes, oldEdges);
+    },
+    addNode() {
+      if (!this.selectedNode) {
+        this.$message.error('请先选择父节点!');
+        return;
+      }
+      let newId = this.generateUniqueId();
+      const newNode = Object.assign(
+        {},
+        {
+          id: newId,
+          label: '新增节点',
+          fullPath: `${this.selectedNode.fullPath}->${this.selectedNode.label}`,
+          type: (this.selectedNode.type || 0) + 1,
+          level: (this.selectedNode.level || 0) + 1,
+        },
+      );
+      newNode.group = `type${newNode.type}`;
+
+      newId = this.generateUniqueId();
+      this.edges.add({
+        id: newId,
+        from: this.selectedNode.id,
+        to: newNode.id,
+        arrows: 'to',
+      });
+
+      // this.nodes.add(newNode);
+      this.addOrUpdate(newNode);
+    },
+
+    // 保存编辑
+    saveEdit() {
+      if (!this.editingNode) return;
+
+      const updatedNode = {
+        id: this.editingNode,
+        label: this.editForm.label,
+        type: parseInt(this.editForm.type),
+      };
+
+      // this.nodes.update(updatedNode);
+      // this.$emit('node-updated', updatedNode);
+      this.addOrUpdate(updatedNode);
+      this.cancelEdit();
+    },
+
+    // 取消编辑
+    cancelEdit() {
+      this.showEditModal = false;
+      this.editingNode = null;
+      this.editForm = { label: '', type: '0' };
+    },
+
+    // 删除节点
+    deleteNode() {
+      if (!this.editingNode) return;
+      this.$confirm('确定要删除此节点吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          this.nodes.remove({ id: this.editingNode });
+          this.$emit('node-deleted', this.editingNode);
+          this.cancelEdit();
+        })
+        .catch(() => {});
+    },
+    saveData() {
+      if (!this.hasData) return null;
+      let nodes = this.nodes.get();
+      let edges = this.edges.get();
+      this.positions = this.network.getPositions() || {};
+      nodes.map((x) => {
+        if (x.id && this.positions[x.id]) {
+          let tmpPosition = this.positions[x.id];
+          x.x = tmpPosition.x;
+          x.y = tmpPosition.y;
+        }
+      });
+      return { nodes, edges };
+    },
+    generateUniqueId() {
+      return Date.now() + Math.random().toString(36).substr(2, 9);
+    },
+
+    convertToVisNetworkData(res) {
+      if (res && res.status === 1) {
+        this.loadingTitle = '数据渲染中...';
+        if (res.content_node && res.content_edge) {
+          let nodes = (JSON.parse(res.content_node) || {}).nodes;
+          let edges = (JSON.parse(res.content_edge) || {}).edges;
+          nodes.map((x) => (x.group = `type${x.type || 0}`));
+          // nodes=nodes.filter(x=>x.level<4)
+          this.jsonData = {
+            nodes,
+            edges,
+          };
+          this.hasData = true;
+          this.init();
+        }
+      }
+      this.loadingTitle = '数据未生成';
+    },
+  },
+};
+</script>
+
+<style scoped>
+.vis-network-container {
+  width: 100%;
+  padding: 1px;
+}
+
+.network {
+  width: 100%;
+  height: calc(100vh - 280px);
+  margin-bottom: 5px;
+  border: 1px solid #e0e0e0;
+  border-radius: 8px;
+}
+
+.noData {
+  display: flex;
+  align-items: center; /* 垂直居中 */
+  justify-content: center; /* 水平居中 */
+  height: calc(100vh - 250px);
+}
+</style>

+ 6 - 0
src/router/modules/project.js

@@ -158,6 +158,12 @@ const projectPage = {
       name: 'ProjectMindMap',
       component: () => import('@/views/project_manage/project/ProjectMindMap.vue'),
     },
+    // 机构用户 -> 项目管理 -> 项目 -> 知识图谱
+    {
+      path: 'project/vis_network/:projectId',
+      name: 'ProjectVisNetwork',
+      component: () => import('@/views/project_manage/project/ProjectVisNetwork.vue'),
+    },
     // 机构用户 -> 项目管理 -> 已上架教材
     {
       path: 'book',

+ 10 - 2
src/views/book/courseware/create/components/question/dialogue_article/Article.vue

@@ -136,11 +136,11 @@
           />
         </div>
       </div>
-      <el-dialog title="标注" :visible.sync="remarkVisible" width="50%">
+      <el-dialog title="标注" :visible.sync="remarkVisible" width="50%" :close-on-click-modal="false">
         <div v-if="remark" class="remark">
           <div class="adult-book-input-item">
             <span class="adult-book-lable">中文:</span>
-            <el-input
+            <!-- <el-input
               v-model="remark.chs"
               class="adult-book-input"
               type="textarea"
@@ -149,6 +149,14 @@
               maxlength="200"
               show-word-limit
               @blur="onBlur(remark, 'chs')"
+            /> -->
+            <RichText
+              ref="richText"
+              v-model="remark.chs"
+              toolbar="fontselect fontsizeselect forecolor backcolor | underline | bold italic strikethrough alignleft aligncenter alignright"
+              :wordlimit-num="200"
+              :font-size="data?.unified_attrib?.font_size"
+              :font-family="data?.unified_attrib?.font"
             />
           </div>
           <div class="adult-book-input-item">

+ 2 - 2
src/views/book/courseware/create/components/question/table/Table.vue

@@ -49,13 +49,13 @@
           >
             <SvgIcon icon-class="strikethrough" size="20" />
           </span>
-          <span :class="[data.styles.textAlign === 'start' ? 'active' : '']" @click="data.styles.textAlign = 'left'">
+          <span :class="[data.styles.textAlign === 'start' ? 'active' : '']" @click="data.styles.textAlign = 'start'">
             <SvgIcon icon-class="align-left" size="20" />
           </span>
           <span :class="[data.styles.textAlign === 'center' ? 'active' : '']" @click="data.styles.textAlign = 'center'">
             <SvgIcon icon-class="align-center" size="20" />
           </span>
-          <span :class="[data.styles.textAlign === 'end' ? 'active' : '']" @click="data.styles.textAlign = 'right'">
+          <span :class="[data.styles.textAlign === 'end' ? 'active' : '']" @click="data.styles.textAlign = 'end'">
             <SvgIcon icon-class="align-right" size="20" />
           </span>
         </div>

File diff suppressed because it is too large
+ 0 - 5
src/views/book/courseware/preview/CoursewarePreview.vue


+ 1 - 1
src/views/book/courseware/preview/components/character/CharacterPreview.vue

@@ -178,7 +178,7 @@
               <div class="words-left" :style="{}">
                 <AudioPlay
                   v-if="isEnable(data.property.is_enable_voice)"
-                  :file-id="item.audio_file_id ? item.audio_file_id.file_url : ''"
+                  :file-id="item.audio_file_id ? item.audio_file_id : ''"
                   :theme-color="
                     data.unified_attrib && data.unified_attrib.topic_color ? data.unified_attrib.topic_color : ''
                   "

+ 2 - 2
src/views/book/courseware/preview/components/dialogue_article/NormalModelChs.vue

@@ -1133,7 +1133,7 @@ export default {
 
           &.hasRemark {
             box-sizing: border-box;
-            width: 553px;
+            width: 60%;
             border-right: 1px rgba(0, 0, 0, 10%) solid;
           }
         }
@@ -1184,7 +1184,7 @@ export default {
       padding: 8px 24px;
 
       &.hasRemark {
-        width: 553px;
+        width: 60%;
         padding: 8px 0 8px 23px;
         border-right: 1px rgba(0, 0, 0, 10%) solid;
       }

+ 2 - 2
src/views/book/courseware/preview/components/dialogue_article/PhraseModelChs.vue

@@ -1119,7 +1119,7 @@ export default {
 
           &.hasRemark {
             box-sizing: border-box;
-            width: 553px;
+            width: 60%;
             border-right: 1px rgba(0, 0, 0, 10%) solid;
           }
         }
@@ -1175,7 +1175,7 @@ export default {
       padding: 8px 24px;
 
       &.hasRemark {
-        width: 553px;
+        width: 60%;
         padding: 8px 0 8px 23px;
         border-right: 1px rgba(0, 0, 0, 10%) solid;
       }

+ 11 - 6
src/views/book/courseware/preview/components/dialogue_article/RemarkChs.vue

@@ -8,8 +8,8 @@
     class="remarkChs"
     :style="{ top: marginTop ? marginTop + 'px' : '0px' }"
   >
-    <div v-if="remarkDetail.chs" class="remark-chs">{{ remarkDetail.chs }}</div>
-    <div v-if="remarkDetail.en" class="remark-en">{{ remarkDetail.en }}</div>
+    <div v-if="remarkDetail.chs" class="remark-chs" v-html="remarkDetail.chs"></div>
+    <div v-if="remarkDetail.en" class="remark-en" v-html="remarkDetail.en"></div>
     <div v-if="remarkDetail.img_list && remarkDetail.img_list.length > 0" class="remark-img">
       <el-image
         :style="{
@@ -55,7 +55,7 @@ export default {
   position: absolute;
   top: 0;
   box-sizing: border-box;
-  width: 178px;
+  width: 95%;
   border-radius: 8px;
   box-shadow: 0 4px 8px rgba(0, 0, 0, 10%);
 
@@ -74,13 +74,14 @@ export default {
     background: #988ed6;
     border: 1px solid rgba(0, 0, 0, 10%);
     border-radius: 8px 8px 0 0;
+
+    :deep p {
+      margin: 0;
+    }
   }
 
   > .remark-en {
     box-sizing: border-box;
-    display: flex;
-    align-items: center;
-    justify-content: center;
     min-height: 34px;
     font-size: 14px;
     line-height: 22px;
@@ -90,6 +91,10 @@ export default {
     border: 1px solid rgba(0, 0, 0, 10%);
     border-top: 0;
     border-radius: 0 0 8px 8px;
+
+    :deep p {
+      margin: 0;
+    }
   }
 
   > .remark-img {

+ 1 - 1
src/views/book/courseware/preview/components/dialogue_article/WordModelChs.vue

@@ -993,7 +993,7 @@ export default {
 
           &.hasRemark {
             box-sizing: border-box;
-            width: 553px;
+            width: 60%;
             border-right: 1px rgba(0, 0, 0, 10%) solid;
           }
         }

+ 2 - 1
src/views/book/courseware/preview/components/new_word/NewWordPreview.vue

@@ -927,6 +927,7 @@
                                     ? data.unified_attrib.topic_color
                                     : '#f44444',
                               }"
+                              v-if="itemh.hzDetail.hz_json"
                             />
                           </div>
                         </div>
@@ -1605,7 +1606,7 @@ export default {
     box-sizing: border-box;
 
     // width: 48px;
-    width: 60px;
+    // width: 60px;
     font-family: 'robot', 'alabo';
     text-align: left;
     word-break: break-word;

+ 1 - 0
src/views/book/courseware/preview/components/new_word/components/writeTableZoom.vue

@@ -139,6 +139,7 @@
                   :class="[indexh !== 0 ? 'writeTop-item-noLeft' : '']"
                   class="writeTop-item"
                   :style="{ borderColor: attrib && attrib.topic_color ? attrib.topic_color : '#f44444' }"
+                  v-if="itemh.hzDetail.hz_json"
                 />
               </div>
             </div>

+ 2 - 2
src/views/book/courseware/preview/components/rich_text/RichTextPreview.vue

@@ -3,7 +3,7 @@
     <SerialNumberPosition v-if="isEnable(data.property.sn_display_mode)" :property="data.property" />
 
     <div class="main">
-      <div ref="leftDiv" :style="{ width: data.note_list.length > 0 ? '' : '100%' }">
+      <div ref="leftDiv" :style="{ width: data.note_list?.length > 0 ? '' : '100%' }">
         <PinyinText
           v-if="isEnable(data.property.view_pinyin)"
           :paragraph-list="data.paragraph_list"
@@ -18,7 +18,7 @@
       </div>
 
       <div v-show="showLang" class="lang">
-        {{ data.multilingual.find((item) => item.type === getLang())?.translation }}
+        {{ data.multilingual?.find((item) => item.type === getLang())?.translation }}
       </div>
     </div>
 

+ 1 - 1
src/views/book/courseware/preview/components/table/TablePreview.vue

@@ -449,7 +449,7 @@ $border-color: #e6e6e6;
       align-items: end;
 
       p {
-        width: 100%;
+        max-width: 100%;
         margin: 0;
       }
 

+ 128 - 0
src/views/project_manage/project/ProjectVisNetwork.vue

@@ -0,0 +1,128 @@
+<template>
+  <div class="project-mind-map">
+    <MenuPage cur-key="/project_manage/project" />
+
+    <div class="project-mind-map__header">
+      <span class="name">{{ project_info.name }}</span>
+      <div class="courseware">
+        <div class="operator flex">
+          <span class="link" @click="getMangerGenerateVisNetworkByBook">根据教材内容生成知识图谱</span>
+          <span class="link" @click="saveBookVisnetwork">保存</span>
+          <span class="link" @click="goBackBookList">返回项目列表</span>
+        </div>
+      </div>
+    </div>
+    <p v-if="isWaiting" class="waiting">数据生成中...请等待</p>
+    <VisNetwork ref="visNetworRef" v-if="isChildDataLoad" :book-id="book_id" :is-edit="true" />
+  </div>
+</template>
+
+<script>
+import MenuPage from '@/views/personal_workbench/common/menu.vue';
+import VisNetwork from '@/components/VisNetwork.vue';
+import { GetProjectBaseInfo } from '@/api/project';
+import { MangerGenerateKnowledgeGraphByBookContent, SaveBookKnowledgeGraph } from '@/api/book';
+
+export default {
+  name: 'ProjectVisNetwork',
+  components: {
+    MenuPage,
+    VisNetwork,
+  },
+  data() {
+    return {
+      project_id: this.$route.params.projectId || '',
+      book_id: '',
+      project_info: {}, // 项目基本信息
+      isWaiting: false,
+      isChildDataLoad: false,
+      jsonData: '',
+    };
+  },
+  created() {
+    this.getProjectBaseInfo();
+  },
+  methods: {
+    goBackBookList() {
+      this.$router.push({ path: `/project_manage/project` });
+    },
+    /**
+     * 获取项目基本信息
+     * @param {string} id - 项目ID
+     */
+    getProjectBaseInfo() {
+      GetProjectBaseInfo({ id: this.project_id }).then(({ project_info, book_info_PBE }) => {
+        this.project_info = project_info;
+        this.book_id = book_info_PBE.id;
+        this.isChildDataLoad = true;
+      });
+    },
+    /**
+     * 生成知识图谱
+     * @param {string} id - 教材ID
+     */
+    getMangerGenerateVisNetworkByBook() {
+      this.isChildDataLoad = false;
+      this.isWaiting = true;
+      MangerGenerateKnowledgeGraphByBookContent({ book_id: this.book_id }).then((res) => {
+        if (res && res.status == 1) {
+          this.isChildDataLoad = true;
+          this.isWaiting = false;
+        }
+      });
+    },
+    saveBookVisnetwork() {
+      let data = this.$refs?.visNetworRef.saveData();
+      if (!data) {
+        this.$message.error('请先生成数据');
+        return;
+      }
+      var node = { nodes: data.nodes };
+      var edge = { edges: data.edges };
+      const loading = this.$loading({
+        lock: true,
+        text: '保存中...',
+        spinner: 'el-icon-loading',
+        background: 'rgba(0, 0, 0, 0.7)',
+      });
+      SaveBookKnowledgeGraph({
+        book_id: this.book_id,
+        content_node: JSON.stringify(node),
+        content_edge: JSON.stringify(edge),
+      }).then(() => {
+        this.$message.success('保存成功');
+        loading.close();
+      });
+    },
+  },
+};
+</script>
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.project-mind-map {
+  @include page-content(true);
+
+  &__header {
+    .name {
+      width: 240px;
+      font-size: 16px;
+      font-weight: bold;
+      border-right: $border;
+    }
+
+    .operator {
+      display: flex;
+      flex: 1;
+      justify-content: flex-end;
+    }
+  }
+
+  .waiting {
+    display: flex;
+    align-items: center; /* 垂直居中 */
+    justify-content: center; /* 水平居中 */
+    height: calc(100vh - 250px);
+  }
+}
+</style>

+ 5 - 1
src/views/project_manage/project/index.vue

@@ -23,11 +23,12 @@
         </el-table-column>
         <el-table-column prop="version_desc_YSJ" label="已上架教材版本" header-align="center" />
 
-        <el-table-column label="操作" fixed="right" width="320" align="center" header-align="center">
+        <el-table-column label="操作" fixed="right" width="400" align="center" header-align="center">
           <template slot-scope="{ row }">
             <span class="link">查看信息</span>
             <span class="link" @click="previewProject(row.id)">预览项目</span>
             <span class="link" @click="viewMindMap(row.id)">思维导图</span>
+            <span class="link" @click="viewVisNetwork(row.id)">知识图谱</span>
             <span class="link">预览历史版本</span>
           </template>
         </el-table-column>
@@ -73,6 +74,9 @@ export default {
     viewMindMap(projectId) {
       this.$router.push({ path: `/project_manage/project/mind_map/${projectId}` });
     },
+    viewVisNetwork(projectId) {
+      this.$router.push({ path: `/project_manage/project/vis_network/${projectId}` });
+    },
   },
 };
 </script>

Some files were not shown because too many files changed in this diff