知识图谱编辑器完成

master
Your Name 8 months ago
parent b8bccf0e45
commit 755a8d9522
  1. 2
      index.html
  2. 7
      package.json
  3. 640
      pnpm-lock.yaml
  4. 99
      public/data.json
  5. 2
      src/Layout/footer/index.vue
  6. 1
      src/Layout/index.vue
  7. 10
      src/Layout/main/index.vue
  8. 65
      src/Layout/tabbar/index.vue
  9. BIN
      src/assets/images/banner2.png
  10. BIN
      src/assets/videos/920b6703c5f95b9ff774f27abf5d4f29.mp4
  11. 8
      src/main.ts
  12. 6
      src/permissions.ts
  13. 64
      src/router/module/constRouter/index.ts
  14. 69
      src/store/module/editAtlas.ts
  15. 32
      src/styles/index.scss
  16. 44
      src/utils/commen.js
  17. 118
      src/utils/rem.js
  18. 2
      src/views/dailyTeaching/index.vue
  19. 79
      src/views/editAtlas/actions/add_edge.js
  20. 32
      src/views/editAtlas/actions/add_node.js
  21. 28
      src/views/editAtlas/actions/hover_node.js
  22. 33
      src/views/editAtlas/actions/select_edge.js
  23. 397
      src/views/editAtlas/components/sidebar copy.vue
  24. 408
      src/views/editAtlas/components/sidebar.vue
  25. 521
      src/views/editAtlas/components/toolbar copy.vue
  26. 627
      src/views/editAtlas/components/toolbar.vue
  27. 25
      src/views/editAtlas/default/default_edge.js
  28. 29
      src/views/editAtlas/default/default_node.js
  29. 227
      src/views/editAtlas/index.vue
  30. 67
      src/views/home/index.vue
  31. 14
      src/views/home/test.vue
  32. 2
      src/views/outstandingStudents/index.vue
  33. 2
      src/views/pedagogicalReform/index.vue
  34. 22
      src/views/presentationAchievements/dev.vue
  35. 16
      src/views/presentationAchievements/index.vue
  36. 17
      src/views/presentationAchievements/test.vue
  37. 2
      src/views/productFusion/index.vue
  38. 68
      src/views/professionalProfile/index copy.vue
  39. 148
      src/views/professionalProfile/index.vue
  40. 2
      src/views/scientificResearch/index.vue
  41. 2
      src/views/talentDevelopment/index.vue
  42. 2
      tsconfig.json
  43. 25
      vite.config.ts

@ -9,5 +9,7 @@
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script src="https://gw.alipayobjects.com/os/lib/antv/g6/3.8.3/dist/g6.min.js"></script>
<!-- <script src="https://unpkg.com/3d-force-graph@1.73.3/dist/3d-force-graph.min.js"></script> -->
</body>
</html>

@ -16,10 +16,17 @@
"preinstall": "node ./scripts/preinstall.js"
},
"dependencies": {
"3d-force-graph": "^1.73.3",
"@antv/g6": "3.8.3",
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.6.8",
"element-plus": "^2.6.2",
"install": "^0.13.0",
"lib-flexible": "^0.3.2",
"pinia": "^2.1.7",
"postcss-plugin-px2rem": "^0.8.1",
"px2rem-loader": "^0.1.9",
"three": "^0.163.0",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},

File diff suppressed because it is too large Load Diff

@ -0,0 +1,99 @@
{
"nodes": [
{ "id": "node1", "label": "计算机", "color": "#4682B4" },
{ "id": "node2", "label": "前端", "color": "rgba(254, 241, 0, 1)" },
{ "id": "node3", "label": "js", "color": "rgba(239, 242, 18, 1)" },
{ "id": "node4", "label": "html", "color": "rgba(230, 234, 10, 1)" },
{ "id": "node5", "label": "css", "color": "rgba(244, 231, 0, 1)" },
{ "id": "node6", "label": "less", "color": "rgba(15, 245, 57, 1)" },
{ "id": "node7", "label": "scss", "color": "rgba(133, 255, 11, 1)" },
{ "id": "node8", "label": "VUE", "color": "rgba(42, 255, 0, 1)" },
{ "id": "node9", "label": "React", "color": "rgba(76, 73, 245, 1)" },
{ "id": "node10", "label": "模块化", "color": "#4682B4" },
{ "id": "node11", "label": "webpack", "color": "#4682B4" },
{ "id": "node12", "label": "vite", "color": "#4682B4" },
{ "id": "node13", "label": "uniapp", "color": "rgba(77, 255, 0, 1)" },
{ "id": "node14", "label": "element", "color": "rgba(33, 162, 255, 1)" },
{ "id": "node15", "label": "web3", "color": "rgba(255, 0, 251, 1)" },
{ "id": "node16", "label": "webGl", "color": "rgba(208, 0, 249, 1)" },
{ "id": "node17", "label": "three", "color": "rgba(225, 0, 255, 1)" },
{ "id": "node18", "label": "后端", "color": "rgba(0, 229, 255, 1)" },
{ "id": "node19", "label": "java", "color": "rgba(237, 229, 85, 1)" },
{ "id": "node20", "label": "PHP", "color": "rgba(195, 206, 215, 1)" },
{ "id": "node21", "label": "Go", "color": "rgba(255, 0, 0, 1)" },
{ "id": "node22", "label": "Python", "color": "rgba(109, 238, 180, 1)" },
{ "id": "node23", "label": "MySQL", "color": "#4682B4" },
{ "id": "node24", "label": "人工智能", "color": "rgba(180, 5, 255, 1)" },
{ "id": "node25", "label": "python", "color": "rgba(255, 8, 8, 1)" },
{ "id": "node26", "label": "AI模型", "color": "rgba(10, 138, 244, 1)" },
{
"id": "node27",
"label": "Spring Framework",
"color": "rgba(242, 238, 14, 1)"
},
{ "id": "node29", "label": "Hibernate", "color": "rgba(242, 238, 14, 1)" },
{ "id": "node31", "label": "Spring MVC", "color": "rgba(242, 238, 14, 1)" },
{ "id": "node33", "label": "Gin", "color": "rgba(255, 0, 0, 1)" },
{ "id": "node34", "label": "Echo", "color": "rgba(255, 0, 0, 1)" },
{ "id": "node35", "label": "Beego", "color": "rgba(255, 8, 0, 1)" },
{ "id": "node36", "label": "Laravel", "color": "rgba(200, 209, 217, 1)" },
{ "id": "node37", "label": "Symfony", "color": "rgba(182, 194, 204, 1)" },
{
"id": "node38",
"label": "CodeIgniter",
"color": "rgba(188, 197, 204, 1)"
},
{ "id": "node39", "label": "Django", "color": "rgba(36, 245, 144, 1)" },
{ "id": "node40", "label": "Flask", "color": "rgba(41, 244, 176, 1)" },
{ "id": "node41", "label": "FastAPI", "color": "rgba(58, 244, 142, 1)" }
],
"links": [
{ "source": "node2", "target": "node3", "label": "" },
{ "source": "node2", "target": "node5", "label": "" },
{ "source": "node2", "target": "node4", "label": "" },
{ "source": "node1", "target": "node2", "label": "" },
{ "source": "node5", "target": "node6", "label": "" },
{ "source": "node5", "target": "node7", "label": "" },
{ "source": "node3", "target": "node8", "label": "" },
{ "source": "node3", "target": "node9", "label": "" },
{ "source": "node3", "target": "node10", "label": "" },
{ "source": "node10", "target": "node11", "label": "" },
{ "source": "node10", "target": "node12", "label": "" },
{ "source": "node11", "target": "node9", "label": "" },
{ "source": "node11", "target": "node8", "label": "" },
{ "source": "node12", "target": "node8", "label": "" },
{ "source": "node8", "target": "node13", "label": "" },
{ "source": "node8", "target": "node14", "label": "" },
{ "source": "node11", "target": "node13", "label": "" },
{ "source": "node12", "target": "node13", "label": "" },
{ "source": "node2", "target": "node15", "label": "" },
{ "source": "node15", "target": "node16", "label": "" },
{ "source": "node16", "target": "node17", "label": "" },
{ "source": "node1", "target": "node1", "label": "" },
{ "source": "node1", "target": "node18", "label": "" },
{ "source": "node18", "target": "node21", "label": "" },
{ "source": "node18", "target": "node20", "label": "" },
{ "source": "node18", "target": "node19", "label": "" },
{ "source": "node18", "target": "node22", "label": "" },
{ "source": "node22", "target": "node23", "label": "" },
{ "source": "node19", "target": "node23", "label": "" },
{ "source": "node20", "target": "node23", "label": "" },
{ "source": "node21", "target": "node23", "label": "" },
{ "source": "node1", "target": "node24", "label": "" },
{ "source": "node24", "target": "node25", "label": "" },
{ "source": "node24", "target": "node26", "label": "" },
{ "source": "node12", "target": "node9", "label": "" },
{ "source": "node19", "target": "node27", "label": "" },
{ "source": "node19", "target": "node29", "label": "" },
{ "source": "node19", "target": "node31", "label": "" },
{ "source": "node21", "target": "node33", "label": "" },
{ "source": "node21", "target": "node34", "label": "" },
{ "source": "node21", "target": "node35", "label": "" },
{ "source": "node20", "target": "node36", "label": "" },
{ "source": "node20", "target": "node37", "label": "" },
{ "source": "node20", "target": "node38", "label": "" },
{ "source": "node22", "target": "node39", "label": "" },
{ "source": "node22", "target": "node40", "label": "" },
{ "source": "node22", "target": "node41", "label": "" }
]
}

