yangdr 3 miesięcy temu
commit
f037fb6cd9
76 zmienionych plików z 15262 dodań i 0 usunięć
  1. 30 0
      .gitignore
  2. 6 0
      .vscode/extensions.json
  3. 39 0
      README.md
  4. 12 0
      auto-imports.d.ts
  5. 61 0
      components.d.ts
  6. 1 0
      env.d.ts
  7. 16 0
      index.html
  8. 6467 0
      package-lock.json
  9. 50 0
      package.json
  10. BIN
      public/bg.jpeg
  11. BIN
      public/bg1.jpeg
  12. BIN
      public/favicon.ico
  13. 106 0
      src/App.vue
  14. 29 0
      src/api/drg/index.ts
  15. 42 0
      src/api/drugs/index.ts
  16. 17 0
      src/api/icd/index.ts
  17. 792 0
      src/api/svg/index.ts
  18. 86 0
      src/assets/base.css
  19. BIN
      src/assets/images/图层50@2x.png
  20. BIN
      src/assets/images/密码@2x.png
  21. BIN
      src/assets/images/组11拷贝2x.png
  22. BIN
      src/assets/images/账号2x.png
  23. 1 0
      src/assets/logo.svg
  24. 160 0
      src/assets/main.css
  25. 55 0
      src/components/ActionBar.vue
  26. 64 0
      src/components/BasicDialog.vue
  27. 67 0
      src/components/ConfirmDialog.vue
  28. 289 0
      src/components/Graph.vue
  29. 230 0
      src/components/GraphCreateDialog.vue
  30. 41 0
      src/components/HelloWorld.vue
  31. 156 0
      src/components/NewTable.vue
  32. 164 0
      src/components/NodeCreateDialog.vue
  33. 301 0
      src/components/NodeLinkDialog.vue
  34. 259 0
      src/components/NodeLinksEditDialog.vue
  35. 270 0
      src/components/NodeMergeDialog.vue
  36. 99 0
      src/components/Pagination.vue
  37. 90 0
      src/components/TheWelcome.vue
  38. 87 0
      src/components/WelcomeItem.vue
  39. 11 0
      src/components/__tests__/HelloWorld.spec.ts
  40. 11 0
      src/components/data1.json
  41. 7 0
      src/components/icons/IconCommunity.vue
  42. 7 0
      src/components/icons/IconDocumentation.vue
  43. 7 0
      src/components/icons/IconEcosystem.vue
  44. 7 0
      src/components/icons/IconSupport.vue
  45. 19 0
      src/components/icons/IconTooling.vue
  46. 33 0
      src/compositionApi/pagination.ts
  47. 29 0
      src/config/app.ts
  48. 19 0
      src/config/request.ts
  49. 1581 0
      src/js/svg-pan-zoom.js
  50. 18 0
      src/main.ts
  51. 127 0
      src/router/index.ts
  52. 12 0
      src/stores/counter.ts
  53. 65 0
      src/utils/app.ts
  54. 96 0
      src/utils/request.ts
  55. 13 0
      src/utils/sessioin.ts
  56. 315 0
      src/views/LoginView.vue
  57. 15 0
      src/views/Main/AboutView.vue
  58. 230 0
      src/views/Main/Annotation.vue
  59. 201 0
      src/views/Main/FileBrowse.vue
  60. 19 0
      src/views/Main/GraphMap.vue
  61. 19 0
      src/views/Main/GraphUpdate.vue
  62. 326 0
      src/views/Main/GraphView.vue
  63. 157 0
      src/views/Main/HomeView.vue
  64. 1089 0
      src/views/Main/MindMap.vue
  65. 102 0
      src/views/Main/index.vue
  66. 124 0
      src/views/dict/Drg.vue
  67. 115 0
      src/views/dict/Drugs.vue
  68. 55 0
      src/views/dict/ICD.vue
  69. 189 0
      src/views/dict/KgSchemas.vue
  70. 40 0
      src/views/dict/extraAction.ts
  71. 14 0
      tsconfig.app.json
  72. 14 0
      tsconfig.json
  73. 19 0
      tsconfig.node.json
  74. 11 0
      tsconfig.vitest.json
  75. 41 0
      vite.config.ts
  76. 18 0
      vitest.config.ts

+ 30 - 0
.gitignore

@@ -0,0 +1,30 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+*.tsbuildinfo

+ 6 - 0
.vscode/extensions.json

@@ -0,0 +1,6 @@
+{
+  "recommendations": [
+    "Vue.volar",
+    "vitest.explorer"
+  ]
+}

+ 39 - 0
README.md

