Browse Source

知识图谱

zq 1 week ago
parent
commit
1279a3544c

+ 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);
+}

+ 18 - 2
src/components/CommonPreview.vue

@@ -252,6 +252,16 @@
         @child-click="handleNodeClick"
       />
     </el-dialog>
+    <el-dialog
+      title=""
+      :visible="visibleVisNetwork"
+      width="1100px"
+      class="audit-dialog"
+      @close="dialogClose('VisNetwork')"
+      :close-on-click-modal="false"
+    >
+      <VisNetwork ref="visNetworkRef" :book-id="projectId" />
+    </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: '', // 抽屉类型
@@ -438,6 +451,7 @@ export default {
       oldRichData: {},
       newSelectedInfo: null,
       allCottectList: [],
+      book_id: '',
     };
   },
   computed: {
@@ -697,7 +711,9 @@ export default {
       });
       this.visibleMindMap = true;
     },
-
+    async openVisNetwork() {
+      this.visibleVisNetwork = true;
+    },
     async handleNodeClick(data) {
       let [nodeId, componentId] = data.split('#');
       if (nodeId) this.selectNode(nodeId);

+ 416 - 0
src/components/VisNetwork.vue

@@ -0,0 +1,416 @@
+<template>
+  <div class="vis-network-container">
+    <div class="toolbar" v-if="isEdit">
+      说明:双击节点可以进行操作
+      <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 type="textarea" disabled v-model="editForm.fullPath"></el-input>
+        </el-form-item>
+        <el-form-item label="节点名称">
+          <el-input type="textarea" v-model="editForm.label" placeholder="请输入节点名称"></el-input>
+        </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, SaveBookKnowledgeGraph } 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();
+      let rootData = this.jsonData;
+      console.info(this.jsonData);
+      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;
+
+      var options = {
+        nodes: {
+          shape: 'box',
+          margin: 10,
+          size: 20,
+          font: {
+            size: 14,
+          },
+          borderWidth: 2,
+          margin: 10,
+        },
+        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];
+          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: 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;
+      var oldNodes = this.nodes.get();
+      var oldEdges = this.edges.get();
+      const node = this.nodes.get(data.id);
+      if (node) {
+        oldNodes = oldNodes.filter((x) => x.id != node.id);
+        var 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]) {
+          var 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;
+      }
+      var 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;
+
+      var 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;
+      var nodes = this.nodes.get();
+      var edges = this.edges.get();
+      this.positions = this.network.getPositions() || {};
+      nodes.map((x) => {
+        if (x.id && this.positions[x.id]) {
+          var tmpPosition = this.positions[x.id];
+          x.x = tmpPosition.x;
+          x.y = tmpPosition.y;
+        }
+      });
+      return { nodes: nodes, edges: 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) {
+          var nodes = (JSON.parse(res.content_node) || {}).nodes;
+          var 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: nodes,
+            edges: 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',

+ 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>