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