@@ -0,0 +1,39 @@
+# kg-viewer
+
+This template should help get you started developing with Vue 3 in Vite.
+
+## Recommended IDE Setup
+
+[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
+
+## Type Support for `.vue` Imports in TS
+
+TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
+
+## Customize configuration
+
+See [Vite Configuration Reference](https://vite.dev/config/).
+
+## Project Setup
+
+```sh
+npm install
+```
+
+### Compile and Hot-Reload for Development
+
+```sh
+npm run dev
+```
+
+### Type-Check, Compile and Minify for Production
+
+```sh
+npm run build
+```
+
+### Run Unit Tests with [Vitest](https://vitest.dev/)
+
+```sh
+npm run test:unit
+```

+ 12 - 0
auto-imports.d.ts

@@ -0,0 +1,12 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// noinspection JSUnusedGlobalSymbols
+// Generated by unplugin-auto-import
+// biome-ignore lint: disable
+export {}
+declare global {
+  const ElMessage: typeof import('element-plus/es')['ElMessage']
+  const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
+  const ElNotification: typeof import('element-plus/es')['ElNotification']
+}

+ 61 - 0
components.d.ts

@@ -0,0 +1,61 @@
+/* eslint-disable */
+// @ts-nocheck
+// Generated by unplugin-vue-components
+// Read more: https://github.com/vuejs/core/pull/3399
+export {}
+
+/* prettier-ignore */
+declare module 'vue' {
+  export interface GlobalComponents {
+    ActionBar: typeof import('./src/components/ActionBar.vue')['default']
+    BasicDialog: typeof import('./src/components/BasicDialog.vue')['default']
+    ConfirmDialog: typeof import('./src/components/ConfirmDialog.vue')['default']
+    ElAside: typeof import('element-plus/es')['ElAside']
+    ElButton: typeof import('element-plus/es')['ElButton']
+    ElCard: typeof import('element-plus/es')['ElCard']
+    ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
+    ElCol: typeof import('element-plus/es')['ElCol']
+    ElContainer: typeof import('element-plus/es')['ElContainer']
+    ElDialog: typeof import('element-plus/es')['ElDialog']
+    ElDropdown: typeof import('element-plus/es')['ElDropdown']
+    ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
+    ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
+    ElForm: typeof import('element-plus/es')['ElForm']
+    ElFormItem: typeof import('element-plus/es')['ElFormItem']
+    ElHeader: typeof import('element-plus/es')['ElHeader']
+    ElIcon: typeof import('element-plus/es')['ElIcon']
+    ElInput: typeof import('element-plus/es')['ElInput']
+    ElMain: typeof import('element-plus/es')['ElMain']
+    ElOption: typeof import('element-plus/es')['ElOption']
+    ElPagination: typeof import('element-plus/es')['ElPagination']
+    ElRadio: typeof import('element-plus/es')['ElRadio']
+    ElRow: typeof import('element-plus/es')['ElRow']
+    ElSelect: typeof import('element-plus/es')['ElSelect']
+    ElTable: typeof import('element-plus/es')['ElTable']
+    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
+    ElTag: typeof import('element-plus/es')['ElTag']
+    ElTree: typeof import('element-plus/es')['ElTree']
+    Graph: typeof import('./src/components/Graph.vue')['default']
+    GraphCreateDialog: typeof import('./src/components/GraphCreateDialog.vue')['default']
+    HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
+    IconCommunity: typeof import('./src/components/icons/IconCommunity.vue')['default']
+    IconDocumentation: typeof import('./src/components/icons/IconDocumentation.vue')['default']
+    IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default']
+    IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default']
+    IconTooling: typeof import('./src/components/icons/IconTooling.vue')['default']
+    NewTable: typeof import('./src/components/NewTable.vue')['default']
+    NodeCreateDialog: typeof import('./src/components/NodeCreateDialog.vue')['default']
+    NodeLinkDialog: typeof import('./src/components/NodeLinkDialog.vue')['default']
+    NodeLinksEditDialog: typeof import('./src/components/NodeLinksEditDialog.vue')['default']
+    NodeMergeDialog: typeof import('./src/components/NodeMergeDialog.vue')['default']
+    Pagination: typeof import('./src/components/Pagination.vue')['default']
+    RouterLink: typeof import('vue-router')['RouterLink']
+    RouterView: typeof import('vue-router')['RouterView']
+    TheWelcome: typeof import('./src/components/TheWelcome.vue')['default']
+    WelcomeItem: typeof import('./src/components/WelcomeItem.vue')['default']
+  }
+  export interface ComponentCustomProperties {
+    vLoading: typeof import('element-plus/es')['ElLoadingDirective']
+  }
+}

+ 1 - 0
env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 16 - 0
index.html

@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="">
+
+<head>
+  <meta charset="UTF-8">
+  <link rel="icon" href="/favicon.ico">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>知识图谱平台</title>
+</head>
+
+<body>
+  <div id="app"></div>
+  <script type="module" src="/src/main.ts"></script>
+</body>
+
+</html>

Plik diff jest za duży
+ 6467 - 0
package-lock.json


+ 50 - 0
package.json

@@ -0,0 +1,50 @@
+{
+  "name": "kg-viewer",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "run-p type-check \"build-only {@}\" --",
+    "preview": "vite preview",
+    "test:unit": "vitest",
+    "build-only": "vite build",
+    "type-check": "vue-tsc --build --force"
+  },
+  "dependencies": {
+    "axios": "^1.4.0",
+    "core-js": "^3.8.3",
+    "d3": "^7.9.0",
+    "d3-context-menu": "^2.1.0",
+    "date-fns": "^4.1.0",
+    "echarts": "^5.5.1",
+    "echarts-gl": "^2.0.9",
+    "element-plus": "^2.9.1",
+    "pinia": "^2.2.6",
+    "svg-pan-zoom": "^3.6.2",
+    "vue": "^3.5.12",
+    "vue-echarts": "^7.0.3",
+    "vue-router": "^4.4.5",
+    "vue-svg-pan-zoom": "^2.1.0"
+  },
+  "devDependencies": {
+    "@tsconfig/node22": "^22.0.0",
+    "@types/jsdom": "^21.1.7",
+    "@types/node": "^22.9.0",
+    "@vitejs/plugin-vue": "^5.1.4",
+    "@vue/test-utils": "^2.4.6",
+    "@vue/tsconfig": "^0.5.1",
+    "jsdom": "^25.0.1",
+    "less": "^4.2.1",
+    "less-loader": "^12.2.0",
+    "npm-run-all2": "^7.0.1",
+    "sass": "^1.53.0",
+    "typescript": "~5.6.3",
+    "unplugin-auto-import": "^0.19.0",
+    "unplugin-vue-components": "^0.28.0",
+    "vite": "^5.4.10",
+    "vite-plugin-vue-devtools": "^7.5.4",
+    "vitest": "^2.1.4",
+    "vue-tsc": "^2.1.10"
+  }
+}

BIN
public/bg.jpeg


BIN
public/bg1.jpeg


BIN
public/favicon.ico


+ 106 - 0
src/App.vue

@@ -0,0 +1,106 @@
+<template>
+  <!-- 将element-plus设置为中文 -->
+  <el-config-provider :locale="zhCn">
+    <!-- <el-container>
+      <el-header>
+        <el-row>
+          <el-col :span="12">
+            <span class="nav">
+              <RouterLink to="/home" class="nav_item">首页</RouterLink>
+              <RouterLink to="/graphmap" class="nav_item">图谱</RouterLink>
+              <RouterLink to="/graphupdate" class="nav_item"
+                >图谱管理</RouterLink
+              >
+              <RouterLink to="/schema" class="nav_item">数据定义</RouterLink>
+              <RouterLink to="/icd" class="nav_item">ICD字典</RouterLink>
+              <RouterLink to="/drg" class="nav_item">DRG字典</RouterLink>
+              <RouterLink to="/drug" class="nav_item">药品字典</RouterLink>
+              <RouterLink to="/file" class="nav_item">文献库</RouterLink>
+              <RouterLink to="/annotation" class="nav_item">标注</RouterLink>
+              <RouterLink to="/g" class="nav_item">图谱构建</RouterLink>
+              <RouterLink to="/about" class="nav_item">关于</RouterLink>
+            </span></el-col
+          >
+          <el-col :span="12"
+            ><span> {{ username ? username : "G+ 知识图谱" }} </span></el-col
+          >
+        </el-row></el-header
+      >
+      <el-container
+        ><hr />
+        <el-container>
+          <el-main>
+            <RouterView />
+          </el-main>
+        </el-container>
+      </el-container>
+      <el-footer>&copy;2024 v1.0</el-footer>
+    </el-container> -->
+    <router-view></router-view>
+  </el-config-provider>
+</template>
+
+<script setup lang="ts">
+import { ref, watch, onMounted, inject } from "vue";
+import { getSessionVar } from "@/utils/sessioin";
+import { RouterLink, RouterView, useRoute, useRouter } from "vue-router";
+
+import { ElConfigProvider } from "element-plus";
+import zhCn from "element-plus/es/locale/lang/zh-cn";
+
+// let username = ref("");
+// const isLogin = ref(true);
+// const currentPath = ref("");
+// const router = useRouter();
+// const pathWatch = watch(
+//   () => router.currentRoute.value,
+//   (newValue, oldValue) => {
+//     currentPath.value = newValue.path;
+//     if (currentPath.value == "/login") {
+//       isLogin.value = false;
+//     }
+//   },
+//   { immediate: true }
+// );
+
+// watch(window.sessionStorage.getItem("username"), (newVal) => {
+//   username.value = newVal;
+// });
+// watch(inject("username"), (newVal) => {
+//   username.value = newVal;
+// });
+
+// onMounted(() => {
+//   let token = getSessionVar("access_token");
+//   username.value = getSessionVar("username");
+//   if (token != null) {
+//     isLogin.value = true;
+//   }
+// });
+
+const cachedViews = ref([]);
+const route = useRoute();
+
+const updateCachedViews = () => {
+  const keepAliveComponent = route.matched.filter((record) => {
+    return record.meta && record.meta.keepAlive;
+  });
+  // console.log("keepAliveComponent", keepAliveComponent);
+  cachedViews.value = keepAliveComponent.map((record) => record.name);
+};
+
+watch(
+  route,
+  () => {
+    updateCachedViews();
+    console.log("App-route", route);
+  },
+  { immediate: false, deep: true }
+);
+onMounted(() => {
+  updateCachedViews();
+});
+</script>
+
+<style >
+</style>

+ 29 - 0
src/api/drg/index.ts

@@ -0,0 +1,29 @@
+import  r  from "@/utils/request.ts"
+import { ref } from 'vue'; 
+//paginationList("").then(r=>{
+//   r.length
+// })
+let customerId = 0;
+export function paginationList(param:any)  {
+  let reqUrl = '/api/dict/drg/'+param.pageNum+'/'+param.pageSize;
+  if (param.name.length > 0){
+    reqUrl = '/api/dict/drg/search/'+param.pageNum+"/"+param.pageSize+'/'+param.name;
+  }
+  return r.request<string[]>({//r.request会做拦截,因此响应的数据就是string[] 类型
+    url: reqUrl,
+    method: 'get',    
+  })
+}
+
+export function customerDrgList(param:any)  {
+  const dataForm = ref();
+  console.log(dataForm);
+  return r.request<string[]>({//r.request会做拦截,因此响应的数据就是string[] 类型
+    url: '/cdrg/page?customerId='+customerId+'&page='+param.pageNum+'&size='+param.pageSize,
+    method: 'get',    
+  })
+}
+
+export function setCustomerId(param:number){
+  customerId = param;
+}

+ 42 - 0
src/api/drugs/index.ts

@@ -0,0 +1,42 @@
+import  r  from "@/utils/request.ts"
+
+//paginationList("").then(r=>{
+//   r.length
+// })
+export function paginationList(param:any)  {
+  let reqUrl = '/api/dict/drug/'+param.pageNum+'/'+param.pageSize;
+  if (param.name.length > 0){
+    reqUrl = '/api/dict/drug/search/'+param.pageNum+"/"+param.pageSize+'/'+param.name;
+  }
+  return r.request<string[]>({//r.request会做拦截,因此响应的数据就是string[] 类型
+    url: reqUrl,
+    method: 'get',
+    
+  })
+}
+
+export function search(param: string){
+  return r.request<string[]>({//r.request会做拦截,因此响应的数据就是string[] 类型
+    url: '/drugs/search',
+    method: 'post',
+    data: { cond : param, size: 10}, 
+  })
+}
+
+export function getDrugById(param: number){
+  return r.request<string[]>({//r.request会做拦截,因此响应的数据就是string[] 类型
+    url: '/drugs/' + param,
+    method: 'get',
+  })
+}
+
+export function getDrug(param: any){
+  return r.request<string[]>({//r.request会做拦截,因此响应的数据就是string[] 类型
+    url: '/drugs/get/' + param.regName + '/'+param.regSpec,
+    method: 'get',
+  })  
+}
+
+
+
+

+ 17 - 0
src/api/icd/index.ts

@@ -0,0 +1,17 @@
+import  r  from "@/utils/request.ts"
+
+//paginationList("").then(r=>{
+//   r.length
+// })
+export function paginationList(param)  {
+  let reqUrl = '/api/dict/icd/'+param.pageNum+'/'+param.pageSize;
+  if (param.name.length > 0){
+    reqUrl = '/api/dict/icd/search/'+param.pageNum+"/"+param.pageSize+'/'+param.name;
+  }
+  return r.request<string[]>({//r.request会做拦截,因此响应的数据就是string[] 类型
+    url: reqUrl,
+    method: 'get',    
+  })
+}
+
+

+ 792 - 0
src/api/svg/index.ts

@@ -0,0 +1,792 @@
+import { ref,reactive } from "vue";
+import r from "@/utils/request.ts";
+import * as d3 from 'd3';
+import d3ContextMenu from 'd3-context-menu';
+
+/**********data_def*********/
+export const dataNodes = ref([]);
+export const dataEdges = ref([]);
+//节点配色方案
+export const nodeColors = reactive({
+    'Department': { 'color' :"#990000", 'name':'科室','visible':true },
+    'Food':{ 'color' :"#ffff00", 'name':'食品','visible':true },
+    'Drug':{ 'color' :"rgb(201, 153, 90)", 'name':'药品','visible':true },
+    'Disease':{ 'color' :"rgb(15, 150, 55)", 'name':'疾病','visible':true },
+    'Symptom':{ 'color' :"rgb(121, 139, 165)", 'name':'症状','visible':true },
+    'Check':{ 'color' :"rgb(40, 108, 210)", 'name':'检查','visible':true },
+    '_default':{ 'color' :"#CDCDCD", 'name':'缺省','visible':true },
+  });
+//用户选中的节点列表
+const selectedNodes = [];
+//图数据
+const mindGraph = {
+  svg: null,
+  svgContainer: null,
+  width: 0,
+  height: 0,
+  click: null,
+  rootG: null,
+  linkGroup: null,
+  linkTextGroup: null,
+  nodeGroup: null,
+  nodeTextGroup: null,
+  simulation: null,
+}
+/**********node_context_menu*********/
+const node_menu = [
+  {
+    title:function (data, event) {      
+      return `${data.name}`;
+    }, 
+  },
+  {
+    divider: true
+  }, 
+  {
+    title: '探索...',
+    action: function (data, event) {
+      console.log('Item clicked', 'element:', this, 'data:', data, 'event:', event);
+      loadData(data.id);
+    }
+  }, 
+  {
+    title: '修改链接...',
+    action: function (data, event) {           
+      nodeLinksEdit.value(data);
+    }
+  },
+  {
+    title: '融合到其他节点...',
+    action: function (data, event) {      
+      mergeToOtherNode.value(data);
+    }
+  },
+  {
+    title: '链接到其他节点...',
+    action: function (data, event) {      
+      linkToOtherNode.value(data);
+    }
+  },
+  {
+    divider: true
+  },  
+  {
+    title: '编辑操作'
+  },
+  {
+    title: function (data, event) {      
+        return '从当前画布移除';
+    },
+    action: function (data, event) {
+      console.log("remove node from graph:" + data.name);
+      removeNodeFromGraph(data.id);
+    }
+  },
+  {
+    title: '从数据库中删除',
+    disabled: function (data, event) {
+      return false;
+    },
+    action: function (data, event) {   
+      deleteFromDatabase.value(data);
+    }
+  },
+
+];
+
+/**********callback_functions*********/
+export const deleteFromDatabase = ref((data)=>{});
+export const mergeToOtherNode = ref((data)=>{});
+export const linkToOtherNode = ref((data)=>{});
+export const nodeLinksEdit = ref((data)=>{});
+
+/**********ui_handler*********/
+export function toggleNodeVisibility(category){  
+  var {nodes, links} = initData();
+  nodeColors[category].visible = !nodeColors[category].visible
+
+  console.dir(nodeColors);
+  d3.select(".node-texts").selectAll("text").data(nodes).style('display',(d)=>{     
+    if (nodeColors[d.category].visible) return 'block' ;    
+    return 'none'
+  });
+
+  d3.select(".nodes").selectAll("circle").data(nodes).style('display',(d)=>{      
+    if (nodeColors[d.category].visible) return 'block' ;    
+    return 'none'
+  });
+  
+  d3.select(".links").selectAll("path").data(links).style('display',(d)=>{  
+    var visible = true;    
+    if (nodeColors[d.source.category].visible == false ){        
+      visible = false;
+    }
+    if (nodeColors[d.target.category].visible == false ){        
+      visible = false;
+    }
+    if (visible) return 'block' ;    
+    return 'none'
+  });
+  d3.select(".link-texts").selectAll("text").data(links).style('display',(d)=>{  
+    
+    if (nodeColors[d.source.category].visible == false ){
+      return 'none';
+    }
+    if (nodeColors[d.target.category].visible == false ){
+      return 'none';
+    }    
+    return 'block';
+  });
+  mindGraph.simulation.nodes(nodes);
+  mindGraph.simulation.force("link", d3.forceLink(links))
+}
+/**********graph_apis*********/
+/** 清空所有图形数据 */
+export function clearGraph() {
+  
+  dataNodes.value.length = 0;
+  dataEdges.value.length = 0;
+  //dataLinks.value.length = 0;
+  var {nodes, links} = initData();
+  //remove
+  var graphNodes = d3.selectAll(".node").data(nodes, function(d) {
+    return d.id;
+  });
+  graphNodes.exit().remove();
+  var graphNodeText = d3.selectAll(".nodeText").data(nodes, function(d) {
+    return d.id;
+  });
+  graphNodeText.exit().remove();
+  var graphLink = d3.selectAll(".link").data(links, function(d) {
+    return d.index;
+  });
+  graphLink.exit().remove();
+  var graphLinkText = d3.selectAll(".linkText").data(links, function(d) {
+    return d.index;
+  });
+  graphLinkText.exit().remove();
+}
+/**从服务器加载数据并重新绘制 */
+export function loadData(id) {  
+    const response = r.request<string[]>({
+        url: "/api/nodes/"+id+"/3/1",
+        method: "get"
+      });
+    response.then(result => {
+      addGraphNodes(result["nodes"],false);
+      addGraphEdges(result["edges"],false);           
+      draw();
+    });
+} 
+/**从图中移除一个节点 */
+export function removeNodeFromGraph(node_id){
+  var nodes = dataNodes.value;
+  var links = dataEdges.value;
+  dataNodes.value = nodes.filter(function(d){
+    return d.id != node_id;
+  });
+  dataEdges.value = links.filter(function(d){
+    if (d.src_id == node_id || d.dest_id == node_id) {            
+      return false;
+    }
+    return true;
+  });
+  draw();
+};
+/**获取节点数据 */
+export function getGraphNodes() {
+  return dataNodes.value;
+}
+export function getGraphEdges() {
+  return dataEdges.value;
+}
+/**增加、替换节点数据 */
+export function addGraphNodes(nodes, replaceData = true){
+  if (replaceData) {
+    dataNodes.value = nodes;
+    return;
+  }
+  nodes?.forEach(e => {
+    var existed = false;
+    for (var i=0;i<dataNodes.value.length; i++){      
+      if (dataNodes.value[i].id == e.id){
+        existed = true;
+        break;
+      }
+    }
+    if (!existed){
+      dataNodes.value.push(e);
+    } else {
+      console.log(e.name + ' already existed, skip');
+    }
+  });
+}
+/**增加、替换边数据 */
+export function addGraphEdges(edges, replaceData = true){
+  console.log(`addGraphEdges(): replace mode = ${replaceData}`);
+  if (replaceData) {
+    dataEdges.value = edges;    
+  } else {
+    edges?.forEach(e => {
+      var existed = false;
+      for (var i=0;i<dataEdges.value.length; i++){
+        if (dataEdges.value[i].id == e.id){
+          existed = true;
+          break;
+        }
+      }
+      if (!existed){
+        dataEdges.value.push(e);
+      }
+    });
+  }
+  
+  console.log(`addGraphEdges(): finished, total edges = ${dataEdges.value.length}`);
+}
+/**初始化图 */
+export function initGraph(funClick) {
+  mindGraph.svgContainer = d3.select(document.getElementById("mind-map"));
+  mindGraph.svg = d3.select(document.getElementById("mindSvg"));
+  const svgNode = mindGraph.svg.node();
+  mindGraph.width = svgNode.clientWidth; //getAttribute("width");
+  mindGraph.height = svgNode.clientHeight; //getAttribute("height");
+  //if (mindGraph.rootG == null){
+  mindGraph.rootG = mindGraph.svg.append("g");     
+  //} 
+  mindGraph.click = funClick;
+}
+/**更新图 */
+export function updateGraph(){
+  var {nodes, links} = initData();
+  console.log(`draw(): total nodes ${nodes.length}, total edges: ${links.length}`);
+  var {node, link, nodeText, linkText} = drawNodesAndLinks(nodes, links);
+  
+  mindGraph.simulation.stop();
+  mindGraph.simulation.nodes(nodes);
+  mindGraph.simulation.force("link", d3.forceLink(links));
+  
+  mindGraph.simulation.alphaMin(0.05).alphaDecay(0.05);
+  mindGraph.simulation.velocityDecay(0.4);
+  mindGraph.simulation.alphaTarget(0.3).restart()
+}
+/**重新绘制图 */
+export function draw(){
+  console.log("draw(): draw graph");
+  
+  mindGraph.rootG.selectAll("g").remove();
+  
+  mindGraph.linkGroup = mindGraph.rootG.append("g").attr("class", "links");  
+  mindGraph.linkTextGroup = mindGraph.rootG.append("g").attr("class", "link-texts");
+  mindGraph.nodeGroup = mindGraph.rootG.append("g").attr("class", "nodes");  
+  mindGraph.nodeTextGroup = mindGraph.rootG.append("g").attr("class", "node-texts");
+
+  //var nodesData = dataNodes.value;  
+  //var edges = dataEdges.value;
+  var {nodes, links} = initData();
+
+  console.log(`draw(): total nodes ${nodes.length}, total edges: ${links.length}`);
+
+  var width = mindGraph.width;
+  var height = mindGraph.height;
+  //simulation part
+  var svg = mindGraph.rootG;
+  
+  var {node, link, nodeText, linkText} = drawNodesAndLinks(nodes, links);
+
+  mindGraph.simulation = d3.forceSimulation(nodes);
+  mindGraph.simulation.force("link", d3.forceLink(links).id(d => d.id).strength(0.3))
+    .force("charge", d3.forceManyBody().strength(-400))
+    .force("x",d3.forceX())
+    .force("y", d3.forceY());
+    //.force("center", d3.forceCenter(width / 2, height / 2));
+
+  mindGraph.simulation.alphaMin(0.05).alphaDecay(0.05);
+  mindGraph.simulation.velocityDecay(0.4);
+
+  mindGraph.simulation.on("end", ()=>{
+    console.log("simulation end");
+  });
+  mindGraph.simulation.on("tick", () => {
+    
+    // 更新节点位置
+    d3.selectAll(".node")
+    .attr("cx", d => d.x)
+    .attr("cy", d => d.y);
+
+    // 更新节点文本位置
+    d3.selectAll(".nodeText")
+    .attr("x", d => d.x ) // 同步移动文本,考虑 dx 的偏移
+    .attr("y", d => d.y ); // 同步移动文本,考虑 dy 的偏移
+    // 更新链接上的文本位置和旋转
+  
+    
+    d3.selectAll(".link").attr("d", function(d) {
+      const x1 = d.source.x;
+      const y1 = d.source.y;
+      const x2 = d.target.x;
+      const y2 = d.target.y;
+  
+      // 计算路径的1/3点
+      const dx = x2 - x1;
+      const dy = y2 - y1;
+      const len = Math.sqrt(dx * dx + dy * dy);
+      const scale = len * 4 / 5 ; // 1/3长度的比例因子
+      const thirdPointX = x1 + (dx / len) * scale;
+      const thirdPointY = y1 + (dy / len) * scale;
+  
+      // 创建一个包含两个线段的路径字符串,只在1/3处添加箭头
+      return `M${x1},${y1}L${thirdPointX},${thirdPointY} M${thirdPointX},${thirdPointY}L${x2},${y2}`;
+    }).attr("marker-mid", "url(#arrow)"); // 应用箭头标记到中间位置    
+    
+    d3.selectAll(".linkText").each(function(d) {
+      const thisText = d3.select(this);
+      const xMid = (d.source.x + d.target.x) / 2;
+      const yMid = (d.source.y + d.target.y) / 2;
+      let angle = Math.atan2(d.target.y - d.source.y, d.target.x - d.source.x) * (180 / Math.PI);
+  
+      // 如果角度在90度到270度之间,则翻转文本以保持可读性
+      if (angle > 90 || angle < -90) {
+        angle += 180;
+      }
+  
+      thisText
+        .attr("transform", `translate(${xMid},${yMid}) rotate(${angle})`);
+    });
+  }); 
+
+
+}
+/**********graph_funcs*********/
+function initData() {  
+  var nodes = dataNodes.value?.map((node, i)=>{
+    return {
+      id: node.id,
+      name: node.name,
+      category: node.category,
+    };
+  });
+
+  var links = dataEdges.value?.map((link, i) => {    
+    return {    
+      source: nodes.find(node => node.id === link.src_id),
+      target: nodes.find(node => node.id === link.dest_id),
+      text: link.category,
+      id: link.id // 用于生成唯一ID
+    }
+  });
+
+  return {nodes, links};
+}
+
+function drawNodesAndLinks(nodes, links){
+  const link = mindGraph.linkGroup
+  .attr("class", "links")
+  .selectAll("path")
+  .data(links)
+  .enter().append("path")
+    .attr("id", (d)=> d.id)
+    .attr("class", "link")
+    .attr("stroke", "#CDCDCD")
+    .attr("fill", "none");
+  console.log(`drawNodesAndLinks(): ${links.length} links finished `);
+  mindGraph.linkGroup.selectAll("path").data(links).exit().remove();
+  const node = mindGraph.nodeGroup
+  .attr("class", "nodes")
+  .selectAll("circle")
+  .data(nodes)
+  .enter().append("circle")
+    .attr("class", "node")
+    .attr("r", 10)
+    .attr("id", (d)=> d.id)
+    .attr("class", "node")
+    .attr("fill", (d)=>{
+      var color = nodeColors["_default"]['color'];
+      var keys = Object.keys(nodeColors);
+      
+      if (keys.indexOf(d.category)>=0){
+        return nodeColors[d.category]['color'];
+      }        
+      return color;
+    }).attr('stroke', (d)=>{
+      var color = nodeColors["_default"]['color'];
+      var keys = Object.keys(nodeColors);
+      
+      if (keys.indexOf(d.category)>=0){
+        return d3.color(nodeColors[d.category]['color']).darker(1);
+      }        
+      return d3.color(color).darker(1);;
+
+    })
+    .call(d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended))
+    .on('click', (event, d) => {               
+      mindGraph.click(d.id);
+      //console.log(d3.select( event.currentTarget).data());
+      if (selectedNodes.includes(d.id)){  
+        console.log('clear current node selection state');
+        d3.select( event.currentTarget).attr('stroke', (d)=>{
+          var color = nodeColors["_default"]['color'];
+          var keys = Object.keys(nodeColors);
+          
+          if (keys.indexOf(d.category)>=0){
+            return d3.color(nodeColors[d.category]['color']).darker(1);
+          }        
+          return d3.color(color).darker(1);;
+
+        });     
+        d3.select( event.currentTarget).attr('r',8);     
+        d3.select( event.currentTarget).attr('stroke-width','1'); 
+        selectedNodes.pop(d.id);              
+      } else {
+        
+        console.log('clear previous nodes selection state');
+        var selectedCircles = d3.selectAll("circle").filter(function() {
+          var stroke = d3.select(this).attr("stroke");                
+          if  (stroke && stroke.toLowerCase() === 'red') {
+            d3.select(this).attr('stroke', (d)=>{
+              var color = nodeColors["_default"]['color'];
+              var keys = Object.keys(nodeColors);
+              
+              if (keys.indexOf(d.category)>=0){
+                return d3.color(nodeColors[d.category]['color']).darker(1);
+              }        
+              return d3.color(color).darker(1);;
+    
+            });     
+            d3.select(this).attr('r',8);     
+            d3.select(this).attr('stroke-width','1'); 
+          }
+        });
+        selectedNodes.length = 0;
+        console.log('set current selection state');
+        selectedNodes.push(d.id);
+        d3.select( event.currentTarget).attr('stroke','red');  
+        d3.select( event.currentTarget).attr('r',12);     
+        d3.select( event.currentTarget).attr('stroke-width','2');                   
+      }
+      // if (timer == null){
+      //   timer = setTimeout(()=>{
+      //     timer = null;
+      //     console.log("click node "+ d.id);
+      //     funcClick(d.id);
+      //   }, 250);
+      //   return;
+      // }
+      // console.log("db click node");
+      // clearTimeout(timer);
+      // timer = null;
+      //loadData(d.id);
+    }).on('contextmenu',  d3ContextMenu(node_menu, {
+      onOpen: function(data, event){
+        console.log("context menu open");
+      },
+      onClose: function(data, event){
+        console.log("context menu close");
+      },
+      position: function(data, event){
+        console.log("context menu position");
+        var bounds = this.getBoundingClientRect();
+        return {
+          left: bounds.left + bounds.width + 10,
+          top: bounds.top
+        }
+      }
+    
+    }));
+  
+  console.log(`drawNodesAndLinks(): ${nodes.length} nodes finished `);
+  mindGraph.nodeGroup.selectAll("circle").data(nodes).exit().remove();  
+  const nodeText = mindGraph.nodeTextGroup
+  .attr("class", "node-texts")
+  .selectAll("text")
+  .data(nodes)
+  .enter().append("text")
+    .attr("id", (d)=> d.id)
+    .style("font-size", "12px")
+    .style("fill","#333333")
+    .style("pointer-events", "none") // 确保文本不会干扰鼠标事件
+    .attr("class", "nodeText")
+    .attr("dx", 16) // 将文本偏移一定距离,以避免覆盖圆圈
+    .attr("dy", ".35em") // 垂直对齐文本  
+    .text(d => d.name); // 设置文本内容为节点数据中的text属性  
+
+  mindGraph.nodeTextGroup.selectAll("text").data(nodes).exit().remove();  
+  // 绘制链接上的文本
+  const linkText =  mindGraph.linkTextGroup
+  .attr("class", "link-texts")
+  .selectAll("text")
+  .data(links)
+  .enter().append("text")
+    .attr("id", (d)=> d.id)
+    .attr("class", "linkText")
+    .style("text-anchor", "middle")
+    .style("dominant-baseline", "central")
+    .style("font-size","6")
+    .style("fill","#CDCDCD")
+    .attr("pointer-events", "none") // 确保文本不会干扰鼠标事件
+    .text(d => d.text);
+
+  mindGraph.linkTextGroup.selectAll("text").data(links).exit().remove();  
+  function dragstarted(event, d) {
+    if (!event.active) mindGraph.simulation.alphaTarget(0.3).restart();    
+    console.log(`drag(): started at ${d.x},${d.y}`)
+    d.fx = d.x;
+    d.fy = d.y;
+  }
+
+  function dragged(event, d) {
+    d.fx = event.x;
+    d.fy = event.y;
+  }
+
+  function dragended(event, d) {
+    if (!event.active) mindGraph.simulation.alphaTarget(0);
+    
+    console.log(`drag(): end at ${d.fx},${d.fy}`);
+
+    d.fx = null;
+    d.fy = null;
+  }
+    return {node, link, nodeText, linkText};
+}
+
+
+
+// export function draw2() {   
+//   console.log("draw graph");
+    
+//   console.log(`draw(): total nodes ${nodes.length}, total edges: ${edges.length}`);
+//   dataLinks.value = edges?.map((link, i) => {    
+//     return {    
+//       source: nodes.find(node => node.id === link.src_id),
+//       target: nodes.find(node => node.id === link.dest_id),
+//       text: link.category,
+//       index: link.id // 用于生成唯一ID
+//     }
+//   }
+//   );
+
+//   console.log(`draw(): total nodes ${nodes.length}, total links: ${dataLinks.value.length}`);
+//   // 将字符串ID转换为节点对象引用
+//   // Create a force simulation after converting links 
+
+//   // Add paths to the SVG    
+//   const linkPath = mindGraph.linkGroup.selectAll("path")
+//     .data(dataLinks.value)
+//     .enter().append("path")
+//       .attr("class","link")
+//       .attr("id", (d, i) => `linkPath-${i}`) // 为每个路径添加唯一ID
+//       .attr("stroke-width", 1.5)
+//       .attr("stroke", "#999")
+//       .attr("fill", "none")
+//       .attr("marker-end", "url(#arrow)")
+//       .on('click', (event, d) => {
+//         console.log(`Link clicked: ${d.text}`);
+//         // 在这里添加你的链接点击逻辑
+//       });
+//   console.log(`draw(): create linkPath finished`);
+//   // Add text labels to the links
+//   const linkText = d3.select(".link-texts").selectAll("text")
+//     .data(dataLinks.value)
+//     .enter().append("text")
+//       .attr('class', 'linkText')
+//       .style("text-anchor", "middle")
+//       .style("pointer-events", "none") // 将文本居中对齐
+//       .each(function(d) {
+//         const textElement = d3.select(this);
+//         textElement.append("textPath")
+//           .attr("href", d => `#linkPath-${d.index}`) // 使用 href 而不是 xlink:href
+//           .attr("startOffset", "50%") // 文本从路径的50%处开始
+//           .attr("fill", "#555") // 设置文本颜色
+//           .text(d => d.text); // 设置文本内容
+//         console.log(d.text);
+//       });
+//       // .append("textPath")
+//       //   .attr("href", d => `#linkPath-${d.index}`) // 使用 href 而不是 xlink:href
+//       //   .attr("startOffset", "50%") // 文本从路径的50%处开始
+//       //   .attr("fill", "#555") // 设置文本颜色
+//       //   .text(d => d.text) // 设置文本内容
+//       //   .on('click', (event, d) => {
+//       //       console.log(`Link clicked: ${d.prop_name}`);
+//       //       // 在这里添加你的链接点击逻辑
+//       // });
+
+  
+//   console.log(`draw(): create linkText finished`);  
+//   // Add nodes to the SVG
+//   const node = mindGraph.nodeGroup.selectAll("circle")
+//     .data(nodes)
+//     .enter().append("circle")
+//       .attr("r", 8)
+//       .attr("class", "node")
+//       .attr("fill", (d)=>{
+//         var color = nodeColors["_default"]['color'];
+//         var keys = Object.keys(nodeColors);
+        
+//         if (keys.indexOf(d.category)>=0){
+//           return nodeColors[d.category]['color'];
+//         }        
+//         return color;
+//       }).attr('stroke', (d)=>{
+//         var color = nodeColors["_default"]['color'];
+//         var keys = Object.keys(nodeColors);
+        
+//         if (keys.indexOf(d.category)>=0){
+//           return d3.color(nodeColors[d.category]['color']).darker(1);
+//         }        
+//         return d3.color(color).darker(1);;
+
+//       }).call(d3.drag()
+//         .on("start", dragstarted)
+//         .on("drag", dragged)
+//         .on("end", dragended))
+//         .on('click', (event, d) => {               
+//           mindGraph.click(d.id);
+//           //console.log(d3.select( event.currentTarget).data());
+//           if (selectedNodes.includes(d.id)){  
+//             console.log('clear current node selection state');
+//             d3.select( event.currentTarget).attr('stroke', (d)=>{
+//               var color = nodeColors["_default"]['color'];
+//               var keys = Object.keys(nodeColors);
+              
+//               if (keys.indexOf(d.category)>=0){
+//                 return d3.color(nodeColors[d.category]['color']).darker(1);
+//               }        
+//               return d3.color(color).darker(1);;
+    
+//             });     
+//             d3.select( event.currentTarget).attr('r',8);     
+//             d3.select( event.currentTarget).attr('stroke-width','1'); 
+//             selectedNodes.pop(d.id);              
+//           } else {
+            
+//             console.log('clear previous nodes selection state');
+//             var selectedCircles = d3.selectAll("circle").filter(function() {
+//               var stroke = d3.select(this).attr("stroke");                
+//               if  (stroke && stroke.toLowerCase() === 'red') {
+//                 d3.select(this).attr('stroke', (d)=>{
+//                   var color = nodeColors["_default"]['color'];
+//                   var keys = Object.keys(nodeColors);
+                  
+//                   if (keys.indexOf(d.category)>=0){
+//                     return d3.color(nodeColors[d.category]['color']).darker(1);
+//                   }        
+//                   return d3.color(color).darker(1);;
+        
+//                 });     
+//                 d3.select(this).attr('r',8);     
+//                 d3.select(this).attr('stroke-width','1'); 
+//               }
+//             });
+//             selectedNodes.length = 0;
+//             console.log('set current selection state');
+//             selectedNodes.push(d.id);
+//             d3.select( event.currentTarget).attr('stroke','red');  
+//             d3.select( event.currentTarget).attr('r',12);     
+//             d3.select( event.currentTarget).attr('stroke-width','2');                   
+//           }
+//           // if (timer == null){
+//           //   timer = setTimeout(()=>{
+//           //     timer = null;
+//           //     console.log("click node "+ d.id);
+//           //     funcClick(d.id);
+//           //   }, 250);
+//           //   return;
+//           // }
+//           // console.log("db click node");
+//           // clearTimeout(timer);
+//           // timer = null;
+//           //loadData(d.id);
+//         }).on('contextmenu',  d3ContextMenu(node_menu, {
+//           onOpen: function(data, event){
+//             console.log("context menu open");
+//           },
+//           onClose: function(data, event){
+//             console.log("context menu close");
+//           },
+//           position: function(data, event){
+//             console.log("context menu position");
+//             var bounds = this.getBoundingClientRect();
+//             return {
+//               left: bounds.left + bounds.width + 10,
+//               top: bounds.top
+//             }
+//           }
+        
+//         }));
+  
+//   console.log(`draw(): create nodes finished`);  
+//   // Add text labels to the nodes
+//   const nodeText = mindGraph.nodeTextGroup.selectAll("text")
+//     .data(nodes)
+//     .enter().append("text")
+//       .attr("class", "nodeText")
+//       .attr("dx", 16) // 将文本偏移一定距离,以避免覆盖圆圈
+//       .attr("dy", ".35em") // 垂直对齐文本  
+//       .text(d => d.name); // 设置文本内容为节点数据中的text属性
+//   console.log(`draw(): create nodeText finished`);  
+//   //箭头离本体的距离
+//   // Update positions on each tick of the simulation
+
+//   if (mindGraph.simulation == null){
+//     console.log(`draw(): create simulation`);  
+//     mindGraph.simulation = d3.forceSimulation(nodes)
+//     .force("link", d3.forceLink(links).id(d => d.id).distance(80).strength(1))
+//     .force("charge", d3.forceManyBody().strength(-30).distanceMax(80)).force("center", d3.forceCenter(mindGraph.width/2,mindGraph.height/2));
+//   } else {
+//     console.log(`draw(): previous simulation existed, skipped.`);  
+    
+//     mindGraph.simulation = d3.forceSimulation(nodes)
+//     .force("link", d3.forceLink(links).id(d => d.id).distance(80).strength(1))
+//     .force("charge", d3.forceManyBody().strength(-30).distanceMax(80)).force("center", d3.forceCenter(mindGraph.width/2,mindGraph.height/2));
+    
+//     //.force("link", d3.forceLink(links).id(d => d.id).distance(120))
+//     //.force("charge", d3.forceManyBody()).force("center", d3.forceCenter(mindGraph.width/2,mindGraph.height/2));
+//   }
+//   //const simulation = d3.forceSimulation(nodes)
+//   //    .force("link", d3.forceLink(dataLinks.value).id(d => d.id).distance(120))
+//   //    .force("charge", d3.forceManyBody()).force("center", d3.forceCenter(mindGraph.width/2,mindGraph.height/2));
+//       //.force("x", d3.forceX(width/2))
+//       //.force("y", d3.forceY(height/2));
+
+//       //.force("center", d3.forceCenter(400, 300));
+//   mindGraph.simulation.on("end", () => {
+//     console.log("simulation end");
+//     //simulation.force("link", null);
+//     //simulation.force("charge", null);
+//     //simulation.force("center", null);
+//   });
+//   //let maxTick = 80;
+//   //let tickCount = 0;
+//   mindGraph.simulation.on("tick", simulation_ticked);
+//   //mindGraph.simulation.force("link").links(links).distance(d => { return 200 });
+
+//   // Drag handlers
+//   function dragstarted(event, d) {
+//     if (!event.active) {
+//       mindGraph.simulation.alphaTarget(0.3).restart();
+//       console.log("dragstarted(): event.active");
+//     }
+//     event.subject.fx = event.subject.x;
+//     event.subject.fy = event.subject.y;
+//     // d.fx = d.x;
+//     // d.fy = d.y;
+//     // console.log("drag started");
+//   }
+
+//   function dragged(event, d) {
+//     // d.fx = event.x;
+//     // d.fy = event.y;
+    
+//     event.subject.fx = event.x;
+//     event.subject.fy = event.y;
+//   }
+
+//   function dragended(event, d) {
+//     if (!event.active) mindGraph.simulation.alphaTarget(0);
+//     // console.log("drag end ");
+//     // d.fx = null;
+//     // d.fy = null;
+//     event.subject.fx = null;
+//     event.subject.fy = null;
+//   }
+// }