@ -10,6 +10,8 @@ import {} from 'vue'
<style lang="scss" scoped>
.footer {
// position: fixed;
// bottom: 0;
height: 300px;
width: 100%;
background-color: #252527;

@ -20,6 +20,7 @@ import {} from 'vue'
.main-container {
width: 100%;
min-height: 100vh;
background-color: #EEF1FB;
// background-color: skyblue;
// .container {
// width: $base-container-width;

@ -1,9 +1,19 @@
<template>
<!-- <section class="app-main">
<transition name="fade-transform" mode="out-in">
<router-view :key="$route.path" />
</transition>
</section> -->
<router-view></router-view>
</template>
<script lang="ts" setup>
import {} from 'vue'
import {useRoute } from 'vue-router'
const $route = useRoute()
console.log( $route);
</script>
<style lang="scss" scoped></style>

@ -1,5 +1,5 @@
<template>
<div class="tabbar">
<div :class="!flog ? 'tabbar' : 'tabbar-active'">
<div class="container">
<div class="logo">LOGO</div>
<div class="menu">
@ -27,14 +27,27 @@
<script lang="ts" setup>
import { constRouter } from '@/router/module/constRouter'
import { useRouter } from 'vue-router'
// import { ref } from 'vue'
import { onMounted,ref } from 'vue'
import useSettingStore from '@/store/module/setting'
const $router = useRouter()
const settingStore = useSettingStore()
const goToRouter = (item: any, index: number) => {
settingStore.setuseIndex(index)
$router.push({ path: item.path })
$router.push({ path: `${item.path}`})
}
const flog = ref(false)
onMounted(() => {
window.addEventListener('scroll', ()=> {
if(window.scrollY >= 50 ){
flog.value = true
}else if(flog.value && window.scrollY <= 50){
flog.value = false
}
});
})
</script>
<style lang="scss" scoped>
@ -44,8 +57,51 @@ const goToRouter = (item: any, index: number) => {
height: 120px;
width: 100%;
background-color: transparent;
animation-duration: .3s;
animation-name: tabber-to;
}
.tabbar-active {
position: fixed;
top: 0;
height: 120px;
width: 100%;
background-color:#6da0ff;
animation-duration: .3s;
animation-name: tabber;
z-index:999;
box-shadow:
0px 0px 43px rgba(0, 0, 0, 0.2)
;
}
@keyframes tabber {
0%{
// transform: translateY(-120px);
background-color: transparent;
}
100%{
// transform: translateY(0);
background-color:#6da0ff;
}
}
@keyframes tabber-to {
0%{
// transform: translateY(-120px);
background-color:#6da0ff;
}
100%{
// transform: translateY(0);
background-color: transparent;
}
.container {
}
.container {
height: 100%;
display: flex;
align-items: center;
@ -113,5 +169,4 @@ const goToRouter = (item: any, index: number) => {
align-items: center;
}
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

@ -1,4 +1,5 @@
import { createApp } from 'vue'
// import G6 from '@antv/g6'
// 导入svg插件
import 'virtual:svg-icons-register'
import App from './App.vue'
@ -17,6 +18,7 @@ import router from '@/router/index'
import './permissions'
// 引入仓库
import pinia from '@/store/index'
import '@/utils/rem.js'
// 创建vue实例
const app = createApp(App)
// 注册element plus组件库
@ -27,5 +29,11 @@ app.use(ElementPlus, {
app.use(gloablComponent)
app.use(router)
app.use(pinia)
// app.use(G6)
// 挂载点
app.mount('#app')
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}

@ -5,7 +5,11 @@ import { constRouter } from '@/router/module/constRouter'
import useSettingStore from '@/store/module/setting'
const settingStore = useSettingStore(pinia) //重新指向pinia仓储
router.beforeEach((to, from, next) => {
let index = constRouter[0].children.findIndex((item:any) => item.name === to.name);
let index = constRouter[0].children.findIndex((item:any) => {
return to.path.includes(item.path)
});
settingStore.setuseIndex(index)
document.title = `教学一体化-${to.meta.title}`
next()
})

@ -3,7 +3,7 @@ export const constRouter: any = [
path: '/',
component: () => import('@/Layout/index.vue'),
name: 'Layout',
redirect:'/home',
redirect: '/home',
meta: {
icon: '',
title: '',
@ -19,9 +19,21 @@ export const constRouter: any = [
title: '学院首页',
hidden: false,
},
children: [
{
path: 'info',
name: 'Info',
component: () => import('@/views/home/test.vue'),
meta: {
icon: '',
title: '学院首页',
hidden: false,
},
},
],
},
{
path: 'professionalProfile', // 专业概括
path: '/professionalProfile', // 专业概括
component: () => import('@/views/professionalProfile/index.vue'),
name: 'ProfessionalProfile',
meta: {
@ -31,7 +43,7 @@ export const constRouter: any = [
},
},
{
path: 'pedagogicalReform', // 教学改革
path: '/pedagogicalReform', // 教学改革
component: () => import('@/views/pedagogicalReform/index.vue'),
name: 'PedagogicalReform',
meta: {
@ -41,7 +53,7 @@ export const constRouter: any = [
},
},
{
path: 'pedagogicalReform', // 科学研究
path: '/pedagogicalReform', // 科学研究
component: () => import('@/views/pedagogicalReform/index.vue'),
name: 'PedagogicalReform',
meta: {
@ -51,7 +63,7 @@ export const constRouter: any = [
},
},
{
path: 'talentDevelopment', // 人才培养
path: '/talentDevelopment', // 人才培养
component: () => import('@/views/talentDevelopment/index.vue'),
name: 'TalentDevelopment',
meta: {
@ -61,7 +73,7 @@ export const constRouter: any = [
},
},
{
path: 'presentationAchievements', // 成果展示
path: '/presentationAchievements', // 成果展示
component: () => import('@/views/presentationAchievements/index.vue'),
name: 'PresentationAchievements',
meta: {
@ -69,9 +81,34 @@ export const constRouter: any = [
title: '成果展示',
hidden: false,
},
redirect: '/presentationAchievements/dev',
children: [
{
path: 'dev',
component: () =>
import('@/views/presentationAchievements/dev.vue'),
name: 'Dev',
meta: {
icon: '',
title: 'dev',
hidden: false,
},
},
{
path: 'test',
component: () =>
import('@/views/presentationAchievements/test.vue'),
name: 'Test',
meta: {
icon: '',
title: 'test',
hidden: false,
},
},
],
},
{
path: 'productFusion', // 产品融合
path: '/productFusion', // 产品融合
component: () => import('@/views/productFusion/index.vue'),
name: 'ProductFusion',
meta: {
@ -81,7 +118,7 @@ export const constRouter: any = [
},
},
{
path: 'dailyTeaching', // 日常教学
path: '/dailyTeaching', // 日常教学
component: () => import('@/views/dailyTeaching/index.vue'),
name: 'DailyTeaching',
meta: {
@ -91,7 +128,7 @@ export const constRouter: any = [
},
},
{
path: 'outstandingStudents', // 优秀学生
path: '/outstandingStudents', // 优秀学生
component: () => import('@/views/outstandingStudents/index.vue'),
name: 'OutstandingStudents',
meta: {
@ -102,6 +139,15 @@ export const constRouter: any = [
},
],
},
{
path:'/editAtlas',
component:() => import('@/views/editAtlas/index.vue'),
name:'EditAtlas',
meta: {
title: '编辑图谱',
hidden: true,
},
},
{
path: '/404',
component: () => import('@/views/404/index.vue'),

@ -0,0 +1,69 @@
import { defineStore } from 'pinia';
const editAtlasStore = defineStore({
id: 'editAtlasStore',
state: (): any => ({
log: [],
dataList: {
nodes: [],
edges: []
}
}),
actions: {
addLog(param: any) { // 存储操作记录
this.log.unshift(param);
},
deleteLog() { // 删除操作记录
this.log.splice(0, 1);
},
clearData() { // 清空数据
this.dataList = {
nodes: [],
edges: []
};
},
getData(param: any) { // 导入数据
this.dataList = param;
},
addNode(param: any) { // 添加节点
this.dataList.nodes.push(param);
},
addEdge(param: any) { // 添加连线
this.dataList.edges.push(param);
},
deleteNode(param: any) { // 删除节点
const index = this.dataList.nodes.findIndex((value: any) => {
if (typeof param === 'string') {
return value.id === param;
} else {
return value.id === param.id;
}
});
this.dataList.nodes.splice(index, 1);
},
deleteEdge(param: any) { // 删除连线
const index = this.dataList.edges.findIndex((value: any) => {
if (typeof param === 'string') {
return value.id === param;
} else {
return value.id === param.id;
}
});
this.dataList.edges.splice(index, 1);
},
updateNode(param: any) { // 更新节点
this.dataList.nodes.forEach((item: any, index: number) => {
if (item.id === param.id) {
this.dataList.nodes[index] = param;
}
});
},
updateEdge(param: any) { // 更新节点
this.dataList.edges.forEach((item: any, index: number) => {
if (item.id === param.id) {
this.dataList.edges[index] = param;
}
});
}
}
});
export default editAtlasStore

@ -1,5 +1,33 @@
// 引入清除浏览器默认样式文件
@import './reset.scss';
.view-container{
padding-top: 120px;
.banner{
width: 100%;
height: 410px;
background: url('../assets/images/banner2.png') no-repeat;
background-size: cover
}
/* 设置滚动条的宽度和颜色 */
::-webkit-scrollbar {
width: 10px; /* 滚动条宽度 */
}
/* 设置滚动条的轨道背景色 */
::-webkit-scrollbar-track {
background-color: #f1f1f1;
}
/* 设置滚动条上下按钮的样式 */
::-webkit-scrollbar-button {
background-color: #ccc;
}
/* 设置滚动条的滑块样式 */
::-webkit-scrollbar-thumb {
background-color: #ccc;
border-radius: 5px; /* 滑块圆角 */
}
/* 设置滑块在hover状态下的样式 */
::-webkit-scrollbar-thumb:hover {
background-color: #555;
}

@ -0,0 +1,44 @@
// 通用方法
// 判断字符、数组、对象是否为空 空返回true
const isNullAndEmpty = (param) => {
if (param instanceof Array) { // 数组
if (param.length > 0) {
return false
} else {
return true
}
} else if (typeof param === 'string') { // 字符串
if (param === undefined || param === null || param.trim() === '') {
return true
} else {
return false
}
} else if (Object.prototype.toString.call(param) === '[object Object]') { // 对象
for (var key in param) {
return false
}
return true
}
}
const objectJS = { // 对象相关方法
deepClone (obj, hash = new WeakMap()) { // 深拷贝对象
if (obj === null) return obj
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
if (typeof obj !== 'object') return obj
if (hash.get(obj)) return hash.get(obj)
let cloneObj = new obj.constructor()
hash.set(obj, cloneObj)
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = this.deepClone(obj[key], hash)
}
}
return cloneObj
}
}
export {
isNullAndEmpty, objectJS
}

@ -0,0 +1,118 @@
;(function(win, lib) {
var doc = win.document;
var docEl = doc.documentElement;
var metaEl = doc.querySelector('meta[name="viewport"]');
var flexibleEl = doc.querySelector('meta[name="flexible"]');
var dpr = 0;
var scale = 0;
var tid;
var flexible = lib.flexible || (lib.flexible = {});
if (metaEl) {
console.warn('将根据已有的meta标签来设置缩放比例');
var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/);
if (match) {
scale = parseFloat(match[1]);
dpr = parseInt(1 / scale);
}
} else if (flexibleEl) {
var content = flexibleEl.getAttribute('content');
if (content) {
var initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
if (initialDpr) {
dpr = parseFloat(initialDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
if (maximumDpr) {
dpr = parseFloat(maximumDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
}
}
if (!dpr && !scale) {
var isAndroid = win.navigator.appVersion.match(/android/gi);
var isIPhone = win.navigator.appVersion.match(/iphone/gi);
var devicePixelRatio = win.devicePixelRatio;
if (isIPhone) {
// iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
dpr = 2;
} else {
dpr = 1;
}
} else {
// 其他设备下,仍旧使用1倍的方案
dpr = 1;
}
scale = 1 / dpr;
}
docEl.setAttribute('data-dpr', dpr);
if (!metaEl) {
metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
if (docEl.firstElementChild) {
docEl.firstElementChild.appendChild(metaEl);
} else {
var wrap = doc.createElement('div');
wrap.appendChild(metaEl);
doc.write(wrap.innerHTML);
}
}
function refreshRem(){
var width = docEl.getBoundingClientRect().width;
if (width / dpr > 1920) {
width = 1920 * dpr;
}
var rem = width / 10;
docEl.style.fontSize = rem + 'px';
flexible.rem = win.rem = rem;
}
win.addEventListener('resize', function() {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}, false);
win.addEventListener('pageshow', function(e) {
if (e.persisted) {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}
}, false);
if (doc.readyState === 'complete') {
doc.body.style.fontSize = 12 * dpr + 'px';
} else {
doc.addEventListener('DOMContentLoaded', function(e) {
doc.body.style.fontSize = 12 * dpr + 'px';
}, false);
}
refreshRem();
flexible.dpr = win.dpr = dpr;
flexible.refreshRem = refreshRem;
flexible.rem2px = function(d) {
var val = parseFloat(d) * this.rem;
if (typeof d === 'string' && d.match(/rem$/)) {
val += 'px';
}
return val;
}
flexible.px2rem = function(d) {
var val = parseFloat(d) / this.rem;
if (typeof d === 'string' && d.match(/px$/)) {
val += 'rem';
}
return val;
}
})(window, window['lib'] || (window['lib'] = {}));

@ -1,5 +1,6 @@
<template>
<div class="view-container">
<div class="banner"></div>
<div class="container">日常教学</div>
</div>
</template>
@ -11,7 +12,6 @@ import {} from 'vue'
<style lang="scss" scoped>
.view-container {
height: 100vh;
background-color: #2080f7;
.container {
width: $base-container-width;
margin: 0 auto;

@ -0,0 +1,79 @@
import pinia from '@/store'
import editAtlasStore from '@/store/module/editAtlas'
const AtlasStore = editAtlasStore(pinia)
let obj = {
source: '',
target: '',
}
export default{
getEvents () {
return {
'node:click': 'onClick',
mousemove: 'onMousemove',
'edge:click': 'onEdgeClick'
}
},
onClick (e) {
const node = e.item
this.item = e.item
const graph = this.graph
const point = {
x: e.x,
y: e.y
}
const model = node.getModel()
if (this.addingEdge && this.edge) {
graph.updateItem(this.edge, {
target: model.id
})
this.edge = null
this.addingEdge = false
} else {
obj = {
source: model.id,
target: point
}
this.edge = graph.addItem('edge', obj)
AtlasStore.addEdge(obj)
// 操作记录
let logObj = {
id: String('log' + (AtlasStore.log.length + 1)),
action: 'addEdge',
data: obj
}
AtlasStore.addLog(logObj)
this.addingEdge = true
e.item._cfg.model.linkPoints.bottom = false
this.graph.refreshItem(e.item)
}
},
onMousemove (e) {
const point = {
x: e.x,
y: e.y
}
if (this.addingEdge && this.edge) {
if (e.item !== null && e.item._cfg.type === 'node') { // 判断是否为节点
this.graph.updateItem(this.edge, {
target: e.item._cfg.id
})
obj.target = e.item._cfg.id
} else {
this.graph.updateItem(this.edge, {
target: point
})
}
}
},
onEdgeClick (e) {
const currentEdge = e.item
if (this.addingEdge && this.edge === currentEdge) { // 点击空白处后移除连线
let index = AtlasStore.dataList.edges.length - 1
AtlasStore.deleteEdge(AtlasStore.dataList.edges[index])
this.graph.removeItem(this.edge)
this.edge = null
this.addingEdge = false
}
this.graph.setMode('default')
}
}

@ -0,0 +1,32 @@
import pinia from '@/store'
import editAtlasStorefrom from '@/store/module/editAtlas'
const AtlasStore = editAtlasStorefrom(pinia)
export default{
getDefaultCfg () {
return {
trigger: 'shift'
}
},
getEvents () {
return {
'canvas:dblclick': 'onClick' // 双击画布添加节点
}
},
onClick (ev) {
let obj = {
id: String('node' + (AtlasStore.dataList.nodes.length + 1)),
label: String(AtlasStore.dataList.nodes.length + 1),
x: ev.x,
y: ev.y
}
this.graph.addItem('node', obj)
AtlasStore.addNode(obj)
// 操作记录
let logObj = {
id: String('log' + (AtlasStore.log.length + 1)),
action: 'addNode',
data: obj
}
AtlasStore.addLog(logObj)
}
}

@ -0,0 +1,28 @@
export default{
getEvents () {
return {
'node:mouseover': 'onMouseover',
'node:mouseleave': 'onMouseleave',
'node:mousedown': 'onMousedown'
}
},
onMouseover (e) { // 鼠标移入显示锚点
e.item._cfg.model.linkPoints.bottom = true
this.graph.refreshItem(e.item)
},
onMouseleave (e) { // 鼠标移出隐藏锚点
e.item._cfg.model.linkPoints.bottom = false
this.graph.refreshItem(e.item)
},
onMousedown (e) {
const edge = this.graph.findAllByState('edge', 'selected')
if (edge.length !== 0) {
this.graph.setItemState(edge[0], 'selected', false)
}
if (e.target.cfg.className === 'link-point-bottom') { // 点击锚点
this.graph.setMode('addEdge')
} else { // 点击节点
this.graph.setMode('default')
}
}
}

@ -0,0 +1,33 @@
export default{
getEvents () {
return {
'edge:mouseover': 'onMouseover',
'edge:mouseout': 'onMouseout',
'edge:click': 'onEdgeClick',
'canvas:click': 'onCanvasClick'
}
},
changeSelected () { // 取消选中状态
const node = this.graph.findAllByState('node', 'selected')
if (node.length !== 0) {
this.graph.setItemState(node[0], 'selected', false)
}
const edge = this.graph.findAllByState('edge', 'selected')
if (edge.length !== 0) {
this.graph.setItemState(edge[0], 'selected', false)
}
},
onMouseover (e) { // 鼠标移入
this.graph.setItemState(e.item, 'hover', true)
},
onMouseout (e) { // 鼠标移出
this.graph.setItemState(e.item, 'hover', false)
},
onEdgeClick (e) { // 鼠标点击
this.changeSelected()
this.graph.setItemState(e.item, 'selected', true)
},
onCanvasClick () { // 取消选中
this.changeSelected()
}
}

@ -0,0 +1,397 @@
<template>
<div>
<v-expansion-panels multiple tile class="panel" v-model="bigPanels">
<v-expansion-panel>
<v-expansion-panel-header style="font-weight: bold">
配置器
</v-expansion-panel-header>
<v-expansion-panel-content>
<v-expansion-panels
v-if="selectedNodeId"
multiple
tile
class="littlepanel"
v-model="littlePanels"
accordion
>
<v-expansion-panel tile>
<v-expansion-panel-header> 节点样式 </v-expansion-panel-header>
<v-expansion-panel-content>
<el-row type="flex" :gutter="20">
<el-col :span="12"
>&emsp;&emsp;半径<el-input
@input="nodeChange"
v-model="radius"
style="width: 40%"
type="number"
:min="1"
size="mini"
></el-input
></el-col>
<el-col :span="12"
>背景颜色<el-color-picker
@change="nodeChange"
v-model="node.style.fill"
style="vertical-align: top"
size="mini"
show-alpha
></el-color-picker>
</el-col>
</el-row>
<el-row type="flex" :gutter="20">
<el-col :span="12"
>边框宽度<el-input
@input="nodeChange"
v-model="node.style.lineWidth"
style="width: 40%"
type="number"
:min="1"
size="mini"
></el-input
></el-col>
<el-col :span="12"
>边框颜色<el-color-picker
@change="nodeChange"
v-model="node.style.stroke"
style="vertical-align: top"
size="mini"
></el-color-picker>
</el-col>
</el-row>
<el-row type="flex" :gutter="20">
<el-col :span="12"
>阴影颜色<el-color-picker
@change="nodeChange"
v-model="node.style.shadowColor"
style="vertical-align: top"
size="mini"
></el-color-picker>
</el-col>
<el-col :span="12"
>阴影范围<el-input
@input="nodeChange"
v-model="node.style.shadowBlur"
style="width: 40%"
type="number"
:min="0"
size="mini"
></el-input
></el-col>
</el-row>
<el-row type="flex" :gutter="20">
<el-col :span="12"
>X&nbsp;偏移量<el-input
@input="nodeChange"
v-model="node.style.shadowOffsetX"
style="width: 40%"
type="number"
:min="0"
size="mini"
></el-input
></el-col>
<el-col :span="12"
>Y&nbsp;偏移量<el-input
@input="nodeChange"
v-model="node.style.shadowOffsetY"
style="width: 40%"
type="number"
:min="0"
size="mini"
></el-input
></el-col>
</el-row>
</v-expansion-panel-content>
</v-expansion-panel>
<v-expansion-panel tile>
<v-expansion-panel-header> 节点文本 </v-expansion-panel-header>
<v-expansion-panel-content>
<el-row type="flex" :gutter="20">
<el-col :span="24"
>内容<el-input
@input="nodeChange"
v-model="node.label"
style="width: 81%"
size="mini"
></el-input
></el-col>
</el-row>
<el-row type="flex" :gutter="20">
<el-col :span="12"
>大小<el-input
@input="nodeChange"
v-model="node.labelCfg.style.fontSize"
style="width: 60%"
size="mini"
:min="1"
type="number"
></el-input
></el-col>
<el-col :span="12"
>粗细<el-input
@input="nodeChange"
v-model="node.labelCfg.style.fontWeight"
:min="100"
:max="900"
:step="100"
style="width: 60%"
size="mini"
type="number"
></el-input
></el-col>
</el-row>
<el-row type="flex" :gutter="20">
<el-col :span="12"
>颜色<el-color-picker
@change="nodeChange"
v-model="node.labelCfg.style.fill"
style="vertical-align: top"
size="mini"
></el-color-picker>
</el-col>
<el-col :span="12"
>定位<el-select
@change="nodeChange"
v-model="node.labelCfg.position"
style="width: 60%"
size="mini"
>
<el-option
v-for="item in placeList"
:key="item"
:label="item"
:value="item"
>
</el-option> </el-select
></el-col>
</el-row>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
<v-expansion-panels
v-if="selectedEdgeId"
multiple
tile
class="littlepanel"
v-model="littlePanels"
accordion
>
<v-expansion-panel tile>
<v-expansion-panel tile>
<v-expansion-panel-header>
连线样式
</v-expansion-panel-header>
<v-expansion-panel-content>
<el-row type="flex" :gutter="20">
<el-col :span="12"
>类型<el-select
@change="edgeChange"
v-model="edge.type"
style="width: 72%"
size="mini"
>
<el-option
v-for="item in lineList"
:key="item"
:label="item"
:value="item"
>
</el-option> </el-select
></el-col>
</el-row>
<el-row type="flex" :gutter="20">
<el-col :span="12"
>宽度<el-input
@input="edgeChange"
v-model="edge.style.lineWidth"
style="width: 40%"
type="number"
:min="1"
size="mini"
></el-input
></el-col>
<el-col :span="12"
>颜色<el-color-picker
@change="edgeChange"
v-model="edge.style.stroke"
style="vertical-align: top"
size="mini"
></el-color-picker>
</el-col>
</el-row>
</v-expansion-panel-content>
</v-expansion-panel>
<v-expansion-panel-header> 连线文本 </v-expansion-panel-header>
<v-expansion-panel-content>
<el-row type="flex" :gutter="20">
<el-col :span="24"
>内容<el-input
@input="edgeChange"
v-model="edge.label"
style="width: 81%"
size="mini"
></el-input
></el-col>
</el-row>
<el-row type="flex" :gutter="20">
<el-col :span="12"
>大小<el-input
@input="edgeChange"
v-model="edge.labelCfg.style.fontSize"
style="width: 60%"
size="mini"
type="number"
:min="1"
></el-input
></el-col>
<el-col :span="12"
>粗细<el-input
@input="edgeChange"
v-model="edge.labelCfg.style.fontWeight"
:min="100"
:max="900"
:step="100"
style="width: 60%"
size="mini"
type="number"
></el-input
></el-col>
</el-row>
<el-row type="flex" :gutter="20">
<el-col :span="12"
>颜色<el-color-picker
@change="edgeChange"
v-model="edge.labelCfg.style.fill"
style="vertical-align: top"
size="mini"
></el-color-picker>
</el-col>
<el-col :span="12"
>定位<el-select
@change="edgeChange"
v-model="edge.labelCfg.position"
style="width: 60%"
size="mini"
>
<el-option
v-for="item in placeList1"
:key="item"
:label="item"
:value="item"
>
</el-option> </el-select
></el-col>
</el-row>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</v-expansion-panel-content>
</v-expansion-panel>
<v-expansion-panel class="panel" tile>
<v-expansion-panel-header style="font-weight: bold">
导航器
</v-expansion-panel-header>
<v-expansion-panel-content>
<div ref="minimap" class="g6-minimap"></div>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</div>
</template>
<script>
export default {
props: {
selectedNodeId: {
type: String,
default: ''
},
selectedEdgeId: {
type: String,
default: ''
},
graph: {
type: Object,
default: () => {}
}
},
watch: {
selectedEdgeId: {
handler (newVal, oldVal) {
if (newVal !== '') {
let edgeArr = this.$store.state.dataList.edges.filter((item) => {
return item.id === newVal
})
this.edge = edgeArr[0]
this.bigPanels = [0, 1]
this.littlePanels = [0, 1]
}
}
},
selectedNodeId: {
handler (newVal, oldVal) {
if (newVal !== '') {
let nodeArr = this.$store.state.dataList.nodes.filter((item) => {
return item.id === newVal
})
this.node = nodeArr[0]
this.radius = nodeArr[0].size[0] / 2
this.bigPanels = [0, 1]
this.littlePanels = [0, 1]
}
}
}
},
data: () => ({
lineList: ['line', 'polyline', 'arc', 'quardratic', 'cubic', 'cubic-horizontal'],
placeList: ['center', 'top', 'bottom', 'left', 'right'],
placeList1: ['start', 'middle', 'end'],
bigPanels: [0, 1],
littlePanels: [0, 1],
radius: '',
node: {
style: {},
labelCfg: {
style: {}
}
},
edge: {
style: {},
labelCfg: {
style: {}
}
}
}),
methods: {
nodeChange () {
this.node.size = [this.radius * 2, this.radius * 2]
this.graph.updateItem(this.node.id, this.node)
this.$store.commit('updateNode', this.node)
},
edgeChange () {
this.graph.updateItem(this.edge.id, this.edge)
this.$store.commit('updateEdge', this.edge)
}
}
}
</script>
<style lang="scss" scoped>
.g6-minimap {
width: 100%;
height: 20vw;
border: 1px solid #35495e;
}
/deep/ .el-col-12 {
margin-top: 5px;
}
/deep/ .panel .v-expansion-panel-content__wrap {
padding: 0 3px 10px 3px;
box-sizing: border-box;
}
/deep/ .littlepanel .v-expansion-panel-content__wrap {
padding: 0 10px 10px 20px;
box-sizing: border-box;
}
/deep/ .el-input__inner{
padding: 0 0 0 15px;
}
</style>

@ -0,0 +1,408 @@
<template>
<div>
<el-collapse v-model="state.bigPanels" accordion>
<el-collapse-item title="配置器" name="1">
<el-collapse v-model="state.littlePanels" accordion>
<el-collapse-item
title="节点样式"
v-if="selectedNodeId"
name="1-1"
style="padding: 0 15px"
>
<!-- 节点样式配置内容 -->
<el-row type="flex" :gutter="20">
<el-col :span="12">
&emsp;&emsp;半径
<el-input
@input="nodeChange"
v-model="state.radius"
style="width: 40%"
type="number"
:min="1"
size="mini"
></el-input>
</el-col>
<el-col :span="12">
背景颜色
<el-color-picker
@change="nodeChange"
v-model="state.node.style.fill"
style="vertical-align: top"
size="mini"
show-alpha
></el-color-picker>
</el-col>
</el-row>
<el-row type="flex" :gutter="20">
<el-col :span="12">
边框宽度
<el-input
@input="nodeChange"
v-model="state.node.style.lineWidth"
style="width: 40%"
type="number"
:min="1"
size="mini"
></el-input>
</el-col>
<el-col :span="12">
边框颜色
<el-color-picker
@change="nodeChange"
v-model="state.node.style.stroke"
style="vertical-align: top"
size="mini"
></el-color-picker>
</el-col>
</el-row>
<el-row type="flex" :gutter="20">
<el-col :span="12">
阴影颜色
<el-color-picker
@change="nodeChange"
v-model="state.node.style.shadowColor"
style="vertical-align: top"
size="mini"
></el-color-picker>
</el-col>
<el-col :span="12">
阴影范围
<el-input
@input="nodeChange"
v-model="state.node.style.shadowBlur"
style="width: 40%"
type="number"
:min="0"
size="mini"
></el-input>
</el-col>
</el-row>
<el-row type="flex" :gutter="20">
<el-col :span="12">
X&nbsp;偏移量
<el-input
@input="nodeChange"
v-model="state.node.style.shadowOffsetX"
style="width: 40%"
type="number"
:min="0"
size="mini"
></el-input>
</el-col>
<el-col :span="12">
Y&nbsp;偏移量
<el-input
@input="nodeChange"
v-model="state.node.style.shadowOffsetY"
style="width: 40%"
type="number"
:min="0"
size="mini"
></el-input>
</el-col>
</el-row>
</el-collapse-item>
<el-collapse-item
title="节点文本"
v-if="selectedNodeId"
name="1-2"
style="padding: 0 15px"
>
<!-- 节点文本配置内容 -->
<el-row type="flex" :gutter="20">
<el-col :span="24">
内容
<el-input
@input="nodeChange"
v-model="state.node.label"
style="width: 81%"
size="mini"
></el-input>
</el-col>
</el-row>
<el-row type="flex" :gutter="20">
<el-col :span="12">
大小
<el-input
@input="nodeChange"
v-model="state.node.labelCfg.style.fontSize"
style="width: 60%"
size="mini"
:min="1"
type="number"
></el-input>
</el-col>
<el-col :span="12">
粗细
<el-input
@input="nodeChange"
v-model="state.node.labelCfg.style.fontWeight"
:min="100"
:max="900"
:step="100"
style="width: 60%"
size="mini"
type="number"
></el-input>
</el-col>
</el-row>
<el-row type="flex" :gutter="20">
<el-col :span="12">
颜色
<el-color-picker
@change="nodeChange"
v-model="state.node.labelCfg.style.fill"
style="vertical-align: top"
size="mini"
></el-color-picker>
</el-col>
<el-col :span="12">
定位
<el-select
@change="nodeChange"
v-model="state.node.labelCfg.position"
style="width: 60%"
size="mini"
>
<el-option
v-for="item in state.placeList"
:key="item"
:label="item"
:value="item"
></el-option>
</el-select>
</el-col>
</el-row>
</el-collapse-item>
<el-collapse-item title="连线样式" v-if="selectedEdgeId" name="2-1">
<!-- 连线样式配置内容 -->
<el-row type="flex" :gutter="20">
<el-col :span="12">
类型
<el-select
@change="edgeChange"
v-model="state.edge.type"
style="width: 72%"
size="mini"
>
<el-option
v-for="item in state.lineList"
:key="item"
:label="item"
:value="item"
></el-option>
</el-select>
</el-col>
</el-row>
<el-row type="flex" :gutter="20">
<el-col :span="12">
宽度
<el-input
@input="edgeChange"
v-model="state.edge.style.lineWidth"
style="width: 40%"
type="number"
:min="1"
size="mini"
></el-input>
</el-col>
<el-col :span="12">
颜色
<el-color-picker
@change="edgeChange"
v-model="state.edge.style.stroke"
style="vertical-align: top"
size="mini"
></el-color-picker>
</el-col>
</el-row>
</el-collapse-item>
<el-collapse-item title="连线文本" v-if="selectedEdgeId" name="2-2">
<!-- 连线文本配置内容 -->
<el-row type="flex" :gutter="20">
<el-col :span="24">
内容
<el-input
@input="edgeChange"
v-model="state.edge.label"
style="width: 81%"
size="mini"
></el-input>
</el-col>
</el-row>
<el-row type="flex" :gutter="20">
<el-col :span="12">
大小
<el-input
@input="edgeChange"
v-model="state.edge.labelCfg.style.fontSize"
style="width: 60%"
size="mini"
type="number"
:min="1"
></el-input>
</el-col>
<el-col :span="12">
粗细
<el-input
@input="edgeChange"
v-model="state.edge.labelCfg.style.fontWeight"
:min="100"
:max="900"
:step="100"
style="width: 60%"
size="mini"
type="number"
></el-input>
</el-col>
</el-row>
<el-row type="flex" :gutter="20">
<el-col :span="12">
颜色
<el-color-picker
@change="edgeChange"
v-model="state.edge.labelCfg.style.fill"
style="vertical-align: top"
size="mini"
></el-color-picker>
</el-col>
<el-col :span="12">
定位
<el-select
@change="edgeChange"
v-model="state.edge.labelCfg.position"
style="width: 60%"
size="mini"
>
<el-option
v-for="item in state.placeList1"
:key="item"
:label="item"
:value="item"
></el-option>
</el-select>
</el-col>
</el-row>
</el-collapse-item>
</el-collapse>
</el-collapse-item>
<el-collapse-item title="导航器" name="2">
<div ref="minimap" class="g6-minimap"></div>
</el-collapse-item>
</el-collapse>
</div>
</template>
<script setup>
import { reactive, ref, watch, onMounted } from 'vue'
import editAtlasStore from '@/store/module/editAtlas'
const AtlasStore = editAtlasStore()
const props = defineProps({
selectedNodeId: {
type: String,
default: '',
},
selectedEdgeId: {
type: String,
default: '',
},
graph: {
type: Object,
default: () => ({}),
},
})
onMounted(() => {
console.log(AtlasStore.dataList, 'AtlasStore', props.graph)
})
const state = reactive({
lineList: [
'line',
'polyline',
'arc',
'quardratic',
'cubic',
'cubic-horizontal',
],
placeList: ['center', 'top', 'bottom', 'left', 'right'],
placeList1: ['start', 'middle', 'end'],
bigPanels: ['2'],
littlePanels: ['1-2', '1-2'],
radius: '',
node: {
style: {},
labelCfg: {
style: {},
},
},
edge: {
style: {},
labelCfg: {
style: {},
},
},
})
const minimap = ref(null)
const nodeChange = () => {
state.node.size = [state.radius * 2, state.radius * 2]
console.log(state.node.id, state.node)
props.graph.updateItem(state.node.id, state.node)
// Assuming your store is setup correctly in Vue 3
AtlasStore.updateNode(state.node)
// this.$store.commit('updateNode', state.node);
}
const edgeChange = () => {
props.graph.updateItem(state.edge.id, state.edge)
AtlasStore.updateEdge(state.edge)
// this.$store.commit('updateEdge', state.edge);
}
watch(
() => props.selectedEdgeId,
(newVal) => {
// Handle node selection change
if (newVal !== '') {
let edgeArr = AtlasStore.dataList.edges.filter((item) => {
return item.id === newVal
})
console.log(edgeArr)
state.edge = edgeArr[0]
state.bigPanels = [0, 1]
state.littlePanels = [0, 1]
}
},
)
watch(
() => props.selectedNodeId,
(newVal) => {
console.log(newVal)
if (newVal === '') return
// console.log(AtlasStore.dataList.nodes);
// Handle edge selection change
let nodeArr = AtlasStore.dataList.nodes.filter((item) => {
return item.id === newVal
})
console.log(nodeArr)
state.node = nodeArr[0]
state.radius = nodeArr[0].size[0] / 2
state.bigPanels = ['1', '2']
state.littlePanels = ['1-1', '1-2']
},
)
</script>
<style lang="scss" scoped>
.g6-minimap {
width: 100%;
height: 20vw;
border: 1px solid #35495e;
}
::v-deep .el-col-12 {
margin-top: 5px;
}
::v-deep .el-input__inner {
padding: 0 0 0 15px;
}
::v-deep .el-collapse-item__header {
padding: 0 15px !important;
}
</style>

@ -0,0 +1,521 @@
<template>
<div class="toolbar">
<v-toolbar height="50">
<v-tooltip v-for="(item, index) in tools" :key="index" bottom>
<template v-slot:activator="{ on, attrs }">
<v-btn
@click="comment_event(item.event)"
icon
v-bind="attrs"
v-on="on"
>
<v-icon style="color: #35495e">{{ item.icon }}</v-icon>
</v-btn>
<template style="color: #c0c4cc" v-if="index == 8">
{{ size }}%
</template>
<template v-else-if="index == 10">
<v-spacer></v-spacer>
<el-select
v-model="layout"
@change="changeLayout"
size="mini"
placeholder="请选择"
>
<el-option
v-for="item in layouts"
:label="item.label"
:value="item.value"
:key="item.value"
>
</el-option>
</el-select>
<v-spacer></v-spacer>
</template>
<template
v-else-if="
index == 2 ||
index == 5 ||
index == 7 ||
index == 11 ||
index == 13
"
>
<v-divider vertical></v-divider>
</template>
</template>
<span>{{ item.tip }}</span>
</v-tooltip>
</v-toolbar>
<el-dialog title="上传txt文件" :visible.sync="dialogVisible" width="50%">
数据格式<a
href="https://github.com/qiaolufei/KG-Editor/issues/3"
target="_blank"
>进阶版数据格式</a
>
<pre style="color:#000">
{
"nodes":[
{"id": "node1", "label": "luffy"},
{"id": "node2", "label": "24岁"},
{"id": "node3", "label": "62kg"}
...
],
"edges":[
{"source": "node1", "target": "node2", "label": "年龄"},
{"source": "node1", "target": "node3", "label": "体重"}
...
]
}
</pre>
<div style="text-align:center">
<el-upload
drag
:limit="1"
action="https://jsonplaceholder.typicode.com/posts/"
ref="upload"
accept=".txt"
:close-on-click-modal="false"
:file-list="fileList"
:on-success="onSuccess"
:on-remove="onRemove"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">
上传txt文件且只能上传 1 个文件
</div>
</el-upload>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="submitData"> </el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import defaultNode from "@/default/default_node";
import defaultEdge from "@/default/default_edge";
import { isNullAndEmpty, objectJS } from "@/utils/commen";
export default {
props: {
size: {
type: Number,
default: 100
},
selectedNodeId: {
type: String,
default: ""
},
selectedEdgeId: {
type: String,
default: ""
},
graph: {
type: Object,
default: () => {}
}
},
watch: {
selectedEdgeId: {
handler(newVal, oldVal) {
if (newVal !== "") {
let edgeArr = this.$store.state.dataList.edges.filter(item => {
return item.id === newVal;
});
this.edge = edgeArr[0];
}
}
},
selectedNodeId: {
handler(newVal, oldVal) {
if (newVal !== "") {
let nodeArr = this.$store.state.dataList.nodes.filter(item => {
return item.id === newVal;
});
this.node = nodeArr[0];
this.radius = nodeArr[0].size[0] / 2;
}
}
}
},
data: () => ({
tools: [
{ icon: "mdi-undo-variant", tip: "撤销 Ctrl+Z", event: "revoke" },
{ icon: "mdi-backup-restore", tip: "重做", event: "restore" },
{ icon: "mdi-content-copy", tip: "复制 Ctrl+C", event: "copy" },
{ icon: "mdi-content-paste", tip: "粘贴 Ctrl+V", event: "paste" },
{
icon: "mdi-trash-can-outline",
tip: "删除 Ctrl+Backspace",
event: "delete"
},
{ icon: "mdi-vector-arrange-above", tip: "置于顶层", event: "onTop" },
{ icon: "mdi-vector-arrange-below", tip: "置于底层", event: "onBottom" },
{ icon: "mdi-magnify-plus-outline", tip: "放大", event: "plus" },
{ icon: "mdi-magnify-minus-outline", tip: "缩小", event: "minus" },
{ icon: "mdi-arrow-collapse-all", tip: "适应画布", event: "adaptCanvas" },
{ icon: "mdi-cloud-upload", tip: "导入文件", event: "importFile" },
{ icon: "mdi-file-image", tip: "导出图片", event: "saveImage" },
{ icon: "mdi-file-code", tip: "导出JSON", event: "saveJson" },
{ icon: "mdi-help-box", tip: "帮助", event: "help" }
],
node: {},
edge: {},
cloneNode: {},
dialogVisible: false,
uploadData: {},
fileList: [],
layout: "random",
layouts: [
{ label: "随机布局", value: "random" },
{ label: "力导向布局", value: "force" },
{ label: "Fruchterman布局", value: "fruchterman" },
{ label: "环形布局", value: "circular" },
{ label: "辐射布局", value: "radial" },
{ label: "层次布局", value: "dagre" },
{ label: "同心圆布局", value: "concentric" },
{ label: "网格布局", value: "grid" }
]
}),
mounted() {
this.keyCodeForEvent();
},
methods: {
changeLayout() {
this.graph.updateLayout({
type: this.layout
});
},
onSuccess(res, file, fileList) {
let reader = new FileReader();
reader.readAsText(file.raw);
reader.onload = e => {
this.uploadData = [];
const str = String(e.target.result).replace(/\s*/g, "");
if (str === "" || str === null) {
this.$message.error("文档数据不能为空!");
} else {
try {
this.uploadData = JSON.parse(str);
} catch (err) {
this.$message.error(String(err));
}
}
};
},
onRemove(file) {
this.fileList = [];
},
comment_event(event) {
this[event]();
},
revoke() {
//
let log = this.$store.state.log;
let action = log[0].action;
switch (action) {
case "addNode":
this.graph.removeItem(log[0].data.id);
this.$store.commit("deleteNode", log[0].data);
break;
case "deleteNode":
this.graph.addItem("node", log[0].data);
this.$store.commit("addNode", log[0].data);
break;
case "addEdge":
this.graph.removeItem(log[0].data.id);
this.$store.commit("deleteEdge", log[0].data);
break;
case "deleteEdge":
this.graph.addItem("edge", log[0].data);
this.$store.commit("addEdge", log[0].data);
break;
}
this.$store.commit("deleteLog");
},
restore() {
this.graph.clear();
this.$store.commit("clearData");
},
copy() {
if (this.selectedNodeId === "") {
this.$message.error("未选择节点!");
} else {
this.$store.state.dataList.nodes.forEach(node => {
if (node.id === this.selectedNodeId) {
this.cloneNode = objectJS.deepClone(node);
this.$message.success("复制成功");
}
});
}
},
paste() {
if (this.selectedNodeId === "") {
this.$message.error("未选择节点!");
} else {
this.cloneNode.id =
"node" + (this.$store.state.dataList.nodes.length + 1);
this.cloneNode.label = this.$store.state.dataList.nodes.length + 1;
this.cloneNode.x = this.cloneNode.x + 20;
this.cloneNode.y = this.cloneNode.y + 20;
let obj = objectJS.deepClone(this.cloneNode);
this.graph.addItem("node", obj);
this.$store.commit("addNode", obj);
this.$message.success("粘贴成功");
}
},
delete() {
if (this.selectedEdgeId === "" && this.selectedNodeId === "") {
this.$message.error("未选择元素!");
} else if (this.selectedEdgeId !== "") {
let obj = {};
this.graph.getEdges().forEach(edge => {
if (edge._cfg.id === this.selectedEdgeId) {
obj = edge._cfg.model;
}
});
//
let logObj = {
id: String("log" + (this.$store.state.log.length + 1)),
action: "deleteEdge",
data: obj
};
this.$store.commit("addLog", logObj);
this.graph.removeItem(this.selectedEdgeId);
this.$store.commit("deleteEdge", this.selectedEdgeId);
this.$emit("update:selectedEdgeId", "");
} else if (this.selectedNodeId !== "") {
let obj = {};
this.graph.getNodes().forEach(node => {
if (node._cfg.id === this.selectedNodeId) {
obj = node._cfg.model;
}
});
//
let logObj = {
id: String("log" + (this.$store.state.log.length + 1)),
action: "deleteNode",
data: obj
};
this.$store.commit("addLog", logObj);
this.graph.removeItem(this.selectedNodeId);
this.$store.commit("deleteNode", this.selectedNodeId);
this.$emit("update:selectedNodeId", "");
}
},
onTop() {
if (this.selectedEdgeId === "" && this.selectedNodeId === "") {
this.$message.error("未选择元素!");
} else if (this.selectedEdgeId !== "") {
this.graph.getEdges().forEach(edge => {
if (edge._cfg.id === this.selectedEdgeId) {
edge.toFront();
}
});
} else if (this.selectedNodeId !== "") {
this.graph.getNodes().forEach(node => {
if (node._cfg.id === this.selectedNodeId) {
node.toFront();
}
});
}
},
onBottom() {
if (this.selectedEdgeId === "" && this.selectedNodeId === "") {
this.$message.error("未选择元素!");
} else if (this.selectedEdgeId !== "") {
this.graph.getEdges().forEach(edge => {
if (edge._cfg.id === this.selectedEdgeId) {
edge.toBack();
}
});
} else if (this.selectedNodeId !== "") {
this.graph.getNodes().forEach(node => {
if (node._cfg.id === this.selectedNodeId) {
node.toBack();
}
});
}
},
plus() {
const currentZoom = Number(this.graph.getZoom());
this.graph.zoomTo(currentZoom + 0.1);
this.size = Number(((currentZoom + 0.1) * 100).toFixed(0));
},
minus() {
const currentZoom = Number(this.graph.getZoom());
this.graph.zoomTo(currentZoom - 0.1);
this.size = Number(((currentZoom - 0.1) * 100).toFixed(0));
},
adaptCanvas() {
this.graph.fitView(20);
this.graph.fitCenter();
this.graph.zoomTo(1);
},
importFile() {
this.dialogVisible = true;
},
initializeObj(source, target) {
// node/edge
for (let key in source) {
if (!target.hasOwnProperty(key)) {
target[key] = source[key];
}
if (typeof source[key] === "object") {
this.initializeObj(source[key], target[key]);
}
}
return target;
},
submitData() {
const dataList = this.uploadData;
let _this = this;
if (!isNullAndEmpty(dataList.nodes)) {
dataList.nodes.forEach(item => {
this.initializeObj(defaultNode, item);
_this.graph.addItem("node", item);
_this.$store.commit("addNode", item);
});
}
if (!isNullAndEmpty(dataList.edges)) {
dataList.edges.forEach(item => {
this.initializeObj(defaultEdge, item);
_this.graph.addItem("edge", item);
_this.$store.commit("addEdge", item);
});
}
this.graph.updateLayout({
type: "random"
});
this.dialogVisible = false;
},
saveImage() {
if (!isNullAndEmpty(this.$store.state.dataList.nodes)) {
this.graph.downloadFullImage("graph", "image/png", {
backgroundColor: "#fff",
padding: [15, 15, 15, 15]
});
} else {
this.$message.warning("画布为空!");
}
},
saveJson() {
if (!isNullAndEmpty(this.$store.state.dataList.nodes)) {
const dataList = this.$store.state.dataList;
const content = {};
console.log(dataList);
content.nodes = dataList.nodes.map(x => {
return {
id: x.id,
label: x.label,
color:x.style.fill
};
});
content.links = dataList.edges.map(x => {
return {
source: x.source,
target: x.target,
label: x.label
};
});
// content.nodes = dataList.nodes
// content.edges = dataList.edges
var eleLink = document.createElement("a");
eleLink.download = "kg.json";
eleLink.style.display = "none";
var blob = new Blob([JSON.stringify(content)]);
eleLink.href = URL.createObjectURL(blob);
document.body.appendChild(eleLink);
eleLink.click();
document.body.removeChild(eleLink);
} else {
this.$message.warning("暂无数据可导出!");
}
},
help() {
window.open("https://github.com/qiaolufei/KG-Editor/issues/new");
},
//
keyCodeForEvent() {
let code = 0;
let code2 = 0;
let _this = this;
document.onkeydown = function(e) {
let evn = e || event;
let key = evn.keyCode || evn.which || evn.charCode;
if (key === 17) {
code = 17;
}
if (key === 90) {
code2 = 90;
}
if (key === 8) {
code2 = 8;
}
if (key === 67) {
code2 = 67;
}
if (key === 86) {
code2 = 86;
}
if (code === 17 && code2 === 90) {
_this.revoke();
code = 0;
code2 = 0;
}
if (code === 17 && code2 === 8) {
_this.delete();
code = 0;
code2 = 0;
}
if (code === 17 && code2 === 67) {
_this.copy();
code = 0;
code2 = 0;
}
if (code === 17 && code2 === 86) {
_this.paste();
code = 0;
code2 = 0;
}
};
document.onkeyup = function(e) {
if (e.keyCode === 17) {
code = 0;
}
if (e.keyCode === 13) {
code2 = 0;
}
};
}
}
};
</script>
<style lang="less">
.toolbar {
position: fixed;
background: #fff;
width: 100%;
z-index: 999;
}
.v-modal {
display: none;
}
.el-select .el-input.is-focus .el-input__inner {
border-color: #35495e;
}
.el-select-dropdown__item.selected {
color: #35495e;
}
.el-select .el-input__inner:focus {
border-color: #35495e;
}
.el-input__inner:focus {
border-color: #35495e;
}
.el-scrollbar__wrap {
overflow-x: hidden;
}
</style>

@ -0,0 +1,627 @@
<template>
<div class="toolbar">
<div class="el-row">
<div class="el-col" v-for="(item, index) in tools" :key="index">
<el-tooltip
class="item-tooltip"
effect="dark"
:content="item.tip"
placement="bottom"
>
<el-button @click="comment_event(item.event)" type="text">
<el-icon style="color: #000">
<component :is="item.icon"></component>
</el-icon>
</el-button>
</el-tooltip>
<div
v-if="index === 8"
style="width: 50px; font-size: 14px; text-align: center"
>
{{ SIZE }}%
</div>
<template v-if="index === 10">
<el-select
style="width: 120px; margin: 0 10px"
v-model="layout"
@change="changeLayout"
size="mini"
placeholder="请选择"
>
<el-option
v-for="item in layouts"
:label="item.label"
:value="item.value"
:key="item.value"
></el-option>
</el-select>
</template>
<el-divider
v-if="index !== 8 && index !== 10"
direction="vertical"
></el-divider>
</div>
</div>
<el-dialog title="上传txt文件" v-model="dialogVisible" width="50%">
<!-- 此处省略了部分内容可根据需要添加 -->
<pre style="color: #000">
{
"nodes":[
{"id": "node1", "label": "luffy"},
{"id": "node2", "label": "24岁"},
{"id": "node3", "label": "62kg"}
...
],
"edges":[
{"source": "node1", "target": "node2", "label": "年龄"},
{"source": "node1", "target": "node3", "label": "体重"}
...
]
}
</pre>
<div style="text-align: center">
<el-upload
drag
:limit="1"
action="https://jsonplaceholder.typicode.com/posts/"
ref="upload"
accept=".txt"
:close-on-click-modal="false"
:file-list="fileList"
:on-success="onSuccess"
:on-remove="onRemove"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">
将文件拖到此处
<em>点击上传</em>
</div>
<div class="el-upload__tip" slot="tip">
上传txt文件且只能上传 1 个文件
</div>
</el-upload>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="submitData"> </el-button>
</span>
</el-dialog>
</div>
</template>
<script setup>
import { ref, defineProps, onMounted, watch } from 'vue'
import defaultNode from '../default/default_node'
import defaultEdge from '../default/default_edge'
import { isNullAndEmpty, objectJS } from '@/utils/commen'
import editAtlasStore from '@/store/module/editAtlas'
import { Back, Refresh, CopyDocument } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
const $router = useRouter()
const AtlasStore = editAtlasStore()
const props = defineProps({
size: {
type: Number,
default: 100,
},
selectedNodeId: {
type: String,
default: '',
},
selectedEdgeId: {
type: String,
default: '',
},
graph: {
type: Object,
default: () => {},
},
})
const $emit = defineEmits(['update:selectedEdgeId', 'update:selectedNodeId'])
const tools = ref([
{ icon: 'Back', tip: '撤销 Ctrl+Z', event: 'revoke' },
{ icon: 'Refresh', tip: '重做', event: 'restore' },
{ icon: 'CopyDocument', tip: '复制 Ctrl+C', event: 'copy' },
{ icon: 'DocumentCopy', tip: '粘贴 Ctrl+V', event: 'paste' },
{
icon: 'Delete',
tip: '删除 Ctrl+Backspace',
event: 'DELEDT',
},
{ icon: 'Top', tip: '置于顶层', event: 'onTop' },
{ icon: 'Bottom', tip: '置于底层', event: 'onBottom' },
{ icon: 'ZoomIn', tip: '放大', event: 'plus' },
{ icon: 'ZoomOut', tip: '缩小', event: 'minus' },
{ icon: 'FullScreen', tip: '适应画布', event: 'adaptCanvas' },
{ icon: 'UploadFilled', tip: '导入文件', event: 'importFile' },
{ icon: 'PictureFilled', tip: '导出图片', event: 'saveImage' },
{ icon: 'DocumentRemove', tip: '导出JSON', event: 'saveJson' },
{ icon: 'FolderChecked', tip: '保存', event: 'help' },
])
const layout = ref('random')
const layouts = ref([
{ label: '随机布局', value: 'random' },
{ label: '力导向布局', value: 'force' },
{ label: 'Fruchterman布局', value: 'fruchterman' },
{ label: '环形布局', value: 'circular' },
{ label: '辐射布局', value: 'radial' },
{ label: '层次布局', value: 'dagre' },
{ label: '同心圆布局', value: 'concentric' },
{ label: '网格布局', value: 'grid' },
])
const dialogVisible = ref(false)
const uploadData = ref({})
const fileList = ref([])
const changeLayout = () => {
// ...
props.graph.updateLayout({
type: layout.value,
})
}
const onSuccess = (res, file, fileList) => {
console.log(file.raw, 'file.raw')
// ...
let reader = new FileReader()
reader.readAsText(file.raw)
reader.onload = (e) => {
uploadData.value = []
const str = String(e.target.result).replace(/\s*/g, '')
if (str === '' || str === null) {
// this.$message.error("");
ElMessage({
message: '文档数据不能为空!',
type: 'error',
})
} else {
try {
uploadData.value = JSON.parse(str)
} catch (err) {
// this.$message.error(String(err));
ElMessage({
message: String(err),
type: 'error',
})
}
}
}
}
const onRemove = (file) => {
// ...
fileList = []
}
const comment_event = (event) => {
// ...
switch (event) {
case 'revoke':
revoke() //
break
case 'restore':
restore() //
break
case 'copy':
//
copy()
break
case 'paste':
//
paste()
break
case 'DELEDT':
//
DELEDT()
break
case 'onTop':
//
onTop()
break
case 'onBottom':
//
onBottom()
break
case 'plus':
plus()
//
break
case 'minus':
minus()
//
break
case 'adaptCanvas':
adaptCanvas()
//
break
case 'importFile':
//
importFile()
break
case 'saveImage':
//
saveImage()
break
case 'saveJson':
// JSON
saveJson()
break
case 'help':
//
help()
break
default:
//
return
break
}
}
const revoke = () => {
// ...
let log = AtlasStore.log
if (log.length === 0) return
console.log(log)
let action = log[0].action
switch (action) {
case 'addNode':
props.graph.removeItem(log[0].data.id)
AtlasStore.deleteNode(log[0].data)
// this.$store.commit("deleteNode", log[0].data);
break
case 'deleteNode':
props.graph.addItem('node', log[0].data)
AtlasStore.addNode(log[0].data)
// this.$store.commit('addNode', log[0].data)
break
case 'addEdge':
props.graph.removeItem(log[0].data.id)
AtlasStore.deleteEdge(log[0].data)
// this.$store.commit('deleteEdge', log[0].data)
break
case 'deleteEdge':
props.graph.addItem('edge', log[0].data)
AtlasStore.addEdge(log[0].data)
// this.$store.commit('addEdge', log[0].data)
break
}
AtlasStore.deleteLog()
// this.$store.commit("deleteLog");
}
const restore = () => {
props.graph.clear()
AtlasStore.clearData()
// this.$store.commit("clearData");
}
// ...
const cloneNode = ref({})
const copy = () => {
if (props.selectedNodeId === '') {
// this.$message.error("");
ElMessage({
message: '未选择节点!',
type: 'error',
})
} else {
AtlasStore.dataList.nodes.forEach((node) => {
if (node.id === props.selectedNodeId) {
cloneNode.value = objectJS.deepClone(node)
// this.$message.success("");
}
})
}
}
const paste = () => {
if (props.selectedNodeId === '') {
// this.$message.error("");
ElMessage({
message: '未选择节点!',
type: 'error',
})
} else {
cloneNode.value.id = 'node' + (AtlasStore.dataList.nodes.length + 1)
cloneNode.value.label = AtlasStore.dataList.nodes.length + 1
cloneNode.value.x = cloneNode.value.x + 20
cloneNode.value.y = cloneNode.value.y + 20
let obj = objectJS.deepClone(cloneNode.value)
props.graph.addItem('node', obj)
AtlasStore.addNode(obj)
// this.$store.commit("addNode", obj);
// this.$message.success("");
}
}
const DELEDT = () => {
if (props.selectedEdgeId === '' && props.selectedNodeId === '') {
// this.$message.error('')
ElMessage({
message: '未选择元素!',
type: 'error',
})
} else if (props.selectedEdgeId !== '') {
let obj = {}
props.graph.getEdges().forEach((edge) => {
if (edge._cfg.id === props.selectedEdgeId) {
obj = edge._cfg.model
}
})
//
let logObj = {
id: String('log' + (AtlasStore.log.length + 1)),
action: 'deleteEdge',
data: obj,
}
AtlasStore.addLog(logObj)
// this.$store.commit('addLog', logObj)
props.graph.removeItem(props.selectedEdgeId)
AtlasStore.deleteEdge(props.selectedEdgeId)
// this.$store.commit('deleteEdge', props.selectedEdgeId)
$emit('update:selectedEdgeId', '')
} else if (props.selectedNodeId !== '') {
let obj = {}
props.graph.getNodes().forEach((node) => {
if (node._cfg.id === props.selectedNodeId) {
obj = node._cfg.model
}
})
//
let logObj = {
id: String('log' + (AtlasStore.log.length + 1)),
action: 'deleteNode',
data: obj,
}
AtlasStore.addLog(logObj)
// this.$store.commit('addLog', logObj)
props.graph.removeItem(props.selectedNodeId)
AtlasStore.deleteNode(props.selectedNodeId)
// this.$store.commit('deleteNode', props.selectedNodeId)
$emit('selectedNodeId', '')
}
}
const onTop = () => {
if (props.selectedEdgeId === '' && props.selectedNodeId === '') {
// this.$message.error("");
ElMessage({
message: '未选择元素!',
type: 'error',
})
} else if (props.selectedEdgeId !== '') {
props.graph.getEdges().forEach((edge) => {
if (edge._cfg.id === props.selectedEdgeId) {
edge.toFront()
}
})
} else if (props.selectedNodeId !== '') {
props.graph.getNodes().forEach((node) => {
if (node._cfg.id === props.selectedNodeId) {
node.toFront()
}
})
}
}
const onBottom = () => {
if (props.selectedEdgeId === '' && props.selectedNodeId === '') {
// this.$message.error("");
ElMessage({
message: '未选择元素!',
type: 'error',
})
} else if (props.selectedEdgeId !== '') {
props.graph.getEdges().forEach((edge) => {
if (edge._cfg.id === props.selectedEdgeId) {
edge.toBack()
}
})
} else if (props.selectedNodeId !== '') {
props.graph.getNodes().forEach((node) => {
if (node._cfg.id === props.selectedNodeId) {
node.toBack()
}
})
}
}
const help = () => {
$router.push('/professionalProfile')
}
watch(
() => props.size,
(newVAl) => {
console.log(newVAl)
SIZE.value = newVAl
},
)
const SIZE = ref(null)
onMounted(() => {
SIZE.value = props.size
})
const plus = () => {
const currentZoom = Number(props.graph.getZoom())
props.graph.zoomTo(currentZoom + 0.1)
SIZE.value = Number(((currentZoom + 0.1) * 100).toFixed(0))
}
const minus = () => {
const currentZoom = Number(props.graph.getZoom())
props.graph.zoomTo(currentZoom - 0.1)
SIZE.value = Number(((currentZoom - 0.1) * 100).toFixed(0))
}
const adaptCanvas = () => {
props.graph.fitView(20)
props.graph.fitCenter()
props.graph.zoomTo(1)
}
const importFile = () => {
dialogVisible.value = true
}
const initializeObj = (source, target) => {
// node/edge
for (let key in source) {
if (!target.hasOwnProperty(key)) {
target[key] = source[key]
}
if (typeof source[key] === 'object') {
initializeObj(source[key], target[key])
}
}
return target
}
const submitData = () => {
const dataList = uploadData.value
console.log(dataList)
if (!isNullAndEmpty(dataList.nodes)) {
dataList.nodes.forEach((item) => {
initializeObj(defaultNode, item)
props.graph.addItem('node', item)
AtlasStore.addNode(item)
// _this.$store.commit("addNode", item);
})
}
if (!isNullAndEmpty(dataList.links)) {
dataList.links.forEach((item) => {
initializeObj(defaultEdge, item)
props.graph.addItem('edge', item)
AtlasStore.addEdge(item)
// _this.$store.commit("addEdge", item);
})
}
props.graph.updateLayout({
type: 'random',
})
dialogVisible.value = false
}
const saveImage = () => {
if (!isNullAndEmpty(AtlasStore.dataList.nodes)) {
props.graph.downloadFullImage('graph', 'image/png', {
backgroundColor: '#fff',
padding: [15, 15, 15, 15],
})
} else {
// this.$message.warning("");
ElMessage({
message: '画布为空!',
type: 'warning',
})
}
}
const saveJson = () => {
if (!isNullAndEmpty(AtlasStore.dataList.nodes)) {
const dataList = AtlasStore.dataList
const content = {}
console.log(dataList)
content.nodes = dataList.nodes.map((x) => {
return {
id: x.id,
label: x.label,
color: x.style.fill,
}
})
content.links = dataList.edges.map((x) => {
return {
source: x.source,
target: x.target,
label: x.label,
}
})
// content.nodes = dataList.nodes
// content.links = dataList.edges
var eleLink = document.createElement('a')
eleLink.download = 'kg.json'
eleLink.style.display = 'none'
var blob = new Blob([JSON.stringify(content)])
eleLink.href = URL.createObjectURL(blob)
document.body.appendChild(eleLink)
eleLink.click()
document.body.removeChild(eleLink)
} else {
// this.$message.warning("");
ElMessage({
message: '暂无数据可导出!',
type: 'warning',
})
}
}
const keyCodeForEvent = () => {
// ...
let code = 0
let code2 = 0
document.onkeydown = function (e) {
let evn = e || event
let key = evn.keyCode || evn.which || evn.charCode
if (key === 17) {
code = 17
}
if (key === 90) {
code2 = 90
}
if (key === 8) {
code2 = 8
}
if (key === 67) {
code2 = 67
}
if (key === 86) {
code2 = 86
}
if (code === 17 && code2 === 90) {
revoke()
code = 0
code2 = 0
}
if (code === 17 && code2 === 8) {
DELETE()
code = 0
code2 = 0
}
if (code === 17 && code2 === 67) {
copy()
code = 0
code2 = 0
}
if (code === 17 && code2 === 86) {
paste()
code = 0
code2 = 0
}
}
document.onkeyup = function (e) {
if (e.keyCode === 17) {
code = 0
}
if (e.keyCode === 13) {
code2 = 0
}
}
}
keyCodeForEvent() //
</script>
<style scoped>
.toolbar {
position: fixed;
top: 0;
background: #fff;
width: 100%;
z-index: 999;
box-shadow:
0 2px 4px -1px rgba(0, 0, 0, 0.2),
0 4px 5px 0 rgba(0, 0, 0, 0.14),
0 1px 10px 0 rgba(0, 0, 0, 0.12);
padding: 5px 10px;
}
.item-tooltip {
color: #c0c4cc;
}
.el-row {
margin-left: -5px;
margin-right: -5px;
}
.el-col {
padding: 0 5px;
display: flex;
align-items: center;
}
</style>

@ -0,0 +1,25 @@
export default{
type: 'line',
style: {
stroke: '#C0C4CC', // 颜色
lineWidth: 1, // 宽度
lineAppendWidth: 0, // 鼠标检测宽度
startArrow: false, // 开始箭头
endArrow: true, // 结束箭头
cursor: 'pointer'
},
label: '',
labelCfg: {
position: 'middle', // 位置
refX: 0, // X偏移量
refY: 0, // Y偏移量
autoRotate: true, // 跟随边旋转
style: {
fill: '#4682B4', // 文本颜色
fontWeight: 400,
opacity: 1, // 文本透明度
fontFamily: '微软雅黑', // 文本字体
fontSize: 14 // 文本字体大小
}
}
}

@ -0,0 +1,29 @@
export default{
size: [60, 60],
linkPoints: { // 锚点
bottom: false,
stroke: '#4682B4',
fill: '#fff',
size: 10
},
style: {
fill: '#4682B4', // 填充色
stroke: '#4682B4', // 描边颜色
lineWidth: 1, // 描边宽度
shadowColor: '#909399', // 阴影颜色
shadowBlur: 10, // 阴影范围
shadowOffsetX: 3, // 阴影 x 方向偏移量
shadowOffsetY: 3, // 阴影 y 方向偏移量
cursor: 'pointer'
},
labelCfg: {
position: 'center', // 文本位置
style: {
fill: '#fff', // 文本颜色
fontWeight: 400,
opacity: 1, // 文本透明度
fontFamily: '微软雅黑', // 文本字体
fontSize: 18 // 文本字体大小
}
}
}

@ -0,0 +1,227 @@
<template>
<div class="index">
<Toolbar
v-if="flog"
:size="size"
:graph="graph"
v-model:selectedNodeId="selectedNodeId"
v-model:selectedEdgeId="selectedEdgeId"
></Toolbar>
<div class="index__main">
<div
ref="G6REF"
id="G6"
class="index__main-left"
:style="{ width: drawer ? '78%' : '100%' }"
></div>
<div class="index__main-right" :style="{ width: drawer ? '25%' : '0' }">
<Sidebar
ref="sidebar"
:graph="graph"
:selectedNodeId="selectedNodeId"
:selectedEdgeId="selectedEdgeId"
></Sidebar>
</div>
</div>
</div>
</template>
<script setup>
import Toolbar from './components/toolbar.vue'
import Sidebar from './components/sidebar.vue'
// import G6 from '@antv/g6'
import defaultNode from './default/default_node'
import defaultEdge from './default/default_edge'
import addNode from './actions/add_node'
import hoverNode from './actions/hover_node'
import addEdge from './actions/add_edge'
import selectEdge from './actions/select_edge'
import { reactive, ref, onMounted } from 'vue'
import editAtlasStore from '@/store/module/editAtlas'
import { useRoute } from 'vue-router'
const $route = useRoute()
const flog = ref(false)
const AtlasStore = editAtlasStore()
let drawer = ref(true)
let graph = reactive(null)
let selectedNodeId = ref('')
let selectedEdgeId = ref('')
let item = {}
let addingEdge = true
let edge = null
let size = ref(100)
let G6REF = ref(null)
let sidebar = ref(null)
let initG6 = () => {
G6.registerBehavior('hover-node', hoverNode)
//
G6.registerBehavior('click-add-node', addNode)
// 线
G6.registerBehavior('click-add-edge', addEdge)
// console.log(selectEdge,'selectEdge');
G6.registerBehavior('select-edge', selectEdge)
let grid = new G6.Grid()
//
console.log(sidebar.value.$refs.minimap, 'sidebar.value')
let minimap = new G6.Minimap({
container: sidebar.value.$refs.minimap,
size: [
sidebar.value.$refs.minimap.offsetWidth - 8,
sidebar.value.$refs.minimap.offsetHeight,
],
})
graph = new G6.Graph({
container: 'G6',
width: G6REF.value.offsetWidth,
height: G6REF.value.offsetHeight,
plugins: [grid, minimap],
layout: {
type: 'force',
nodeStrength: -30,
preventOverlap: true,
nodeSize: 40,
edgeStrength: 0.1,
linkDistance: 100,
},
modes: {
default: [
'hover-node',
'zoom-canvas', // canvas
'drag-canvas', // canvas
{
type: 'drag-node', // node
},
'click-add-node',
'click-select',
'select-edge',
],
addEdge: [
'click-add-edge',
'hover-node',
'zoom-canvas',
'drag-canvas',
'click-add-node',
],
},
defaultNode: defaultNode,
defaultEdge: defaultEdge,
edgeStateStyles: {
hover: {
stroke: '#409eff', //
},
selected: {
stroke: '#409eff', //
},
},
nodeStateStyles: {
selected: {
stroke: '#409eff',
lineWidth: 1,
fill: '#409eff',
},
},
})
console.log(graph,'graph');
graph.read(AtlasStore.dataList)
graph.on('nodeselectchange', (e) => {
console.log(e);
item = e
selectedEdgeId.value = ''
selectedNodeId.value = e.select ? e.selectedItems.nodes[0]._cfg.id : ''
console.log(selectedNodeId.value,'selectedNodeId');
})
graph.on('edge:click', (e) => {
selectedNodeId.value = ''
selectedEdgeId.value = e.item._cfg.id
})
graph.on('canvas:click', (e) => {
selectedNodeId.value = ''
selectedEdgeId.value = ''
})
graph.on('viewportchange', (e) => {
if (e.action === 'zoom') {
size.value = Number((Number(graph.getZoom()) * 100).toFixed(0))
console.log(size.value);
}
})
}
onMounted(async() => {
const json = await fetch('../../../public/data.json')
json.json().then(res => {
console.log(res);
const nodesData = res.nodes.map((item) => {
return{
id:item.id,
label:item.label,
style:{
fill:item.color
}
}
})
AtlasStore.getData({nodes:nodesData,edges:res.links})
initG6()
flog.value = true
})
console.log($route
,'111');
})
</script>
<style lang="scss" scoped>
.index {
width: 100%;
&__main {
width: 100%;
margin-top: 40px;
display: flex;
flex-direction: row;
box-sizing: border-box;
&-left {
height: 88vh;
border: 2px solid #3333;
margin: 2vw 2vw 0 2vw;
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.12),
0 0 6px rgba(0, 0, 0, 0.04);
}
&-right {
height: 88vh;
border: 1px solid #3333;
margin-top: 2vw;
margin-right: 10px;
overflow-y: scroll;
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.12),
0 0 6px rgba(0, 0, 0, 0.04);
&__pot {
position: absolute;
width: 2vw;
height: 2vw;
background: #35495e;
border-radius: 1vw;
clip: rect(0px 1vw 2vw 0px);
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.12),
0 0 6px rgba(0, 0, 0, 0.04);
top: 50%;
cursor: pointer;
z-index: 1000 !important;
}
&__pot:hover {
background: #dcdfe6;
}
}
}
}
.g6-tooltip {
padding: 10px 6px;
color: #444;
background-color: rgba(255, 255, 255, 0.9);
border: 1px solid #e2e2e2;
border-radius: 4px;
}
.g6-grid-container{
height: 100%;;
}
</style>

@ -1,8 +1,22 @@
<template>
<div class="view-container">
<div class="banner"></div>
<div class="container">
学院首页
<div><SvgIcon name="home" color="pink" width="50px" height="50px"/></div>
<div class="top-video-box">
<div class="video">
<video id="video" width="100%" height="100%" controls>
<source src="../../assets//videos/920b6703c5f95b9ff774f27abf5d4f29.mp4" type="video/mp4" />
您的浏览器不支持视频播放
</video>
</div>
<div class="info-list"></div>
</div>
<div class="title-container">
<div class="title">课程体系</div>
<div class="description">
全面落实立德树人根本任务CDIO工程教育理念
</div>
</div>
</div>
</div>
</template>
@ -13,11 +27,56 @@ import {} from 'vue'
<style lang="scss" scoped>
.view-container {
height: 100vh;
background-color: #2080f7;
height: 100%;
// background-color: #2080f7;
.container {
width: $base-container-width;
margin: 0 auto;
padding-top: 300px;
.top-video-box {
position: absolute;
top: 120px;
display: flex;
height: 545px;
justify-content: space-between;
.video {
width: 1073px;
height: 100%;
background-color: #d9d9d9;
#video{
object-fit: cover;
}
}
.info-list {
margin-left: 20px;
width: 519px;
background-color: #fff;
}
}
.title-container {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
.title {
color: #333333;
font-size: 42px;
font-weight: 700;
}
.description {
margin-top: 30px;
color: #777777;
font-size: 20px;
}
}
}
.banner {
width: 100%;
height: 410px;
background: url('../../assets/images/banner2.png') no-repeat;
background-size: cover;
}
}
</style>

@ -0,0 +1,14 @@
<template>
<div>
测试三级路由
</div>
</template>
<script lang='ts' setup>
import { } from 'vue'
</script>
<style lang='scss' scoped>
</style>

@ -1,5 +1,6 @@
<template>
<div class="view-container">
<div class="banner"></div>
<div class="container">优秀学生</div>
</div>
</template>
@ -11,7 +12,6 @@ import {} from 'vue'
<style lang="scss" scoped>
.view-container {
height: 100vh;
background-color: #2080f7;
.container {
width: $base-container-width;
margin: 0 auto;

@ -1,5 +1,6 @@
<template>
<div class="view-container">
<div class="banner"></div>
<div class="container">教学改革</div>
</div>
</template>
@ -11,7 +12,6 @@ import {} from 'vue'
<style lang="scss" scoped>
.view-container {
height: 100vh;
background-color: #2080f7;
.container {
width: $base-container-width;
margin: 0 auto;

@ -0,0 +1,22 @@
<template>
教研成果
<button @click="goTo">跳转</button>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'
const $router = useRouter()
const goTo = () => {
$router.push('test')
}
</script>
<style lang="scss" scoped>
.view-container {
height: 100vh;
.container {
width: $base-container-width;
margin: 0 auto;
}
}
</style>

@ -1,17 +1,27 @@
<template>
<div class="view-container">
<div class="container">成果展示</div>
<div class="banner"></div>
<div class="container">
<router-view ></router-view>
</div>
</div>
</template>
<script lang="ts" setup>
import {} from 'vue'
// import {ref} from 'vue'
// import { useRouter } from 'vue-router';
// const $router = useRouter()
// const flog = ref(true)
// const goTo = () => {
// flog.value = false
// $router.push({path:'presentationAchievements/test',replace:true})
// }
</script>
<style lang="scss" scoped>
.view-container {
height: 100vh;
background-color: #2080f7;
.container {
width: $base-container-width;
margin: 0 auto;

@ -0,0 +1,17 @@
<template>
<div class="container">测试</div>
<div>当前位置<span v-for="item in $route.matched" :key="item.path" v-show="item.meta.title">{{ item.meta.title }} > </span></div>
</template>
<script lang="ts" setup>
import {} from 'vue'
import { useRoute } from 'vue-router'
const $route = useRoute()
console.log($route)
</script>
<style lang="scss" scoped>
</style>

@ -1,5 +1,6 @@
<template>
<div class="view-container">
<div class="banner"></div>
<div class="container">产品融合</div>
</div>
</template>
@ -11,7 +12,6 @@ import {} from 'vue'
<style lang="scss" scoped>
.view-container {
height: 100vh;
background-color: #2080f7;
.container {
width: $base-container-width;
margin: 0 auto;

@ -0,0 +1,68 @@
<template>
<div class="view-container">
<div class="banner"></div>
<div class="container">
<div class="graph-box">
<div id="3d-graph"></div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue'
import ForceGraph3D from '3d-force-graph'
onMounted(() => {
// Random tree
const N = 50
const gData = {
nodes: [...Array(N).keys()].map((i) => ({
id: i,
color: i % 2 === 0 ? 'pink' : 'skyblue', //
label: String(i), // ID
})),
links: [...Array(N).keys()]
.filter((id) => id)
.map((id) => ({
source: id,
target: Math.round(Math.random() * (id - 1)),
color: 'red', // 线
})),
}
const Graph = ForceGraph3D()(
document.getElementById('3d-graph') as HTMLElement,
)
// .jsonUrl('./data.json')
.graphData(gData)
.backgroundColor('#f5f7fd')
.width(1200)
.nodeLabel('label') // label
})
</script>
<style lang="scss" scoped>
.node-label {
font-size: 12px;
padding: 1px 4px;
border-radius: 4px;
background-color: rgba(0,0,0,0.5);
user-select: none;
}
.view-container {
height: 100vh;
.container {
width: $base-container-width;
margin: 0 auto;
}
.graph-box {
display: flex;
justify-content: center;
width: 100%;
}
}
</style>

@ -1,20 +1,160 @@
<template>
<div class="view-container">
<div class="container">专业概括</div>
<div class="banner"></div>
<div class="container">
<div class="title">知识图谱</div>
<div class="graph-box">
<div class="graph">
<div id="3d-graph"></div>
</div>
<div class="edit-atlas" @click="goToEditAtlas">编辑图谱</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import {} from 'vue'
import { useRouter } from 'vue-router'
import { onMounted, ref } from 'vue'
import ForceGraph3D from '3d-force-graph'
//@ts-ignore
import { CSS2DRenderer, CSS2DObject,} from 'three/examples/jsm/renderers/CSS2DRenderer.js'
const $router = useRouter()
const jsonData = ref(null)
onMounted(() => {
const Graph = ForceGraph3D({
extraRenderers: [new CSS2DRenderer()],
})(document.getElementById('3d-graph') as HTMLElement)
.jsonUrl('../../../public/data.json')
// .nodeAutoColorBy('group')
.nodeThreeObject((node: any) => {
const nodeEl = document.createElement('div')
nodeEl.textContent = node.label
nodeEl.style.color = '#333333'
nodeEl.style.borderRadius = '50%'
// console.log(node, 111, Graph.graphData().nodes)
return new CSS2DObject(nodeEl)
})
.linkColor(() => '#dd92fd') // 线
.backgroundColor('#f5f7fd')
.width(1332)
.height(1332)
.nodeColor((node: any) => {
return node.color
})
.nodeRelSize(7) // 4
.nodeResolution(20)
.linkDirectionalArrowLength(1) // 线3
.linkDirectionalArrowRelPos(1) // 线线
.nodeThreeObjectExtend(true)
.onNodeClick((node: any) => {
// Aim at node from outside it
//
const targetDistance = 200 //
//
const distRatio = 1 + targetDistance / Math.hypot(node.x, node.y, node.z)
const newPos = {
x: node.x * distRatio,
y: node.y * distRatio,
z: node.z * distRatio,
}
//
if (node.x === 0 && node.y === 0 && node.z === 0) {
newPos.z = targetDistance // z
}
//
Graph.cameraPosition(
newPos, //
node, //
3000, //
)
//
const graphData = Graph.graphData()
// 线线
graphData.links.forEach((link: any) => {
// console.log(link);
if (link.source.id === node.id || link.target.id === node.id) {
// 线
// link.color = '#FF0000' // 线
Graph.linkColor((item: any): any => {
if (item.source.id === node.id || item.target.id === node.id) {
return 'red'
} else {
return '#dd92fd'
}
})
} else {
// Graph.linkColor(() => '#a4c7fe') // 线
}
})
//
Graph.graphData(graphData)
})
})
const goToEditAtlas = () => {
console.log(jsonData.value)
$router.push({ name: 'EditAtlas', params: { id: 123 } })
}
</script>
<style lang="scss" scoped>
.node-label {
font-size: 12px;
padding: 1px 4px;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.5);
user-select: none;
}
.view-container {
height: 100vh;
background-color: #2080f7;
// height: 100vh;
// background-color: #2080f7;
.container {
width: $base-container-width;
margin: 0 auto;
background-color: #fff;
height: 100%;
}
.title {
font-size: 32px;
font-weight: 700;
text-align: center;
padding: 30px;
}
.graph-box {
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 764px;
// border-radius: 50%;
background-color: #fff;
overflow: hidden;
.graph {
width: 1333px;
height: 1333px;
border-radius: 50%;
overflow: hidden;
}
.edit-atlas {
top: 0;
right: 5%;
position: absolute;
font-size: 16px;
color: #6da0ff;
cursor: pointer;
}
}
}
</style>

@ -1,5 +1,6 @@
<template>
<div class="view-container">
<div class="banner"></div>
<div class="container">科学研究</div>
</div>
</template>
@ -11,7 +12,6 @@ import {} from 'vue'
<style lang="scss" scoped>
.view-container {
height: 100vh;
background-color: #2080f7;
.container {
width: $base-container-width;
margin: 0 auto;

@ -1,5 +1,6 @@
<template>
<div class="view-container">
<div class="banner"></div>
<div class="container">人才培养</div>
</div>
</template>
@ -11,7 +12,6 @@ import {} from 'vue'
<style lang="scss" scoped>
.view-container {
height: 100vh;
background-color: #2080f7;
.container {
width: $base-container-width;
margin: 0 auto;

@ -26,6 +26,6 @@
},
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/utils/rem.js"],
"references": [{ "path": "./tsconfig.node.json" }]
}

@ -2,7 +2,23 @@ import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
// https://vitejs.dev/config/
//@ts-ignore
import postcssPluginPx2rem from "postcss-plugin-px2rem"; //引入插件
//配置参数
const px2remOptions = {
rootValue: 192, //换算基数, 默认100 ,也就是1440px ,这样的话把根标签的字体规定为1rem为50px,这样就可以从设计稿上量出多少个px直接在代码中写多少px了
unitPrecision: 5, //允许REM单位增长到的十进制数字,其实就是精度控制
// propWhiteList: [], // 默认值是一个空数组,这意味着禁用白名单并启用所有属性。
// propBlackList: [], // 黑名单
// exclude:false, //默认false,可以(reg)利用正则表达式排除某些文件夹的方法,例如/(node_module)/ 。如果想把前端UI框架内的px也转换成rem,请把此属性设为默认值
// selectorBlackList: [], //要忽略并保留为px的选择器
// ignoreIdentifier: false, //(boolean/string)忽略单个属性的方法,启用ignoreidentifier后,replace将自动设置为true。
// replace: true, // (布尔值)替换包含REM的规则,而不是添加回退。
mediaQuery: false, //(布尔值)允许在媒体查询中转换px
minPixelValue: 0 , //设置要替换的最小像素值(3px会被转rem)。 默认 0
}
export default defineConfig({
plugins: [
vue(),
@ -26,5 +42,12 @@ export default defineConfig({
additionalData: '@import "./src/styles/variable.scss";',
},
},
postcss: {
plugins: [
// 配置响应式插件
postcssPluginPx2rem(px2remOptions)
],
},
},
})

Loading…
Cancel
Save