+ 86 - 0
src/assets/base.css

@@ -0,0 +1,86 @@
+/* color palette from <https://github.com/vuejs/theme> */
+:root {
+  --vt-c-white: #ffffff;
+  --vt-c-white-soft: #f8f8f8;
+  --vt-c-white-mute: #f2f2f2;
+
+  --vt-c-black: #181818;
+  --vt-c-black-soft: #222222;
+  --vt-c-black-mute: #282828;
+
+  --vt-c-indigo: #2c3e50;
+
+  --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
+  --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
+  --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
+  --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
+
+  --vt-c-text-light-1: var(--vt-c-indigo);
+  --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
+  --vt-c-text-dark-1: var(--vt-c-white);
+  --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
+}
+
+/* semantic color variables for this project */
+:root {
+  --color-background: var(--vt-c-white);
+  --color-background-soft: var(--vt-c-white-soft);
+  --color-background-mute: var(--vt-c-white-mute);
+
+  --color-border: var(--vt-c-divider-light-2);
+  --color-border-hover: var(--vt-c-divider-light-1);
+
+  --color-heading: var(--vt-c-text-light-1);
+  --color-text: var(--vt-c-text-light-1);
+
+  --section-gap: 160px;
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --color-background: var(--vt-c-black);
+    --color-background-soft: var(--vt-c-black-soft);
+    --color-background-mute: var(--vt-c-black-mute);
+
+    --color-border: var(--vt-c-divider-dark-2);
+    --color-border-hover: var(--vt-c-divider-dark-1);
+
+    --color-heading: var(--vt-c-text-dark-1);
+    --color-text: var(--vt-c-text-dark-2);
+  }
+}
+
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+  margin: 0;
+  font-weight: normal;
+}
+
+body {
+  min-height: 100vh;
+  color: var(--color-text);
+  background: var(--color-background);
+  transition:
+    color 0.5s,
+    background-color 0.5s;
+  line-height: 1.6;
+  font-family:
+    Inter,
+    -apple-system,
+    BlinkMacSystemFont,
+    'Segoe UI',
+    Roboto,
+    Oxygen,
+    Ubuntu,
+    Cantarell,
+    'Fira Sans',
+    'Droid Sans',
+    'Helvetica Neue',
+    sans-serif;
+  font-size: 15px;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}

BIN
src/assets/images/图层50@2x.png


BIN
src/assets/images/密码@2x.png


BIN
src/assets/images/组11拷贝2x.png


BIN
src/assets/images/账号2x.png


+ 1 - 0
src/assets/logo.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

+ 160 - 0
src/assets/main.css

@@ -0,0 +1,160 @@
+@import './base.css';
+
+#app {
+
+  
+  background-image: url('/bg1.jpeg') ;
+  background-position: center center;
+  background-repeat: no-repeat;
+  background-attachment: fixed;
+  background-size: cover;
+  height: 100vh;
+	width: 100vw;
+	&>.el-container{
+		width: 100%;
+		height: 100%;
+	}
+}
+
+
+.d3-context-menu {
+	position: absolute;
+	min-width: 150px;
+	z-index: 1200;
+}
+
+.d3-context-menu ul,
+.d3-context-menu ul li {
+	margin: 0;
+	padding: 0;
+}
+
+.d3-context-menu ul {
+	list-style-type: none;
+	cursor: default;
+}
+
+.d3-context-menu ul li {
+	-webkit-touch-callout: none; /* iOS Safari */
+	-webkit-user-select: none;   /* Chrome/Safari/Opera */
+	-khtml-user-select: none;    /* Konqueror */
+	-moz-user-select: none;      /* Firefox */
+	-ms-user-select: none;       /* Internet Explorer/Edge */
+	user-select: none;
+}
+
+/*
+	Disabled
+*/
+
+.d3-context-menu ul li.is-disabled,
+.d3-context-menu ul li.is-disabled:hover {
+	cursor: not-allowed;
+}
+
+/*
+	Divider
+*/
+
+.d3-context-menu ul li.is-divider {
+	padding: 0;
+}
+
+/* Theming
+------------ */
+
+.d3-context-menu-theme {
+	background-color: #f2f2f2;
+	border-radius: 4px;
+
+	font-family: Arial, sans-serif;
+	font-size: 14px;
+	border: 1px solid #d4d4d4;
+}
+
+.d3-context-menu-theme ul {
+	margin: 4px 0;
+}
+
+.d3-context-menu-theme ul li {
+	padding: 4px 16px;
+}
+
+.d3-context-menu-theme ul li:hover {
+	background-color: #4677f8;
+	color: #fefefe;
+}
+
+/*
+	Header
+*/
+
+.d3-context-menu-theme ul li.is-header,
+.d3-context-menu-theme ul li.is-header:hover {
+	background-color: #f2f2f2;
+	color: #444;
+	font-weight: bold;
+	font-style: italic;
+}
+
+/*
+	Disabled
+*/
+
+.d3-context-menu-theme ul li.is-disabled,
+.d3-context-menu-theme ul li.is-disabled:hover {
+	background-color: #f2f2f2;
+	color: #888;
+}
+
+/*
+	Divider
+*/
+
+.d3-context-menu-theme ul li.is-divider:hover {
+	background-color: #f2f2f2;
+}
+
+.d3-context-menu-theme ul hr {
+	border: 0;
+	height: 0;
+	border-top: 1px solid rgba(0, 0, 0, 0.1);
+	border-bottom: 1px solid rgba(255, 255, 255, 0.3);
+}
+
+/*
+	Nested Menu
+*/
+.d3-context-menu-theme ul li.is-parent:after {
+	border-left: 7px solid transparent;
+	border-top: 7px solid red;
+	content: "";
+	height: 0;
+	position: absolute;
+	right: 8px;
+	top: 35%;
+	transform: rotate(45deg);
+	width: 0;
+}
+
+.d3-context-menu-theme ul li.is-parent {
+	padding-right: 20px;
+	position: relative;
+}
+
+.d3-context-menu-theme ul.is-children {
+	background-color: #f2f2f2;
+	border: 1px solid #d4d4d4;
+	color: black;
+	display: none;
+	left: 100%;
+	margin: -5px 0;
+	padding: 4px 0;
+	position: absolute;
+	top: 0;
+	width: 100%;
+}
+
+.d3-context-menu-theme li.is-parent:hover > ul.is-children {
+	display: block;
+}

+ 55 - 0
src/components/ActionBar.vue

@@ -0,0 +1,55 @@
+<script setup lang="ts">
+import {Search,RefreshRight} from '@element-plus/icons-vue'
+
+const emit = defineEmits<{
+  (e: 'refresh'): void,
+  (e: 'reset'): void,
+}>()
+</script>
+
+<template>
+  <el-card class="action-card" shadow="never">
+    <template #header>
+      <div class="action-card-header">
+        <div class="l">
+          <slot name="left"></slot>
+        </div>
+        <div class="r">
+          <slot name="right"></slot>
+          <el-button style="margin-left: 10px" type="success" @click="emit('refresh')">
+            查询
+            <el-icon class="el-icon--right">
+              <Search  />
+            </el-icon>
+          </el-button>
+          <el-button type="warning" @click="emit('reset')">
+            重置
+            <el-icon class="el-icon--right">
+              <RefreshRight />
+            </el-icon>
+          </el-button>
+        </div>
+      </div>
+    </template>
+  </el-card>
+</template>
+
+<style scoped lang="scss">
+.action-card{
+
+  .action-card-header {
+    display: flex;
+    justify-content: space-between;
+    .r,.l{
+      display: flex;
+    }
+  }
+  :deep(.el-card__header){
+    padding: 10px;
+  }
+
+}
+.el-card{
+  border: none!important;
+}
+</style>

+ 64 - 0
src/components/BasicDialog.vue

@@ -0,0 +1,64 @@
+<template>
+    <div class="modal" v-if="isOpen">
+      <div class="modal-content">
+        <header class="modal-header">
+          <slot name="header">默认标题</slot>
+        </header>
+        <main class="modal-body">
+          <slot>默认内容</slot>
+        </main>
+        <footer class="modal-footer">
+          <el-button @click="closeModal" type="primary">关闭</el-button>
+        </footer>
+      </div>
+    </div>
+  </template>
+   
+  <script setup>
+  import { ref, watch } from 'vue';
+   
+  const props = defineProps({
+    isOpen: Boolean
+  });
+
+  const emit = defineEmits(['close']);
+   
+  const closeModal = () => {
+    emit('close');
+  };
+  </script>
+   
+  <style scoped>
+  .modal {
+    position: fixed;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0, 0, 0, 0.5);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 999;
+  }
+   
+  .modal-content {
+    background-color: white;
+    padding: 20px;
+    border-radius: 8px;
+    width: 700px;
+  }
+   
+  .modal-header, .modal-footer {
+    padding: 10px 0;
+    text-align: left;
+    height: 50px;
+  }
+   
+  .modal-body {
+    padding: 20px;
+    height: 450px;
+    background-color: #EFEFEF;
+    overflow-y: scroll;
+  }
+  </style>

+ 67 - 0
src/components/ConfirmDialog.vue

@@ -0,0 +1,67 @@
+<template>
+    <el-dialog
+      :title="title"
+      v-model="visible"
+      width="30%"
+      :before-close="handleClose"
+    >
+      <p>{{ message }}</p>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="handleCancel">取 消</el-button>
+          <el-button type="primary" @click="handleConfirm">确 定</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </template>
+  
+  <script setup>
+  import { ref, watch, toRefs } from 'vue';
+  import { ElMessage } from 'element-plus';
+  
+  const props = defineProps({
+    modelValue: {
+      type: Boolean,
+      default: false
+    },
+    title: {
+      type: String,
+      default: '确认操作'
+    },
+    objId: {
+        type: String,
+        default: 0
+    },
+    message: {
+      type: String,
+      default: '您确定要执行此操作吗?'
+    }
+  });
+  
+  const emit = defineEmits(['update:modelValue', 'confirm', 'cancel']);
+  
+  const visible = ref(props.modelValue);
+  
+  watch(() => props.modelValue, (newValue) => {
+    visible.value = newValue;
+  });
+  
+  const handleClose = (done) => {
+    emit('update:modelValue', false);
+    done();
+  };
+  
+  const handleConfirm = () => {
+    emit('confirm');
+    emit('update:modelValue', false);
+  };
+  
+  const handleCancel = () => {
+    emit('cancel');
+    emit('update:modelValue', false);
+  };
+  </script>
+  
+  <style scoped>
+  /* 添加样式 */
+  </style>

+ 289 - 0
src/components/Graph.vue

@@ -0,0 +1,289 @@
+<template>
+
+    <div id="echarts" class="chart-container" ></div>
+
+    
+  </template>
+   
+  <script setup>
+  import { onMounted, reactive } from 'vue'
+  import * as echarts from 'echarts'
+ 
+  
+  const props = defineProps({
+    nodes: {
+      type: Object,
+      default: () => {[]}
+    },
+    edges: {
+      type: Object,
+      default: () => {[]}
+    },
+  })
+  
+  const  chartOption = { 
+    toolbox: {
+        show: true,
+        feature: {
+            dataView: {show: false, readOnly: false},
+            restore: {show: true},
+            saveAsImage: {show: true}
+        }
+    },
+    dataZoom: [
+        {
+            type: 'inside', // 内置型数据区域缩放组件
+            start: 0, // 数据窗口范围的起始百分比
+            end: 100 // 数据窗口范围的结束百分比
+        },
+        {
+            type: 'slider', // 滑动条型数据区域缩放组件
+            start: 0, // 数据窗口范围的起始百分比
+            end: 100, // 数据窗口范围的结束百分比
+            handleSize: '80%', // 滑动条手柄的大小
+            height: '20px', // 组件的高度,只对滑动条型有效
+            left: 'center', // 组件的位置
+            top: 'bottom' // 组件的位置
+        }
+    ],
+        backgroundColor: '#ffffff',
+        series: [
+          {
+            color: [
+              'rgb(203,239,15)',
+              'rgb(116,239,15)',
+              'rgb(239,15,58)',
+              'rgb(15,239,174)',
+              'rgb(30,239,15)',
+              'rgb(239,188,15)',
+              'rgb(159,239,15)',
+              'rgb(159,15,239)',
+              'rgb(15,239,44)',
+              'rgb(15,239,87)',
+              'rgb(239,15,102)',
+              'rgb(239,58,15)',
+              'rgb(239,15,145)',
+              'rgb(73,239,15)',
+              'rgb(15,239,131)',
+            ],
+            type: 'graph',
+            roam: true,
+            layout: 'force',
+            symbol:"circle",
+            animation: false,
+            draggable: true,
+            force: {
+                repulsion: 1200,
+              layoutAnimation: false,
+            },
+            //type: 'graphGL',
+            nodes: props.nodes,
+            edges: props.edges,
+            modularity: {
+              resolution: 2,
+              sort: true
+            },
+            lineStyle: {
+              // 连接线的颜色
+              color: 'rgb(255,0,0)',
+              opacity: 1.0
+            },
+            itemStyle:  {
+              opacity: 1,
+              color: function(g) {
+                console.log(g.data.value);
+                if (g.data.value == 7000) {
+                  return "rgb(203,239,15)";
+                }
+                if (g.data.value == 6000) {
+                  return "rgb(116,239,15)";
+                }
+                if (g.data.value == 5000) {
+                  return "rgb(239,15,58)";
+                }
+                if (g.data.value == 5500) {
+                  return "rgb(239,58,15)";
+                }
+                if (g.data.value == 4000) {
+                  return "rgb(15,239,174)";
+                }
+                
+                return 'rgb(73,239,15)';
+              },
+              borderColor: '#fff'
+              // borderWidth: 1
+            },
+            focusNodeAdjacency: false,
+            focusNodeAdjacencyOn: 'click',
+            symbolSize: function(value) {
+              return Math.sqrt(value)
+            },
+            label: {
+              normal:{
+                show: true,
+                position: "inside",
+                    // 文本样式
+                textStyle: {
+                        fontSize: 16,
+                        color: 'black'
+                }
+              },
+              // 字体颜色
+              color: '#01070e'
+            },
+            edgeLabel: {
+                normal: {
+                    show: true,
+                    textStyle: {
+                        fontSize: 9
+                    },
+                    // 标签内容
+                    formatter: function(param) {
+                        return param.data.label;
+                    }
+                }
+            },
+            emphasis: {
+              label: {
+                show: true
+              },
+              lineStyle: {
+                opacity: 0.5,
+                width: 4
+              }
+            },
+            forceAtlas2: {
+              steps: 5,
+              stopThreshold: 20,
+              jitterTolerence: 10,
+              edgeWeight: [0.2, 1],
+              gravity: 50,
+              edgeWeightInfluence: 0
+              // preventOverlap: true
+            }
+          }
+        ]
+      };
+  var timer = null;
+  var dom = null;
+  onMounted (()=>{
+    //draw();
+    dom = echarts.init(document.getElementById('echarts'));
+    timer = null;
+  });
+  
+  const emit = defineEmits(["clickNode","clickEdge","dbClickNode"]);
+  const redraw = () =>{
+    dom.setOption(chartOption,true); 
+  };
+  const handleClick = (param) =>{
+    if (param.dataType === 'node') {
+      emit("clickNode", {
+        id: param.data.id,
+        type: "Node",
+        name: param.name
+      });
+    } else if (param.dataType === 'edge') {      
+      console.log(param.data.id); 
+      emit("clickEdge", {
+        id: param.data.id,
+        type: "Edge",
+        name: param.name
+      });        
+    }
+  };
+  const handleDbClick = (param) =>{
+    if (param.dataType === 'node') {
+      emit("dbClickNode", {
+        id: param.data.id,
+        type: "Node",
+        name: param.name
+      });
+    } else if (param.dataType === 'edge') {       
+      console.log('Edge clicked:', param.data);        
+    }
+  };
+  const draw =() =>{
+      dom.on('click', (param)=>{      
+        if (timer == null){
+          timer = setTimeout(()=>{
+            timer = null;
+            console.log("click node");
+            handleClick(param);
+          }, 250);
+          return;
+        }
+        console.log("db click node");
+        clearTimeout(timer);
+        timer = null;
+        handleDbClick(param);
+    
+      })
+      // var nodes = props.nodes.map(function(node, idx) {
+      //   return {
+      //     name: node["name"],
+      //     draggable: true,
+      //     value: 1000,
+      //     id: node["id"]
+      //   }
+      // })
+      // var edges = []
+      // for (var i = 0; i < props.edges.length; i++) {
+      //   var s = data.edges[i++]
+      //   var t = data.edges[i++]
+      //   edges.push({
+      //     label: "something",
+      //     source: s,
+      //     target: t
+      //   })
+      // }
+      props.nodes.forEach(function(node) {
+        // if (node.value > 100) {
+        node.emphasis = {
+          label: {
+            show: true
+          }
+        }
+        // }
+        if (node.value > 5000) {
+          node.label = {
+            show: true
+          }
+        }
+      })
+      dom.setOption(chartOption);
+  };
+  // export default {
+  //   name: 'Graph',
+  //   data() {
+  //     return {
+   
+  //     }
+  //   },
+  //   mounted() {
+  //     this.draw()
+  //   },
+  //   methods: {
+     
+  //   }
+  // }
+
+  
+  defineExpose({
+    draw, redraw
+  });
+  </script>
+   
+  <style scoped>
+  * {
+    margin: 0;
+    padding: 0;
+  }
+   
+  .chart-container {
+    position: relative;
+    overflow: hidden;
+    width: 100%;
+    height: 100%;
+  }
+  </style>

+ 230 - 0
src/components/GraphCreateDialog.vue

@@ -0,0 +1,230 @@
+<template>
+
+    <el-dialog
+      v-model="visible"
+      width="650"
+      :before-close="handleClose"
+    >
+    <el-form :model="formData">
+      <el-row class="data-category">{{message}}</el-row>
+      <el-row>
+        <el-input maxlength="64" show-word-limit v-model="formData.graph_name"></el-input>
+      </el-row> 
+    </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="handleCancel">取 消</el-button>
+          <el-button type="primary" @click="handleConfirm">保 存</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </template>
+   
+  <script setup lang="ts">
+
+import { ref, watch, toRefs, onMounted} from 'vue';
+import r from '@/utils/request.ts';
+import { ElMessageBox, ElNotification } from 'element-plus';
+/**********emits**********/
+const emit = defineEmits(['update:modelValue', 'confirm', 'cancel','server-saved']);
+
+/**********props**********/
+const props = defineProps({
+    modelValue: {
+      type: Boolean,
+      default: false
+    },
+    title: {
+      type: String,
+      default: '确认操作'
+    },
+    message: {
+      type: String,
+      default: '您确定要执行此操作吗?'
+    },
+    content: {
+      type: String,
+      default:"",
+    },
+    graph_name: {
+      type: String,
+      default:"",
+    },
+    graph_id: {
+      type: Number,
+      default: 0,
+    }
+});
+
+/**********props_watch**********/
+watch(() => props.modelValue, (newValue) => {
+  console.log("watch modelValue");
+  visible.value = newValue;
+
+  if (visible.value){    
+    formData.value.graph_name = props.graph_name;
+    formData.value.graph_content = props.content;
+  }
+});
+watch(()=>props.content,(newvalue, oldvalue)=>{
+  console.log("watch data " + newvalue.length);
+
+});
+
+const handleClose = (done) => {
+  emit('update:modelValue', false);
+  done();
+};
+
+/**********ui_data**********/
+const visible = ref(props.modelValue);
+const formData = ref({
+  id: 0,
+  graph_name: "",
+  graph_content: "",
+  status: 0,
+});
+
+/**********ui_handler**********/
+const update_graph = () =>{
+
+  var requestData = {
+      id: props.graph_id,
+      graph_name: formData.value.graph_name,
+      graph_content: formData.value.graph_content,
+      status: 0,
+    }
+    
+    const response = r.request<string[]>({
+      url: "/api/sub-graph-update",
+      method: "post",
+      data: requestData,
+    });
+    response.then(result => {
+      if (result.error_code == 0){
+        ElNotification({
+          title: '成功',
+          message: '工作区已经成功更新了',
+          type: 'success'
+        });
+        emit('server-saved', result.id);        
+        emit('created', result.edges);
+      } else {
+        ElNotification({
+          title: '失败',
+          message: '工作区更新失败 '+ result.error_msg,
+          type: 'error'
+        });
+
+      }      
+      emit('update:modelValue', false);
+    });  
+}
+const handleConfirm = () => {    
+    if (props.graph_id > 0){
+      update_graph();
+      return;
+    }
+    var requestData = {
+      graph_name: formData.value.graph_name,
+      graph_content: formData.value.graph_content,
+      status: 0,
+    }
+    
+    const response = r.request<string[]>({
+      url: "/api/sub-graph-create",
+      method: "post",
+      data: requestData,
+    });
+    response.then(result => {
+      if (result.error_code == 0){
+        ElNotification({
+          title: '成功',
+          message: '工作区已经成功保存了',
+          type: 'success'
+        });
+        emit('server-saved', result.id);        
+        emit('created', result.edges);
+      } else {
+        ElNotification({
+          title: '失败',
+          message: '工作区保存失败 '+ result.error_msg,
+          type: 'error'
+        });
+
+      }      
+      emit('update:modelValue', false);
+    });
+  };
+  
+  const handleCancel = () => {
+    emit('cancel');
+    emit('update:modelValue', false);
+    visible.value = false;
+  };
+
+/**********vue_event_handler**********/
+onMounted( ()=>{
+
+});
+
+  
+
+/**********vue_export_def**********/
+const openDialog = ()=>{
+  //visible.value = true;
+}
+
+defineExpose({openDialog});
+  </script>
+   
+  <style scoped>
+.data-category{
+    width: 600px;
+    font-size: 18pt;
+    color:rgb(3, 57, 123);
+    font-weight: 800;
+    justify-content: center;
+    margin-bottom: 10px;
+}
+.data-name{
+  margin: 5px;
+  display: flex;
+  flex-direction: column;
+  width: 580px;
+}
+.props-content{
+  width: 600px;
+  height: 350px;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  
+  overflow-y: auto;
+}
+
+.checkbox-content{
+  width: 600px;
+  
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  overflow-y: auto;
+}
+
+.data-prop{
+  margin: 5px;
+  width: 600px;
+  display: flex;
+  flex-direction: column;
+}
+.prop-name{
+  background-color: #efefef;
+  color:rgb(11, 117, 247);
+  width:580px;
+}
+.data-text{
+  width: 580px;
+  background-color: #efefef;
+}
+  </style>

+ 41 - 0
src/components/HelloWorld.vue

@@ -0,0 +1,41 @@
+<script setup lang="ts">
+defineProps<{
+  msg: string
+}>()
+</script>
+
+<template>
+  <div class="greetings">
+    <h1 class="green">{{ msg }}</h1>
+    <h3>
+      You’ve successfully created a project with
+      <a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
+      <a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
+    </h3>
+  </div>
+</template>
+
+<style scoped>
+h1 {
+  font-weight: 500;
+  font-size: 2.6rem;
+  position: relative;
+  top: -10px;
+}
+
+h3 {
+  font-size: 1.2rem;
+}
+
+.greetings h1,
+.greetings h3 {
+  text-align: center;
+}
+
+@media (min-width: 1024px) {
+  .greetings h1,
+  .greetings h3 {
+    text-align: left;
+  }
+}
+</style>

+ 156 - 0
src/components/NewTable.vue

@@ -0,0 +1,156 @@
+<template>
+  <div class="new-table">
+    <div class="table">
+      <el-table
+        :data="tableData"
+        border
+        show-summary
+        style="width: 100%"
+        :summary-method="getSummaries"
+        :max-height="new_table_height"
+      >
+        <el-table-column type="index" align="center" label="序号" width="100" />
+        <el-table-column
+          prop="nodeType"
+          align="center"
+          label="实体类型"
+        ></el-table-column>
+        <el-table-column
+          align="center"
+          prop="nodeNum"
+          sortable
+          label="实体数量"
+        />
+        <!-- <el-table-column
+          align="center"
+          prop="nodePropNum"
+          sortable
+          label="实体属性数量"
+        /> -->
+        <el-table-column
+          prop="relationType"
+          align="center"
+          sortable
+          label="相关关系类型"
+        />
+        <el-table-column
+          prop="relationNum"
+          align="center"
+          sortable
+          label="相关关系数量"
+        />
+      </el-table>
+    </div>
+    <div class="pagination" v-if="total > 10">
+      <el-pagination
+        background
+        layout="total,sizes,prev, pager, next, jumper, ->"
+        :total="total"
+        @change="paginationChange"
+        :page-sizes="[10, 20, 30, 40, 50, 100]"
+      ></el-pagination>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, watch, getCurrentInstance, onMounted } from "vue";
+import { h } from "vue";
+let new_table_height = ref(0);
+
+let currentPage = ref(1); //当前表格页数
+let selectPageSize = ref(10); //表格条目数量
+const { proxy } = getCurrentInstance();
+const emit = defineEmits(["update"]);
+defineExpose({ currentPage, selectPageSize });
+const props = defineProps({
+  total: {
+    type: Number,
+    default: 10,
+  },
+  tableData: {
+    type: Array,
+    default: [
+      {
+        entityType: "疾病",
+        entityNum: 5000,
+        entityAttributeNum: 5000,
+        coefficientType: 5000,
+        coefficientNum: 5000,
+      },
+      {
+        entityType: "症状",
+        entityNum: 5000,
+        entityAttributeNum: 5000,
+        coefficientType: 5000,
+        coefficientNum: 5000,
+      },
+      {
+        entityType: "并发症",
+        entityNum: 5000,
+        entityAttributeNum: 5000,
+        coefficientType: 5000,
+        coefficientNum: 5000,
+      },
+    ],
+  },
+});
+const getSummaries = (param) => {
+  const { columns, data } = param;
+  const sums = [];
+  // console.log("columns", columns, "data", data);
+  columns.forEach((column, index) => {
+    if (index === 0) {
+      //序号
+      sums[index] = h("div", { style: { textDecoration: "underline" } }, []);
+      return;
+    }
+    const values = data.map((item) => Number(item[column.property]));
+    if (!values.every((value) => Number.isNaN(value))) {
+      sums[index] = `${values.reduce((prev, curr) => {
+        const value = Number(curr);
+        if (!Number.isNaN(value)) {
+          return prev + curr;
+        } else {
+          return prev;
+        }
+      }, 0)}`;
+    } else if (index === 1) {
+      sums[index] = "总计:";
+    } else {
+      sums[index] = "";
+    }
+  });
+
+  return sums;
+};
+
+const paginationChange = (getCurrentPages, pageSize) => {
+  // console.log(getCurrentPages, pageSize);
+  currentPage.value = getCurrentPages;
+  selectPageSize.value = pageSize;
+  emit("update", getCurrentPages, pageSize);
+};
+onMounted(() => {
+  let new_table = document.querySelector(".new-table");
+  // console.log("height", window.getComputedStyle(new_table, null).height);
+  new_table_height.value =
+    parseInt(window.getComputedStyle(new_table, null).height, 10) - 160;
+});
+</script>
+
+<style lang="less" scoped>
+.new-table {
+  // height: 100%;
+  flex-grow: 1;
+  .table {
+    // height: 500px;
+  }
+  .pagination {
+    margin-top: 20px;
+    .el-pagination {
+      justify-content: flex-end;
+    }
+  }
+}
+</style>

+ 164 - 0
src/components/NodeCreateDialog.vue

@@ -0,0 +1,164 @@
+<template>
+
+    <el-dialog
+      v-model="visible"
+      width="650"
+      :before-close="handleClose"
+    >
+    <el-row class="data-category">{{data.name}}</el-row>
+    <el-row>
+      <div class="data-name"><div class="prop-name">&nbsp;节点名称&nbsp;</div> 
+      <el-input maxlength="64" show-word-limit v-model="formData.name"></el-input></div></el-row> 
+    <el-row class="props-content">
+      <template v-for="item in formData.props" >
+        <div class="data-prop"><div class="prop-name">{{ item.prop_title }}</div>
+          <el-input v-model="item.prop_value" type="textarea" :rows="5" resize="none" class="data-text"  maxlength="1000" show-word-limit></el-input>
+        </div>
+      </template>      
+    </el-row>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="handleCancel">取 消</el-button>
+          <el-button type="primary" @click="handleConfirm">保 存</el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+  </template>
+   
+  <script setup lang="ts">
+
+import { ref, watch, toRefs } from 'vue';
+import r from '@/utils/request.ts';
+import { ElNotification } from 'element-plus';
+const formData = ref({
+  id: 0,
+  category: "",
+  name: "",
+  props: [],
+  status: 0,
+});
+const props = defineProps({
+    modelValue: {
+      type: Boolean,
+      default: false
+    },
+    title: {
+      type: String,
+      default: '确认操作'
+    },
+    data: {
+      type: Object,
+      default: () => ({})
+    },
+    message: {
+      type: String,
+      default: '您确定要执行此操作吗?'
+    }
+});
+
+  
+  const emit = defineEmits(['update:modelValue', 'confirm', 'cancel','server-saved']);
+  
+  const visible = ref(props.modelValue);
+  
+  watch(() => props.modelValue, (newValue) => {
+    visible.value = newValue;
+  });
+  
+  const handleClose = (done) => {
+    emit('update:modelValue', false);
+    done();
+  };
+  
+  const handleConfirm = () => {
+    
+    emit('created');
+    emit('update:modelValue', false);
+
+    const response = r.request<string[]>({
+      url: "/api/node-create",
+      method: "post",
+      data: formData.value,
+    });
+    response.then(result => {
+      if (result.error_code == 0){
+        ElNotification({
+          title: '成功',
+          message: '节点已经成功创建了',
+          type: 'success'
+        });
+        console.log("Node created: "+ result.id);
+        emit('server-saved', result.id);
+      } else {
+        ElNotification({
+          title: '失败',
+          message: '节点创建失败 '+result.error_msg,
+          type: 'error'
+        });
+
+      }
+      
+      visible.value = false;
+    });
+  };
+  
+  const handleCancel = () => {
+    emit('cancel');
+    emit('update:modelValue', false);
+    visible.value = false;
+  };
+
+  watch(()=>props.data,(newvalue, oldvalue)=>{
+    formData.value.name = '';
+    formData.value.id = 0;
+    formData.value.category = newvalue.category;
+    formData.value.props =[];
+
+    newvalue.props.forEach(element => {
+      formData.value.props.push({id:0, ref_id: 0, prop_title: element.prop_title, prop_name: element.prop_name, prop_value:element.prop_value, category: element.category});
+    });
+
+  });
+  const openDialog = ()=>{
+    //visible.value = true;
+  }
+  
+defineExpose({openDialog});
+  </script>
+   
+  <style scoped>
+.data-category{
+    width: 600px;
+    font-size: 18pt;
+    color:rgb(3, 57, 123);
+    font-weight: 800;
+    justify-content: center;
+    margin-bottom: 10px;
+}
+.data-name{
+  margin: 5px;
+  display: flex;
+  flex-direction: column;
+  width: 580px;
+}
+.props-content{
+  height: 450px;
+  overflow-y: scroll;
+}
+.data-prop{
+  margin: 5px;
+  width: 600px;
+  display: flex;
+  flex-direction: column;
+}
+.prop-name{
+  background-color: #efefef;
+  color:rgb(11, 117, 247);
+  width:580px;
+}
+.data-text{
+  width: 580px;
+  background-color: #efefef;
+}
+  </style>

+ 301 - 0
src/components/NodeLinkDialog.vue

@@ -0,0 +1,301 @@
+<template>
+
+    <el-dialog
+      v-model="visible"
+      width="650"
+      :before-close="handleClose"
+    >
+    <el-form :model="formData" :rules="formRules">
+      <el-row class="data-category">{{message}}</el-row>
+      <el-row>
+        <el-input maxlength="64" show-word-limit v-model="formData.search"></el-input><el-button @click="handleSearch">搜索</el-button></el-row> 
+      <el-row>
+        <el-form-item prop="linkDirection">
+        <el-radio v-model="formData.linkDirection" label="1">进</el-radio>
+        <el-radio v-model="formData.linkDirection" label="2">出</el-radio>
+        </el-form-item>
+        <el-form-item prop="linkType">
+          <el-select v-model="formData.linkType" placeholder="请选择" style="width: 200px">
+            <el-option
+                v-for="item in formData.linkOptions"
+                :key="item.category"
+                :label="item.name"
+                :value="item.category">
+              </el-option>
+            </el-select>
+        </el-form-item>
+      </el-row>
+      <el-row class="props-content">
+        <el-checkbox :indeterminate="formData.isIndeterminate" v-model="formData.checkAll" @change="handleCheckAllChange">全选</el-checkbox>
+        <div style="margin: 15px 0;"></div>        
+        <el-form-item prop="selectedNodes">
+          <el-checkbox-group v-model="formData.selectedNodes" class="checkbox-content" >
+            <el-checkbox style="width:220px" v-for="item in formData.nodes" :value="item.id" :key="item.id">{{item.name}}({{item.category}})</el-checkbox>
+          </el-checkbox-group>
+        </el-form-item>
+      </el-row>
+    </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="handleCancel">取 消</el-button>
+          <el-button type="primary" @click="handleConfirm">保 存</el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+  </template>
+   
+  <script setup lang="ts">
+
+import { ref, watch, toRefs, onMounted} from 'vue';
+import r from '@/utils/request.ts';
+import { ElMessageBox, ElNotification } from 'element-plus';
+/**********emits**********/
+const emit = defineEmits(['update:modelValue', 'confirm', 'cancel','server-saved']);
+
+/**********props**********/
+const props = defineProps({
+    modelValue: {
+      type: Boolean,
+      default: false
+    },
+    title: {
+      type: String,
+      default: '确认操作'
+    },
+    data: {
+      type: Object,
+      default: {},
+    },
+    nodes: {
+      type: Object,
+      default: {},
+    },
+    message: {
+      type: String,
+      default: '您确定要执行此操作吗?'
+    },
+});
+
+/**********props_watch**********/
+watch(() => props.modelValue, (newValue) => {
+  console.log("watch modelValue "+newValue);
+  
+  visible.value = newValue;
+  if (newValue){
+    formData.value.id = 0;
+    formData.value.search = '';
+    formData.value.category = props.data.category;
+    formData.value.name = props.data.name;  
+    formData.value.selectedNodes = [];
+    formData.value.nodes = props.nodes;
+  }
+});
+watch(()=>props.data,(newvalue, oldvalue)=>{
+  console.log("watch data");
+});
+
+watch(()=>props.nodes,(newvalue, oldvalue)=>{
+  console.log("watch nodes");
+  //formData.value.nodes = newvalue;
+});
+const handleClose = (done) => {
+  emit('update:modelValue', false);
+  done();
+};
+
+/**********ui_data**********/
+const visible = ref(props.modelValue);
+const formData = ref({
+  id: 0,
+  category: "",
+  name: "",
+  status: 0,
+  search: "",
+  nodes: {},
+  linkType: "",
+  linkDirection: "2",
+  linkOptions: [{category:0, name:"has"},{category:1, name:"belongs_to"}],
+  checkAll: false,    
+  isIndeterminate: true,
+  selectedNodes: []
+});
+const formRules = ref({
+  linkType: [{required: true, message:"链接类型必须选择", trigger:"blur"}],
+});
+/**********ui_handler**********/
+function handleCheckAllChange(val) {
+  formData.value.selectedNodes = val ? props.data.nodes.map((data,index)=>{ return data.id;}) : [];
+  formData.value.isIndeterminate = false;
+};
+function handleCheckedCitiesChange(value) {
+  let checkedCount = value.length;
+  this.checkAll = checkedCount === data.nodes.length;
+  this.isIndeterminate = checkedCount > 0 && checkedCount < data.nodes.length;
+}
+const handleSearch = () =>{
+  const response = r.request<string[]>({
+      url: "/api/nodes/search/"+formData.value.search+"/0/1",
+      method: "get"
+    });
+  response.then(result => {    
+    formData.value.nodes = result["nodes"];    
+  });
+}
+const handleConfirm = () => {    
+    if (formData.value.linkType.length < 3){
+
+      ElMessageBox.alert(
+        '必须要设置链接的类型',
+        '警告',
+        {
+          confirmButtonText: '确定',
+          callback: action => {
+            console.log(action)
+          }
+        }
+      );
+      return;
+    }
+    if (formData.value.selectedNodes.length < 1){
+      ElMessageBox.alert(
+        '至少需要选择一个目标节点',
+        '警告',
+        {
+          confirmButtonText: '确定',
+          callback: action => {
+            console.log(action)
+          }
+        }
+      );
+      return;
+      }
+    var requestData = [];
+    var edgeData = {
+      src_id: 0,
+      dest_id: 0,
+      category: "",
+      name: "",
+      version:"1.1",
+      status: 1,
+    }
+    
+    edgeData.src_id = props.data.id;
+    if (formData.value.linkDirection == "1"){
+      edgeData.dest_id = props.data.id;
+    }
+    formData.value.selectedNodes.forEach((value)=>{      
+      edgeData.src_id = props.data.id;
+      edgeData.dest_id = value;
+      if (formData.value.linkDirection == "1"){
+        edgeData.dest_id = props.data.id;
+        edgeData.src_id = value;
+      }
+      edgeData.category = formData.value.linkType;
+      edgeData.name = formData.value.linkOptions.find(obj=>obj.category == edgeData.category)?.name;
+      requestData.push({...edgeData})
+    });
+    
+    const response = r.request<string[]>({
+      url: "/api/edge-create",
+      method: "post",
+      data: requestData,
+    });
+    response.then(result => {
+      if (result.error_code == 0){
+        ElNotification({
+          title: '成功',
+          message: '链接已经成功创建了',
+          type: 'success'
+        });
+        emit('server-saved', result.edges);        
+        emit('created');
+      } else {
+        ElNotification({
+          title: '失败',
+          message: '链接创建失败 '+ result.error_msg,
+          type: 'error'
+        });
+
+      }      
+      emit('update:modelValue', false);
+    });
+  };
+  
+  const handleCancel = () => {
+    emit('cancel');
+    emit('update:modelValue', false);
+    visible.value = false;
+  };
+
+/**********vue_event_handler**********/
+onMounted( ()=>{
+
+  const response = r.request<string[]>({
+      url: "/api/edges/c/all",
+      method: "get",
+    });
+    response.then(result => {
+      formData.value.linkOptions = result['records'];
+    });
+});
+
+  
+
+/**********vue_export_def**********/
+const openDialog = ()=>{
+  //visible.value = true;
+}
+
+defineExpose({openDialog});
+  </script>
+   
+  <style scoped>
+.data-category{
+    width: 600px;
+    font-size: 18pt;
+    color:rgb(3, 57, 123);
+    font-weight: 800;
+    justify-content: center;
+    margin-bottom: 10px;
+}
+.data-name{
+  margin: 5px;
+  display: flex;
+  flex-direction: column;
+  width: 580px;
+}
+.props-content{
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: flex-start;
+  overflow-y: auto;
+}
+
+.checkbox-content{
+  width: 600px;  
+  height: 300px;
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  justify-content: flex-start;
+  overflow-y: auto;
+}
+
+.data-prop{
+  margin: 5px;
+  width: 600px;
+  display: flex;
+  flex-direction: column;
+}
+.prop-name{
+  background-color: #efefef;
+  color:rgb(11, 117, 247);
+  width:580px;
+}
+.data-text{
+  width: 580px;
+  background-color: #efefef;
+}
+  </style>

+ 259 - 0
src/components/NodeLinksEditDialog.vue

@@ -0,0 +1,259 @@
+<template>
+
+  <el-dialog
+    v-model="visible"
+    width="850"
+    :before-close="handleClose"
+  >
+  <el-form :model="formData" :rules="formRules">
+    <el-row class="data-category">{{message}}</el-row>
+    
+    <el-row class="props-content">
+      
+      <el-table
+      tooltip-effect="dark"
+      :data="formData.links"
+      style="width: 100%"
+      @selection-change="">
+      <el-table-column
+      type="selection"
+      width="55">
+      </el-table-column>
+      <el-table-column
+        prop="src_node.name"
+        label="源"
+        width="180">
+      </el-table-column>
+      <el-table-column
+        prop="name"
+        label="链接名称"
+        width="180">
+      </el-table-column>
+      <el-table-column
+        prop="dest_node.name"
+        label="目的">
+      </el-table-column>
+      <el-table-column
+        prop="dest_node.status"
+        label="状态">
+        <template v-slot="scope">
+          <div v-if="scope.row.status==0">正常</div>
+          <div v-if="scope.row.status==-1"><el-tag type="danger">无效</el-tag></div>
+          
+        </template>
+      </el-table-column>
+      <el-table-column
+      fixed="right"
+      label="操作"
+      width="180">
+      <template #default="scope">
+        <el-button type="danger" size="small" @click="handleEdgeDelete(scope.row, -99)" >删除</el-button>        
+        <el-button v-if="scope.row.status==0" type="warning" size="small" @click="handleEdgeDelete(scope.row, -1)" >设置无效</el-button>       
+        <el-button v-if="scope.row.status==-1" type="success" size="small" @click="handleEdgeDelete(scope.row, 0)" >设置有效</el-button>
+      </template>
+    </el-table-column>
+    </el-table>
+
+
+    </el-row>
+  </el-form>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="handleCancel">取 消</el-button>
+        <el-button type="primary" @click="handleConfirm">保 存</el-button>
+      </span>
+    </template>
+  </el-dialog>
+
+</template>
+ 
+<script setup lang="ts">
+
+import { ref, watch, toRefs, onMounted} from 'vue';
+import r from '@/utils/request.ts';
+import { ElMessageBox, ElNotification } from 'element-plus';
+/**********emits**********/
+const emit = defineEmits(['update:modelValue', 'confirm', 'cancel','server-saved']);
+
+/**********props**********/
+const props = defineProps({
+  modelValue: {
+    type: Boolean,
+    default: false
+  },
+  title: {
+    type: String,
+    default: '确认操作'
+  },
+  data: {
+    type: Object,
+    default: {},
+  },
+  links: {
+    type: Object,
+    default: {},
+  },
+  message: {
+    type: String,
+    default: '您确定要执行此操作吗?'
+  },
+});
+
+/**********props_watch**********/
+watch(() => props.modelValue, (newValue) => {
+console.log("watch modelValue");
+visible.value = newValue;
+});
+watch(()=>props.data,(newvalue, oldvalue)=>{
+  console.log("watch data");
+  formData.value.name = '';
+  formData.value.id = 0;
+  formData.value.category = newvalue.category;
+  formData.value.name = newvalue.name;
+  formData.value.selectedLinks = [];
+});
+
+watch(()=>props.links,(newvalue, oldvalue)=>{
+  console.log("watch nodes");
+  formData.value.links = newvalue;
+});
+const handleClose = (done) => {
+  emit('update:modelValue', false);
+  done();
+};
+
+/**********ui_data**********/
+const visible = ref(props.modelValue);
+const formData = ref({
+  id: 0,
+  category: "",
+  name: "",
+  status: 0,
+  links: [],
+  checkAll: false,    
+  isIndeterminate: true,
+  selectedLinks: []
+});
+const formRules = ref({
+  linkType: [{required: true, message:"链接类型必须选择", trigger:"blur"}],
+});
+const loadEdgeData = (data) => {
+  const response = r.request<string[]>({
+      url: "/api/edge-of-node/"+props.data.id,
+      method: "get"
+    });
+    response.then(result => {   
+      formData.value.links = result["edges"];
+    });
+}
+const handleEdgeDelete = (data, status) =>{
+  var message = "";
+  if (status == -99){
+    message = '您确认要删除这个链接吗?';
+  }
+  if (status == -1)
+  {
+    message = '您确认要将这个链接设置为无效吗?';
+  }
+  if (status == 0)
+  {
+    message = '您确认要将这个链接设置为有效吗?';
+  }
+  ElMessageBox.confirm(message,'提示',
+    {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+    }
+  ).then(() => {
+    console.log("handleEdgeDelete(): " + data.id);
+    
+    const response = r.request<string[]>({
+      url: "/api/edge-delete/"+data.id+"/"+status,
+      method: "get"
+    });
+    response.then(result => {       
+      loadEdgeData(data);
+    });
+  }).catch(() => {
+    console.log('取消')
+  })
+};
+/**********ui_handler**********/
+
+const handleConfirm = () => {        
+  emit('created');
+  emit('update:modelValue', false);
+  emit('server-saved');
+};
+
+const handleCancel = () => {
+  emit('cancel');
+  emit('update:modelValue', false);
+};
+
+/**********vue_event_handler**********/
+onMounted( ()=>{
+
+});
+
+
+
+/**********vue_export_def**********/
+const openDialog = ()=>{
+//visible.value = true;
+}
+
+defineExpose({openDialog});
+</script>
+ 
+<style scoped>
+.data-category{
+  width: 100%;
+  font-size: 18pt;
+  color:rgb(3, 57, 123);
+  font-weight: 800;
+  justify-content: center;
+  margin-bottom: 10px;
+}
+.data-name{
+margin: 5px;
+display: flex;
+flex-direction: column;
+width: 580px;
+}
+.props-content{
+width: 100%;
+height: 350px;
+display: flex;
+flex-direction: row;
+justify-content: flex-start;
+overflow: auto;
+}
+
+.checkbox-content{
+width: 600px;
+
+display: flex;
+flex-direction: row;
+flex-wrap: wrap;
+justify-content: flex-start;
+overflow-y: auto;
+}
+
+.data-prop{
+margin: 5px;
+width: 600px;
+display: flex;
+flex-direction: column;
+}
+.prop-name{
+background-color: #efefef;
+color:rgb(11, 117, 247);
+width:580px;
+}
+.data-text{
+width: 580px;
+background-color: #efefef;
+}
+</style>

+ 270 - 0
src/components/NodeMergeDialog.vue

@@ -0,0 +1,270 @@
+<template>
+
+  <el-dialog
+    v-model="visible"
+    width="650"
+    :before-close="handleClose"
+  >
+  <el-form :model="formData" :rules="formRules">
+    <el-row class="data-category">{{message}}</el-row>
+    <el-row>
+      <el-input maxlength="64" show-word-limit v-model="formData.search"></el-input><el-button @click="handleSearch">搜索</el-button></el-row> 
+    <!-- <el-row>
+      <el-form-item prop="linkDirection">
+      <el-radio v-model="formData.linkDirection" label="1">被融合(选择节点被替换)</el-radio>
+      <el-radio v-model="formData.linkDirection" label="2">融合(当前节点被替换)</el-radio>
+      </el-form-item>
+    </el-row> -->
+    <el-row class="props-content">
+      <el-checkbox :indeterminate="formData.isIndeterminate" v-model="formData.checkAll" @change="handleCheckAllChange">全选</el-checkbox>
+      <div style="margin: 15px 0;"></div>
+      
+      <el-form-item prop="selectedNodes">
+        <el-checkbox-group v-model="formData.selectedNodes" class="checkbox-content" >
+          <el-checkbox style="width:220px" v-for="item in formData.nodes" :value="item.id" :key="item.id">{{item.name}}({{item.category}})</el-checkbox>
+        </el-checkbox-group>
+      </el-form-item>
+    </el-row>
+  </el-form>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="handleCancel">取 消</el-button>
+        <el-button type="primary" @click="handleConfirm">保 存</el-button>
+      </span>
+    </template>
+  </el-dialog>
+
+</template>
+ 
+<script setup lang="ts">
+
+import { ref, watch, toRefs, onMounted} from 'vue';
+import r from '@/utils/request.ts';
+import { ElMessageBox, ElNotification } from 'element-plus';
+/**********emits**********/
+const emit = defineEmits(['update:modelValue', 'confirm', 'cancel','server-saved']);
+
+/**********props**********/
+const props = defineProps({
+  modelValue: {
+    type: Boolean,
+    default: false
+  },
+  title: {
+    type: String,
+    default: '确认操作'
+  },
+  data: {
+    type: Object,
+    default: {},
+  },
+  nodes: {
+    type: Object,
+    default: {},
+  },
+  message: {
+    type: String,
+    default: '您确定要执行此操作吗?'
+  },
+});
+
+/**********props_watch**********/
+watch(() => props.modelValue, (newValue) => {
+console.log("watch modelValue");
+visible.value = newValue;
+});
+watch(()=>props.data,(newvalue, oldvalue)=>{
+console.log("watch data");
+formData.value.name = '';
+formData.value.id = 0;
+formData.value.category = newvalue.category;
+formData.value.name = newvalue.name;
+formData.value.selectedNodes = [];
+});
+
+watch(()=>props.nodes,(newvalue, oldvalue)=>{
+console.log("watch nodes");
+formData.value.nodes = newvalue;
+});
+const handleClose = (done) => {
+emit('update:modelValue', false);
+done();
+};
+
+/**********ui_data**********/
+const visible = ref(props.modelValue);
+const formData = ref({
+id: 0,
+category: "",
+name: "",
+status: 0,
+search: "",
+nodes: [],
+linkDirection: "2",
+checkAll: false,    
+isIndeterminate: true,
+selectedNodes: []
+});
+const formRules = ref({
+linkType: [{required: true, message:"链接类型必须选择", trigger:"blur"}],
+});
+/**********ui_handler**********/
+function handleCheckAllChange(val) {
+formData.value.selectedNodes = val ? props.nodes.map((data,index)=>{ return data.id;}) : [];
+formData.value.isIndeterminate = false;
+};
+function handleCheckedCitiesChange(value) {
+let checkedCount = value.length;
+this.checkAll = checkedCount === data.nodes.length;
+this.isIndeterminate = checkedCount > 0 && checkedCount < data.nodes.length;
+}
+const handleSearch = () =>{
+const response = r.request<string[]>({
+    url: "/api/nodes/search/"+formData.value.search+"/0/1",
+    method: "get"
+  });
+response.then(result => {    
+  formData.value.nodes = result["nodes"];    
+});
+}
+const handleConfirm = () => {  
+  var targetValidate = false;
+  if (formData.value.linkDirection == "2" ){
+    //替换候选的对象,可以替换多个
+    if (formData.value.selectedNodes.length > 0){
+      targetValidate = true;
+    }
+  }
+  
+  if (formData.value.linkDirection == "1" ){
+    //被候选对象替换,仅允许被1个替换    
+    if (formData.value.selectedNodes.length == 1 ){
+      targetValidate = true;
+    }
+  }
+  if (targetValidate == false){
+      ElMessageBox.alert(
+        '替换模式下,至少需要选择一个目标;被替换模式下,仅可以选择一个目标',
+        '警告',
+        {
+          confirmButtonText: '确定',
+          callback: action => {
+            console.log(action)
+          }
+        }
+      );
+      return;
+  }
+  var requestData = [];
+  var mergeData = {
+    src_id: 0,
+    dest_id: 0,
+  }
+  
+  formData.value.selectedNodes.forEach((value)=>{      
+    mergeData.src_id = props.data.id;
+    mergeData.dest_id = value;
+    if (formData.value.linkDirection == "1"){  
+      mergeData.dest_id = props.data.id;
+      mergeData.src_id = value;
+    }
+    requestData.push({...mergeData})
+  });
+  const response = r.request<string[]>({
+    url: "/api/node-merge",
+    method: "post",
+    data: requestData,
+  });
+  response.then(result => {
+    if (result.error_code == 0){
+      ElNotification({
+        title: '成功',
+        message: '链接已经成功创建了',
+        type: 'success'
+      });
+      emit('server-saved', result.edges);        
+      emit('created');
+    } else {
+      ElNotification({
+        title: '失败',
+        message: '链接创建失败 '+ result.error_msg,
+        type: 'error'
+      });
+
+    }      
+    emit('update:modelValue', false);
+  });
+};
+
+const handleCancel = () => {
+  emit('cancel');
+  emit('update:modelValue', false);
+  visible.value = false;
+};
+
+/**********vue_event_handler**********/
+onMounted( ()=>{
+
+
+
+});
+
+
+
+/**********vue_export_def**********/
+const openDialog = ()=>{
+//visible.value = true;
+}
+
+defineExpose({openDialog});
+</script>
+ 
+<style scoped>
+.data-category{
+  width: 600px;
+  font-size: 18pt;
+  color:rgb(3, 57, 123);
+  font-weight: 800;
+  justify-content: center;
+  margin-bottom: 10px;
+}
+.data-name{
+margin: 5px;
+display: flex;
+flex-direction: column;
+width: 580px;
+}
+
+.props-content{
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: flex-start;
+  overflow-y: auto;
+}
+
+.checkbox-content{
+  width: 600px;  
+  height: 300px;
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  justify-content: flex-start;
+  overflow-y: auto;
+}
+.data-prop{
+margin: 5px;
+width: 600px;
+display: flex;
+flex-direction: column;
+}
+.prop-name{
+background-color: #efefef;
+color:rgb(11, 117, 247);
+width:580px;
+}
+.data-text{
+width: 580px;
+background-color: #efefef;
+}
+</style>

+ 99 - 0
src/components/Pagination.vue

@@ -0,0 +1,99 @@
+<template>
+  <div class="pagination">
+    <el-pagination
+        @size-change="sizeChange"
+        @current-change="pageNumChange"
+        :current-page="data.pageParams.pageNum"
+        :page-sizes="pageSizesArr"
+        :page-size="data.pageParams.pageSize"
+        layout="total, sizes, prev, pager, next, jumper"
+        :total="data.total"
+    >
+    </el-pagination>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {pageSizesArr} from "@/config/app";
+import {reactive, withDefaults, onMounted} from "vue";
+
+
+export interface Props {
+  reqFunc: (params: any) => Promise<any>,
+  filterParamsFunc?: (params: object) => object,
+  params?: object,
+  pageField?: string,
+  pageSizesArr?: number[],
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  filterParamsFunc: (params: object) => params,
+  params: () => {
+    return {}
+  },
+  pageSizesArr: () => (pageSizesArr),
+})
+
+
+const emit = defineEmits<{
+  (e: 'pageData', pageData: object): void,
+}>()
+
+const data = reactive({
+  total: 0,
+  pageParams: {
+    pageSize: props.pageSizesArr[0],
+    pageNum: 1
+  }
+})
+
+const sizeChange = (pageSize: number): void => {
+  data.pageParams.pageSize = pageSize
+  getPageData();
+}
+const pageNumChange = (pageNum: number): void => {
+  data.pageParams.pageNum = pageNum
+  getPageData();
+}
+
+const getPageData = (): void => {
+  if (!props.reqFunc) {
+    emit("pageData", {})
+    return;
+  }
+  let params = props.filterParamsFunc({...data.pageParams, ...props.params});
+
+  props.reqFunc(params).then((result: any) => {
+    let r = props.pageField ? result[props.pageField] : result
+    data.pageParams.pageNum = r.pages;
+    data.total = parseInt(r.total) || 0;
+    emit("pageData", result);
+  }).catch((e) => {
+    console.log(e)
+  });
+
+}
+
+onMounted(() => {
+  getPageData()
+})
+
+const Refresh = (pageNum = 1): void => {
+  data.pageParams.pageNum = pageNum
+  getPageData()
+}
+
+const QueryParams = (): object => {
+  return {...props.params, ...data.pageParams}
+}
+
+defineExpose({Refresh, QueryParams})
+
+</script>
+
+<style scoped>
+.pagination {
+  padding: 10px;
+  display: flex;
+}
+</style>

+ 90 - 0
src/components/TheWelcome.vue

@@ -0,0 +1,90 @@
+<script setup lang="ts">
+import WelcomeItem from './WelcomeItem.vue'
+import DocumentationIcon from './icons/IconDocumentation.vue'
+import ToolingIcon from './icons/IconTooling.vue'
+import EcosystemIcon from './icons/IconEcosystem.vue'
+import CommunityIcon from './icons/IconCommunity.vue'
+import SupportIcon from './icons/IconSupport.vue'
+</script>
+
+<template>
+  <WelcomeItem>
+    <template #icon>
+      <DocumentationIcon />
+    </template>
+    <template #heading>Documentation</template>
+
+    Vue’s
+    <a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
+    provides you with all information you need to get started.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <ToolingIcon />
+    </template>
+    <template #heading>Tooling</template>
+
+    This project is served and bundled with
+    <a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
+    recommended IDE setup is
+    <a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
+    +
+    <a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
+    you need to test your components and web pages, check out
+    <a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
+    and
+    <a href="https://on.cypress.io/component" target="_blank" rel="noopener"
+      >Cypress Component Testing</a
+    >.
+
+    <br />
+
+    More instructions are available in <code>README.md</code>.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <EcosystemIcon />
+    </template>
+    <template #heading>Ecosystem</template>
+
+    Get official tools and libraries for your project:
+    <a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
+    <a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
+    <a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
+    <a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
+    you need more resources, we suggest paying
+    <a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
+    a visit.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <CommunityIcon />
+    </template>
+    <template #heading>Community</template>
+
+    Got stuck? Ask your question on
+    <a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
+    Discord server, or
+    <a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
+      >StackOverflow</a
+    >. You should also subscribe to
+    <a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a>
+    and follow the official
+    <a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
+    twitter account for latest news in the Vue world.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <SupportIcon />
+    </template>
+    <template #heading>Support Vue</template>
+
+    As an independent project, Vue relies on community backing for its sustainability. You can help
+    us by
+    <a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
+  </WelcomeItem>
+</template>

+ 87 - 0
src/components/WelcomeItem.vue

@@ -0,0 +1,87 @@
+<template>
+  <div class="item">
+    <i>
+      <slot name="icon"></slot>
+    </i>
+    <div class="details">
+      <h3>
+        <slot name="heading"></slot>
+      </h3>
+      <slot></slot>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.item {
+  margin-top: 2rem;
+  display: flex;
+  position: relative;
+}
+
+.details {
+  flex: 1;
+  margin-left: 1rem;
+}
+
+i {
+  display: flex;
+  place-items: center;
+  place-content: center;
+  width: 32px;
+  height: 32px;
+
+  color: var(--color-text);
+}
+
+h3 {
+  font-size: 1.2rem;
+  font-weight: 500;
+  margin-bottom: 0.4rem;
+  color: var(--color-heading);
+}
+
+@media (min-width: 1024px) {
+  .item {
+    margin-top: 0;
+    padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
+  }
+
+  i {
+    top: calc(50% - 25px);
+    left: -26px;
+    position: absolute;
+    border: 1px solid var(--color-border);
+    background: var(--color-background);
+    border-radius: 8px;
+    width: 50px;
+    height: 50px;
+  }
+
+  .item:before {
+    content: ' ';
+    border-left: 1px solid var(--color-border);
+    position: absolute;
+    left: 0;
+    bottom: calc(50% + 25px);
+    height: calc(50% - 25px);
+  }
+
+  .item:after {
+    content: ' ';
+    border-left: 1px solid var(--color-border);
+    position: absolute;
+    left: 0;
+    top: calc(50% + 25px);
+    height: calc(50% - 25px);
+  }
+
+  .item:first-of-type:before {
+    display: none;
+  }
+
+  .item:last-of-type:after {
+    display: none;
+  }
+}
+</style>

+ 11 - 0
src/components/__tests__/HelloWorld.spec.ts

@@ -0,0 +1,11 @@
+import { describe, it, expect } from 'vitest'
+
+import { mount } from '@vue/test-utils'
+import HelloWorld from '../HelloWorld.vue'
+
+describe('HelloWorld', () => {
+  it('renders properly', () => {
+    const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
+    expect(wrapper.text()).toContain('Hello Vitest')
+  })
+})

+ 11 - 0
src/components/data1.json

@@ -0,0 +1,11 @@
+{
+    "nodes": [
+      "beijing",
+      "shanghai",
+      "guangzhou",
+      "chongqing",
+      "wuhan"
+    ],
+    "edges": [0,1,1,2,1,3,3,4],
+    "dependentsCount": [10000,10000,10000,10000, 10000]
+  }

Plik diff jest za duży
+ 7 - 0
src/components/icons/IconCommunity.vue


Plik diff jest za duży
+ 7 - 0
src/components/icons/IconDocumentation.vue


Plik diff jest za duży
+ 7 - 0
src/components/icons/IconEcosystem.vue


+ 7 - 0
src/components/icons/IconSupport.vue

@@ -0,0 +1,7 @@
+<template>
+  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
+    <path
+      d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
+    />
+  </svg>
+</template>

+ 19 - 0
src/components/icons/IconTooling.vue

@@ -0,0 +1,19 @@
+<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
+<template>
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    xmlns:xlink="http://www.w3.org/1999/xlink"
+    aria-hidden="true"
+    role="img"
+    class="iconify iconify--mdi"
+    width="24"
+    height="24"
+    preserveAspectRatio="xMidYMid meet"
+    viewBox="0 0 24 24"
+  >
+    <path
+      d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
+      fill="currentColor"
+    ></path>
+  </svg>
+</template>

+ 33 - 0
src/compositionApi/pagination.ts

@@ -0,0 +1,33 @@
+import { ref,nextTick} from 'vue'
+import { resetArgs } from "@/utils/app"
+
+
+export default function () {
+    const tableData = ref([])
+    const paginationRef = ref<{ Refresh:()=>void, QueryParams:()=>void } | null>(null);
+    const searchParams = ref<any>({"id":"","name":"","tags":[]})
+
+    const setTableData = (r:any) :void => {        
+        if(Array.isArray(r.records) && r.records.length!==0){
+            tableData.value = r.records||[]
+        }
+    }
+
+    const refreshTable = ():void => {
+        paginationRef.value?.Refresh()
+    }
+    const resetParams = async () => {
+        searchParams.value = resetArgs(searchParams.value)
+        await nextTick()
+        refreshTable()
+    }
+
+    return {
+        searchParams,
+        tableData,
+        paginationRef,
+        setTableData,
+        refreshTable,
+        resetParams
+    }
+}

+ 29 - 0
src/config/app.ts

@@ -0,0 +1,29 @@
+export enum EnvModeEnum {
+  Dev = "development",//开发环境
+  Test = "test",
+  Pre = "pre",
+  Pro = "production",
+}
+
+export const version = "0.01"
+
+export const storagePrefixKey = "V-E-U-A:"
+
+
+export const tokenKey = storagePrefixKey+"token"
+
+export const userInfoKey = storagePrefixKey+"userInfo"
+
+
+export const appName = "G+"
+
+export const pageSizesArr = [30, 60, 100]
+
+export const envMode: EnvModeEnum = import.meta.env.MODE as EnvModeEnum
+
+export const isProduction = envMode == EnvModeEnum.Pro
+
+export const minScreenMaxWidth = 1024 //设置小屏幕最大临界值,如果是小屏幕菜单的布局会改变
+
+
+

+ 19 - 0
src/config/request.ts

@@ -0,0 +1,19 @@
+export const  baseURL:string = import.meta.env.VITE_BASE_URL
+
+export const  timeout = 80000
+
+export const statusDesc:{[value: number]:string} = {
+  0: "SUCCESS",
+  400: "请求错误(400)",
+  401: "未授权,请重新登录(401)",
+  403: "拒绝访问(403)",
+  404: "请求出错(404)",
+  408: "请求超时(408)",
+  500: "服务器错误(500)",
+  501: "服务未实现(501)",
+  502: "网络错误(502)",
+  503: "服务不可用(503)",
+  504: "网络超时(504)",
+  505: "HTTP版本不受支持(505)"
+}
+

Plik diff jest za duży
+ 1581 - 0
src/js/svg-pan-zoom.js


+ 18 - 0
src/main.ts

@@ -0,0 +1,18 @@
+import './assets/main.css'
+
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import ElementPlus from 'element-plus'
+// import 'element-plus/dist/index.css'
+import zhCn from 'element-plus/es/locale/lang/zh-cn'
+import App from './App.vue'
+import router from './router'
+
+
+
+
+const app = createApp(App)
+// app.use(ElementPlus, { locale: zhCn, })
+app.use(createPinia())
+app.use(router)
+app.mount('#app')

+ 127 - 0
src/router/index.ts

@@ -0,0 +1,127 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import HomeView from '@/views/Main/HomeView.vue'
+import { getSessionVar } from '@/utils/sessioin'
+//import Annotation from '@/views/Annotation'
+//import MindMap from '@/views/MindMap'
+const router = createRouter({
+
+
+  history: createWebHistory(import.meta.env.BASE_URL),
+  routes: [
+    {
+      path: "/",
+      redirect: "/login", //重定位到登录页面
+      meta: { requiresAuth: false },
+      children: []
+    },
+    {
+      path: '/main',
+      name: 'main',
+      redirect: "/main/home",
+      component: () => import('../views/Main/index.vue'),
+      meta: { requiresAuth: false, keepAlive: true },
+      children: [
+        {
+          path: 'home',
+          name: 'home',
+          component: HomeView,
+          meta: { requiresAuth: true, keepAlive: true },
+        },
+        {
+          path: "graphmap",
+          name: "graphmap",
+          component: () => import("../views/Main/GraphMap.vue"),
+          meta: { requiresAuth: true, keepAlive: true },
+
+        },
+        {
+          path: "graphupdate",
+          name: "graphupdate",
+          component: () => import("../views/Main/GraphUpdate.vue"),
+          meta: { requiresAuth: true, keepAlive: true },
+
+        },
+        {
+          path: 'about',
+          name: 'about',
+          // route level code-splitting
+          // this generates a separate chunk (About.[hash].js) for this route
+          // which is lazy-loaded when the route is visited.
+          component: () => import('../views/Main/AboutView.vue'),
+        },
+        {
+          path: 'graph',
+          name: 'graph',
+          // route level code-splitting
+          // this generates a separate chunk (About.[hash].js) for this route
+          // which is lazy-loaded when the route is visited.
+          component: () => import('../views/Main/GraphView.vue'),
+          meta: { requiresAuth: true, keepAlive: false },
+        },
+        {
+          path: 'file',
+          name: 'file',
+          component: () => import('../views/Main/FileBrowse.vue'),
+          meta: { requiresAuth: true, keepAlive: false },
+        },
+        {
+          path: 'g',
+          name: 'MindMap',
+          component: () => import('../views/Main/MindMap.vue'),
+          meta: { requiresAuth: true, keepAlive: false },
+        },
+        {
+          path: 'Annotation',
+          name: 'Annotation',
+          component: () => import('../views/Main/Annotation.vue'),
+          meta: { requiresAuth: true, keepAlive: false },
+        },
+        {
+          path: 'schema',
+          name: 'Schema',
+          component: () => import('../views/dict/KgSchemas.vue'),
+          meta: { requiresAuth: true, keepAlive: false },
+        },
+        {
+          path: 'icd',
+          name: 'ICD',
+          component: () => import('../views/dict/ICD.vue'),
+          meta: { requiresAuth: true, keepAlive: false },
+        },
+        {
+          path: 'drg',
+          name: 'DRG',
+          component: () => import('../views/dict/Drg.vue'),
+          meta: { requiresAuth: true, keepAlive: false },
+        },
+        {
+          path: 'drug',
+          name: 'Drug',
+          component: () => import('../views/dict/Drugs.vue'),
+          meta: { requiresAuth: true, keepAlive: false },
+        },
+      ]
+    },
+    {
+      path: '/login',
+      name: 'login',
+      component: () => import('../views/LoginView.vue'),
+      meta: { requiresAuth: false }
+    },
+
+
+  ],
+})
+
+router.beforeEach((to, from, next) => {
+  let isAuthenticated = false;
+  if (getSessionVar("access_token") != null) {
+    isAuthenticated = true;
+  }
+  if (to.matched.some(record => record.meta.requiresAuth) && !isAuthenticated) {
+    next('/login');
+  } else {
+    next();
+  }
+})
+export default router

+ 12 - 0
src/stores/counter.ts

@@ -0,0 +1,12 @@
+import { ref, computed } from 'vue'
+import { defineStore } from 'pinia'
+
+export const useCounterStore = defineStore('counter', () => {
+  const count = ref(0)
+  const doubleCount = computed(() => count.value * 2)
+  function increment() {
+    count.value++
+  }
+
+  return { count, doubleCount, increment }
+})

+ 65 - 0
src/utils/app.ts

@@ -0,0 +1,65 @@
+
+/**
+ * 重置一个参数对象
+ * @param args
+ * @param def
+ */
+export function resetArgs<T>(args:T, def:Partial<T> = {}):T {
+
+    let val: { [k:string]:any }  = {}
+  
+    for (let key in args) {
+      if (def.hasOwnProperty(key)) {
+        val[key]  = def[key]
+      } else {
+        if (Array.isArray(args[key])) val[key] = [];
+        if ('string' == typeof args[key]) val[key] = '';
+        if ('number' == typeof args[key]) val[key] = null;
+        if ('boolean' == typeof args[key]) val[key] = false;
+      }
+    }
+    return val as T
+  }
+  
+  /**
+   * 下载或者保存一个Blob
+   * @param blob
+   * @param fileName
+   * @param isOpen
+   * 接口返回数据流时,如果是pdf可以设置isOpen直接新窗口打开
+   * export function exportReport(params: { fileCode:string }) {
+   *   return request({
+   *     responseType:"blob",
+   *     closeResponseInterceptors:true,
+   *     url: '/customer-service/open/api/report/getReport',
+   *     method: 'get',
+   *     params
+   *   })
+   * }
+   */
+  export function saveBlob(blob:Blob,fileName:string,isOpen = false):void{
+  
+    let url = window.URL.createObjectURL(blob);
+    if(isOpen){
+      window.open(url)
+    }else {
+      let a = document.createElement("a");
+      document.body.appendChild(a);
+      a.setAttribute("display","none")
+      a.href = url;
+      a.download = fileName;
+      a.click();
+      a.remove();
+      window.URL.revokeObjectURL(url);
+    }
+  }
+  
+  
+  export const toFormData = (data:any) =>{
+    const formData = new FormData()
+    for (const key in data) {
+      formData.append(key, data[key])
+    }
+    return formData
+  }
+  

+ 96 - 0
src/utils/request.ts

@@ -0,0 +1,96 @@
+import axios from "axios";
+import { toFormData } from "./app"
+import { baseURL,timeout,statusDesc } from "../config/request"
+import type { AxiosInstance, AxiosRequestConfig, AxiosResponse,InternalAxiosRequestConfig } from "axios";
+import { getSessionVar } from "./sessioin";
+import { useRouter } from 'vue-router'
+export type ResponseResult<T> = {
+  code: number;
+  msg: string;
+  data: T;
+};
+
+declare module 'axios' {
+  export interface AxiosRequestConfig{
+    closeLoading?:boolean,//默认所有请求Loading,可关闭
+    token?:string,//默认获取本地token,可针对某个请求写死或置空
+    isFormRequest?:boolean,//将请求自动转换为表单请求
+    closeInstance?:boolean
+  }
+}
+
+export type RequestConfig = Omit<AxiosRequestConfig, 'closeInstance' | 'transformRequest'>
+
+export class Request {
+  instance: AxiosInstance;
+  constructor(config: AxiosRequestConfig) {
+    this.instance = axios.create(config);
+    this.instance.interceptors.request.use(
+        (config:InternalAxiosRequestConfig) => {          
+          if ("token" in config){
+            config.headers.Authorization = config.token
+          }else {
+            config.headers.Authorization = 'Bearer ' + getSessionVar('access_token');
+            //config.headers.Authorization = getToken()
+          }
+          if (config.isFormRequest){config.transformRequest = toFormData}
+          if(!config.closeLoading){
+            //Loading
+          }
+          return config;
+        },
+        (err: any) => {
+          // 请求错误,这里可以用全局提示框进行提示
+          return Promise.reject(err);
+        }
+    );
+    this.instance.interceptors.response.use(
+        (res: AxiosResponse) => {
+          // 直接返回res,当然你也可以只返回res.data
+          // 系统如果有自定义code也可以在这里处理
+          return res.data.data;
+        },
+        (err: any) => {
+          
+          let message = "";
+          if (statusDesc[err.response.status]){
+            message = statusDesc[err.response.status]
+          }else {
+            message = `连接出错(${err.response.status})!`
+          }
+          // 这里错误消息可以使用全局弹框展示出来
+          // 比如element plus 可以使用 ElMessage
+          // ElMessage({
+          //   showClose: true,
+          //   message: `${message},请检查网络或联系管理员!`,
+          //   type: "error",
+          // });
+          // 这里是AxiosError类型,所以一般我们只reject我们需要的响应即可
+          if (err.response.status == 401){
+            //const router = useRouter();
+           
+            //router.replace({path:"/login"});
+            document.location = "/login";
+            return [];
+          }
+          console.log(message)
+          return Promise.reject(err.response);
+        }
+    );
+  }
+
+  //未拦截请求,响应原封不动返回
+
+  unhandledRequest<T>(config: RequestConfig): Promise<AxiosResponse<ResponseResult<T>>> {
+    return this.instance.request({...config,closeInstance:true});
+  }
+  //做了拦截处理,自动报错,只返回关心的数据
+  request<T>(config: RequestConfig): Promise<T> {
+    return this.instance.request(config);
+  }
+}
+
+export  default new Request({baseURL,timeout})
+
+
+

+ 13 - 0
src/utils/sessioin.ts

@@ -0,0 +1,13 @@
+export function saveSessionVar(name: string, value:string) {
+
+    sessionStorage.setItem(name, value);
+}
+
+export function getSessionVar(name: string){
+    return sessionStorage.getItem(name);
+}
+
+
+export function deleteSessionVar(name: string){
+    return sessionStorage.removeItem(name);
+}

+ 315 - 0
src/views/LoginView.vue

@@ -0,0 +1,315 @@
+<template>
+  <!-- <main>
+    <el-form :model="formData">
+      <el-form-item label="用户名">
+        <el-input v-model="formData.username"></el-input>
+      </el-form-item>
+      <el-form-item label="密码">
+        <el-input type="password" v-model="formData.password"></el-input>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="onSubmit">提交</el-button>
+      </el-form-item>
+    </el-form>
+  </main> -->
+  <div class="app">
+    <div class="content">
+      <div class="left"></div>
+      <div class="right">
+        <div class="login-form">
+          <div class="login-form-title">欢迎登录知识图谱平台</div>
+          <el-form
+            :model="formData"
+            status-icon
+            :rules="rules"
+            ref="ruleFormRef"
+            label-width="0px"
+            class="demo-ruleForm"
+          >
+            <el-form-item label="" prop="username">
+              <el-input
+                type="username"
+                v-model.trim="formData.username"
+                autocomplete="off"
+                placeholder="请输入账号"
+              >
+                <template #prefix>
+                  <i class="icon-username"></i>
+                </template>
+              </el-input>
+            </el-form-item>
+            <el-form-item label="" l prop="password">
+              <el-input
+                type="password"
+                v-model.trim="formData.password"
+                autocomplete="off"
+                placeholder="请输入密码"
+              >
+                <template #prefix><i class="icon-password"></i></template>
+              </el-input>
+            </el-form-item>
+            <el-form-item label="" prop="captcha" class="captcha">
+              <el-input
+                class="input-captcha"
+                v-model.trim="formData.captcha"
+                placeholder="请输入验证码"
+              ></el-input>
+              <img src="@/assets/images/组11拷贝2x.png" alt="验证码" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" @click="onSubmit" class="submit"
+                ><span>登录</span></el-button
+              >
+              <div class="other clearfix">
+                <span class="forgot-password">忘记密码?</span>
+                <span class="sign-up">立即注册</span>
+                <span class="no-account">还没账号?</span>
+              </div>
+            </el-form-item>
+          </el-form>
+        </div>
+      </div>
+    </div>
+
+    <div class="copyright">
+      Copyright © 2019-2024 浙大启真未来城市科技(杭州)有限公司
+    </div>
+  </div>
+</template>
+
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, provide } from "vue";
+import r from "@/utils/request.ts";
+import { useRouter } from "vue-router";
+import { saveSessionVar } from "@/utils/sessioin";
+const formData = ref({
+  grant_type: "password",
+  username: "",
+  password: "",
+  captcha: "",
+});
+const ruleFormRef = ref();
+const rules = {
+  username: [{ required: true, message: "账号是必填", trigger: "blur" }],
+  password: [{ required: true, message: "密码是必填", trigger: "blur" }],
+};
+
+const router = useRouter();
+function onSubmit() {
+  ruleFormRef.value.validate((valid: any) => {
+    if (valid) {
+      if (formData.value.captcha.toLowerCase() === "xvxr") {
+        const response = r.request<string[]>({
+          url: "/api/token",
+          method: "post",
+          headers: { "Content-Type": "application/x-www-form-urlencoded" },
+          data:
+            "username=" +
+            formData.value.username +
+            "&password=" +
+            formData.value.password +
+            "&grant_type=password",
+        });
+        response.then((result) => {
+          // console.log("result", result);
+          saveSessionVar("access_token", result["access_token"]);
+          saveSessionVar("token_type", "bearer");
+          saveSessionVar("username", formData.value.username);
+          provide("username", formData.value.username);
+          router.push({ name: "home" });
+        });
+      } else {
+        ElMessage({
+          showClose: true,
+          message: "验证码错误",
+          type: "error",
+        });
+      }
+    }
+  });
+}
+</script>
+  
+<style lang="less" scoped>
+main {
+  display: flex;
+  flex-wrap: wrap;
+  flex-direction: row;
+  padding: 50px;
+  justify-content: start;
+  align-content: start;
+  height: 100%;
+  background-color: white;
+}
+.label-item {
+  display: flex;
+  flex-direction: column;
+  flex-wrap: wrap;
+  border-radius: 10pt;
+  border: 1px solid lightblue;
+  justify-content: center;
+  margin: 15px;
+  height: fit-content;
+  width: 150px;
+  align-items: center;
+}
+.label-count {
+  width: 100%;
+  font-size: 16pt;
+  font-weight: bold;
+  align-content: center;
+  text-align: center;
+}
+.label-category {
+  width: 100%;
+  background-color: rgb(152, 175, 175);
+  border-radius: 10pt 10pt 0 0;
+  border: 1px solid lightblue;
+  font-size: 18pt;
+  font-weight: bold;
+  text-align: center;
+}
+
+.app {
+  // z-index: 100;
+  position: fixed;
+  top: 0;
+  left: 0;
+  background-image: url("@/assets/images/图层50@2x.png");
+  width: 100%;
+  height: 100%;
+  background-repeat: no-repeat;
+  background-size: cover;
+
+  .content {
+    display: flex;
+    flex-wrap: nowrap;
+    width: 100%;
+    height: 100%;
+
+    .left {
+      width: 50%;
+    }
+
+    .right {
+      position: relative;
+      width: 50%;
+
+      .login-form {
+        position: absolute;
+        // right: 201px;
+        left: 50%;
+        top: 50%;
+        transform: translate(-50%, -50%);
+        width: 400px;
+        background-color: rgb(238, 246, 255);
+        padding: 50px;
+        box-sizing: border-box;
+        border-radius: 20px;
+
+        .login-form-title {
+          font-size: 30px;
+          font-weight: 700;
+          text-align: center;
+          margin-bottom: 20px;
+        }
+
+        .demo-ruleForm {
+          .captcha {
+            .el-form-item__content {
+              display: flex;
+              flex-wrap: nowrap;
+              align-items: center;
+              position: relative;
+
+              img {
+                height: 40px;
+                position: absolute;
+                right: 0;
+              }
+
+              .input-captcha {
+                width: 150px;
+              }
+            }
+          }
+        }
+
+        .submit {
+          width: 100%;
+
+          span {
+            font-size: 25px;
+            font-weight: Regular;
+          }
+        }
+
+        .other {
+          font-size: 16px;
+          font-weight: Regular;
+
+          .forgot-password {
+            cursor: pointer;
+          }
+
+          .no-account {
+            // margin-left: auto
+            cursor: pointer;
+            float: right;
+            color: #409ef1;
+          }
+
+          .sign-up {
+            // margin-left: auto
+            cursor: pointer;
+            float: right;
+          }
+        }
+      }
+    }
+  }
+
+  .copyright {
+    color: #409ef1;
+    font-size: 22px;
+    font-weight: Regular;
+
+    position: absolute;
+    left: 50%;
+    bottom: 0px;
+    transform: translate(-50%, -33px);
+  }
+}
+
+.icon {
+  height: 16px;
+  width: 16px;
+  // line-height: 40px;
+  display: inline-block;
+  background-size: cover;
+  vertical-align: middle;
+}
+
+.icon-username {
+  .icon();
+  background-image: url("@/assets/images/账号2x.png");
+}
+
+.icon-password {
+  .icon();
+  background-image: url("@/assets/images/密码@2x.png");
+}
+
+.el-input__inner {
+  // padding-left: 60px;
+  background-color: rgb(243, 244, 249);
+  // margin: 0px 50px;
+}
+
+.el-input__prefix {
+  padding: 0px 5px;
+  text-align: center;
+  vertical-align: middle;
+}
+</style>

+ 15 - 0
src/views/Main/AboutView.vue

@@ -0,0 +1,15 @@
+<template>
+  <div class="about">
+    <h1>This is an about page</h1>
+  </div>
+</template>
+
+<style>
+@media (min-width: 1024px) {
+  .about {
+    min-height: calc(100vh -120px);
+    display: flex;
+    align-items: center;
+  }
+}
+</style>

Plik diff jest za duży
+ 230 - 0
src/views/Main/Annotation.vue


+ 201 - 0
src/views/Main/FileBrowse.vue

@@ -0,0 +1,201 @@
+<template>
+  <el-row class="grid-content">
+    <el-col style="text-align: right; padding-right: 50px"
+      ><div><h2>资料库</h2></div></el-col
+    >
+  </el-row>
+  <el-row>
+    <el-col>
+      <ul
+        style="
+          list-style: none;
+          display: flex;
+          flex-direction: row;
+          text-align: left;
+        "
+      >
+        <li style="display: inline-flex">
+          <a href="#" @click="handleFolderClick('')" class="goback"
+            >返回&nbsp;</a
+          >
+        </li>
+        <li style="display: inline-flex" v-for="item in filesPath">
+          {{ item }}/
+        </li>
+      </ul></el-col
+    >
+  </el-row>
+  <el-row class="grid-content">
+    <el-col>
+      <el-table :data="filesAndDirs" style="width: 100%">
+        <el-table-column prop="is_directory" label="" width="25">
+          <template v-slot="scope">
+            <span v-if="scope.row.is_directory" class="folder-icon">📁</span>
+            <span v-else class="file-icon">📄</span></template
+          >
+        </el-table-column>
+        <el-table-column prop="name" label="文件名">
+          <template v-slot="scope">
+            <span v-if="scope.row.is_directory">
+              <a
+                href="#"
+                @click="handleFolderClick(scope.row.name)"
+                class="file-link"
+                >{{ scope.row.name }}</a
+              >
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="size" label="大小" width="80"></el-table-column>
+        <el-table-column prop="created_at" label="创建日期" width="180">
+          <template v-slot="scope">
+            {{ formatDate(scope.row.created_at) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="modified_at" label="修改日期" width="180">
+          <template v-slot="scope">
+            {{ formatDate(scope.row.modified_at) }}
+          </template>
+        </el-table-column>
+      </el-table>
+      <!-- <thead>
+              <tr>
+                  <td></td>
+                  <td width="350">文件名</td>
+                  <td>大小</td>
+                  <td>创建日期</td>
+                  <td>修改日期</td>
+              </tr>
+          </thead>
+          <tbody>
+            <tr v-for="item in filesAndDirs" :key="item.id">
+          <td width="50"><span v-if="item.is_directory" class="folder-icon">📁</span>
+              <span v-else class="file-icon">📄</span></td>
+          <td></td>
+                      <span v-if="item.is_directory"><a>{{item.name }}</a> </span>
+            <span v-else>{{ item.name }}</span>
+          <td width="80">{{ formatSize(item.size) }}</td>
+          <td width="180">{{ formatDate(item.created_at) }}</td>
+          <td width="180">{{ formatDate(item.modified_at) }}</td>
+          </tr>
+          </tbody> -->
+    </el-col>
+  </el-row>
+</template>
+  
+  <script setup lang="ts">
+import { ref, onMounted, reactive, handleError } from "vue";
+import r from "@/utils/request";
+import { format } from "date-fns";
+const filesAndDirs = ref([]);
+const filesPath = ref([]);
+
+const handleFolderClick = (name: string) => {
+  if (name == "") {
+    filesPath.value.pop();
+  }
+  let path = "";
+  filesPath.value.forEach((e) => {
+    path = path + e + "|";
+  });
+  path = path + name;
+  console.log(path);
+  const response = r.request<string[]>({
+    url: "/api/browse/" + path,
+    method: "get",
+  });
+  response.then((result) => {
+    if (name != "") {
+      filesPath.value.push(name);
+    }
+    filesAndDirs.value = result;
+  });
+};
+const fetchFilesAndDirs = () => {
+  const response = r.request<string[]>({
+    url: "/api/browse/",
+    method: "get",
+  });
+  response.then((result) => {
+    console.log(result);
+    filesAndDirs.value = result;
+  });
+};
+// 格式化文件大小
+const formatSize = (size) => {
+  if (size === 0) return "0 Bytes";
+  const k = 1024;
+  const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
+  const i = Math.floor(Math.log(size) / Math.log(k));
+  return parseFloat((size / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
+};
+
+const formatDate = (dateString) => {
+  return format(new Date(dateString), "yyyy-MM-dd HH:mm:ss"); // 可以根据需要调整格式字符串
+};
+// 格式化日期
+// const formatDate = (dateString) => {
+//   const options = { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' };
+//   return new Date(dateString).toLocaleDateString(undefined, options);
+// };
+
+onMounted(() => {
+  fetchFilesAndDirs();
+});
+</script>
+  
+  <style scoped>
+.file-link {
+  text-decoration: none;
+
+  color: rgb(0, 51, 255);
+}
+.el-main {
+  height: 100%;
+}
+.grid-content {
+  min-height: 36px;
+  max-height: 45px;
+}
+.file-browser {
+  font-family: Arial, sans-serif;
+  background-color: white;
+  padding-left: 30px;
+  overflow-y: scroll;
+}
+
+.file-list {
+  list-style-type: none;
+  padding: 0;
+}
+
+.file-list li {
+  margin-bottom: 10px;
+  display: flex;
+  align-items: center;
+}
+
+.file-list .folder-icon,
+.file-list .file-icon {
+  margin-right: 10px;
+}
+
+.file-details {
+  margin-left: 20px;
+}
+
+.is-directory {
+  color: #007bff;
+}
+
+.is-directory:hover {
+  cursor: pointer;
+  text-decoration: underline;
+}
+.goback {
+  color: white;
+  font-size: 20px;
+  font-weight: bold;
+  text-decoration: none;
+}
+</style>

+ 19 - 0
src/views/Main/GraphMap.vue

@@ -0,0 +1,19 @@
+<template>
+  <iframe
+    src="http://172.16.8.57:8080/knowledgeGraph.html"
+    frameborder="0"
+    id="iframe"
+  ></iframe>
+</template>
+
+<script setup>
+</script>
+
+<style lang="less" scoped>
+#iframe {
+  width: 100%;
+  height: 100%;
+  background: white;
+  overflow: hidden;
+}
+</style>

+ 19 - 0
src/views/Main/GraphUpdate.vue

@@ -0,0 +1,19 @@
+<template>
+  <iframe
+    src="http://172.16.8.57:8080/homeMini.html"
+    frameborder="0"
+    id="iframe"
+  ></iframe>
+</template>
+
+<script setup>
+</script>
+
+<style lang="less" scoped>
+#iframe {
+  width: 100%;
+  height: 100%;
+  background: white;
+  overflow: hidden;
+}
+</style>

+ 326 - 0
src/views/Main/GraphView.vue

@@ -0,0 +1,326 @@
+
+<template>
+  <main>
+    <BasicDialog :isOpen="data.linkDialogOpen" @close="modalIsOpen = false">
+      <!-- 自定义标题和内容 -->
+      <template v-slot:header>自定义标题</template>
+      <template v-slot>自定义内容</template>
+    </BasicDialog>
+    <div>
+      <div class="graph-toolbar">
+        &nbsp;<input size="20" v-model="action.search" class="search-input" />
+        <input
+          type="button"
+          value="Search"
+          @click="handleSearch"
+          class="search-btn"
+        />
+      </div>
+    </div>
+    <div class="graph-col">
+      <div class="graph">
+        <Graph
+          ref="g"
+          :nodes="nodes"
+          :edges="edges"
+          @clickNode="handleNodeClick"
+          @dbClickNode="handleNodeDbClick"
+          @clickEdge="handleEdgeClick"
+        ></Graph>
+      </div>
+      <div class="props">
+        <div style="font-weight: bold; font-size: 16pt">Properties</div>
+        <div class="props-table">
+          <table
+            style="background-color: white; border: 1px solid #000; width: 90%"
+          >
+            <tbody>
+              <tr>
+                <td class="prop-section" colspan="2">Basic</td>
+              </tr>
+              <tr>
+                <td class="prop-attr">ID</td>
+                <td>{{ data.id }}</td>
+              </tr>
+              <tr>
+                <td>Type</td>
+                <td>{{ data.type }}</td>
+              </tr>
+              <tr>
+                <td>Name</td>
+                <td>{{ data.name }}</td>
+              </tr>
+              <tr>
+                <td class="prop-section" colspan="2">Properties</td>
+              </tr>
+              <tr v-for="item in data.props" :key="item.id">
+                <td class="prop-attr">{{ item.name }}</td>
+                <td>{{ item.value }}</td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+        <div style="font-weight: bold; font-size: 16pt">Objects</div>
+        <div class="objs-table">
+          <div>
+            &nbsp;
+            <input
+              type="button"
+              class="action-btn"
+              value="Link"
+              @click="handleLinkButton"
+            />&nbsp;
+            <input type="button" class="action-btn" value="Merge" />&nbsp;
+            <input type="button" class="action-btn" value="Delete" />&nbsp;
+          </div>
+          <table
+            style="background-color: white; border: 1px solid #000; width: 100%"
+          >
+            <tbody>
+              <tr v-for="item in data.objs" :key="item.id">
+                <td width="20">
+                  <input type="checkbox" v-model="item.checked" />
+                </td>
+
+                <td width="50">{{ item.type }}</td>
+                <td>
+                  <a
+                    href="#"
+                    @click="
+                      handleNodeDbClick({
+                        id: item.id,
+                        type: item.type,
+                        name: item.name,
+                      });
+                      handleNodeClick({
+                        id: item.id,
+                        type: item.type,
+                        name: item.name,
+                      });
+                    "
+                    >{{ item.name }}</a
+                  >
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+    </div>
+  </main>
+</template>
+
+
+<script setup lang="ts">
+import { ref, onMounted, reactive } from "vue";
+import Graph from "@/components/Graph.vue";
+import BasicDialog from "@/components/BasicDialog.vue";
+import r from "@/utils/request";
+
+const g: Graph = ref();
+const nodes = reactive([]);
+const edges = reactive([]);
+const action = reactive({
+  search: "",
+});
+const data = reactive({
+  type: "Sample",
+  name: "N/A",
+  id: "0",
+  props: [
+    {
+      id: 0,
+      name: "",
+      value: "N/A",
+    },
+  ],
+  objs: [],
+  linkDialogOpen: false,
+});
+let isReady = false;
+
+function handleLinkButton() {
+  data.linkDialogOpen = true;
+}
+
+function handleNodeClick(nodeData: any) {
+  console.log("click on " + nodeData.id);
+  data.id = nodeData.id;
+  data.type = nodeData.type;
+  data.name = nodeData.name;
+  data.props = [];
+  const response = r.request<string[]>({
+    url: "/api/node/" + data.id,
+    method: "get",
+  });
+  response.then((result) => {
+    if (result["nodes"].length > 0) {
+      var prop = result["nodes"][0]["props"];
+      data.type = result["nodes"][0]["type"];
+      for (var i = 0; i < prop.length; i++) {
+        data.props[i] = { name: prop[i].name, value: prop[i].value };
+      }
+    }
+    data.objs.push({
+      id: data.id,
+      name: data.name,
+      type: data.type,
+      checked: false,
+      category: "Node",
+    });
+    if (data.objs.length > 10) {
+      data.objs.shift();
+    }
+  });
+}
+function handleEdgeClick(edgeData: any) {
+  console.log("click on " + edgeData.id);
+  data.id = edgeData.id;
+  data.type = edgeData.type;
+  data.name = edgeData.name;
+  data.props = [];
+  const response = r.request<string[]>({
+    url: "/api/edge/" + data.id,
+    method: "get",
+  });
+  response.then((result) => {
+    if (result["edges"].length > 0) {
+      var prop = result["edges"][0]["props"];
+      data.id = result["edges"][0]["id"];
+      data.type = result["edges"][0]["type"];
+      data.name = result["edges"][0]["name"];
+      for (var i = 0; i < prop.length; i++) {
+        data.props[i] = { name: prop[i].name, value: prop[i].value };
+      }
+      data.objs.push({
+        id: data.id,
+        name: data.name,
+        type: data.type,
+        checked: false,
+        category: "Edge",
+      });
+    }
+  });
+}
+function handleNodeDbClick(nodeData: any) {
+  console.log("click on " + nodeData.id);
+  data.id = nodeData.id;
+  data.type = nodeData.type;
+  data.name = nodeData.name;
+  data.props = [];
+  const response = r.request<string[]>({
+    url: "/api/graph/" + data.id,
+    method: "get",
+  });
+  response.then((result) => {
+    redrawGraph(result);
+  });
+}
+function handleSearch() {
+  const response = r.request<string[]>({
+    url: "/api/graph-search/" + action.search,
+    method: "get",
+  });
+  response.then((result) => {
+    redrawGraph(result);
+  });
+}
+function redrawGraph(result: any) {
+  nodes.length = 0;
+  edges.length = 0;
+  for (var i = 0; i < result["nodes"].length; i++) {
+    var node = {
+      name: result["nodes"][i]["name"],
+      draggable: true,
+      value: result["nodes"][i]["value"],
+      id: result["nodes"][i]["id"],
+    };
+    nodes.push(node);
+  }
+  for (var i = 0; i < result["edges"].length; i++) {
+    var edge = {
+      id: result["edges"][i]["id"],
+      source: result["edges"][i]["start"],
+      target: result["edges"][i]["end"],
+      label: result["edges"][i]["name"],
+    };
+    //var edge = { source: 0, target: 1, label: result["edges"][i]["name"]};
+    edges.push(edge);
+  }
+  if (isReady == false) {
+    g.value.draw();
+    isReady = true;
+  } else {
+    g.value.redraw();
+  }
+}
+function queryGraph(id: number) {
+  const response = r.request<string[]>({
+    url: "/api/graph/" + id,
+    method: "get",
+  });
+  response.then((result) => {
+    redrawGraph(result);
+  });
+}
+onMounted(() => {
+  console.log("GraphView mounted");
+  queryGraph(35152);
+});
+</script>
+<style>
+main {
+  display: grid;
+  grid-template-rows: 30pt calc(100vh - 30pt);
+  background-color: rgb(198, 198, 198);
+}
+.graph {
+  width: calc(100vw - 450pt);
+  height: 100%;
+  background-color: rgb(113, 113, 113);
+  border: 5px solid #c0c0c0;
+}
+.graph-col {
+  display: grid;
+  grid-template-columns: calc(100vw - 450pt) 450pt;
+}
+.graph-toolbar {
+  height: 30px;
+}
+.props {
+  width: 350pt;
+  height: 100%;
+  overflow-y: scroll;
+  background-color: #efefef;
+}
+.props-table {
+  height: 350pt;
+  overflow-y: scroll;
+}
+.objs-table {
+  width: 300pt;
+  height: 250pt;
+  overflow-y: scroll;
+  background-color: #efefef;
+}
+.prop-section {
+  background-color: #cdcdcd;
+}
+.prop-attr {
+  width: 50pt;
+  vertical-align: top;
+}
+.search-input {
+  font-size: 18pt;
+  height: 30pt;
+  width: 150pt;
+}
+.search-btn {
+  vertical-align: top;
+  height: 30pt;
+}
+.action-btn {
+  height: 25pt;
+  font-size: 12pt;
+}
+</style>

+ 157 - 0
src/views/Main/HomeView.vue

@@ -0,0 +1,157 @@
+<template>
+  <div class="main">
+    <div class="top">
+      <div
+        v-for="item in countInfo.infos"
+        class="label-item"
+        v-show="item['name'] !== '实体属性'"
+      >
+        <div class="label-category">
+          {{ item["name"] }}
+        </div>
+        <div class="label-count">{{ item["num"] }}</div>
+      </div>
+    </div>
+    <NewTable
+      ref="newTableRef"
+      :tableData="newTableData.content"
+      :total="newTableData.total"
+      @update="update"
+    ></NewTable>
+  </div>
+</template>
+<script setup lang="ts">
+import { ref, reactive, onMounted, watch } from "vue";
+import r from "@/utils/request.ts";
+import NewTable from "@/components/NewTable.vue"; //新表格
+
+const newTableRef = ref();
+const newTableData = ref({
+  content: [],
+  total: 0,
+});
+
+const data = ref({
+  summary: null,
+});
+const countInfo = ref([]);
+
+const nodeColors = reactive<Object>({
+  Department: { color: "#ff0000", name: "科室" },
+  Food: { color: "#ffff00", name: "食品" },
+  Drug: { color: "rgb(201, 153, 90)", name: "药品" },
+  Disease: { color: "rgb(15, 150, 55)", name: "疾病" },
+  Symptom: { color: "rgb(40, 108, 210)", name: "症状" },
+  Producer: { color: "rgb(40, 108, 210)", name: "厂商" },
+  Check: { color: "rgb(40, 108, 210)", name: "检查" },
+  _default: { color: "#CDCDCD", name: "缺省" },
+});
+/**
+ * 接收组件NewTable的页数和每页的条目数量
+ */
+const update = (currentPage: number, pageSize: number) => {
+  getCountList(currentPage, pageSize);
+  // console.log("update", currentPage, pageSize);
+};
+
+const getCountInfo = async () => {
+  try {
+    countInfo.value = await r.request<any>({
+      url: "/kg/count/getCountInfo",
+      method: "post",
+    });
+    // console.log("countInfo", countInfo);
+    if (countInfo.value) {
+      // console.log("countInfo", countInfo.value);
+    } else {
+      console.log("getCountInfo返回值为空");
+    }
+  } catch (err) {
+    console.log("/kg/count/getCountInfo获取统计信息 接口错误", err);
+  }
+};
+const getCountList = async (pageNo: number, pageSize: number) => {
+  try {
+    let countList = await r.request<any>({
+      url: "/kg/count/getCountList",
+      method: "post",
+      data: {
+        pageNo: pageNo,
+        pageSize: pageSize,
+      },
+    });
+    // console.log("countInfo", countInfo);
+    if (countList) {
+      newTableData.value.total = countList.totalElements;
+      newTableData.value.content = countList.content;
+    } else {
+      console.log("getCountList返回值为空");
+    }
+  } catch (err) {
+    console.log("/kg/count/getCountList获取统计列表 接口错误", err);
+  }
+};
+
+getCountInfo();
+onMounted(() => {
+  const response = r.request<string[]>({
+    url: "/api/nodes/summary",
+    method: "get",
+  });
+  response.then((result) => {
+    data.value.summary = result;
+  });
+  // console.log("newTableRef", newTableRef.value.selectPageSize);
+  update(1, 10);
+});
+</script>
+
+<style scoped>
+.main {
+  /* display: flex; */
+  flex-wrap: wrap;
+  flex-direction: column;
+  padding: 10px 50px;
+  justify-content: start;
+  align-content: start;
+  background-color: white;
+  height: 100%;
+  overflow-y: auto;
+  display: flex;
+  flex-wrap: nowrap;
+}
+.main > .top {
+  display: flex;
+  justify-content: center;
+  margin-bottom: 10px;
+}
+.label-item {
+  display: flex;
+  flex-direction: column;
+  flex-wrap: wrap;
+  border-radius: 10pt;
+  background-color: antiquewhite;
+  border: 1px solid lightblue;
+  justify-content: center;
+  margin: 15px;
+  height: fit-content;
+  width: 150px;
+  align-items: center;
+}
+.label-count {
+  width: 100%;
+  font-size: 16pt;
+  font-weight: bold;
+  align-content: center;
+  text-align: center;
+}
+.label-category {
+  width: 100%;
+  background-color: rgb(152, 175, 175);
+  border-radius: 10pt 10pt 0 0;
+  border: 1px solid lightblue;
+  font-size: 18pt;
+  font-weight: bold;
+  text-align: center;
+}
+</style>

Plik diff jest za duży
+ 1089 - 0
src/views/Main/MindMap.vue


+ 102 - 0
src/views/Main/index.vue

@@ -0,0 +1,102 @@
+<script setup lang="ts">
+import { ref, watch, onMounted, inject, onUnmounted } from "vue";
+import { getSessionVar } from "@/utils/sessioin.ts";
+import { RouterLink, RouterView, useRouter, useRoute } from "vue-router";
+
+let username = ref("");
+const isLogin = ref(true);
+const currentPath = ref("");
+const router = useRouter();
+const route = useRoute();
+onMounted(() => {
+  let token = getSessionVar("access_token");
+  username.value = getSessionVar("username");
+  if (token != null) {
+    isLogin.value = true;
+  }
+});
+onUnmounted(() => {
+  // 清理操作,如果需要的话
+});
+</script>
+
+<template>
+  <el-container>
+    <el-header>
+      <el-row>
+        <el-col :span="12">
+          <span class="nav">
+            <RouterLink to="/main/home" class="nav_item">首页</RouterLink>
+            <RouterLink to="/main/graphmap" class="nav_item">图谱</RouterLink>
+            <RouterLink to="/main/graphupdate" class="nav_item"
+              >图谱管理</RouterLink
+            >
+            <RouterLink to="/main/schema" class="nav_item">数据定义</RouterLink>
+            <RouterLink to="/main/icd" class="nav_item">ICD字典</RouterLink>
+            <RouterLink to="/main/drg" class="nav_item">DRG字典</RouterLink>
+            <RouterLink to="/main/drug" class="nav_item">药品字典</RouterLink>
+            <RouterLink to="/main/file" class="nav_item">文献库</RouterLink>
+            <RouterLink to="/main/annotation" class="nav_item">标注</RouterLink>
+            <RouterLink to="/main/g" class="nav_item">图谱平台</RouterLink>
+            <RouterLink to="/main/about" class="nav_item">关于</RouterLink>
+          </span></el-col
+        >
+        <el-col :span="12"
+          ><span> {{ username ? username : "G+ 知识图谱" }} </span></el-col
+        >
+      </el-row></el-header
+    >
+    <el-container
+      ><hr />
+      <el-container>
+        <el-main>
+          <RouterView></RouterView>
+        </el-main>
+      </el-container>
+    </el-container>
+    <!-- <el-footer>&copy;2024 v1.0</el-footer> -->
+  </el-container>
+</template>
+
+<style scoped>
+.el-header,
+.el-footer {
+  color: #e9e9e9;
+  text-align: right;
+  font-size: 12pt;
+  font-weight: bold;
+  line-height: 60px;
+}
+
+.el-aside {
+  background-color: rgb(79, 79, 79, 0.9);
+  color: #ffffff;
+  text-align: left;
+  line-height: 45px;
+  padding-left: 20px;
+}
+
+.el-main {
+  /* background: #ffffff; */
+  margin: 0;
+  padding: 10px;
+  background-color: rgba(246, 247, 196, 0.15);
+  color: #333;
+  text-align: center;
+  /* height: calc(100vh - 120px); */
+}
+
+.nav {
+  display: flex;
+  align-items: start;
+  flex-direction: row;
+  flex-wrap: nowrap;
+}
+.nav_item {
+  height: 40px;
+  text-decoration: none;
+  color: rgb(255, 255, 255);
+  font-weight: 700;
+  margin-right: 20px;
+}
+</style>

+ 124 - 0
src/views/dict/Drg.vue

@@ -0,0 +1,124 @@
+<template>
+    
+    <ConfirmDialog
+        ref="confirmDialog"
+      v-model="confirmDialogVisible"
+      objId = "0"
+      title="确认删除"
+      message="您确定要删除这条记录吗?"
+      @confirm=""
+      @cancel=""
+    />
+
+    <div class="box">
+      <ActionBar @reset="resetParams" @refresh="refreshTable">
+        <template #left> 
+          <h2>DRG字典</h2>
+        </template>
+        <template #right>
+          <el-input v-model="searchParams.name" placeholder="请输入ID"  clearable />
+        </template>
+      </ActionBar>
+      <el-table :data="tableData" style="width: 100%" max-height="calc(100vh - 270px)">        
+        <el-table-column prop="drg_code" label="代码"/>
+        <el-table-column prop="drg_name" label="目录名称"/>  
+        <el-table-column prop="drg_weight" label="权重"/>        
+<!-- 
+        <el-table-column fixed="right" width="170">
+          <template #header>
+            <div style="display: flex;justify-content: center;align-items: center">
+              <div style="margin-right: 10px">操作</div>
+            </div>
+          </template>
+          <template #default="scope">
+            <el-button type="primary" @click="handleEdit(scope.row)">
+              编辑
+            </el-button>
+            <el-button type="primary" @click="handleDelete(scope.row)">
+              删除
+            </el-button>
+          </template>
+        </el-table-column> -->
+      </el-table>
+      <Pagination ref="paginationRef" :params="searchParams" :reqFunc="paginationList" @pageData="setTableData" />
+    </div>
+
+    
+  </template>
+  
+  <script setup lang="ts">
+  
+  import ActionBar from "@/components/ActionBar.vue";
+  import Pagination from "@/components/Pagination.vue"
+    import { paginationList } from "@/api/drg/index.ts"
+    import usePagination from "@/compositionApi/pagination.ts"
+    import ConfirmDialog from '@/components/ConfirmDialog.vue';
+    import { ElMessage } from 'element-plus';
+    import { ref  } from 'vue'
+
+    const {searchParams, tableData, paginationRef, setTableData, refreshTable,resetParams} =  usePagination()
+    // const {tableFields} = useTableField()
+    // const { openForm,rules,formDesc,submitForm ,editRef} = useFromEdit(refreshTable)
+
+    const selectedData = ref({
+        id: 1,
+        prodName: '',
+        prodContent: '',
+        prodData: '',
+    });
+ 
+    const dataDialog = ref()
+
+    const openEditDialog = () => {
+        
+      selectedData.value.id = 0;
+      selectedData.value.prodName = '' ;
+      selectedData.value.prodContent = '' ;
+      selectedData.value.prodData = '' ;
+      dataDialog.value.openEditDialog();
+        
+        
+    }
+ 
+    function handleEdit(row: any){
+      selectedData.value.id = row.id
+      selectedData.value.prodName = row.prodName
+      selectedData.value.prodContent = row.prodContent
+      selectedData.value.prodData = row.prodData
+      dataDialog.value.openEditDialog()
+        
+        
+    }
+
+    const confirmDialogVisible = ref(false);
+    const confirmDialog = ref();
+    function handleDelete(row: any){
+        confirmDialog.value.objId = row.id
+        showConfirmDialog()
+    }
+
+    const showConfirmDialog = () => {
+        confirmDialogVisible.value = true
+    };
+
+  </script>
+  
+  <style lang="scss" scoped>
+  .box {
+    width: 100%;
+    height: 20pt;
+  }
+  .textbox_lower {
+    width: 100%;
+    height: 50pt;
+    padding: 10px 0;
+    resize: none;
+  }
+  .textbox {
+    width: 100%;
+    height: 100pt;
+    padding: 10px 0;
+    resize: none;
+  }
+  </style>
+  

+ 115 - 0
src/views/dict/Drugs.vue

@@ -0,0 +1,115 @@
+<template>
+    <div class="box">      
+      <BasicDialog :isOpen="data.dataDialogOpen" @close="data.dataDialogOpen = false">
+        <!-- 自定义标题和内容 -->
+        <template v-slot:header>{{data.dialogHeader}}</template>
+        <template v-slot>
+          <table >
+            <tbody>
+              <tr><td class="prop_header">ID</td><td class="prop_value">{{data.dialogContent['id']}}</td></tr>
+              <tr><td class="prop_header">药品代码</td><td class="prop_value">{{data.dialogContent['drug_code']}}</td></tr>
+              <tr><td class="prop_header">注册名</td><td class="prop_value">{{data.dialogContent['reg_name']}}</td></tr>
+              <tr><td class="prop_header">商品名</td><td class="prop_value">{{data.dialogContent['prod_name']}}</td></tr>
+              <tr><td class="prop_header">注册剂型</td><td class="prop_value">{{data.dialogContent['reg_dosage_form']}}</td></tr>
+              <tr><td class="prop_header">商品剂型</td><td class="prop_value">{{data.dialogContent['act_dosage_form']}}</td></tr>
+              <tr><td class="prop_header">注册规格</td><td class="prop_value">{{data.dialogContent['reg_spec']}}</td></tr>
+              <tr><td class="prop_header">商品规格</td><td class="prop_value">{{data.dialogContent['act_spec']}}</td></tr>
+              <tr><td class="prop_header">包装材料</td><td class="prop_value">{{data.dialogContent['pkg_mat']}}</td></tr>
+              <tr><td class="prop_header">最小包装</td><td class="prop_value">{{data.dialogContent['min_pack_size']}}</td></tr>
+              <tr><td class="prop_header">最小包装单位</td><td class="prop_value">{{data.dialogContent['min_pack_unit']}}</td></tr>
+              <tr><td class="prop_header">最小用量单位</td><td class="prop_value">{{data.dialogContent['min_dosage_unit']}}</td></tr>
+              <tr><td class="prop_header">生产厂商</td><td class="prop_value">{{data.dialogContent['prod_factory']}}</td></tr>
+              <tr><td class="prop_header">准字</td><td class="prop_value">{{data.dialogContent['license_no']}}</td></tr>
+              <tr><td class="prop_header">药品标准码</td><td class="prop_value">{{data.dialogContent['drug_std_code']}}</td></tr>
+              <tr><td class="prop_header">分销厂商</td><td class="prop_value">{{data.dialogContent['subpkg_factory']}}</td></tr>
+              <tr><td class="prop_header">销售状态</td><td class="prop_value">{{data.dialogContent['sales_status']}}</td></tr>
+              <tr><td class="prop_header">社保名称</td><td class="prop_value">{{data.dialogContent['social_insurance_name']}}</td></tr>
+              <tr><td class="prop_header">甲乙类</td><td class="prop_value">{{data.dialogContent['jiayi_category']}}</td></tr>
+              <tr><td class="prop_header">社保剂型</td><td class="prop_value">{{data.dialogContent['social_dosage_form']}}</td></tr>
+              <tr><td class="prop_header">序列号</td><td class="prop_value">{{data.dialogContent['serial_no']}}</td></tr>
+              <tr><td class="prop_header">注释</td><td class="prop_value">{{data.dialogContent['comments']}}</td></tr>
+
+            </tbody>
+          </table>
+</template>
+      </BasicDialog>
+      <ActionBar @reset="resetParams" @refresh="refreshTable">
+        <template #left>
+          <h2>药品字典</h2>
+        </template>
+        <template #right>
+          <el-input v-model="searchParams.name" placeholder="请输入ID"  clearable />
+        </template>
+      </ActionBar>
+      <el-table :data="tableData" style="width: 100%" max-height="calc(100vh - 270px)">
+        <el-table-column prop="drug_code" label="Code"/>
+        <el-table-column prop="reg_name" label="Name"/>
+        <el-table-column prop="reg_dosage_form" label="Dosage Form"/>
+        <el-table-column prop="reg_spec" label="Spec."/>
+        <el-table-column prop="prod_factory" label="Prod. Factory"/>
+
+        <el-table-column fixed="right" width="170">
+          <template #header>
+            <div style="display: flex;justify-content: center;align-items: center">
+              <div style="margin-right: 10px">操作</div>
+            </div>
+          </template>
+          <template #default="scope">
+            <el-button type="primary"  @click="handleEdit(scope.row)">
+              详细资料
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <Pagination ref="paginationRef" :params="searchParams" :reqFunc="paginationList" @pageData="setTableData" />
+    </div>
+  </template>
+  
+  <script setup lang="ts">
+  import { reactive } from "vue";
+  import BasicDialog from "@/components/BasicDialog.vue"
+  import ActionBar from "@/components/ActionBar.vue";
+  import Pagination from "@/components/Pagination.vue"
+  import { paginationList,save } from "@/api/drugs/index.ts"
+  import usePagination from "@/compositionApi/pagination.ts"
+
+
+  
+  const {searchParams, tableData, paginationRef, setTableData, refreshTable,resetParams} =  usePagination();
+  // const {tableFields} = useTableField()
+  // const { openForm,rules,formDesc,submitForm ,editRef} = useFromEdit(refreshTable)
+
+  const data = reactive({    
+    dataDialogOpen: false,
+    dialogHeader: null,
+    dialogContent: {},
+    search: "",
+    id: 0,
+  });
+  
+
+  function handleEdit(row: any){
+      data.id = row.id
+      data.dialogHeader = row.reg_name;
+      data.dialogContent = row;        
+      data.dataDialogOpen = true;
+    }
+
+  
+  </script>
+  
+  <style lang="scss" scoped>
+  .box {
+    width: 100%;
+    height: 100%;
+  }
+  .prop_header{
+    background-color: #CDCDCD;    
+    font-weight: bold;
+  }
+  .prop_value{
+    background-color: #adcddb;    
+    padding-left: 10px;
+  }
+  </style>
+  

+ 55 - 0
src/views/dict/ICD.vue

@@ -0,0 +1,55 @@
+<template>
+    <div class="box">
+      <ActionBar @reset="resetParams" @refresh="refreshTable">
+        <template #left>
+
+          <h2>ICD字典</h2>
+        </template>
+        <template #right>
+          <el-input v-model="searchParams.name" placeholder="请输入名字"  clearable />
+        </template>
+      </ActionBar>
+      <el-table :data="tableData" style="width: 100%" max-height="calc(100vh - 270px)">
+        <el-table-column prop="icd_code" label="Code"/>
+        <el-table-column prop="icd_name" label="Name"/>
+
+        <el-table-column fixed="right" width="170">
+          <template #header>
+            <div style="display: flex;justify-content: center;align-items: center">
+              <div style="margin-right: 10px">操作</div>
+            </div>
+          </template>
+          <template #default="scope">
+            <el-button type="primary">
+              用药规则
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <Pagination ref="paginationRef" :params="searchParams" :reqFunc="paginationList" @pageData="setTableData" />
+    </div>
+  </template>
+  
+  <script setup lang="ts">
+  import ActionBar from "@/components/ActionBar.vue";
+  import Pagination from "@/components/Pagination.vue"
+  import { paginationList,save } from "@/api/icd/index.ts"
+  import usePagination from "@/compositionApi/pagination.ts"
+
+  
+  const {searchParams, tableData, paginationRef, refreshTable, setTableData,resetParams} =  usePagination()
+  // const {tableFields} = useTableField()
+  // const { openForm,rules,formDesc,submitForm ,editRef} = useFromEdit(refreshTable)
+
+  
+  
+  </script>
+  
+  <style lang="scss" scoped>
+  .box {
+    width: 100%;
+    height: 100%;
+    padding: 0;
+  }
+  </style>
+  

+ 189 - 0
src/views/dict/KgSchemas.vue

@@ -0,0 +1,189 @@
+<template>
+     
+     <BasicDialog :isOpen="data.dataDialogOpen" @close="data.dataDialogOpen = false">
+        <!-- 自定义标题和内容 -->
+        <template v-slot:header>{{data.dialogHeader}}</template>
+        <template v-slot>
+          <el-form :model="data.schemaData" ref="dataForm" label-width="80px">
+            <el-form-item label="类型" prop="category">
+              <el-input v-model="data.schemaData.category" placeholder="category"></el-input>
+            </el-form-item >
+            <el-form-item label="数据名称" prop="name">
+              <el-input v-model="data.schemaData.name" placeholder="name"></el-input>
+            </el-form-item >
+            <el-form-item label="内容 (格式: code|name... 举例 cause|病因)" prop="content">
+              <el-input type="textarea" v-model="data.schemaData.content" placeholder="" rows="6" resize="none"></el-input>
+            </el-form-item >
+            <el-form-item label="版本" prop="version">
+              <el-input v-model="data.schemaData.version" placeholder="1.0"></el-input>
+            </el-form-item >
+            <el-form-item>
+              <el-button @click="handleSaveButton()">保存</el-button>
+              <el-button>重置</el-button>
+            </el-form-item>
+          </el-form>
+        </template>
+      </BasicDialog>
+    <ConfirmDialog
+        ref="confirmDialog"
+      v-model="confirmDialogVisible"
+      objId = "0"
+      title="确认删除"
+      message="您确定要删除这条记录吗?"
+      @confirm="handleConfirmDelete"
+      @cancel=""
+    />
+
+    <div class="box">
+      <ActionBar @reset="resetParams" @refresh="refreshTable">
+        <template #left> 
+          <h2>数据定义</h2>
+        </template>
+        <template #right>
+          <el-button type="primary" @click="createDataset()">新建数据集</el-button>&nbsp;
+          <el-input v-model="searchParams.name" placeholder="请输入ID"  clearable />
+        </template>
+      </ActionBar>
+      <el-table :data="tableData" style="width: 100%" >        
+        <el-table-column prop="category" label="代码"/>
+        <el-table-column prop="name" label="名称"/>  
+        <el-table-column prop="version" label="版本"/>        
+
+        <el-table-column fixed="right" width="170">
+          <template #header>
+            <div style="display: flex;justify-content: center;align-items: center">
+              <div style="margin-right: 10px">操作</div>
+            </div>
+          </template>
+          <template #default="scope">
+            <el-button type="success" @click="handleEdit(scope.row)">
+              编辑
+            </el-button>
+            <el-button type="success" @click="handleDelete(scope.row)">
+              删除
+            </el-button>
+          </template>
+        </el-table-column> 
+      </el-table>
+      <Pagination ref="paginationRef" :params="searchParams" :reqFunc="paginationList" @pageData="setTableData" />
+    </div>
+
+    
+  </template>
+  
+  <script setup lang="ts">
+  import BasicDialog from "@/components/BasicDialog.vue";
+  import ActionBar from "@/components/ActionBar.vue";
+  import Pagination from "@/components/Pagination.vue"
+    import usePagination from "@/compositionApi/pagination.ts"
+    import ConfirmDialog from '@/components/ConfirmDialog.vue';
+    import { ElMessage } from 'element-plus';
+    import { ref  } from 'vue'
+    import  r  from "@/utils/request.ts"
+
+    const {searchParams, tableData, paginationRef, setTableData, refreshTable,resetParams} =  usePagination()
+    // const {tableFields} = useTableField()
+    // const { openForm,rules,formDesc,submitForm ,editRef} = useFromEdit(refreshTable)
+    const data = ref({
+      dataDialogOpen:false,
+      dialogHeader:'',
+      schemaData: { id: 0, name :'', category:"", content:"", version:""},
+    });
+    const selectedData = ref({
+        id: 1,
+        prodName: '',
+        prodContent: '',
+        prodData: '',
+    });
+const handleSaveButton  = () =>{
+  let reqUrl = '/api/schemas-create';
+  if (data.value.schemaData.id > 0){
+    reqUrl = '/api/schemas-update';
+  }
+  const response = r.request<string[]>({
+    url:reqUrl,
+    method: 'post',
+    data: data.value.schemaData,
+  })
+  response.then(result =>{
+    console.log(result);
+    data.value.dataDialogOpen = false;
+    refreshTable();
+  });
+}
+const handleEdit = (row) => {
+
+  data.value.dialogHeader = row.name;
+  data.value.schemaData.name = row.name;
+  data.value.schemaData.category = row.category;
+  data.value.schemaData.content = row.content;
+  data.value.schemaData.version = row.version;
+  data.value.schemaData.id = row.id;
+  data.value.dataDialogOpen = true;
+};
+function createDataset(){
+  data.value.dialogHeader = '新数据集定义';
+  data.value.schemaData.id = 0;
+  data.value.schemaData.name = '';
+  data.value.schemaData.category = '';
+  data.value.schemaData.content = '';
+  data.value.schemaData.version = '';
+  data.value.dataDialogOpen = true;
+}
+function paginationList(param)  {
+  let reqUrl = '/api/schemas/'+param.pageNum+'/'+param.pageSize;
+
+  if (param.name.length > 0){
+    reqUrl = '/api/schemas-search/'+param.pageNum+"/"+param.pageSize+'/'+param.name;
+  }
+  return r.request<string[]>({//r.request会做拦截,因此响应的数据就是string[] 类型
+    url: reqUrl,
+    method: 'get',    
+  })
+}
+
+
+
+    const confirmDialogVisible = ref(false);
+    const confirmDialog = ref();
+    function handleDelete(row: any){
+        confirmDialog.value.objId = row.id
+        showConfirmDialog()
+    }
+
+    const showConfirmDialog = () => {
+        confirmDialogVisible.value = true
+    };
+    const handleConfirmDelete = () =>{
+      let id = confirmDialog.value.objId;
+      let reqUrl = '/api/schemas-delete/' + id;
+      const response = r.request<string[]>({
+        url:reqUrl,
+        method: 'get',
+      })
+      response.then(result =>{
+        console.log(result);
+        refreshTable();
+      });
+    };
+  </script>
+  
+  <style lang="scss" scoped>
+  .box {
+    width: 100%;
+    height: 20pt;
+  }
+  .textbox_lower {
+    width: 100%;
+    height: 50pt;
+    padding: 10px 0;
+    resize: none;
+  }
+  .textbox {
+    width: 100%;
+    height: 100pt;
+    padding: 10px 0;
+    resize: none;
+  }
+  </style>
+  

+ 40 - 0
src/views/dict/extraAction.ts

@@ -0,0 +1,40 @@
+import {ElMessage, ElMessageBox} from 'element-plus'
+import {useRouter} from "vue-router";
+
+
+export default function (refreshTable: () => void) {
+    const router = useRouter()
+
+    const deleteRow = (row:any) => {
+        ElMessageBox.confirm(
+            '确认删除此记录吗?',
+            '确认操作',
+            {
+                confirmButtonText: '确认',
+                cancelButtonText: '关闭',
+                type: 'warning',
+            }
+        )
+            .then(() => {
+                let { id } = row;
+                // deleteMaterial({id}).then(()=>{
+                //     ElMessage({
+                //         type: 'success',
+                //         message: '删除成功',
+                //     })
+                //     refreshTable()
+                // }).catch(()=>{})
+
+            })
+            .catch(() => {})
+    }
+    const details = (row:any) => {
+        router.push({path:"/",query:{id:row.id}})
+    }
+
+    return {
+        details,
+        deleteRow,
+    }
+
+}

+ 14 - 0
tsconfig.app.json

@@ -0,0 +1,14 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.dom.json",
+  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
+  "exclude": ["src/**/__tests__/*"],
+  "compilerOptions": {
+    "composite": true,
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  }
+}

+ 14 - 0
tsconfig.json

@@ -0,0 +1,14 @@
+{
+  "files": [],
+  "references": [
+    {
+      "path": "./tsconfig.node.json"
+    },
+    {
+      "path": "./tsconfig.app.json"
+    },
+    {
+      "path": "./tsconfig.vitest.json"
+    }
+  ]
+}

+ 19 - 0
tsconfig.node.json

@@ -0,0 +1,19 @@
+{
+  "extends": "@tsconfig/node22/tsconfig.json",
+  "include": [
+    "vite.config.*",
+    "vitest.config.*",
+    "cypress.config.*",
+    "nightwatch.conf.*",
+    "playwright.config.*"
+  ],
+  "compilerOptions": {
+    "composite": true,
+    "noEmit": true,
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+
+    "module": "ESNext",
+    "moduleResolution": "Bundler",
+    "types": ["node"]
+  }
+}

+ 11 - 0
tsconfig.vitest.json

@@ -0,0 +1,11 @@
+{
+  "extends": "./tsconfig.app.json",
+  "exclude": [],
+  "compilerOptions": {
+    "composite": true,
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo",
+
+    "lib": [],
+    "types": ["node", "jsdom"]
+  }
+}

+ 41 - 0
vite.config.ts

@@ -0,0 +1,41 @@
+import { fileURLToPath, URL } from 'node:url'
+
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+// import vueDevTools from 'vite-plugin-vue-devtools' //vue的内置调试工具
+import AutoImport from 'unplugin-auto-import/vite'
+import Components from 'unplugin-vue-components/vite'
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
+// https://vite.dev/config/
+export default defineConfig({
+  server: {
+    host: "172.16.8.57",
+    proxy: {
+      '/api': {
+        target: 'http://172.16.8.64:8000/api',
+        changeOrigin: true,
+        rewrite: path => path.replace(/^\/api/, '')
+      },
+      '/kg': { //知识图谱统计API
+        target: "http://172.16.8.59:8086/healsphere",
+        changeOrigin: true,
+        //  rewrite: path => path.replace(/^\/kg/, '')
+      }
+    }
+  },
+  plugins: [
+    vue(),
+    // vueDevTools(),
+    AutoImport({
+      resolvers: [ElementPlusResolver()],
+    }),
+    Components({
+      resolvers: [ElementPlusResolver()],
+    }),
+  ],
+  resolve: {
+    alias: {
+      '@': fileURLToPath(new URL('./src', import.meta.url))
+    },
+  },
+})

+ 18 - 0
vitest.config.ts

@@ -0,0 +1,18 @@
+import { fileURLToPath } from 'node:url'
+import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
+import viteConfig from './vite.config'
+
+export default mergeConfig(
+  viteConfig,
+  defineConfig({
+    test: {
+      environment: 'jsdom',
+      exclude: [...configDefaults.exclude, 'e2e/**'],
+      root: fileURLToPath(new URL('./', import.meta.url)),
+    },
+    define: {
+      // 具体环境设置是否开启
+      __VUE_PROD_DEVTOOLS__: true
+    }
+  }),
+)