提交 a7a092f6 authored 作者: caijian's avatar caijian

first commit

上级
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
mapWithLabel
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Vue 3 + Vite
\ No newline at end of file
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hypergraph</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "techhypergraph",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@antv/g6": "^4.8.21",
"@turf/turf": "^7.2.0",
"axios": "^1.8.3",
"echarts": "^5.6.0",
"element-plus": "^2.9.6",
"leaflet": "^1.9.4",
"leaflet-ant-path": "^1.3.0",
"leaflet-draw": "^1.0.4",
"leaflet-minimap": "^3.6.1",
"leaflet-polylinedecorator": "^1.6.0",
"leaflet-textpath": "^1.2.3",
"lodash": "^4.17.21",
"mitt": "^3.0.1",
"proj4": "^2.19.7",
"proj4leaflet": "^1.0.2",
"sass": "^1.86.0",
"stylus": "^0.64.0",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vuex": "^4.1.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"less": "^4.2.2",
"less-loader": "^12.2.0",
"unplugin-auto-import": "^20.0.0",
"unplugin-vue-components": "^29.0.0",
"vite": "^6.2.0"
}
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
\ No newline at end of file
<template>
<div class="container">
<router-view />
</div>
</template>
<style>
.container {
width: 100vw;
height: 100vh;
}
#app {
font-family: "Microsoft YaHei";
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #ffffff;
margin: 0;
padding: 0;
}
</style>
<script setup>
</script>
import request from './request'
//创建超图
export const createGraph = async (params) => {
let {
data
} = await request.post('/api/hypergraph/create')
return data
}
//超图列表
export const getGraphList = async (params) => {
let {
data
} = await request.get('/api/hypergraph/get_list')
return data
}
//创建实体
export const createEntity = async (params) => {
// let {
// data
// } = await request.post('/api/entity/create')
// return data
let data = await request({
method: "post",
url: "/api/entity/create",
data: params
})
return data
}
// 在超图中插入节点
export const insertEntityIntoHypergraph = async (params) => {
let data = await request({
method: "post",
url: "/api/hypergraph/insert_entity_in_hypergraph",
data: params
})
return data
}
//实体列表
export const getEntityList = async (params) => {
let {
data
} = await request.get('/api/entity/get_list')
return data
}
//实体检索
export const searchEntity = async (params) => {
let {
data
} = await request.get('/api/entity/search')
return data
}
//创建事件
export const createItem = async (params) => {
let {
data
} = await request.post('/api/news/create')
return data
}
//新闻列表
export const getItemList = async (params) => {
let {
data
} = await request.get('/api/news/get_list')
return data
}
//新闻检索
export const searchItem = async (params) => {
let {
data
} = await request.get('/api/news/search?query=' + params.id)
return data
}
//获取超图详情
export const getGraphInfo = async (params) => {
let {
data
} = await request.get('/api/hypergraph/get_hypergraph_by_id?id=' + params.id
+ '&cur_lt_lon=' + params.cur_lt_lon + '&cur_lt_lat=' + params.cur_lt_lat
+ '&cur_lt_x=' + params.cur_lt_x + '&cur_lt_y=' + params.cur_lt_y
+ '&cur_rb_lon=' + params.cur_rb_lon + '&cur_rb_lat=' + params.cur_rb_lat
+ '&cur_rb_x=' + params.cur_rb_x + '&cur_rb_y=' + params.cur_rb_y)
return data
}
//获取多个超图详情
export const getGraphInfoByIds = async (params) => {
let {
data
} = await request({
method: "post",
url: "/api/hypergraph/get_hypergraph_by_ids",
data: params
})
return data
}
//保存超图
export const saveHypergraph = async (params) => {
let data = await request({
method: "post",
url: "/api/hypergraph/save_hypergraph",
data: params
})
return data
}
//根据ID获取超图
export const getHypermapById = async (params) => {
let data = await request.get('/api/hypergraph/get_hypermap_by_id?id=' + params.id)
return data
}
//折线图
export const getChart = async () => {
let {
data
} = await request.get('/api/analysis/get_chart')
return data
}
//矩阵图
export const getMatrix = async () => {
let {
data
} = await request.get('/api/analysis/get_matrix')
return data
}
//时序图
export const getEvents = async () => {
let data = await request.get('/api/analysis/get_events')
return data
}
//获取折线统计的编辑数据
export const getChartEditTable = async (params) => {
let data = request.get('/api/analysis/get_chart_edit_table?hypergraph_id=' + params.id)
return data
}
//保存折线统计编辑
export const saveChartTable = async (params) => {
let data = await request({
method: "post",
headers: {
'Content-Type': 'application/json'
},
url: "/api/analysis/save_chart",
data: params
})
return data
}
//获取矩阵编辑数据
export const getMatrixEditTable = async (params) => {
let data = await request.get('/api/analysis/get_matrix_edit_table')
return data
}
//保存矩阵数据
export const saveMatrixTable = async (params) => {
let data = await request({
method: "post",
headers: {
'Content-Type': 'application/json'
},
url: "/api/analysis/save_matrix",
data: params
})
return data
}
//获取事件编辑数据
export const getEventsEditTable = async (params) => {
let data = await request.get('/api/analysis/get_events_edit_table')
return data
}
//保存事件数据
export const saveEventsTable = async (params) => {
let data = await request({
method: "post",
headers: {
'Content-Type': 'application/json'
},
url: "/api/analysis/save_events",
data: params
})
return data
}
/**
* 上传文件,后端解析Excel文件
* @param file 文件,表单数据
* @returns
*/
export const uploadFileService = (file) => {
// return request.post("", file, {
// // 上传文件,需设置 headers 信息,将"Content-Type"设置为"multipart/form-data"
// headers: {
// "Content-Type": "multipart/form-data"
// }
// });
};
//引入axios请求
import axios from 'axios'
//引入element-plus里面的消息提示
import {
ElMessage
} from 'element-plus'
const BASE_API = "/"
// 创建axios实例
const service = axios.create({
baseURL: BASE_API, //所有的后端接口请求地址前缀部分(没有后端请求不用写)
timeout: 15000 // 请求超时时间,这里15秒
//withCredentials: true,// 异步请求携带cookie,true为携带,false为不携带
//请求头里面设置通用传参类型
/*headers: {
//设置后端需要的传参类型
'Content-Type': 'application/json',
'token': 'x-auth-token',//一开始就要token
'X-Requested-With': 'XMLHttpRequest',
}*/
})
// request拦截器
service.interceptors.request.use(config => {
/*if (store.getters.token) {
config.headers['Authorization'] = getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}*/
return config
}, error => {
console.log(error)
Promise.reject(error)
})
// response拦截器
service.interceptors.response.use(
response => {
//对数据返回做什么
return response.data
},
error => {
console.log('err' + error)
ElMessage({
message: error.message,
type: 'error',
duration: 3 * 1000
})
return Promise.reject(error)
}
)
export default service
\ No newline at end of file
* {
padding: 0;
margin: 0;
}
a {
cursor: pointer;
}
.container {
/*display: grid;*/
margin: 0;
padding: 0;
}
router-view, div {
margin: 0;
padding: 0;
}
\ No newline at end of file
@font-face {
font-family: "FZZDHJW--GB1-0";
src: url("./FZZDHJW.TTF");
}
@font-face {
font-family: "FZZZHONGJW--GB1-0";
src: url("./FZZZHONGHJW.TTF");
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
\ No newline at end of file
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import { ElMessage, ElMessageBox } from 'element-plus'
import 'element-plus/dist/index.css'
import mitt from 'mitt'//全局通信
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import './assets/fonts/font.scss';
const app = createApp(App)
// 注册图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
router.beforeEach((to, from, next) => {
if (to.path === '/' || to.path === '') {
router.push('/home')
} else {
next()
}
})
window.mapIP = "/static/mapWithLabel/{z}/{x}/{y}.webp"
const bus = mitt();//全局通信
app.config.globalProperties.$bus = bus;
app.use(store)
app.use(router)
app.use(ElementPlus, { locale: zhCn })
app.config.globalProperties.$message = ElMessage;
app.config.globalProperties.$messageBox = ElMessageBox;
app.config.globalProperties.$confirm = ElMessageBox.confirm;
app.mount('#app')
\ No newline at end of file
import { createRouter, createWebHashHistory } from 'vue-router'
import Lauout from '../views/layout/index.vue'
import HomeView from '../views/home/index.vue'
import ThemeManagementView from '../views/themeList/ThemeManagement.vue'
import ThemeSearchView from '../views/themeList/ThemeSearch.vue'
import CreateThemeView from '../views/themeList/createTheme/index.vue'
import MechanismAnalysisView from '../views/mechanism/MechanismAnalysisView.vue'
const routes = [
{
path: '/',
name: 'index',
component: Lauout,
children: [
{
path: '/home',
name: 'HomeView',
component: HomeView
},
{
path: '/themeManage',
name: 'ThemeManagement',
component: ThemeManagementView
},
{
path: '/themeSearch',
name: 'ThemeSearch',
component: ThemeSearchView
},
{
path: '/createTheme',
name: 'CreateTheme',
component: CreateThemeView
},
{
path: '/mechanismAnalysis',
name: 'MechanismAnalysis',
component: MechanismAnalysisView
}
]
},
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
\ No newline at end of file
import { createStore } from 'vuex'
import graph from './modules/graph'
export default createStore({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
graph
}
})
const state = {
// 专题生成--步骤数据
create_graph_form: {},
activeSteps: 0, // 当前
stepList: ["创建专题", "数据导入", "定义关系", "专题生成"],
graph_name: "",
graph_id: "",
maxStepsTotal: 3, // 步骤总条数
selectData: {},
graphJSON: {}, // 当前画布数据
};
// 定义所需的 mutations
const mutations = {
// 设置当前步骤
SET_ACTIVE_STEP(state, payload) {
state.activeSteps = payload;
},
// 获取缓存数据
GET_GRAPH_DATA(state, payload) {
const { activeSteps } = payload;
if (payload[0] && payload[0].graphName) {
const {graphName,graph_id} = payload[0]
state.graph_name = graphName;
state.graph_id = graph_id
}
state.activeSteps = activeSteps;
state.create_graph_form = payload;
},
// 设置缓存数据
SET_GRAPH_DATA(state, payload) {
const { data = {}, currentStep, nextStep } = payload;
if (currentStep == 0) {
const { graphName } = data;
state.graph_name = graphName;
}
state.activeSteps = nextStep;
state.create_graph_form.activeSteps = nextStep;
state.create_graph_form[currentStep] = data;
},
// 设置选中的数据
SET_SELECT_DATA(state, payload) {
const { selectData, currentStep } = payload;
// 若步骤数据中有大于当前步骤的数据则删除
for (let key in state.create_graph_form) {
if (!isNaN(key) ) {
if (key > currentStep) {
delete state.create_graph_form[key];
}
}
}
state.create_graph_form.selectData = selectData;
},
// 设置图谱数据
SET_GRAPH_JSON(state, payload) {
state.graphJSON = payload;
},
// 清除缓存
SET_CLEAR_CACHE(state) {
state.create_graph_form = {};
state.activeSteps = 0;
state.graph_name = "";
state.selectData = {};
}
};
const actions = {};
const getters = {};
// 创建 store 实例
export default {
namespaced: true,
actions,
getters,
state,
mutations,
};
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
width: 100%;
margin: 0 auto;
height: 100%;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar:hover {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
border-radius: 10px;
background-color: #29517F;
border: 1px solid transparent;
background-clip: padding-box;
}
import {
ElMessage
} from "element-plus";
import * as xlsx from "xlsx";
import {
uploadFileService
} from "@/api/graphApi";
/**
* 从Excel文件导入数据,由后端解析文件,获取数据
* @param file 导入文件
* @returns
*/
export async function importExcelFileByServer(file, keyColMap) {
// 定义及初始化需要返回的列表数据
let dataList = [];
// 文件校验
// 校验文件名后缀
if (!/\.(xls|xlsx)$/.test(file.name)) {
ElMessage.warning("请导入excel文件!");
return dataList;
}
// 校验文件格式
// application/vnd.ms-excel 为 .xls文件
// application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 为 .xlsx文件
else if (
file.type !== "application/vnd.ms-excel" &&
file.type !== "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
) {
ElMessage.warning("excel文件已损坏,请检查!");
return dataList;
}
// 校验文件大小
else if (convertFileSize(file.size, "B", "MB") > 1) {
ElMessage.warning("文件大小不能超过1MB!");
return dataList;
}
// 文件读取
const fileReader = new FileReader();
// 以二进制的方式读取文件内容
fileReader.readAsArrayBuffer(file);
// 等待打开加载完成文件,其实就是执行 fileReader.onloadend = () => {},返回 true 表示成功,false 表示失败
const result = await loadedFile(fileReader);
let resultData = null;
if (result) {
// 通过 FormData 对象实现文件上传
const formData = new FormData();
// 将文件对象 file 添加到 formData 对象中,uploadFile 需要与后端接口中接收文件的参数名一致
formData.append("uploadFile", file);
// 发送请求,上传文件到后端服务器,后端接收文件,进行解析,并返回数据集
resultData = await uploadFileService(formData);
}
// 返回列表数据
return resultData;
}
function loadedFile(fileReader) {
fileReader.onloadend = () => {
return true
}
}
function convertFileSize(size, fromUnit, toUnit, decimalPoint=2) {
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const fromIndex = units.indexOf(fromUnit);
const toIndex = units.indexOf(toUnit);
if (fromIndex === -1 || toIndex === -1) throw new Error('Invalid units');
const exponent = toIndex - fromIndex;
return parseFloat(size / Math.pow(1024, exponent)).toFixed(decimalPoint) + ' ' + toUnit;
}
\ No newline at end of file
export const numberFormat = (
number,
decimals = 0,
dec_point = '.',
thousands_sep = ','
) => {
/*
3 * 参数说明:
4 * number:要格式化的数字
5 * decimals:保留几位小数
6 * dec_point:小数点符号
7 * thousands_sep:千分位符号
8 * */
number = (number + '').replace(/[^0-9+-Ee.]/g, '');
var n = !isFinite(+number) ? 0 : +number,
prec = !isFinite(+decimals) ? 0 : Math.abs(decimals),
sep = typeof thousands_sep === 'undefined' ? ',' : thousands_sep,
dec = typeof dec_point === 'undefined' ? '.' : dec_point,
s = '',
toFixedFix = function(n, prec) {
var k = Math.pow(10, prec);
return '' + Math.ceil(n * k) / k;
};
s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.');
var re = /(-?\d+)(\d{3})/;
while (re.test(s[0])) {
s[0] = s[0].replace(re, '$1' + sep + '$2');
}
if ((s[1] || '').length < prec) {
s[1] = s[1] || '';
s[1] += new Array(prec - s[1].length + 1).join('0');
}
return s.join(dec);
};
export const getRandomStr = num => {
const word =
'abcdefghijklmnopqrstuvwxyz' + 'abcdefghijklmnopqrstuvwxyz'.toUpperCase();
let name = '';
while (num) {
name += word[parseInt(Math.random() * word.length)];
num--;
}
return name;
};
// 生成随机颜色
/**
*
* @param {*} type hex->16进制,rgb->rgb,rgba->rgba
*/
export function randomColor(type){
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
const alpha = Math.random()
let custom_color = ""
if(type == 'hex'){
let hex_color = Math.random().toString(16).substr(2,6)
custom_color = `#${hex_color}`;
}
if(type == 'rgb'){
custom_color = `rgb(${r},${g},${b})`
}
if(type == 'rgba'){
custom_color = `rgb(${r},${g},${b},${alpha})`
}
return custom_color
}
// 16进制颜色转为rgb
/**
*
* @param {*} hex : 16进制颜色
*/
// 16进制一般有3位或者6位,若为3位需补齐为6位
export function hexToRgb(hex,alpha = 1){
let reg = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/
if(!reg.test(hex)){return}
let newStr = (hex.toLowerCase()).replace(/\#/g,'')
let len = newStr.leng
if(len == 3){
let t = ''
for(let i = 0; i < len; i++){
t += newStr.slice(i,i+1).concat(newStr.slice(i,i+1))
}
newStr = t
}
let arr = []
// 将字符串分隔,两个两个的分隔
for(let i = 0; i < 6; i = i+2){
let s = newStr.slice(i,i+2)
arr.push(parseInt('0x' + s))
}
// return 'rgba(' + arr.join(',')+',+'alpha''+')'
return `rgba(${arr.join(',')},${alpha})`
}
// rgb转换为16进制
export function rgbToHex(rgb){
let reg = /^(rgb|RGB)/;
if(!res.test(rgb)){return}
let arr = rgb.slice(4,rgb.length - 1).split(',')
let color = '#'
for(let i = 0; i < arr.length; i ++){
let t = Number(arr[i].toString(16))
if(t == '0'){
t = t + '0'
}
color += t
}
return color
}
// 非空验证
export function UnEmptyValidation(value){
let reg = /^[\s\S]*.*[^\s][\s\S]*$/
return reg.test(value)
}
// 文本过长判断展示
export const textRange = (el)=>{
const textContent = el;
// 元素自身占据的大小
const targetWidth = textContent.getBoundingClientRect().width;
// 一段连续的区域
const range = document.createRange();
// 某个节点中的某个位置定为区域的起点位置
/**
* 两个参数
* 1.节点为一段文字,指定将第几个文字结束文字作为区域的起点/结束位置(num从0开始)
*
*/
range.setStart(textContent, 0);
// 结束位置
range.setEnd(textContent, textContent.childNodes.length);
const rangeWidth = range.getBoundingClientRect().width;
return rangeWidth > targetWidth;
}
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1755766600996" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7415" width="256" height="256" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M914.45248 881.11104l-344.38144-344.39168 344.52992-344.52992a52.26496 52.26496 0 0 0 14.76608-36.51584c0-28.97408-23.57248-52.54144-52.55168-52.54144-13.70624 0-26.6752 5.248-36.66432 14.91968L495.7696 462.41792 151.22944 117.888a52.25984 52.25984 0 0 0-36.51072-14.76096c-28.96896 0-52.54144 23.57248-52.54144 52.54144 0 13.70112 5.24288 26.67008 14.91456 36.67456l344.37632 344.37632-344.53504 344.54528a52.2496 52.2496 0 0 0-14.75584 36.49536c0 28.9792 23.57248 52.5568 52.54144 52.5568 13.71136 0 26.68032-5.248 36.66944-14.9248l344.37632-344.3712 344.36096 344.35072a52.03456 52.03456 0 0 0 37.17632 15.47264c14.0544 0 27.25376-5.49376 37.13536-15.44192a52.1728 52.1728 0 0 0 15.40096-37.15072c0-14.02368-5.46304-27.2128-15.3856-37.14048z" p-id="7416" fill="#cccccc"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1755766883552" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7765" width="256" height="256" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M914.45248 881.11104l-344.38144-344.39168 344.52992-344.52992a52.26496 52.26496 0 0 0 14.76608-36.51584c0-28.97408-23.57248-52.54144-52.55168-52.54144-13.70624 0-26.6752 5.248-36.66432 14.91968L495.7696 462.41792 151.22944 117.888a52.25984 52.25984 0 0 0-36.51072-14.76096c-28.96896 0-52.54144 23.57248-52.54144 52.54144 0 13.70112 5.24288 26.67008 14.91456 36.67456l344.37632 344.37632-344.53504 344.54528a52.2496 52.2496 0 0 0-14.75584 36.49536c0 28.9792 23.57248 52.5568 52.54144 52.5568 13.71136 0 26.68032-5.248 36.66944-14.9248l344.37632-344.3712 344.36096 344.35072a52.03456 52.03456 0 0 0 37.17632 15.47264c14.0544 0 27.25376-5.49376 37.13536-15.44192a52.1728 52.1728 0 0 0 15.40096-37.15072c0-14.02368-5.46304-27.2128-15.3856-37.14048z" p-id="7766" fill="#ea9518"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1755738817685" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5632" width="256" height="256" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M928.242888 229.368775L549.374823 10.239677c-23.039274-13.311581-51.710371-13.311581-74.237662 0l-378.868065 219.129098c-23.039274 12.799597-36.862839 37.374823-36.862839 63.486v438.770179c0 26.111177 14.335548 50.17442 36.862839 63.486l378.868065 219.129097a75.466423 75.466423 0 0 0 74.237662 0l378.868065-219.129097c22.52729-12.799597 36.862839-37.374823 36.862839-63.486V292.342791c-0.511984-26.111177-14.847532-50.17442-36.862839-62.974016zM512 684.522438c-20.991339 0-38.39879 16.895468-38.39879 37.886806v202.745614L173.57866 752.616293l340.981259-200.697678 354.804824 187.898081-318.965953 185.850145V721.89726c0-20.991339-17.407452-37.374823-38.39879-37.374822z m-377.332114-343.029195l178.170388 67.581871c9.727694 3.583887 19.967371 3.071903 29.18308-1.023968 9.21571-4.095871 16.383484-11.775629 19.967371-21.503322a37.630815 37.630815 0 0 0-21.503322-49.150452L172.554693 273.399388l301.046517-174.074517v387.571792l-337.909356 199.161726-1.023968-344.565146z m415.730904 142.8435V99.324871l305.142388 174.586501-161.274919 108.540581c-17.407452 11.775629-21.503323 35.326887-9.727694 52.734339s35.326887 22.52729 52.734339 10.751661l151.547226-101.884791 1.023968 319.477937-339.445308-179.194356z" p-id="5633" fill="#eeeeee"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1755738792386" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5330" width="256" height="256" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M509.952 161.512727c148.945455-82.850909 300.730182-91.229091 371.479273-20.945454s62.324364 221.509818-20.526546 370.501818h-0.465454a429.335273 429.335273 0 0 1 65.163636 284.392727 168.727273 168.727273 0 0 1-44.683636 85.643637 173.754182 173.754182 0 0 1-86.109091 44.683636 435.665455 435.665455 0 0 1-285.277091-65.675636 430.731636 430.731636 0 0 1-282.530909 63.813818 172.218182 172.218182 0 0 1-86.109091-44.683637c-70.283636-69.818182-62.370909-220.206545 19.502545-368.174545-81.966545-148.48-89.786182-298.309818-19.502545-368.686546s220.625455-62.324364 369.058909 19.130182z m291.886545 440.785455a901.818182 901.818182 0 0 1-92.16 106.589091 934.027636 934.027636 0 0 1-108.916363 93.602909 586.891636 586.891636 0 0 0 58.600727 21.410909c74.938182 22.341818 127.069091 19.502545 155.508364-8.843636l-0.465455 0.884363c28.811636-28.392727 31.697455-80.523636 8.843637-155.508363a546.443636 546.443636 0 0 0-21.41091-58.135273z m-582.74909-0.465455a539.927273 539.927273 0 0 0-20.433455 55.854546c-22.295273 75.357091-19.549091 127.022545 8.797091 155.368727s80.151273 31.697455 155.508364 8.936727h-0.558546a539.927273 539.927273 0 0 0 55.854546-20.526545 967.400727 967.400727 0 0 1-199.214546-199.726546z m290.90909-332.753454a851.781818 851.781818 0 0 0-131.258181 108.404363 823.296 823.296 0 0 0-109.847273 133.12 823.854545 823.854545 0 0 0 109.847273 133.12v-0.884363a852.293818 852.293818 0 0 0 131.211636 108.357818 846.754909 846.754909 0 0 0 133.538909-109.800727 856.436364 856.436364 0 0 0 108.962909-131.258182 852.852364 852.852364 0 0 0-108.962909-131.211637 829.998545 829.998545 0 0 0-133.538909-109.847272zM503.994182 418.909091a94.347636 94.347636 0 1 1-35.84 10.705454 92.811636 92.811636 0 0 1 35.84-10.705454z m310.877091-212.340364c-28.253091-28.299636-80.151273-31.557818-155.508364-8.750545a591.592727 591.592727 0 0 0-58.600727 21.876363 933.794909 933.794909 0 0 1 108.869818 93.556364 947.060364 947.060364 0 0 1 92.718545 107.054546 545.326545 545.326545 0 0 0 21.41091-58.181819q33.559273-113.058909-8.843637-155.508363zM363.054545 199.68c-74.938182-22.295273-127.069091-19.549091-155.508363 8.843636v-0.465454c-28.997818 28.392727-31.744 80.523636-8.936727 155.508363a507.345455 507.345455 0 0 0 20.433454 56.273455A976.663273 976.663273 0 0 1 418.909091 220.206545a541.230545 541.230545 0 0 0-55.854546-20.526545z m0 0" fill="#eeeeee" p-id="5331"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1755738838352" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5886" width="256" height="256" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M585.457092 18.974118l328.704 181.308235A154.714353 154.714353 0 0 1 993.882504 335.811765v352.37647a154.624 154.624 0 0 1-79.721412 135.529412l-328.704 181.308235a151.913412 151.913412 0 0 1-146.883765 0L109.869327 823.717647a154.714353 154.714353 0 0 1-79.721412-135.529412v-352.37647a154.624 154.624 0 0 1 79.721412-135.529412L438.573327 18.974118a151.913412 151.913412 0 0 1 146.883765 0z m-103.664941 79.811764L153.027915 280.033882a63.698824 63.698824 0 0 0-32.858353 55.838118v352.376471a63.789176 63.789176 0 0 0 32.858353 55.808l328.764236 181.187764a62.343529 62.343529 0 0 0 60.416 0l328.764235-181.248a63.698824 63.698824 0 0 0 32.858353-55.838117v-352.376471a63.789176 63.789176 0 0 0-32.858353-55.838118L542.208151 98.755765a62.584471 62.584471 0 0 0-60.416 0z m319.427764 257.92753a45.628235 45.628235 0 0 1-18.070588 61.590588L561.664151 539.105882v236.453647a45.025882 45.025882 0 1 1-90.051765 0v-236.423529l-221.364706-120.771765a45.658353 45.658353 0 0 1-18.070588-61.590588 44.845176 44.845176 0 0 1 61.04847-18.311529l223.503059 121.916235 223.683765-122.036706a44.815059 44.815059 0 0 1 61.04847 18.371765z" p-id="5887" fill="#eeeeee"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1755738848182" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6146" width="256" height="256" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M969.485 337.562a248.27 248.27 0 0 1-10.956 73.472c-1.792 5.632-1.28 9.42 2.816 14.08a251.904 251.904 0 0 1 59.187 126.976c2.457 13.721 3.635 27.648 3.584 41.625A259.123 259.123 0 0 1 976.5 740.147a255.846 255.846 0 0 1-98.1 83.763 240.538 240.538 0 0 1-56.32 19.764 10.496 10.496 0 0 0-8.703 7.68c-28.058 75.315-79.872 127.488-155.648 156.518a248.115 248.115 0 0 1-77.108 15.77c-67.584 3.276-127.692-16.026-180.224-58.01a315.238 315.238 0 0 1-24.32-22.63 11.162 11.162 0 0 0-11.264-3.584c-61.952 10.905-121.036 2.457-175.872-28.365C117.415 870.86 74.356 809.83 58.484 730.522a239.974 239.974 0 0 1 6.707-117.556l1.126-3.584a7.168 7.168 0 0 0-1.74-8.09A255.334 255.334 0 0 1 0.73 448.615a249.19 249.19 0 0 1 28.672-134.912 255.283 255.283 0 0 1 118.016-114.585 242.79 242.79 0 0 1 54.477-18.842c4.25-0.563 7.68-3.584 8.755-7.68a250.368 250.368 0 0 1 68.557-103.577 266.138 266.138 0 0 1 302.336-38.605c24.371 12.697 46.285 29.337 64.922 49.305 4.352 4.66 8.448 5.632 14.643 4.608a261.018 261.018 0 0 1 148.685 16.282 253.931 253.931 0 0 1 88.422 61.03 257.229 257.229 0 0 1 71.27 175.924zM581.901 111.104c-1.69-1.638-2.816-2.867-4.096-3.891a194.1 194.1 0 0 0-140.492-39.936 187.392 187.392 0 0 0-107.828 46.592c-43.366 37.939-66.201 85.811-66.713 143.053-0.666 78.08-0.103 156.16-0.256 234.29-0.461 4.455 2.048 8.705 6.144 10.548 26.112 14.643 52.019 29.542 78.029 44.34 1.536 0.92 3.276 1.535 5.734 2.662V262.86c0-13.978 5.888-24.218 18.125-31.232L575.04 115.2l6.86-4.147zM195.188 251.75c-2.253 0.666-3.79 1.024-5.376 1.639a181.197 181.197 0 0 0-59.904 37.53c-47.104 44.8-68.25 99.379-60.519 163.532 7.68 62.874 39.885 111.104 95.079 143.309 68.147 39.68 137.01 78.234 205.465 117.402a9.677 9.677 0 0 0 11.11-0.103c25.754-14.848 51.56-29.491 77.415-44.185 1.895-1.024 3.533-2.202 5.735-3.687-1.895-1.331-3.277-2.355-4.71-3.174L281.715 562.637l-69.632-39.68a29.542 29.542 0 0 1-14.49-17.152 57.651 57.651 0 0 1-2.355-16.18 56377.81 56377.81 0 0 1 0-227.942v-9.933z m703.744 119.296a175.104 175.104 0 0 0-0.359-67.84c-13.824-65.894-51.558-113.97-114.278-140.697-60.877-25.959-120.832-21.043-178.432 11.776a114064.282 114064.282 0 0 1-201.728 114.893 8.14 8.14 0 0 0-4.864 7.987c0.205 30.515 0.102 60.928 0.102 91.443 0 1.434 0.256 2.816 0.512 5.12l11.11-6.298L589.89 285.542l60.723-34.61a36.25 36.25 0 0 1 21.504-5.12 40.346 40.346 0 0 1 16.793 5.99c35.072 20.07 70.144 40.09 105.268 60.057l100.3 57.14a41.318 41.318 0 0 0 4.455 2.047zM124.276 651.93c-0.82 10.854-1.792 20.07-2.048 29.337a177.203 177.203 0 0 0 25.088 98.304c41.216 68.199 102.912 100.25 182.937 97.28 31.335-1.075 60.416-11.366 87.501-27.033 29.389-16.999 58.983-33.741 88.525-50.535a40451.27 40451.27 0 0 1 113.152-64.358 8.96 8.96 0 0 0 5.12-9.063c-0.154-29.644-0.051-59.29-0.102-88.883 0-2.048-0.205-3.993-0.359-6.656-2.406 1.229-4.096 1.997-5.734 2.919a20989.282 20989.282 0 0 0-79.616 45.414l-113.664 64.768-52.941 30.157a32.82 32.82 0 0 1-34.202 0l-5.068-2.816L146.804 664.73c-7.015-4.096-14.03-7.988-22.528-12.8z m435.046-295.373c2.048 1.331 3.02 2.15 4.096 2.713 52.736 30.157 105.472 60.212 158.157 90.215l90.214 51.405c7.63 4.096 13.21 11.161 15.36 19.456a45.142 45.142 0 0 1 1.639 12.544c0.102 77.619 0.102 155.29 0 232.96v6.553c3.635-1.331 6.656-2.355 9.574-3.584 22.835-9.472 43.52-23.296 60.826-40.755 40.345-40.755 60.21-89.6 56.525-146.586-4.455-67.788-36.864-119.5-95.488-154.982-19.354-11.725-39.27-22.63-58.983-33.74L653.735 308.53a9.216 9.216 0 0 0-10.547 0.154c-17.562 10.24-35.277 20.173-52.89 30.157l-30.976 17.715z m112.23 118.528V759.91c0 14.183-5.324 24.986-17.92 32.052l-103.116 58.726-103.578 59.085c-1.536 0.87-2.97 2.048-4.966 3.43 2.355 1.895 4.096 3.482 5.939 4.864 56.832 40.807 118.784 50.842 185.088 27.7A190.822 190.822 0 0 0 761.1 768.46c0.41-78.95 0-157.901 0.154-236.8a9.114 9.114 0 0 0-5.222-9.165c-26.47-14.848-52.736-29.952-79.156-45.005-1.177-0.717-2.56-1.126-5.324-2.406z m-272.179 101.12l112.64 64.153 112.026-63.846a12.8 12.8 0 0 0 0.41-1.894c0-41.012 0-82.074 0.153-123.136 0-3.38-1.69-4.762-4.3-6.247a41552.384 41552.384 0 0 1-98.612-56.115l-9.728-5.376-3.942 2.15-104.243 59.188a7.936 7.936 0 0 0-4.506 8.09c0.154 28.978 0 57.958 0 86.988l0.051 36.045z" p-id="6147" fill="#eeeeee"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1755738730247" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4931" width="256" height="256" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M79.11424 270.67904l116.2496 66.9952a97.1776 97.1776 0 0 0-4.87424 30.57664v268.7232a97.53088 97.53088 0 0 0 47.39584 83.72736l219.42784 131.31264a97.28 97.28 0 0 0 15.4112 7.45984v153.84576a97.6384 97.6384 0 0 1-11.06944-5.70368L120.32 803.34848a97.51552 97.51552 0 0 1-47.44704-83.62496v-414.72c0-11.89376 2.19648-23.5008 6.2464-34.32448z m871.4752 34.32448v414.67392a97.52576 97.52576 0 0 1-47.44704 83.67104L561.8176 1007.6672c-3.6096 2.0992-7.31648 3.99872-11.06944 5.6576v-157.6448c2.28864-1.1264 4.5312-2.34496 6.72768-3.6608l219.42784-131.31264a97.53088 97.53088 0 0 0 47.44192-83.72736V368.25088a97.07008 97.07008 0 0 0-1.36192-16.384l124.3904-71.68c2.0992 7.99744 3.2256 16.3328 3.2256 24.81664zM561.8176 17.06496l336.59904 201.39008-118.2464 68.11648c-1.0752-0.70144-2.16576-1.3824-3.26656-2.048l-219.42784-131.31264a97.52576 97.52576 0 0 0-100.15744 0L248.32 278.28224l-113.76128-65.536L461.65504 17.06496a97.52576 97.52576 0 0 1 100.15744 0z" p-id="4932" fill="#eeeeee"></path><path d="M264.60672 365.22496L483.84 491.0336v275.98848a97.34144 97.34144 0 0 1-31.7952-12.09344L304.7936 667.1616a97.52576 97.52576 0 0 1-47.54432-83.77344V402.3808c0-12.96896 2.5856-25.5488 7.31136-37.15584h0.0512z m482.10944 37.15584v181.00736c0 34.3552-18.07872 66.18112-47.5904 83.77344l-147.21536 87.76704c-2.58048 1.51552-5.21728 2.92864-7.90016 4.2496V499.6608l201.04704-115.27168c1.11104 5.9392 1.664 11.9552 1.65888 17.99168z m-194.80576-171.54048l147.26144 87.77216a97.536 97.536 0 0 1 17.26464 13.16352l-193.39264 110.83776h-3.21536L304.29696 318.90432l0.49152-0.34304L452.0448 230.78912a97.52576 97.52576 0 0 1 99.86048 0v0.0512z" p-id="4933" fill="#eeeeee"></path></svg>
\ No newline at end of file
<template>
<div class="home-wrap">
<div class="card-box">
<div
class="card"
:class="{ cardActive: curModelIndex === index }"
v-for="(item, index) in largeModelList"
:key="index"
@click="handleToTheme(index)"
>
<div class="logo">
<img :src="item.logo" alt="" />
</div>
<div class="title">{{ item.name }}</div>
<div class="desc">{{ item.description }}</div>
<div class="close" @click.stop="handleDelte(item.id)"></div>
<div
class="delete-dialog"
v-if="isShowDeleteDialog && activeModelId === item.id"
>
<div class="delete-tip">确认删除当前专题吗?</div>
<div class="footer">
<div class="btn cancle" @click.stop="handleCancleDelete">取消</div>
<div class="btn confirm" @click.stop="handleConfirm">确认</div>
</div>
</div>
</div>
</div>
<div class="btn-box">
<!-- <div
class="btn enter"
:class="{ enterActive: curModelIndex > -1 }"
@click="handleToTheme"
>
进入专题
</div> -->
<div class="btn add" @click="handleAdd">新增</div>
<!-- <div
class="btn delete"
:class="{ deleteActive: curModelIndex > -1 }"
@click.stop="handleDelte"
>
删除
</div> -->
<div class="total-box">
共计<span>{{ totalNum }}</span
>条数据
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import router from "@/router/index.js";
import { getGraphList } from "@/api/graphApi";
import logo1 from "./assets/images/icon1.svg";
import logo2 from "./assets/images/icon2.svg";
import logo3 from "./assets/images/icon3.svg";
import logo4 from "./assets/images/icon4.svg";
import logo5 from "./assets/images/icon5.svg";
const largeModelList = ref([
// {
// logo: logo1,
// name: "全球主流大模型",
// description: "1234567890123456789012345678901234567890123",
// },
]);
const totalNum = ref(0);
const handleGetGraphList = async () => {
try {
const res = await getGraphList();
if (res.data && res.data.length) {
largeModelList.value = res.data.map((item) => ({
...item,
id: item.group_id,
name: item.group_name,
}));
totalNum.value = res.total;
largeModelList.value.forEach((item, index) => {
switch (index) {
case 1:
item.logo = logo1;
break;
case 2:
item.logo = logo2;
break;
case 3:
item.logo = logo3;
break;
case 4:
item.logo = logo4;
break;
default:
item.logo = logo5;
}
});
}
} catch (error) {}
};
// 新增
const handleAdd = () => {
router.push("createTheme");
};
const isShowDeleteDialog = ref(false);
const activeModelId = ref("");
// 删除
const handleDelte = (id) => {
isShowDeleteDialog.value = true;
activeModelId.value = id;
};
// 确认删除
const handleConfirm = () => {
if (id == event_graph_id.value) {
event_graph_id.value = null;
event_graph_name.value = null;
}
// requestDeleteEntityById({
// entity_id: id
// }).then((res) => {
// if (res.data && res.data.success) {
// globalProxy.success("删除成功");
// handleRefresh();
// } else {
// globalProxy.$message.error(res.data.error_msg);
// }
// }).catch(() => {
// globalProxy.$message({
// type: "info",
// message: "已取消删除",
// });
// });
};
// 取消删除
const handleCancleDelete = () => {
isShowDeleteDialog.value = false;
};
// 进入专题
const handleToTheme = (index) => {
window.localStorage.setItem(
"theme",
JSON.stringify(largeModelList.value[index])
);
router.push({ path: "/themeManage" });
};
onMounted(() => {
handleGetGraphList();
});
</script>
<style lang="scss" scoped>
.home-wrap {
width: 100%;
height: 100%;
display: flex;
position: relative;
.card-box {
flex: 7;
display: flex;
flex-wrap: wrap;
overflow-y: auto;
justify-content: left;
align-items: center;
.card {
width: 300px;
height: 300px;
// border-radius: 5px;
border: 2px solid #aaa;
margin: 30px;
background: rgba(125, 125, 125, 0.2);
cursor: pointer;
position: relative;
&:hover {
// background: rgba(157, 104, 7, 0.3);
border: 2px solid rgba(249, 162, 1);
}
.close {
position: absolute;
top: 15px;
right: 15px;
width: 24px;
height: 24px;
background: url("./assets/images/close.svg");
background-size: 100% 100%;
&:hover {
background: url("./assets/images/closeActive.svg") !important;
background-size: 100% 100% !important;
}
}
.logo {
width: 90px;
height: 90px;
margin-left: 110px;
margin-top: 20px;
img {
width: 100%;
height: 100%;
}
}
.title {
margin-top: 30px;
height: 30px;
line-height: 30px;
text-align: center;
color: #fff;
font-weight: bold;
font-size: 24px;
color: rgb(122, 167, 243);
}
.desc {
margin-top: 20px;
height: 120px;
line-height: 30px;
overflow-y: auto;
word-wrap: break-word;
// background: rgba(85,85,85,0.3);
border-radius: 5px;
color: #aaa;
}
}
.cardActive {
// background: rgba(157, 104, 7, 0.5);
border: 2px solid rgba(249, 162, 1);
}
}
.btn-box {
flex: 1;
position: relative;
.total-box {
position: absolute;
left: 30px;
bottom: 24px;
width: 100%;
text-align: left;
font-size: 20px;
span {
font-weight: bold;
font-size: 30px;
padding: 0 5px;
color: rgba(45, 118, 245, 1);
}
}
.btn {
width: 110px;
height: 50px;
text-align: center;
line-height: 50px;
// border-radius: 10px;
font-size: 20px;
cursor: pointer;
color: #ccc;
}
.enterActive {
background: rgba(45, 118, 245, 0.3) !important;
border: 1px solid rgba(45, 118, 245, 1) !important;
&:hover {
background: rgba(45, 118, 245, 0.8);
color: #fff;
}
}
.enter {
position: absolute;
left: 30px;
bottom: 320px;
background: rgba(111, 114, 118, 0.3);
border: 1px solid rgb(59, 61, 65);
}
.add {
position: absolute;
left: 30px;
top: 40px;
background: rgba(45, 118, 245, 0.3);
border: 1px solid rgba(45, 118, 245, 1);
&:hover {
background: rgba(45, 118, 245, 0.8);
color: #fff;
}
}
.delete {
position: absolute;
left: 30px;
bottom: 120px;
background: rgba(111, 114, 118, 0.3);
border: 1px solid rgb(59, 61, 65);
}
.deleteActive {
background: rgba(245, 65, 45, 0.3) !important;
border: 1px solid rgba(245, 65, 45, 1) !important;
&:hover {
background: rgba(245, 65, 45, 0.8);
color: #fff;
}
}
}
.delete-dialog {
position: absolute;
z-index: 99;
right: -320px;
top: 20px;
width: 300px;
height: 200px;
border: 1px solid #aaa;
background: rgba(5, 30, 55, 1);
.delete-tip {
margin-top: 50px;
height: 30px;
font-size: 24px;
color: orange;
}
.footer {
margin-top: 50px;
height: 40px;
display: flex;
justify-content: space-around;
.btn {
height: 40px;
width: 80px;
font-size: 18px;
text-align: center;
line-height: 40px;
border-radius: 5px;
cursor: pointer;
}
.cancle {
background: rgba(111, 114, 118, 0.3);
border: 1px solid rgba(111, 114, 118, 1);
&:hover {
background: rgba(111, 114, 118, 0.7) !important;
}
}
.confirm {
background: rgba(45, 118, 245, 0.3);
border: 1px solid rgba(45, 118, 245, 1);
&:hover {
background: rgba(45, 118, 245, 0.7) !important;
}
}
}
}
}
</style>
<template>
<div class="home-page">
<el-header class="home-header layout-header">
<div class="g-logo">
<span class="home-header-logo" @click="defaultSelect">Hypergraph</span>
</div>
<div class="layout-menu">
<el-menu
class="home-menu"
mode="horizontal"
:default-active="activeIndex"
v-if="route.path !=='/home'"
>
<el-menu-item
v-for="(item, index) in menuList"
:key="index.url"
:index="item.url"
@click="handleSelect(item.url)"
class="home-menu-item"
>{{ item.name }}</el-menu-item
>
</el-menu>
</div>
</el-header>
<div class="layout-container home-content">
<router-view @changeIndex="changeActiveIndex"></router-view>
</div>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance,
} from "vue";
import { useRoute, useRouter } from "vue-router";
let route = useRoute();
let router = useRouter();
let activeIndex = ref("");
let menuList = ref([
{
url: "/themeManage",
name: "专题管理",
includes: [""],
},
// {
// url: "/themeSearch",
// name: "专题搜索",
// includes: [""],
// },
{
url: "/mechanismAnalysis",
name: "机理分析",
includes: [""],
},
]);
watch(
() => route.path,
(val) => {
activeIndex.value = val
}
);
onMounted(() => {
if (route.fullPath === "/createTheme") {
activeIndex.value = "/themeManage";
} else {
activeIndex.value = route.fullPath;
}
});
function defaultSelect() {
router.push({
path: "/home",
});
activeIndex.value = "/home";
}
function changeActiveIndex(path) {
router.push({
path: path,
});
activeIndex.value = path;
}
function handleSelect(url) {
router.push({
path: url,
});
activeIndex.value = url;
}
</script>
<style lang="scss" scoped>
.home-page {
width: 100%;
height: 100%;
position: fixed;
background-image: url("@/assets/images/main-bg-dark-new.png");
.home-header {
height: 60px;
padding: 0;
border-bottom: 1px solid rgba(204, 204, 204, 0.3);
.g-logo {
width: 300px;
min-width: 240px;
line-height: 60px;
text-align: center;
color: #ffffff;
font-size: 24px;
cursor: pointer;
}
}
.layout-header {
display: flex;
.home-header-logo {
width: 10%;
min-width: 240px;
font-family: "FZZZHONGJW--GB1-0";
}
.layout-menu {
flex: 1;
.home-menu {
border-bottom: 1px solid rgba(204, 204, 204, 0.3);
margin: 0 auto;
.home-menu-item {
font-size: 24px;
font-family: "FZZZHONGJW--GB1-0";
padding-top: 6px;
}
}
}
}
.layout-container {
height: calc(100vh - 60px);
}
}
</style>
<style>
.home-page .el-menu {
background-color: transparent;
}
.home-page .el-menu--horizontal > .el-menu-item {
color: #ffffff;
}
.home-page .el-menu--horizontal .el-menu-item:not(.is-disabled):focus {
background-color: transparent !important;
}
.home-page .el-menu--horizontal > .el-menu-item.is-active {
border-bottom: none;
}
.home-page .el-menu--horizontal > .el-menu-item {
border-bottom: none;
}
</style>
<template>
<div class="theme-search">
<MechanismCom :graphData="graphData"></MechanismCom>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import {
useRoute,
useRouter
} from 'vue-router';
import MechanismCom from "./component/MechanismAnalysis.vue";
import { getEvents } from "@/api/graphApi";
let route = useRoute();
let router = useRouter();
let graphData = ref({
nodes: {},
edges: [],
time: [],
type: {}
})
let eventsData = ref()
// 获取图谱数据列表
function getGraphList() {
getEvents().then((res) => {
if (res) {
eventsData.value = res.data;
graphData.value.nodes = eventsData.value.events;
graphData.value.edges = eventsData.value.events_relations;
graphData.value.time = eventsData.value.events_time;
graphData.value.type = eventsData.value.events_type;
graphData.value.relation_type = eventsData.value.relation_type;
}
})
}
onMounted(() => {
getGraphList();
})
</script>
<style lang="scss" scoped>
.theme-search {
position: relative;
width: 100%;
height: 100%;
.header {
position: absolute;
z-index: 1000;
width: 40%;
height: 90px;
position: absolute;
top: 0;
left: 50px;
display: flex;
align-items: center;
}
.map {
position: absolute;
height: 100%;
width: 100%;
z-index: 99;
}
.graph {
position: absolute;
width: 100%;
height: 100%;
}
}
</style>
<template>
<div class="conclusions-main-right-content-panel">
<template v-for="(item, index) in conclusionsArr" :key="index">
<div class="conclusions-main-right-content">
<div class="conclusions-main-right-content-title">
<div style="display: flex">
<div class="conclusions-main-right-content-title-span">{{item.title}}</div>
</div>
<div class="conclusions-main-right-content-text" :title="item.content">
{{item.content}}
</div>
</div>
</div>
</template>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted
} from 'vue'
let props = defineProps({
conclusionsArr: {
type: Array,
default: () => []
}
})
</script>
<style lang="scss" scoped>
.conclusions-main-right-content-panel {
height: 100%;
overflow-y: auto;
overflow-x: hidden;
.conclusions-main-right-content {
width: 100%;
display: flex;
border: 2px solid transparent;
background-image: linear-gradient(113deg, rgba(0, 116, 200, 0.30) 0%, rgba(0, 45, 79, 0.00) 100%);
border-image: linear-gradient(113deg, #05457F 0%, rgba(5, 47, 97, 0.3) 100%)1;
border-radius: 2px;
margin-bottom: 10px;
.conclusions-main-right-content-title {
margin: 14px 0 10px 14px;
width: 100%;
display: flex;
flex-direction: column;
justify-content: left;
.conclusions-main-right-content-title-span {
height: 22px;
width: 234px;
font-size: 24px;
color: #39C4FF;
letter-spacing: 0;
font-weight: 700;
text-align: justify;
}
.conclusions-main-right-content-text {
width: calc(100% - 16px);
font-size: 22px;
color: #FFFFFF;
letter-spacing: 0;
text-align: justify;
line-height: 32px;
font-weight: 500;
margin-top: 10px;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
</style>
\ No newline at end of file
<template>
<div class="data-list">
<template v-for="(item, index) in list" :key="item.id">
<div
class="data-item text-ellipsis"
:class="{
'cursor-pointer': selectable
}"
@click="handleClick(item)"
>
<div class="title-container">
<div class="index text-ellipsis" :title="index + 1">
<span>{{ index + 1 }}</span>
</div>
<div class="name-text text-ellipsis">
<span :title="item.title">{{ item.title || '-' }}</span>
</div>
</div>
<div class="extra-container">
<div class="location-text text-ellipsis">
<span :title="item.country">{{ item.country }}</span>
</div>
<div class="date-text text-ellipsis">
<span :title="item.date">{{ item.date || '-' }}</span>
</div>
</div>
</div>
</template>
</div>
</template>
<script setup>
defineProps({
list: {
type: Array,
default: () => []
},
selectable: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['select']);
function handleClick(item) {
emit('select', item);
}
</script>
<style lang="scss" scoped>
.data-list {
}
.data-item {
padding: 4px 0;
border-bottom: 1px solid #6E87AF;
&:last-child {
border: none;
}
.title-container {
display: flex;
align-items: center;
gap: 12px;
.index {
flex: none;
min-width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
font-size: 18px;
color: #FFF;
font-weight: 700;
background: #48BCFF;
border-radius: 1px;
&.index1 {
background: #E22626;
}
&.index2 {
background: #FFAE51;
}
&.index3 {
background: #48BCFF;
}
}
.name-text {
font-size: 18px;
color: #fff;
font-weight: 500;
text-align: left;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
}
.extra-container {
margin-top: 2px;
font-size: 18px;
color: #AFD4F5;
font-weight: 500;
display: flex;
justify-content: space-between;
.location-text {
color: #B3E19D;
}
.date-text {
flex: none;
}
}
}
</style>
\ No newline at end of file
<template>
<div class="data-list">
<template v-for="(item, index) in list" :key="item.id">
<div
class="data-item text-ellipsis"
:class="{
'cursor-pointer': selectable
}"
@click="handleClick(item)"
>
<div class="title-container">
<div class="index text-ellipsis" :title="index + 1">
<span>{{ index + 1 }}</span>
</div>
<div class="name-text text-ellipsis">
<span :title="item.date + item.country" style="color: #AFD4F5;" >{{ "(" + item.date + "-" + item.country + ") " }}</span>
<span :title="item.title">{{ item.title || '-' }}</span>
</div>
</div>
<div class="extra-container">
<div class="location-text text-ellipsis">
<span :title="item.achievement">{{ item.achievement }}</span>
</div>
</div>
</div>
</template>
</div>
</template>
<script setup>
defineProps({
list: {
type: Array,
default: () => []
},
selectable: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['select']);
function handleClick(item) {
emit('select', item);
}
</script>
<style lang="scss" scoped>
.data-list {
}
.data-item {
padding: 4px 0;
border-bottom: 1px solid #6E87AF;
&:last-child {
border: none;
}
.title-container {
display: flex;
align-items: center;
gap: 12px;
.index {
flex: none;
min-width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
font-size: 18px;
color: #FFF;
font-weight: 700;
background: #48BCFF;
border-radius: 1px;
&.index1 {
background: #E22626;
}
&.index2 {
background: #FFAE51;
}
&.index3 {
background: #48BCFF;
}
}
.name-text {
font-size: 18px;
color: #fff;
font-weight: 500;
text-align: left;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
}
.extra-container {
margin-top: 2px;
font-size: 18px;
color: #AFD4F5;
font-weight: 500;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
text-align: left;
.location-text {
color: #B3E19D;
}
.date-text {
flex: none;
}
}
}
</style>
\ No newline at end of file
<template>
<div class="dialog-container">
<div style="width: 100%;text-align: left;">
<el-button @click="addRowCommon()" type="primary" icon="CirclePlus">新增行</el-button>
<el-button @click="addColumnCommon('end')" type="primary" icon="CirclePlus">新增列</el-button>
<el-button type="primary" @click.stop="saveData()">保存</el-button>
</div>
<el-table border :data="tableData" style="width: 100%;align: center;margin-top: 10px" max-height="600" :cell-class-name="tableCellClassName" @cell-click="cellClick" @header-click="headerTest" @header-contextmenu="headercontextmenu">
<template v-for="item in tableColumnList" :key="item.id">
<el-table-column align="center" :label="item.name" :prop="item.id">
<template v-slot="scope">
<!-- 判断是哪个单元格被点击 -->
<template v-if="scope.row.index === tabClickIndex && scope.column.index === tableCellClickColumnIndex">
<!-- 被点击的单元格变成输入框 -->
<el-input ref="focusingRef" v-model="scope.row[item.id]" autofocus style="height: 100%" @blur="inputBlur" />
</template>
<template v-else-if="scope.column.label === '颜色配置'"><el-color-picker v-model="scope.row[item.id]" @click.stop="handleColorClick"/></template>
<template v-else>{{ scope.row[item.id] }}</template>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="320" align="center">
<template v-slot="scope">
<el-button @click="addRowCommon(scope, 'previous')" type="primary">向上新增行</el-button>
<el-button @click="addRowCommon(scope, 'next')" type="primary">向下新增行</el-button>
<!-- <el-button type="primary" @click.stop="edit(scope)">编辑</el-button> -->
<el-button type="danger" @click.stop="deleteRow(scope)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="dialogFormVisible" title="修改名称" width="800">
<el-form ref="form" :model="tableHeader" label-width="80px">
<el-form-item label="表头名称">
<el-input v-model="tableHeader.tableHeaderName" placeholder="请输入表头名称" />
</el-form-item>
</el-form>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</el-dialog>
<el-dialog v-model="dialogDeleteVisible" title="删除/新增列" width="400">
<el-button @click="deleteColumn">删除此列</el-button>
<el-button type="primary" @click="addColumnCommon('previous')">左侧新增列</el-button>
<el-button type="primary" @click="addColumnCommon('next')">右侧新增列</el-button>
</el-dialog>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import {
Plus
} from '@element-plus/icons-vue'
const {
appContext
} = getCurrentInstance();
const globalProxy = appContext.config.globalProperties;
const emit = defineEmits(['saveTableData']);
let contextMenuVisible = ref(false)
let tableCellIndex = ref("") // 列计数器
let columnName = ref("") // 列名
let tableCell = ref({
tableCellData: ""
}) // 表格定义
let dialogForTable = ref(false) // 表格编辑弹出框
let tabClickIndex = ref(null) // 点击的单元格
let tableHeader = ref({
tableHeaderName: "",
property: ""
}) // 表头定义
let dialogFormVisible = ref(false) // 表头编辑弹出框
let dialogDeleteVisible = ref(false)
let tableCellClickColumnIndex = ref(0)
let focusingRef = ref()
let tableColumnList = ref([])
let tableData = ref([])
let num = ref(0) // 列个数
let currentColIndex = ref(0)
let currentColProperty = ref("")
let currentColLabel = ref("")
let props = defineProps({
editTableData: {
type: Object,
default: {}
},
tabIndex: {
type: String,
default: ""
}
})
watch(
() => props.editTableData,
(newVal, oldVal) => {
if (newVal){
tableColumnList.value = newVal.titles;
tableData.value = newVal.data;
num.value = tableColumnList.value.length;
}
}, {
deep: true,
immediate: true
}
);
function getTableData() {
tableColumnList.value = [];
tableData.value = [];
props.editTableData.data.forEach((item, index) => {
tableColumnList.value.push({
id: index + 1,
name: item.event_title
})
});
tableColumnList.value.unshift({
id: 0,
name: "类型"
});
let namesArr = tableColumnList.value.map(item => item.id);
for (let i = 0; i < props.editTableData.data[0].target.length; i++) {
let newRow = {}
newRow[0] = props.editTableData.data[0].target[i].name;
tableData.value.push(newRow);
}
props.editTableData.data.forEach((value, index) => {
for (let j = 0; j < value.target.length; j++) {
const ele = value.target[j];
tableData.value[j][namesArr[index + 1]] = ele.value;
}
});
}
// function getIndex(index) {
// //获取点击的索引
// tableCellIndex.value = null;
// tableCellIndex.value = index;
// }
function cellClick(row, column, cell, event) {
//cell-click 当某个单元格被点击点击事件
tabClickIndex.value = row.index;
tableCellClickColumnIndex.value = column.index;
}
function handleColorChange(value) {
};
function handleColorClick(value) {
};
// 添加表头,修改表头
function headerTest(val) {
// 点击的不是添加表头表头,则是其他表头
tableHeader.value.tableHeaderName = "";
tableHeader.value.property = "";
// tableHeader存储当前被点击头部的label和属性 绑定到弹框的form表单上
tableHeader.value.tableHeaderName = val.label;
tableHeader.value.property = val.property;
// 弹框
dialogFormVisible.value = true;
}
// 右键
function headercontextmenu(val, event) {
currentColIndex.value = val.index;
currentColProperty.value = val.property;
currentColLabel.value = val.label;
event.preventDefault(); // 隐藏浏览器默认右击菜单
dialogDeleteVisible.value = true;
// globalProxy.$confirm("是否需要删除此列", "提示", {
// confirmButtonText: "确定",
// cancelButtonText: "取消",
// type: "warning",
// })
// .then(() => {
// tableColumnList.value.splice(val.index, 1); //删除对应的表头
// tableData.value.forEach((item) => {
// //删除对应的表头内容
// delete item[val.property];
// });
// })
// .catch(() => {
// globalProxy.$message({
// type: "info",
// message: "已取消删除",
// });
// });
}
function inputBlur(row, event, column) {
//失去焦点事件
tabClickIndex.value = null;
tableCellClickColumnIndex.value = null;
}
function tableCellClassName({
row,
column,
rowIndex,
columnIndex
}) {
// 把每一行的索引放进row
row.index = rowIndex;
column.index = columnIndex;
}
// 表头编辑提交
function submitForm() {
for (let i = 0; i < tableColumnList.value.length; i++) {
if (tableColumnList.value[i].id === tableHeader.value.property) {
tableColumnList.value[i].name = tableHeader.value.tableHeaderName;
}
}
dialogFormVisible.value = false;
}
// 取消表头编辑
function cancel() {
dialogFormVisible.value = false;
}
// 新增行
function addRowCommon(scope, type) {
let newRow = {}
let namesArr = tableColumnList.value.map(item => item.id);
namesArr.forEach((item, index) => {
newRow[item] = null;
});
if (scope){
if (type === "next"){
tableData.value.splice(scope.$index + 1, 0, newRow)
} else if (type === "previous"){
tableData.value.splice(scope.$index, 0, newRow)
}
} else {
tableData.value.push(newRow)
}
}
//新增列
function addColumnCommon(type) {
let newId = generateRandomId();
let newHeader = {
id: newId.toString(),
name: "点击编辑表头名称",
};
if (type === "previous"){
if (currentColIndex.value === 0){
globalProxy.$message({
type: "info",
message: "该列为第一列,不可向前新增",
});
} else {
tableColumnList.value.splice(currentColIndex.value, 0, newHeader);
for (let i = 0; i < tableData.value.length; i++) {
// 向对象中追加属性, 参数 object/Array property/index value
tableData.value[i][parseInt(newId)] = "";
}
}
} else if (type === "next"){
if (currentColLabel.value == "颜色配置"){
globalProxy.$message({
type: "info",
message: "该列为最后一列,不可向后新增",
});
} else {
tableColumnList.value.splice(currentColIndex.value + 1, 0, newHeader);
for (let i = 0; i < tableData.value.length; i++) {
// 向对象中追加属性, 参数 object/Array property/index value
tableData.value[i][parseInt(newId)] = "";
}
}
} else if (type === "end"){
if (props.tabIndex == "0"){
addElementToSecondToLast({
id: newId.toString(),
name: "点击编辑表头名称",
});
} else if (props.tabIndex == "1"){
addElementToLast({
id: newId.toString(),
name: "点击编辑表头名称",
});
}
for (let i = 0; i < tableData.value.length; i++) {
// 向对象中追加属性, 参数 object/Array property/index value
tableData.value[i][parseInt(newId)] = "";
}
}
dialogDeleteVisible.value = false;
}
function addElementToSecondToLast(value) {
if (tableColumnList.value.length > 1) {
tableColumnList.value.splice(tableColumnList.value.length - 1, 0, value);
} else {
tableColumnList.value.push(value);
}
}
function addElementToLast(value) {
if (tableColumnList.value.length > 1) {
tableColumnList.value.splice(tableColumnList.value.length, 0, value);
} else {
tableColumnList.value.push(value);
}
}
function deleteColumn() {
if (currentColLabel.value == "颜色配置" || currentColLabel.value == "操作"){
globalProxy.$message({
type: "info",
message: "该列不可删除",
});
} else {
tableColumnList.value.splice(currentColIndex.value, 1); //删除对应的表头
tableData.value.forEach((item) => {
//删除对应的表头内容
delete item[currentColProperty.value];
});
dialogDeleteVisible.value = false;
}
}
function generateRandomId() {
let timestamp = Date.now().toString(); // 获取当前时间戳
num.value++; // 递增计数器加1
return timestamp + num.value;
}
function edit(scope) {
}
function saveData(scope) {
let params = {
data: tableData.value,
titles: tableColumnList.value
}
emit("saveTableData", params);
}
function deleteRow(scope) {
tableData.value.splice(scope.$index, 1)
}
</script>
<style lang="scss" scoped>
</style>
<template>
<div class="dialog-container">
<el-tabs v-model="activeName" type="card" class="dialog-tabs" @tab-click="handleClick">
<el-tab-pane label="事件" name="item">
<div style="width: 100%;text-align: left;">
<el-button @click="addRowCommon()" type="primary" icon="CirclePlus">新增行</el-button>
<el-button type="danger" @click.stop="delData(1)">删除所有行</el-button>
</div>
<el-table border :data="tableData" style="width: 100%;align: center;margin-top: 10px" max-height="600" :cell-class-name="tableCellClassName" @cell-click="cellClick">
<template v-for="item in tableColumnList" :key="item.id">
<el-table-column align="center" :label="item.name" :prop="item.id" v-if="item.name === 'id' ? false : true">
<template v-slot="scope">
<!-- 判断是哪个单元格被点击 -->
<template v-if="scope.row.index === tabClickIndex && scope.column.index === tableCellClickColumnIndex && scope.column.label !== '事件类型'">
<!-- 被点击的单元格变成输入框 -->
<!-- <el-form-item :rules="tableFormRules.item"> -->
<el-input ref="focusingRef" v-model="scope.row[item.id]" autofocus style="height: 100%" placeholder="请输入" @blur="inputBlur" />
<!-- </el-form-item> -->
</template>
<template v-else-if="scope.column.label === '事件类型'">
<el-select v-model="scope.row[item.id]" filterable placeholder="请选择事件类型" clearable>
<el-option v-for="item in itemsTypeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</template>
<template v-else-if="scope.column.label === '时间' || scope.column.label === '事件'">{{ scope.row[item.id] }}</template>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="320" align="center">
<template v-slot="scope">
<el-button @click="addRowCommon(scope, 'previous')" type="primary">向上新增行</el-button>
<el-button @click="addRowCommon(scope, 'next')" type="primary">向下新增行</el-button>
<el-button type="danger" @click.stop="deleteRow(scope, 1)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="事件类型" name="itemType">
<div style="width: 100%;text-align: left;">
<el-button @click="addRowCommonType()" type="primary" icon="CirclePlus">新增行</el-button>
<el-button type="danger" @click.stop="delData(2)">删除所有行</el-button>
</div>
<el-table border :data="tableDataType" style="width: 100%;align: center;margin-top: 10px" max-height="600" :cell-class-name="tableCellClassName" @cell-click="cellClick">
<template v-for="item in tableColumnListType" :key="item.id">
<el-table-column align="center" :label="item.name" :prop="item.id">
<template v-slot="scope">
<template v-if="scope.row.index === tabClickIndex && scope.column.index === tableCellClickColumnIndex">
<el-input ref="focusingRef" v-model="scope.row[item.id]" autofocus style="height: 100%" placeholder="请输入" @blur="inputBlur" />
</template>
<template v-else>{{ scope.row[item.id] }}</template>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="320" align="center">
<template v-slot="scope">
<el-button @click="addRowCommonType(scope, 'previous')" type="primary">向上新增行</el-button>
<el-button @click="addRowCommonType(scope, 'next')" type="primary">向下新增行</el-button>
<el-button type="danger" @click.stop="deleteRow(scope, 2)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="关系" name="relation">
<div style="width: 100%;text-align: left;">
<el-button @click="addRowCommonRel()" type="primary" icon="CirclePlus">新增行</el-button>
<el-button type="danger" @click.stop="delData(3)">删除所有行</el-button>
</div>
<el-table border :data="tableDataRel" style="width: 100%;align: center;margin-top: 10px" max-height="600" :cell-class-name="tableCellClassName" @cell-click="cellClick">
<template v-for="item in tableColumnListRel" :key="item.id">
<el-table-column align="center" :label="item.name" :prop="item.id">
<template v-slot="scope">
<template v-if="scope.column.label === '事件1' || scope.column.label === '事件2'">
<el-select v-model="scope.row[item.id]" filterable placeholder="请选择事件" clearable>
<el-option v-for="item in itemsList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</template>
<template v-else-if="scope.column.label === '关系类型'">
<el-select v-model="scope.row[item.id]" filterable placeholder="请选择关系类型" clearable>
<el-option v-for="item in relTypeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</template>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="320" align="center">
<template v-slot="scope">
<el-button @click="addRowCommonRel(scope, 'previous')" type="primary">向上新增行</el-button>
<el-button @click="addRowCommonRel(scope, 'next')" type="primary">向下新增行</el-button>
<el-button type="danger" @click.stop="deleteRow(scope, 3)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="关系类型" name="relationType">
<div style="width: 100%;text-align: left;">
<el-button @click="addRowCommonRelType()" type="primary" icon="CirclePlus">新增行</el-button>
<el-button type="danger" @click.stop="delData(4)">删除所有行</el-button>
</div>
<el-table border :data="tableDataRelType" style="width: 100%;align: center;margin-top: 10px" max-height="600" :cell-class-name="tableCellClassName" @cell-click="cellClick">
<template v-for="item in tableColumnListRelType" :key="item.id">
<el-table-column align="center" :label="item.name" :prop="item.id">
<template v-slot="scope">
<template v-if="scope.row.index === tabClickIndex && scope.column.index === tableCellClickColumnIndex">
<el-input ref="focusingRef" v-model="scope.row[item.id]" autofocus style="height: 100%" placeholder="请输入" @blur="inputBlur" />
</template>
<template v-else>{{ scope.row[item.id] }}</template>
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="320" align="center">
<template v-slot="scope">
<el-button @click="addRowCommonRelType(scope, 'previous')" type="primary">向上新增行</el-button>
<el-button @click="addRowCommonRelType(scope, 'next')" type="primary">向下新增行</el-button>
<el-button type="danger" @click.stop="deleteRow(scope, 4)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
<el-button class="save-data" type="primary" @click.stop="saveDataCommon()">保存</el-button>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import {
Plus
} from '@element-plus/icons-vue'
const emit = defineEmits(['saveTableDataItems']);
const {
appContext
} = getCurrentInstance();
const globalProxy = appContext.config.globalProperties;
const activeName = ref('item')
let tabClickIndex = ref(null) // 点击的单元格
let tableCellClickColumnIndex = ref(null)
let focusingRef = ref()
let itemsTypeList = ref([])
let relTypeList = ref([])
let tableColumnList = ref([])
let tableColumnListRel = ref([])
let tableColumnListType = ref([])
let tableColumnListRelType = ref([])
let tableData = ref([])
let tableDataType = ref([])
let tableDataRel = ref([])
let tableDataRelType = ref([])
let itemsList = ref([])
const tableFormRules = reactive({
id: [
{ required: true, message: "请输入id", trigger: "blur" }
],
time: [
{ required: true, message: "请输入时间", trigger: "blur" }
],
item: [
{ required: true, message: "请输入事件", trigger: "blur" }
],
itemType: [
{ required: true, message: "请选择事件类型", trigger: "change" }
]
});
let props = defineProps({
editTableData: {
type: Object,
default: {}
},
tabIndex: {
type: String,
default: ""
}
})
watch(
() => props.editTableData,
(newVal, oldVal) => {
if (Object.keys(newVal).length > 0){
getTableData(newVal)
} else {
clearTables()
}
}, {
immediate: true
}
);
watch(
() => tableData.value,
(newVal, oldVal) => {
if (Object.keys(newVal).length > 0){
refreshItemsList();
}
}, {
immediate: true,
deep: true
}
);
watch(
() => tableDataType.value,
(newVal, oldVal) => {
if (Object.keys(newVal).length > 0){
refreshItemsTypeList();
}
}, {
immediate: true,
deep: true
}
);
watch(
() => tableDataRelType.value,
(newVal, oldVal) => {
if (Object.keys(newVal).length > 0){
refreshRelTypeList();
}
}, {
immediate: true,
deep: true
}
);
const handleClick = (tab, event) => {
tabClickIndex.value = null;
tableCellClickColumnIndex.value = null;
}
function clearTables() {
tableData.value = [];
tableColumnList.value = [];
tableDataType.value = [];
tableColumnListType.value = [];
tableDataRel.value = [];
tableColumnListRel.value = [];
tableDataRelType.value = [];
tableColumnListRelType.value = [];
}
function getTableData(val) {
tableData.value = val.events.data;
tableColumnList.value = val.events.titles;
tableDataType.value = val.event_types.data;
tableColumnListType.value = val.event_types.titles;
tableDataRel.value = val.relations.data;
tableColumnListRel.value = val.relations.titles;
tableDataRelType.value = val.relation_types.data;
tableColumnListRelType.value = val.relation_types.titles;
}
function refreshItemsList() {
let idsObject = tableColumnList.value.find(item => item.name === "id");
let itemsObject = tableColumnList.value.find(item => item.name === "事件");
itemsList.value = [];
for (let index = 0; index < tableData.value.length; index++) {
const element = tableData.value[index];
itemsList.value.push({
value: element[idsObject.id],
label: element[itemsObject.id]
})
}
}
function refreshItemsTypeList(params) {
let idsObject = tableColumnListType.value.find(item => item.name === "类型名称");
itemsTypeList.value = [];
for (let index = 0; index < tableDataType.value.length; index++) {
const element = tableDataType.value[index];
itemsTypeList.value.push({
value: element[idsObject.id],
label: element[idsObject.id]
})
}
}
function refreshRelTypeList(params) {
let idsObject = tableColumnListRelType.value.find(item => item.name === "类型名称");
relTypeList.value = [];
for (let index = 0; index < tableDataRelType.value.length; index++) {
const element = tableDataRelType.value[index];
relTypeList.value.push({
value: element[idsObject.id],
label: element[idsObject.id]
})
}
}
function cellClick(row, column, cell, event) {
// cell-click 当某个单元格被点击点击事件
tabClickIndex.value = row.index;
tableCellClickColumnIndex.value = column.index;
}
function inputBlur(row, event, column) {
// 失去焦点事件
tabClickIndex.value = null;
tableCellClickColumnIndex.value = null;
}
function tableCellClassName({
row,
column,
rowIndex,
columnIndex
}) {
// 把每一行的索引放进row
row.index = rowIndex;
column.index = columnIndex;
}
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function generateRandomId() {
let timestamp = Date.now().toString(); // 获取当前时间戳
let randomNum = getRandomInt(0, 32);
return parseInt(timestamp + randomNum);
}
// 新增行
function addRowCommon(scope, position) {
let newRow = {}
let namesArr = tableColumnList.value.map(item => item.id);
namesArr.forEach((item, index) => {
if (index === 0){
newRow[item] = generateRandomId();
} else {
newRow[item] = "";
}
});
if (scope) {
if (position === "next") {
tableData.value.splice(scope.$index + 1, 0, newRow)
} else if (position === "previous") {
tableData.value.splice(scope.$index, 0, newRow)
}
} else {
tableData.value.push(newRow)
}
}
// 新增行
function addRowCommonType(scope, position) {
let newRow = {}
let namesArr = tableColumnListType.value.map(item => item.id);
namesArr.forEach((item, index) => {
newRow[item] = "";
});
if (scope) {
if (position === "next") {
tableDataType.value.splice(scope.$index + 1, 0, newRow)
} else if (position === "previous") {
tableDataType.value.splice(scope.$index, 0, newRow)
}
} else {
tableDataType.value.push(newRow)
}
}
// 新增行
function addRowCommonRel(scope, position) {
let newRow = {}
let namesArr = tableColumnListRel.value.map(item => item.id);
namesArr.forEach((item, index) => {
newRow[item] = "";
});
if (scope) {
if (position === "next") {
tableDataRel.value.splice(scope.$index + 1, 0, newRow)
} else if (position === "previous") {
tableDataRel.value.splice(scope.$index, 0, newRow)
}
} else {
tableDataRel.value.push(newRow)
}
}
function addRowCommonRelType(scope, position) {
let newRow = {}
let namesArr = tableColumnListRelType.value.map(item => item.id);
namesArr.forEach((item, index) => {
newRow[item] = "";
});
if (scope) {
if (position === "next") {
tableDataRelType.value.splice(scope.$index + 1, 0, newRow)
} else if (position === "previous") {
tableDataRelType.value.splice(scope.$index, 0, newRow)
}
} else {
tableDataRelType.value.push(newRow)
}
}
function saveDataCommon() {
globalProxy.$confirm("是否保存所有内容?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
let params = {
"event_types": {
data: tableDataType.value,
titles: tableColumnListType.value
},
"events": {
data: tableData.value,
titles: tableColumnList.value
},
"relation_types": {
data: tableDataRelType.value,
titles: tableColumnListRelType.value
},
"relations": {
data: tableDataRel.value,
titles: tableColumnListRel.value
}
}
emit("saveTableDataItems", params);
})
.catch(() => {
globalProxy.$message({
type: "info",
message: "已取消保存",
});
});
}
function deleteRow(scope, type) {
if (type === 1){
tableData.value.splice(scope.$index, 1);
} else if (type === 2){
tableDataType.value.splice(scope.$index, 1);
} else if (type === 3){
tableDataRel.value.splice(scope.$index, 1);
} else if (type === 4){
tableDataRelType.value.splice(scope.$index, 1);
}
}
function delData(type) {
globalProxy.$confirm("是否需要删除所有数据?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
if (type === 1){
tableData.value = [];
} else if (type === 2){
tableDataType.value = [];
} else if (type === 3){
tableDataRel.value = [];
} else if (type === 4){
tableDataRelType.value = [];
}
})
.catch(() => {
globalProxy.$message({
type: "info",
message: "已取消删除",
});
});
}
</script>
<style lang="scss" scoped>
.save-data {
position: absolute;
right: 15px;
top: 60px;
}
</style>
<template>
<div class="graph-analysis-wrap">
<!-- <div class="graph-analysis-tabs">
<span class="btn-list">
<SliderButton :dataInfo="sliderButton" :active="focusBotton" @handleClick="handleClick" />
</span>
</div> -->
<div class="graph-analysis-header">
<div class="tab-list">
<div v-for="(item, index) in sliderButton" :key="index" class="tab-item" :class="{ active: focusBotton === item.index }" @click="handleClick(item)">
<span class="tab-item-text">{{ item.name }}</span>
</div>
</div>
</div>
<div v-show="focusBotton == '0'" class="chart-panel">
<div class="chart-left-panel">
<div class="chart-datalist-title">
<span>事件列表</span>
</div>
<DataListChart :list="newsListChart" class="chart-datalist"></DataListChart>
<div class="bottom-button-group">
<!-- <el-button type="primary" @click="curvatureDrawing()">指标曲线绘制</el-button> -->
<!-- <el-button type="primary" @click="curvatureAnalysis()">曲率分析</el-button> -->
<el-checkbox v-model="curDrawing" label="指标曲线绘制" border @change="curvatureDrawing()" style="margin-right: 4px;"/>
<el-checkbox v-if="curDrawing" v-model="curAnalysis" label="曲率分析" border @change="curvatureAnalysis()" style="margin-right: 4px;"/>
<el-button type="primary" @click="generateConclusion()">生成结论</el-button>
</div>
</div>
<div class="main-chart" id="main-chart" ref="middleChartDom"></div>
<div class="chart-right-panel">
<div class="conclusionlist-title">
<span>结论</span>
</div>
<ConclusionList :conclusionsArr="conclusionsList" class="conclusion-list"></ConclusionList>
</div>
<el-button type="primary" class="edit-button" @click="editDataFun()">编辑</el-button>
</div>
<div v-show="focusBotton == '1'" class="bubble-panel">
<el-button type="primary" class="edit-button" @click="editDataFun()">编辑</el-button>
<div class="bubble-left-panel">
<div class="bubble-datalist-select">
<div class="bubble-datalist-select-top">
<span>选择年份:</span>
<el-select v-model="selectedYear" multiple placeholder="请选择" collapse-tags collapse-tags-tooltip :max-collapse-tags="2">
<el-option v-for="item in yearList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</div>
<div class="bubble-datalist-select-bottom">
<span>选择领域:</span>
<el-select v-model="selectedArea" multiple placeholder="请选择" collapse-tags collapse-tags-tooltip :max-collapse-tags="2">
<el-option v-for="item in areaList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</div>
</div>
<div class="bubble-datalist-title">
<span>事件列表</span>
</div>
<DataList :list="newsListChartBubble" class="bubble-datalist"></DataList>
<div class="bottom-button-group">
<el-button type="primary" @click="bubbleDrawing()">气泡图绘制</el-button>
<el-button type="primary" @click="modelScaleAnalysis()">模型规模分析</el-button>
<el-button type="primary" @click="lagAnalysis()">滞后性分析</el-button>
</div>
</div>
<div class="bubble-box">
<div class="bubble-item" v-for="(item,index) in targetList" :key="index">
<div class="info">
<div style="display: flex;flex-direction: column;justify-content: center;height: 10%;">
<div class="img">
<el-icon>
<Document />
</el-icon>
</div>
<div class="title">{{item.event_title}}</div>
</div>
<div class="icon" v-for="(scale) in item.target" :key="scale" style="text-align: center">
<div class="circle" :style="{width:(scale.value*80)+'px',height:(scale.value*80)+'px','margin-top':(100-scale.value*80)/2 +'px'}"></div>
</div>
</div>
<!-- <div class="arrow" v-if="index != targetList.length - 1" style="height: 10%;">
<el-icon style="vertical-align: middle;height: 100%;">
<ArrowRight />
</el-icon>
</div> -->
<div v-if="index === targetList.length - 1">
<div style="height: 10%"></div>
<div class="legendBox">
<div class="legend" v-for="x in item.target" :key="x">{{x.name}}</div>
</div>
</div>
</div>
</div>
<div class="bubble-right-panel">
<div class="conclusionlist-title">
<span>模型规模分析结论</span>
</div>
<ConclusionList :conclusionsArr="conListBubble" class="conclusion-list"></ConclusionList>
<div class="conclusionlist-title-bottom">
<span>滞后性分析结论</span>
</div>
<ConclusionList :conclusionsArr="conListBubbleBottom" class="conclusion-list-bottom"></ConclusionList>
</div>
</div>
<div v-show="focusBotton == '2'" class="g6-panel">
<div class="g6-left-panel">
<div class="g6-datalist-select">
<div class="g6-datalist-select-top">
<span>选择模型:</span>
<el-select v-model="selectedModel" multiple placeholder="请选择" collapse-tags collapse-tags-tooltip :max-collapse-tags="2">
<el-option v-for="item in modelList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</div>
<div class="g6-datalist-select-bottom">
<span>选择年份:</span>
<el-select v-model="selectedYearG6" multiple placeholder="请选择" collapse-tags collapse-tags-tooltip :max-collapse-tags="2">
<el-option v-for="item in yearListG6" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</div>
</div>
<div class="g6-datalist-title">
<span>事件列表</span>
</div>
<DataList :list="newsListChartG6" class="g6-datalist"></DataList>
<div class="bottom-button-group">
<el-button type="primary" @click="createGraphA()">美国机理图</el-button>
<el-button type="primary" @click="createGraphC()">中国机理图</el-button>
<el-button type="primary" @click="generateConclusionG6()">生成结论</el-button>
</div>
</div>
<div class="g6-center-panel">
<div id="g6Container" class="g6-panel-main" ref="graphRef"></div>
<div id="g6ContainerBottom" class="g6-bottom-panel-main" ref="graphBottomRef"></div>
</div>
<div class="g6-right-panel">
<div class="conclusionlist-title">
<span>结论</span>
</div>
<ConclusionList :conclusionsArr="conListG6" class="conclusion-list"></ConclusionList>
</div>
<el-button type="primary" class="edit-button" @click="editDataFun2()">编辑</el-button>
<ul class="g6-legend">
<li v-for="(item, index) in legendData.edges" :key="index">
<label class="edges-label">
<el-icon :style="{color: item.style.stroke}">
<Right />
</el-icon>
</label>
<span>{{item.label}}</span>
</li>
<li v-for="(ele, ind) in legendData.nodes" :key="ind + 'a'">
<label class="legend-edges-nodes" :style="{background: ele.style.fill}"></label>
<span>{{ele.label}}</span>
</li>
</ul>
<ul class="g6-legend-bottom">
<li v-for="(item, index) in legendDataBottom.edges" :key="index">
<label class="edges-label">
<el-icon :style="{color: item.style.stroke}">
<Right />
</el-icon>
</label>
<span>{{item.label}}</span>
</li>
<li v-for="(ele, ind) in legendDataBottom.nodes" :key="ind + 'a'">
<label class="legend-edges-nodes" :style="{background: ele.style.fill}"></label>
<span>{{ele.label}}</span>
</li>
</ul>
</div>
<el-dialog title="编辑数据" v-model="editDialog.visible" width="80%" :before-close="handleClose" :close-on-click-modal="false" :modal="false">
<EditDataComp v-if="editDialog.visible" ref="editDataRef" :editTableData="chartData" :tabIndex="focusBotton" @saveTableData="saveEditTableData"></EditDataComp>
</el-dialog>
<el-dialog title="编辑数据" v-model="editDialogEvents.visible" width="80%" :before-close="handleClose2" :close-on-click-modal="false" :modal="false">
<EditDataG6Comp v-if="editDialogEvents.visible" ref="editDataRef" :editTableData="g6Data" :tabIndex="focusBotton" @saveTableDataItems="saveEditTableDataEvents"></EditDataG6Comp>
</el-dialog>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import G6 from "@antv/g6";
import * as echarts from "echarts";
import {
getChart,
getChartEditTable,
saveChartTable,
getMatrix,
getMatrixEditTable,
saveMatrixTable,
getEvents,
getEventsEditTable,
saveEventsTable
} from "@/api/graphApi";
import SliderButton from './SliderButtonRadioPadding.vue'
import EditDataComp from './EditDataChart.vue'
import EditDataG6Comp from './EditDataG6.vue'
import DataList from "./DataList.vue"
import DataListChart from "./DataListChart.vue"
import ConclusionList from "./ConclusionList.vue"
import newsList from "../data/newsList.json"
import newsListG6 from "../data/newsListG6.json"
import lineChartData from "../data/chart.json"
import gptData from "../data/gpt.json"
const {
appContext
} = getCurrentInstance();
const globalProxy = appContext.config.globalProperties;
let curDrawing = ref(false)
let curAnalysis = ref(false)
let graphData = ref({
nodes: {},
edges: [],
time: [],
type: {}
})
let graphDataBottom = ref({
nodes: {},
edges: [],
time: [],
type: {}
})
let myChart = null;
let editDialog = ref({
visible: false
});
let editDialogEvents = ref({
visible: false
});
let graphRef = ref();
let graphBottomRef = ref();
let editDataRef = ref();
// 处理后的图谱数据
let cleanedGraphData = ref({
nodes: [],
edges: [],
combos: []
});
let cleanedGraphDataBottom = ref({
nodes: [],
edges: [],
combos: []
});
// g6图数据
let graph = ref({});
let graphBottom = ref({});
// 图例数据
let legent = ref({});
// 节点和边的颜色备选项
let colors = ref([
"#00BFFF",
"#F4A460",
"#7CFC00",
"#FFFF00",
"#FF0000",
"#800080",
"#00FFFF",
"#FF00FF",
"#000000",
"#00FF00",
"#0000FF",
"#FFFFFF",
"#FFA500",
"#FFC0CB",
"#808080",
"#5C3317",
"#B0C4DE",
"#87CEEB",
"#FFB6C1",
"#FFFF66",
]);
// 节点类型与对应的颜色
let nodeColorList = ref({})
// 边类型与对应的颜色
let edgeColorList = ref({
// '因果': 'blue',
// '时序': 'yellow'
})
// 图例数据
let legendData = ref({
nodes: [],
edges: []
})
let legendDataBottom = ref({
nodes: [],
edges: []
})
let sliderButton = ref([{
name: '要素投入推进科技水平机理',
index: '0'
}, {
name: '跨领域科技态势机理',
index: '1'
}, {
name: '商转军科技态势机理',
index: '2'
}])
let focusBotton = ref('0')
let selectedYear = ref(["2025", "2024", "2023", "2022", "2021", "2020"])
let yearList = ref([{
value: "2025",
label: "2025年"
},{
value: "2024",
label: "2024年"
},{
value: "2023",
label: "2023年"
},{
value: "2022",
label: "2022年"
},{
value: "2021",
label: "2021年"
},{
value: "2020",
label: "2020年"
}])
let selectedArea = ref(["自然语言大模型", "A大模型", "B大模型", "C大模型"])
let areaList = ref([{
value: "自然语言大模型",
label: "自然语言大模型"
},{
value: "A大模型",
label: "A大模型"
},{
value: "B大模型",
label: "B大模型"
},{
value: "C大模型",
label: "C大模型"
}])
let selectedYearG6 = ref(["2025", "2024", "2023", "2022", "2021", "2020"])
let yearListG6 = ref([{
value: "2025",
label: "2025年"
},{
value: "2024",
label: "2024年"
},{
value: "2023",
label: "2023年"
},{
value: "2022",
label: "2022年"
},{
value: "2021",
label: "2021年"
},{
value: "2020",
label: "2020年"
}])
let selectedModel = ref(["GPT", "DeepSeek"])
let modelList = ref([{
value: "GPT",
label: "GPT"
},{
value: "DeepSeek",
label: "DeepSeek"
}])
let targetList = ref([])
let targetKeys = ref([])
let chartData = ref({})
let g6Data = ref({})
let newsListChart = ref([])
let newsListChartBubble = ref([])
let newsListChartG6 = ref([])
newsListChart.value = newsList.data;
newsListChartBubble.value = newsList.data;
newsListChartG6.value = newsListG6.data;
let conclusionsList = ref([])
let conListBubble = ref([])
let conListBubbleBottom = ref([])
let conListG6 = ref([])
onMounted(() => {
})
function handleClose() {
editDialog.value.visible = false;
}
function handleClose2(params) {
editDialogEvents.value.visible = false;
}
// 获取图谱数据列表
function getGraphList() {
// getEvents().then((res) => {
// if (res) {
// graphData.value.nodes = res.data.events
// graphData.value.edges = res.data.events_relations
// graphData.value.time = res.data.events_time
// graphData.value.type = res.data.events_type
// graphData.value.relation_type = res.data.relation_type
// initGraph(graphData.value);
// }
// })
graphData.value.nodes = gptData.data.events
graphData.value.edges = gptData.data.events_relations
graphData.value.time = gptData.data.events_time
graphData.value.type = gptData.data.events_type
graphData.value.relation_type = gptData.data.relation_type
initGraph(graphData.value);
}
function getGraphListBottom(){
getEvents().then((res) => {
if (res) {
graphDataBottom.value.nodes = res.data.events
graphDataBottom.value.edges = res.data.events_relations
graphDataBottom.value.time = res.data.events_time
graphDataBottom.value.type = res.data.events_type
graphDataBottom.value.relation_type = res.data.relation_type
initGraphBottom(graphDataBottom.value);
}
})
}
function initGraph(newVal) {
legendData.value = {
nodes: [],
edges: []
};
if (JSON.stringify(newVal) !== "{}") {
const nodes = [],
edges = [],
combos = [];
// 动态生成节点颜色、配置图例节点数据
Object.keys(graphData.value.type).forEach((item, index) => {
nodeColorList.value[item] = colors.value[index];
legendData.value.nodes.push({
id: item,
label: item,
type: "circle",
size: 20,
style: {
fill: colors.value[index]
}
});
});
//动态生成边颜色、配置图例边数据
graphData.value.relation_type.forEach((item, index) => {
edgeColorList.value[item] = colors.value[index];
legendData.value.edges.push({
id: item,
label: item,
type: "line",
style: {
width: 20,
stroke: colors.value[index]
}
});
});
// 用于节点定位的x、y值,用于生成假节点的id值的z值
let x = 50,
y = 50,
z = 1;
for (const item in graphData.value.nodes) {
combos.push({
id: item,
label: item
});
for (const itemType in graphData.value.type) {
if (typeof graphData.value.nodes[item][itemType] !== "undefined") {
if (
graphData.value.nodes[item][itemType].length ==
graphData.value.type[itemType]
) {
graphData.value.nodes[item][itemType].forEach(itemChild => {
nodes.push({
id: itemChild.id.toString(),
x: x,
y: y,
comboId: itemChild.event_time,
label: superLongTextHandle(itemChild.event_title, 220, 14),
eventType: itemChild.event_type,
style: {
fill: nodeColorList.value[itemChild.event_type]
}
});
y = y + 80;
});
} else {
graphData.value.nodes[item][itemType].forEach(itemChild => {
nodes.push({
id: itemChild.id.toString(),
x: x,
y: y,
comboId: itemChild.event_time,
label: superLongTextHandle(itemChild.event_title, 220, 14),
type: itemChild.event_type,
style: {
fill: nodeColorList.value[itemChild.event_type]
}
});
y = y + 80;
});
for (
let i = 0; i <
graphData.value.type[itemType] -
graphData.value.nodes[item][itemType].length; i++
) {
nodes.push({
id: z * 3 + "a",
x: x,
y: y,
comboId: item,
style: {
fill: "red",
opacity: 0
}
});
y = y + 80;
z++;
}
}
} else {
for (let i = 0; i < graphData.value.type[itemType]; i++) {
nodes.push({
id: z * 3 + "a",
x: x,
y: y,
comboId: item,
style: {
fill: "pink",
opacity: 0
}
});
y = y + 80;
z++;
}
}
}
x = x + 200;
y = 50;
}
graphData.value.edges.forEach(item => {
let typeFlag = "";
let event_type1 = "";
nodes.forEach(item1 => {
if (item1.id == item.source) {
event_type1 = item1.eventType;
}
});
let event_type2 = "";
nodes.forEach(item2 => {
if (item2.id == item.target) {
event_type2 = item2.eventType;
}
});
let xSameFlag = false;
for (let xName in graphData.value.nodes) {
let sourceXName = "";
let targetXName = "";
for (let yName in graphData.value.nodes[xName]) {
graphData.value.nodes[xName][yName].forEach(xyItem => {
if (xyItem.id == item.source) {
sourceXName = xName;
} else if (xyItem.id == item.target) {
targetXName = xName;
}
});
}
if (
sourceXName == targetXName &&
sourceXName.length > 0 &&
targetXName.length > 0
) {
xSameFlag = true;
}
}
if (event_type1 == event_type2 || xSameFlag) {
typeFlag = "line";
} else {
typeFlag = "cubic-horizontal";
}
edges.push({
source: item.source.toString(),
target: item.target.toString(),
label: item.relation_type,
type: typeFlag,
style: {
stroke: edgeColorList.value[item.relation_type],
endArrow: {
path: G6.Arrow.triangle(),
fill: edgeColorList.value[item.relation_type]
}
}
});
});
cleanedGraphData.value.nodes = nodes;
cleanedGraphData.value.edges = edges;
cleanedGraphData.value.combos = combos;
nextTick(() => {
initG6Graph();
})
}
}
function initG6Graph() {
// const legend = new G6.Legend({
// data: legendData.value
// });
// 更新数据后清空上次画布
if (Object.keys(graph.value).length !== 0) {
graph.value.destroy();
graph.value = null;
}
graph.value = new G6.Graph({
container: "g6Container",
width: graphRef.scrollWidth,
height: graphRef.scrollHeight,
modes: {
default: ['zoom-canvas', 'drag-canvas', 'drag-node']
},
// plugins: [legend],
defaultEdge: {
// 被动态初始化数据覆盖
type: "line",
style: {
radius: 10,
offset: 30,
endArrow: true,
},
labelCfg: {
position: "bottom",
style: {
height: 30,
fill: "skyblue" // 节点字体颜色
}
}
},
defaultNode: {
size: 20,
type: "circle",
color: "transparent", // 边框颜色
labelCfg: {
position: "bottom",
style: {
height: 30,
fill: "#fff" // 节点字体颜色
}
}
},
groupByTypes: false,
defaultCombo: {
type: "rect",
labelCfg: {
position: 'top',
offset: [10, 10, 10, 10],
style: {
fill: '#fff',
},
},
style: {
opacity: 0
},
},
fitView: true,
animate: true
});
graph.value.data(cleanedGraphData.value);
graph.value.render();
}
function initGraphBottom(newVal) {
legendDataBottom.value = {
nodes: [],
edges: []
};
if (JSON.stringify(newVal) !== "{}") {
const nodes = [],
edges = [],
combos = [];
// 动态生成节点颜色、配置图例节点数据
Object.keys(graphDataBottom.value.type).forEach((item, index) => {
nodeColorList.value[item] = colors.value[index];
legendDataBottom.value.nodes.push({
id: item,
label: item,
type: "circle",
size: 20,
style: {
fill: colors.value[index]
}
});
});
//动态生成边颜色、配置图例边数据
graphDataBottom.value.relation_type.forEach((item, index) => {
edgeColorList.value[item] = colors.value[index];
legendDataBottom.value.edges.push({
id: item,
label: item,
type: "line",
style: {
width: 20,
stroke: colors.value[index]
}
});
});
// 用于节点定位的x、y值,用于生成假节点的id值的z值
let x = 50,
y = 50,
z = 1;
for (const item in graphDataBottom.value.nodes) {
combos.push({
id: item,
label: item
});
for (const itemType in graphDataBottom.value.type) {
if (typeof graphDataBottom.value.nodes[item][itemType] !== "undefined") {
if (
graphDataBottom.value.nodes[item][itemType].length ==
graphDataBottom.value.type[itemType]
) {
graphDataBottom.value.nodes[item][itemType].forEach(itemChild => {
nodes.push({
id: itemChild.id.toString(),
x: x,
y: y,
comboId: itemChild.event_time,
label: superLongTextHandle(itemChild.event_title, 220, 14),
eventType: itemChild.event_type,
style: {
fill: nodeColorList.value[itemChild.event_type]
}
});
y = y + 80;
});
} else {
graphDataBottom.value.nodes[item][itemType].forEach(itemChild => {
nodes.push({
id: itemChild.id.toString(),
x: x,
y: y,
comboId: itemChild.event_time,
label: superLongTextHandle(itemChild.event_title, 220, 14),
type: itemChild.event_type,
style: {
fill: nodeColorList.value[itemChild.event_type]
}
});
y = y + 80;
});
for (
let i = 0; i <
graphDataBottom.value.type[itemType] -
graphDataBottom.value.nodes[item][itemType].length; i++
) {
nodes.push({
id: z * 3 + "a",
x: x,
y: y,
comboId: item,
style: {
fill: "red",
opacity: 0
}
});
y = y + 80;
z++;
}
}
} else {
for (let i = 0; i < graphDataBottom.value.type[itemType]; i++) {
nodes.push({
id: z * 3 + "a",
x: x,
y: y,
comboId: item,
style: {
fill: "pink",
opacity: 0
}
});
y = y + 80;
z++;
}
}
}
x = x + 200;
y = 50;
}
graphDataBottom.value.edges.forEach(item => {
let typeFlag = "";
let event_type1 = "";
nodes.forEach(item1 => {
if (item1.id == item.source) {
event_type1 = item1.eventType;
}
});
let event_type2 = "";
nodes.forEach(item2 => {
if (item2.id == item.target) {
event_type2 = item2.eventType;
}
});
let xSameFlag = false;
for (let xName in graphDataBottom.value.nodes) {
let sourceXName = "";
let targetXName = "";
for (let yName in graphDataBottom.value.nodes[xName]) {
graphDataBottom.value.nodes[xName][yName].forEach(xyItem => {
if (xyItem.id == item.source) {
sourceXName = xName;
} else if (xyItem.id == item.target) {
targetXName = xName;
}
});
}
if (
sourceXName == targetXName &&
sourceXName.length > 0 &&
targetXName.length > 0
) {
xSameFlag = true;
}
}
if (event_type1 == event_type2 || xSameFlag) {
typeFlag = "line";
} else {
typeFlag = "cubic-horizontal";
}
edges.push({
source: item.source.toString(),
target: item.target.toString(),
label: item.relation_type,
type: typeFlag,
style: {
stroke: edgeColorList.value[item.relation_type],
endArrow: {
path: G6.Arrow.triangle(),
fill: edgeColorList.value[item.relation_type]
}
}
});
});
cleanedGraphDataBottom.value.nodes = nodes;
cleanedGraphDataBottom.value.edges = edges;
cleanedGraphDataBottom.value.combos = combos;
nextTick(() => {
initG6GraphBottom();
})
}
}
function initG6GraphBottom() {
// const legend = new G6.Legend({
// data: legendDataBottom.value
// });
// 更新数据后清空上次画布
if (Object.keys(graphBottom.value).length !== 0) {
graphBottom.value.destroy();
graphBottom.value = null;
}
graphBottom.value = new G6.Graph({
container: "g6ContainerBottom",
width: graphBottomRef.scrollWidth,
height: graphBottomRef.scrollHeight,
modes: {
default: ['zoom-canvas', 'drag-canvas', 'drag-node']
},
// plugins: [legend],
defaultEdge: {
// 被动态初始化数据覆盖
type: "line",
style: {
radius: 10,
offset: 30,
endArrow: true,
},
labelCfg: {
position: "bottom",
style: {
height: 30,
fill: "skyblue" // 节点字体颜色
}
}
},
defaultNode: {
size: 20,
type: "circle",
color: "transparent", // 边框颜色
labelCfg: {
position: "bottom",
style: {
height: 30,
fill: "#fff" // 节点字体颜色
}
}
},
groupByTypes: false,
defaultCombo: {
type: "rect",
labelCfg: {
position: 'top',
offset: [10, 10, 10, 10],
style: {
fill: '#fff',
},
},
style: {
opacity: 0
},
},
fitView: true,
animate: true
});
graphBottom.value.data(cleanedGraphDataBottom.value);
graphBottom.value.render();
}
function handleClick(item) {
focusBotton.value = item.index;
}
function editDataFun() {
if (focusBotton.value === "0") {
getChartEditTable({
id: 1
}).then((res) => {
chartData.value = res.data;
editDialog.value.visible = true;
})
} else if (focusBotton.value === "1") {
getMatrixEditTable().then((res) => {
chartData.value = res.data;
editDialog.value.visible = true;
})
}
}
function editDataFun2(params) {
getEventsEditTable().then((res) => {
g6Data.value = res.data;
nextTick(() => {
editDialogEvents.value.visible = true;
})
})
}
function curvatureDrawing(){
if (curDrawing.value){
initChart();
} else {
curAnalysis.value = false;
if (myChart) {
myChart.dispose()
myChart = null;
}
}
}
function curvatureAnalysis(){
if (!curDrawing.value){
globalProxy.$message({
type: "warning",
message: "请先绘制折线图",
});
} else {
if (curAnalysis.value){
markMaxAngleSegment();
} else {
let graphicOptions = myChart.getOption();
graphicOptions.graphic = [];
myChart.setOption(graphicOptions, true);
}
}
}
function generateConclusion(){
conclusionsList.value = [{
title: "结论一",
content: "2017年与2023年,国外AI的技术性能均有大幅度提升,2017年全年处于急速上升的状态,但2023年前半年提升较慢,下半年飞速提升,创建了第二次飞速发展的阶段"
},{
title: "结论二",
content: "我国的AI技术,在2018年得到了很大的提升,缩短了与国外AI技术的差距,在2024年我国再一次大幅度提升AI技术,并最终非常拉近了与国外的距离"
},{
title: "结论三",
content: "我国的两次AI技术提升,均晚于国外,说明AI技术提升,我国还是受国外影响较多,但近十年我国整体提升幅度相交国外更明显,处于追赶与拉近距离的地位"
}]
}
function bubbleDrawing(){
initMatrix();
}
function modelScaleAnalysis(){
conListBubble.value = [{
title: "结论一",
content: "人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能"
},{
title: "结论二",
content: "人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能"
},{
title: "结论三",
content: "人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能"
}]
}
function lagAnalysis(){
conListBubbleBottom.value = [{
title: "结论一",
content: "人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能"
},{
title: "结论二",
content: "人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能"
},{
title: "结论三",
content: "人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能"
}]
}
function createGraphC(){
getGraphListBottom();
}
function createGraphA(){
getGraphList();
}
function generateConclusionG6(){
conListG6.value = [{
title: "结论一",
content: "人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能"
},{
title: "结论二",
content: "人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能"
},{
title: "结论三",
content: "人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能人工智能"
}]
}
function initMatrix() {
getMatrix().then((res) => {
if (res.data && res.data.length > 0) {
targetList.value = res.data;
}
})
}
function saveEditTableData(params) {
if (focusBotton.value === "0") {
saveChartTable(params).then((res) => {
if (res.code === 1) {
globalProxy.$message({
type: "success",
message: "保存成功!",
});
setTimeout(() => {
initChart();
}, 500)
}
})
} else if (focusBotton.value === "1") {
saveMatrixTable(params).then((res) => {
if (res.code === 1) {
globalProxy.$message({
type: "success",
message: "保存成功!",
});
setTimeout(() => {
initMatrix();
}, 500)
}
})
}
}
function saveEditTableDataEvents(params) {
saveEventsTable(params).then((res) => {
if (res.code === 1) {
globalProxy.$message({
type: "success",
message: "保存成功!",
});
getGraphListBottom();
} else if (res.code === 0) {
globalProxy.$message({
type: "error",
message: res.message,
});
}
})
}
function superLongTextHandle(str, maxWidth, fontSize) {
let currentWidth = 0;
let res = str;
// 区分汉字和字母
const pattern = new RegExp("[\u4E00-\u9FA5]+");
str.split("").forEach((letter, i) => {
if (currentWidth > maxWidth) return;
if (pattern.test(letter)) {
// 中文字符
currentWidth += fontSize;
} else {
// 根据字体大小获取单个字母的宽度
currentWidth += G6.Util.getLetterWidth(letter, fontSize);
}
if (currentWidth > maxWidth) {
res = `${str.substr(0, i)}\n${superLongTextHandle(
str.substr(i),
maxWidth,
fontSize
)}`;
}
});
return res;
}
// function initChart() {
// let chartDom = document.getElementById("main-chart");
// if (!myChart) {
// myChart = echarts.init(chartDom);
// }
// getChart().then((res) => {
// if (res) {
// let legendListData = res.legend.data;
// let xAxisData = res.xAxis.data;
// let seriesData = res.series;
// let option = {
// // title: {
// // text: '数值'
// // },
// tooltip: {
// trigger: 'axis'
// },
// legend: {
// data: legendListData,
// textStyle: {
// color: "#FFFFFF",
// fontSize: 14
// }
// },
// grid: {
// left: '10px',
// right: '10px',
// bottom: '10px',
// containLabel: true
// },
// xAxis: {
// type: 'category',
// data: xAxisData,
// nameTextStyle: {
// color: "#FFFFFF",
// fontSize: 20,
// },
// axisLabel: {
// color: "#FFFFFF",
// fontSize: 20,
// interval: 2,
// fontWeight: 400,
// fontFamily: "Source Han Sans CN"
// // rotate: -30,
// },
// },
// yAxis: {
// type: 'value',
// nameTextStyle: {
// color: "#FFFFFF",
// fontSize: 20,
// },
// axisLabel: {
// color: "#FFFFFF",
// fontSize: 20,
// interval: 0,
// fontWeight: 400,
// fontFamily: "Source Han Sans CN"
// // rotate: -30,
// },
// splitLine: {
// lineStyle: {
// color: 'rgba(106, 110, 126, 0.6)'
// }
// }
// },
// series: seriesData
// };
// option && myChart.setOption(option);
// window.addEventListener("resize", function () {
// if (myChart) {
// myChart.resize();
// }
// })
// }
// })
// }
function initChart() {
let chartDom = document.getElementById("main-chart");
if (!myChart) {
myChart = echarts.init(chartDom);
}
let legendListData = lineChartData.data.legend.data;
let xAxisData = lineChartData.data.xAxis.data;
let seriesData = lineChartData.data.series;
let option = {
tooltip: {
trigger: 'axis'
},
legend: {
data: legendListData,
textStyle: {
color: "#FFFFFF",
fontSize: 14
}
},
grid: {
left: '10px',
right: '10px',
bottom: '10px',
containLabel: true
},
xAxis: {
type: 'category',
data: xAxisData,
nameTextStyle: {
color: "#FFFFFF",
fontSize: 20,
},
axisLabel: {
color: "#FFFFFF",
fontSize: 20,
// interval: 2,
fontWeight: 400,
fontFamily: "Source Han Sans CN"
// rotate: -30,
},
},
yAxis: {
type: 'value',
nameTextStyle: {
color: "#FFFFFF",
fontSize: 20,
},
axisLabel: {
color: "#FFFFFF",
fontSize: 20,
interval: 0,
fontWeight: 400,
fontFamily: "Source Han Sans CN"
// rotate: -30,
},
splitLine: {
lineStyle: {
color: 'rgba(106, 110, 126, 0.6)'
}
}
},
series: seriesData
};
option && myChart.setOption(option);
window.addEventListener("resize", function () {
if (myChart) {
myChart.resize();
}
if (curAnalysis.value){
markMaxAngleSegment();
}
})
}
// 计算相邻两点之间的倾斜角
function calculateSlopeAngles(points) {
const angles = [];
if (points.length < 2) {
return angles;
}
for (let i = 0; i < points.length - 1; i++) {
const p1 = points[i];
const p2 = points[i + 1];
const dx = p2[0] - p1[0];
const dy = p2[1] - p1[1];
let angleRad = Math.atan2(dy, dx);
let angleDeg = angleRad * 180 / Math.PI;
angles.push({
segment: `${i+1}-${i+2}`,
angle: angleDeg,
absAngle: Math.abs(angleDeg), // 存储绝对值用于比较
points: [p1, p2]
});
}
return angles;
}
function findSecondMaxValueObject(arr) {
// 提取value值并排序
let values = arr.map(item => item.absAngle);
values.sort((a, b) => b - a); // 降序排序
// 获取第二大的值(如果有的话)
let secondMaxValue = values[1]; // 注意:如果只有一个元素或数组为空,这将返回undefined
if (secondMaxValue === undefined) {
return null; // 或者可以根据需求返回其他值或抛出错误
}
// 找到具有这个值的对象
return arr.find(item => item.absAngle === secondMaxValue);
}
// 获取折线图数据并标记最大的两个倾斜角线段
function markMaxAngleSegment() {
const chartOption = myChart.getOption();
const xAxisData = chartOption.xAxis[0].data;
const allMaxSegments = []; // 存储所有系列的最大倾斜角线段
// 对每个系列分别处理
chartOption.series.forEach((series, seriesIndex) => {
const seriesData = series.data;
// 将数据转换为 [x, y] 格式的点数组
const points = seriesData.map((value, index) => [index, value]);
// 计算倾斜角
const angles = calculateSlopeAngles(points);
// 找出绝对值最大的倾斜角
let maxAngleSegment = null;
let maxAbsAngle = -1;
angles.forEach(segment => {
if (segment.absAngle > maxAbsAngle) {
maxAbsAngle = segment.absAngle;
maxAngleSegment = segment;
}
});
if (maxAngleSegment) {
maxAngleSegment.seriesIndex = seriesIndex;
maxAngleSegment.seriesName = series.name;
maxAngleSegment.textContent = "first";
allMaxSegments.push(maxAngleSegment);
}
});
chartOption.series.forEach((series, seriesIndex) => {
const seriesData = series.data;
// 将数据转换为 [x, y] 格式的点数组
const points = seriesData.map((value, index) => [index, value]);
// 计算倾斜角
const angles = calculateSlopeAngles(points);
// 找出绝对值最大的倾斜角
let secondMaxAngleSegment = findSecondMaxValueObject(angles);
if (secondMaxAngleSegment) {
secondMaxAngleSegment.seriesIndex = seriesIndex;
secondMaxAngleSegment.seriesName = series.name;
secondMaxAngleSegment.textContent = "second";
allMaxSegments.push(secondMaxAngleSegment);
}
});
// 创建图形元素配置
const graphicOptions = [];
// 获取 x 轴的位置
const xAxisModel = myChart.getModel().getComponent('xAxis', 0);
const xAxisY = myChart.convertToPixel({
xAxisIndex: 0
}, 0)[1];
// 为每个系列的最大倾斜角线段创建标记
allMaxSegments.forEach((maxSegment, index) => {
const seriesIndex = maxSegment.seriesIndex;
const p1 = maxSegment.points[0];
const p2 = maxSegment.points[1];
// 将原始坐标转换为图表上的像素坐标
const pixelP1 = myChart.convertToPixel({
seriesIndex
}, p1);
const pixelP2 = myChart.convertToPixel({
seriesIndex
}, p2);
// 计算矩形框的尺寸
const padding = 10;
const minX = Math.min(pixelP1[0], pixelP2[0]) - padding;
const maxX = Math.max(pixelP1[0], pixelP2[0]) + padding;
const minY = Math.min(pixelP1[1], pixelP2[1]) - padding;
const maxY = Math.max(pixelP1[1], pixelP2[1]) + padding;
const width = maxX - minX;
const height = maxY - minY;
// 箭头配置
const arrowLength = 100; // 超过 x 轴的长度
const arrowWidth = 8; // 箭头宽度
const labelOffset = 10; // 文字与箭头的间距
// 矩形框中心 x 坐标
const rectCenterX = minX + width / 2;
// 矩形框底部 y 坐标
const rectBottomY = maxY;
// 计算箭头终点
// const arrowEndY = xAxisY + arrowLength;
const arrowEndY = rectBottomY + arrowLength;
// 设置不同系列的颜色
const colors = ['#FF6347', '#1E90FF'];
const color = colors[index % colors.length];
// 添加矩形框
graphicOptions.push({
type: 'rect',
left: minX,
top: minY,
shape: {
width: width,
height: height
},
style: {
fill: `${color}10`, // 半透明填充
stroke: color,
lineWidth: 2,
z: 100
},
textContent: maxSegment.textContent,
// 绑定点击事件
onclick: function(params) {
addArrow(rectCenterX, rectBottomY, minY, arrowEndY, arrowWidth, labelOffset, color, maxSegment.textContent)
}
});
});
// 更新图表配置,添加图形元素
myChart.setOption({
graphic: graphicOptions
});
// myChart.on('click', function(params) {
// // 检查点击的是否是 graphic 元素
// if (params.componentType === 'graphic') {
// const rectName = params;
// const message = `点击了 ${rectName} (位置: x=${params.event.offsetX.toFixed(0)}, y=${params.event.offsetY.toFixed(0)})`;
// }
// });
}
function addArrow(rectCenterX, rectBottomY, minY, arrowEndY, arrowWidth, labelOffset, color, order) {
let graphicOptions = myChart.getOption().graphic[0].elements;
// 添加矩形框上方的文字
graphicOptions.push({
type: 'text',
left: rectCenterX,
top: minY - 20,
style: {
fill: color,
fontSize: 14,
textAlign: 'center',
z: 101
}
});
// 添加向下延伸的箭头
graphicOptions.push({
type: 'polyline',
shape: {
points: [
[rectCenterX, rectBottomY],
[rectCenterX, arrowEndY - arrowWidth]
],
strokeLinecap: 'round'
},
style: {
stroke: color,
lineWidth: 2,
z: 102
}
});
// 添加箭头头部
graphicOptions.push({
type: 'polygon',
shape: {
points: [
[rectCenterX, arrowEndY],
[rectCenterX - arrowWidth / 2, arrowEndY - 8],
[rectCenterX + arrowWidth / 2, arrowEndY - 8]
]
},
style: {
fill: color,
z: 102
}
});
// 添加箭头下方的文字标注
graphicOptions.push({
type: 'text',
left: rectCenterX - 48,
top: arrowEndY + labelOffset,
style: {
text: order === "first" ? "资金投入增加" : "人才投入增加",
fill: color,
fontSize: 16,
fontWeight: 'bold',
textAlign: 'center',
z: 102
}
});
myChart.setOption({
graphic: graphicOptions
});
}
</script>
<style lang="less" scoped>
.graph-analysis-wrap {
position: relative;
margin: 10px;
width: calc(100% - 20px);
height: calc(100% - 20px);
.graph-analysis-tabs {
display: flex;
align-items: center;
width: 100%;
margin-bottom: 2px;
}
.graph-analysis-header {
flex: none;
width: 100%;
display: flex;
.tab-list {
height: 34px;
border-radius: calc(42px / 2);
border: 1px solid rgba(182, 214, 246, 0.5);
padding: 0 4px;
font-size: 16px;
color: #ffffff;
font-weight: 500;
display: flex;
align-items: center;
gap: 4px;
margin: 0 0 8px 0;
.tab-item {
padding: 0 12px;
cursor: pointer;
border-radius: calc(34px / 2);
color: #ffffff;
display: flex;
align-items: center;
gap: 8px;
.tab-item-text {
text-align: center;
width: 100%;
line-height: 28px;
}
.icon {
width: 16px;
display: block;
}
&:hover,
&.active {
color: #fff;
background: linear-gradient(254.23deg, #409eff 9.5%, #447fff 94.05%);
}
}
}
}
.chart-panel {
position: relative;
width: 100%;
height: calc(100% - 50px);
overflow: auto;
display: flex;
.chart-left-panel {
width: calc(20% - 10px);
padding-right: 10px;
border-right: 1px solid #6E87AF;
.chart-datalist-title {
height: 40px;
font-size: 26px;
font-weight: 500;
text-align: left;
padding-left: 8px;
background-color: rgba(48, 49, 51, 0.5);
border-bottom: 1px solid #6E87AF;
}
.chart-datalist {
height: calc(100% - 100px);
overflow-y: auto;
padding: 10px 4px 0 4px;
}
.bottom-button-group {
margin-top: 10px;
text-align: left;
display: flex;
align-items: center;
}
}
.main-chart {
width: 60%;
height: 100%;
}
.chart-right-panel {
padding-left: 10px;
width: calc(20% - 10px);
border-left: 1px solid #6E87AF;
.conclusionlist-title {
height: 40px;
font-size: 26px;
font-weight: 500;
text-align: left;
padding-left: 8px;
background-color: rgba(48, 49, 51, 0.5);
border-bottom: 1px solid #6E87AF;
}
.conclusion-list {
margin-top: 10px;
}
}
}
.bubble-panel {
position: relative;
display: flex;
height: calc(100% - 50px);
.bubble-left-panel {
width: calc(20% - 10px);
padding-right: 10px;
border-right: 1px solid #6E87AF;
.bubble-datalist-select {
.bubble-datalist-select-top {
display: flex;
align-items: center;
span {
width: 100px;text-align: left
}
}
.bubble-datalist-select-bottom {
margin-top: 6px;
display: flex;
align-items: center;
span {
width: 100px;text-align: left
}
}
}
.bubble-datalist-title {
margin-top: 10px;
height: 40px;
font-size: 26px;
font-weight: 500;
text-align: left;
padding-left: 8px;
background-color: rgba(48, 49, 51, 0.5);
border-bottom: 1px solid #6E87AF;
}
.bubble-datalist {
height: calc(100% - 180px);
overflow-y: auto;
padding: 10px 4px 0 4px;
}
.bottom-button-group {
margin-top: 10px;
text-align: left;
}
}
.bubble-box {
display: flex;
justify-content: space-between;
align-items: center;
overflow: auto;
width: 60%;
height: 100%;
.bubble-item {
height: 100%;
display: flex;
.info {
width: 200px;
.img {
text-align: center;
}
.title {
text-align: center;
}
.icon {
height: 100px;
display: block;
.circle {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
background: #3f89ec;
}
}
}
.arrow {
width: 100px;
text-align: center;
}
.legendBox {
width: 260px;
text-align: center;
.legend {
line-height: 100px;
}
}
}
}
.bubble-right-panel {
padding-left: 10px;
width: calc(20% - 10px);
border-left: 1px solid #6E87AF;
.conclusionlist-title {
height: 40px;
font-size: 26px;
font-weight: 500;
text-align: left;
padding-left: 8px;
background-color: rgba(48, 49, 51, 0.5);
border-bottom: 1px solid #6E87AF;
}
.conclusion-list {
height: calc(50% - 60px);
margin-top: 10px;
}
.conclusionlist-title-bottom {
height: 40px;
font-size: 26px;
font-weight: 500;
text-align: left;
padding-left: 8px;
margin-top: 10px;
background-color: rgba(48, 49, 51, 0.5);
border-bottom: 1px solid #6E87AF;
}
.conclusion-list-bottom {
height: calc(50% - 60px);
margin-top: 10px;
}
}
}
.g6-panel {
position: relative;
width: 100%;
height: calc(100% - 50px);
display: flex;
.g6-left-panel {
width: calc(20% - 10px);
padding-right: 10px;
border-right: 1px solid #6E87AF;
.g6-datalist-select {
.g6-datalist-select-top {
display: flex;
align-items: center;
span {
width: 100px;text-align: left
}
}
.g6-datalist-select-bottom {
margin-top: 6px;
display: flex;
align-items: center;
span {
width: 100px;text-align: left
}
}
}
.g6-datalist-title {
margin-top: 10px;
height: 40px;
font-size: 26px;
font-weight: 500;
text-align: left;
padding-left: 8px;
background-color: rgba(48, 49, 51, 0.5);
border-bottom: 1px solid #6E87AF;
}
.g6-datalist {
height: calc(100% - 180px);
overflow-y: auto;
padding: 10px 4px 0 4px;
}
.bottom-button-group {
margin-top: 10px;
text-align: left;
}
}
.g6-center-panel {
width: 60%;
.g6-panel-main {
width: 100%;
height: calc(50% - 5px);
background-image: linear-gradient(90deg, rgba(0,82,255,0.20) 1%, rgba(0,82,255,0.40) 51%, rgba(0,82,255,0.20) 100%);
}
.g6-bottom-panel-main {
width: 100%;
margin-top: 10px;
height: calc(50% - 5px);
background-image: linear-gradient(90deg, rgba(255,0,0,0.20) 0%, rgba(255,0,0,0.40) 51%, rgba(255,0,0,0.20) 100%);
}
}
.g6-right-panel {
padding-left: 10px;
width: calc(20% - 10px);
border-left: 1px solid #6E87AF;
.conclusionlist-title {
height: 40px;
font-size: 26px;
font-weight: 500;
text-align: left;
padding-left: 8px;
background-color: rgba(48, 49, 51, 0.5);
border-bottom: 1px solid #6E87AF;
}
.conclusion-list {
margin-top: 10px;
}
}
.g6-legend {
position: absolute;
left: calc(20% + 8px);
bottom: calc(50% + 8px);
font-size: 12px;
list-style-type: none;
max-height: 100px;
overflow-y: auto;
margin: 0;
padding: 0;
text-align: left;
label {
margin-right: 6px;
}
.legend-edges-nodes {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
}
}
.g6-legend-bottom {
position: absolute;
left: calc(20% + 8px);
bottom: 8px;
font-size: 12px;
list-style-type: none;
max-height: 100px;
overflow-y: auto;
margin: 0;
padding: 0;
text-align: left;
label {
margin-right: 6px;
}
.legend-edges-nodes {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
}
}
}
.edit-button {
z-index: 10;
position: absolute;
right: calc(20% + 10px);
}
}
:deep(.el-dialog) {
margin-top: 10vh;
}
</style>
<template>
<div class="slider-botton-radio">
<div class="botton-group">
<div class="slider" ref="sliderRef" :style="{width: `${width}px`, height: `${height}px`, background: sliderColor}"></div>
<div ref="bottonItem" :class="{ 'botton-item': true, active: item.focus, 'fs14': true }" :style="{ width: `${width}px`, height: `${height}px`, lineHeight: `${height}px`,overflow:'hidden'}" v-for="(item, index) in bottonData" :key="index" @click="handleClick(item, index)" v-html="item.name"></div>
</div>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
const emit = defineEmits(['handleClick']);
let props = defineProps({
active: {
type: String,
default: ""
},
dataInfo: {
type: Array,
default: () => {
return [];
}
},
width: {
type: Number,
default: 220
},
height: {
type: Number,
default: 30
},
sliderColor: {
type: String,
default: "#0b88e0"
},
backgroundColor: {
type: String,
default: "#e6e6e6"
},
defaultTextColor: {
type: String,
default: "#000000"
},
activeTextColor: {
type: String,
default: "#ffffff"
},
})
let bottonData = ref([])
let sliderRef = ref()
watch(
() => props.dataInfo,
(newVal, oldVal) => {
handler(newVal)
}, {
immediate: true
}
);
function handler(val) {
if (val && val.length > 0) {
let info = [];
val.forEach(item => {
info.push({
name: item.name,
index: item.index || item.name,
focus: item.index == props.active ? true : false
});
});
bottonData.value = info;
}
return val;
}
function handleClick(item, index) {
bottonData.value.forEach(x => {
x.focus = x.index == item.index ? true : false;
});
sliderRef.value.style.transform = `translateX(${props.width * index}px)`;
emit("handleClick", item);
}
onMounted(() => {
let activeIndex = 0;
props.dataInfo.find((x, idn) => {
if (x.index == props.active) {
activeIndex = idn;
}
});
sliderRef.value.style.transform = `translateX(${props.width * activeIndex}px)`;
})
</script>
<style lang="scss" scoped>
.slider-botton-radio {
position: relative;
.botton-group {
transition: all 0.4s;
display: inline-flex;
border-radius: 10px;
box-shadow: 0 1px 5px rgba(0, 0, 0, 15%);
padding: 5px;
.botton-item {
text-align: center;
cursor: pointer;
transition: all 0.4s;
border-radius: 10px;
z-index: 100;
&.active {
transition: all 0.4s;
}
}
.slider {
border-radius: 10px;
position: absolute;
transition: all 0.2s;
}
}
}
.slider-botton-radio .botton-group .botton-item.active {
color: #ffffff;
}
</style>
{
"code": 1,
"data": {
"legend": {
"data": [
"国内AI技术性能",
"国外AI技术性能"
]
},
"series": [
{
"data": [
50,
66,
112,
124,
149,
171,
189,
216,
268
],
"itemStyle": {
"color": "#FF6347"
},
"lineStyle": {
"color": "#FF6347"
},
"name": "国内AI技术性能",
"smooth": true,
"type": "line"
},
{
"data": [
80,
130,
152,
174,
189,
201,
210,
256,
278
],
"itemStyle": {
"color": "#1E90FF"
},
"lineStyle": {
"color": "#1E90FF"
},
"name": "国外AI技术性能",
"smooth": true,
"type": "line"
}
],
"xAxis": {
"data": [
"2017",
"2018",
"2019",
"2020",
"2021",
"2022",
"2023",
"2024",
"2025"
]
}
},
"message": null
}
\ No newline at end of file
{
"code": 1,
"data": {
"events": {
"2018-06": {
"GPT-1发布": [
{
"event_time": "2018-06",
"event_title": "GPT-1发布,首个将Transformer与无监督预训练相结合的模型,开启了大规模语言模型的探索,参数量为1.17亿",
"event_type": "GPT-1发布",
"id": 1
}
]
},
"2019-02": {
"GPT-2发布": [
{
"event_time": "2019-02",
"event_title": "GPT-2发布,参数增至15亿,引入零样本学习能力,支持直接通过指令执行任务,如翻译、摘要等",
"event_type": "GPT-2发布",
"id": 2
}
]
},
"2020-06": {
"GPT-3发布": [
{
"event_time": "2020-06",
"event_title": "GPT-3发布,参数量达1750亿,显著提升了自然语言理解和生成能力,支持175种语言,首次实现少样本学习",
"event_type": "GPT-3发布",
"id": 3
}
]
},
"2022-11-30": {
"GPT-3.5发布": [
{
"event_time": "2022-11-30",
"event_title": "GPT-3.5发布,通过人类反馈强化学习(RLHF)进行微调,使回答更加准确、安全且符合人类价值观",
"event_type": "GPT-3.5发布",
"id": 4
}
]
},
"2023-03-14": {
"GPT-4发布": [
{
"event_time": "2023-03-14",
"event_title": "GPT-4发布,不仅支持文本,还能处理图像输入,在复杂任务中表现更接近人类水平,同时大幅降低错误率",
"event_type": "GPT-4发布",
"id": 5
}
]
},
"2023-09-26": {
"GPT-4V发布": [
{
"event_time": "2023-09-26",
"event_title": "GPT-4V发布,是GPT-4的视觉增强版本",
"event_type": "GPT-4V发布",
"id": 6
}
]
},
"2023-11-06": {
"GPT-4 Turbo发布": [
{
"event_time": "2023-11-06",
"event_title": "GPT-4 Turbo发布,具有更快的速度和更低的成本,上下文窗口提升到128K",
"event_type": "GPT-4 Turbo发布",
"id": 7
}
]
},
"2024-05-14": {
"GPT-4o发布": [
{
"event_time": "2024-05-14",
"event_title": "GPT-4o发布,“o”代表“omni”,意为“全能”,是真正的全模态模型,支持图、声、语等多种模态,可进行实时语音对话",
"event_type": "GPT-4o发布",
"id": 8
}
]
},
"2025-02-27": {
"GPT-4.5发布": [
{
"event_time": "2025-02-27",
"event_title": "GPT-4.5发布,代表了无监督学习扩展方面的一个进步,拥有更广泛的知识库,能更好地理解用户意图,幻觉率显著下降",
"event_type": "GPT-4.5发布",
"id": 9
}
]
},
"2025-04-15": {
"GPT-4.1发布": [
{
"event_time": "2025-04-15",
"event_title": "GPT-4.1发布,包括GPT‑4.1、GPT‑4.1 mini和GPT‑4.1 nano,在编码、指令遵循和长上下文处理方面实现了重大突破,首次支持高达1百万tokens的超长上下文窗口",
"event_type": "GPT-4.1发布",
"id": 10
}
]
}
},
"events_number": 10,
"events_relations": [
{
"relation_type": "模型版本变更",
"source": 1,
"target": 2
},
{
"relation_type": "模型版本变更",
"source": 2,
"target": 3
},
{
"relation_type": "模型版本变更",
"source": 3,
"target": 4
},
{
"relation_type": "模型版本变更",
"source": 4,
"target": 5
},
{
"relation_type": "模型版本变更",
"source": 5,
"target": 6
},
{
"relation_type": "模型版本变更",
"source": 6,
"target": 7
},
{
"relation_type": "模型版本变更",
"source": 7,
"target": 8
},
{
"relation_type": "模型版本变更",
"source": 8,
"target": 9
},
{
"relation_type": "模型版本变更",
"source": 9,
"target": 10
}
],
"events_time": [
"2018-06",
"2019-02",
"2020-06",
"2022-11-30",
"2023-03-14",
"2023-09-26",
"2023-11-06",
"2024-05-14",
"2025-02-27",
"2025-04-15"
],
"events_type": {
"GPT-1发布": 1,
"GPT-2发布": 1,
"GPT-3发布": 1,
"GPT-3.5发布": 1,
"GPT-4发布": 1,
"GPT-4V发布": 1,
"GPT-4 Turbo发布": 1,
"GPT-4o发布": 1,
"GPT-4.5发布": 1,
"GPT-4.1发布": 1
},
"relation_type": [
"模型版本变更"
]
},
"message": null
}
\ No newline at end of file
{
"success": true,
"data": {
"result": [
{
"_key": "54833195",
"_id": "event_line_entity/54833195",
"id": 1,
"event_title": "模版匹配",
"event_time": "2022年7月19日",
"event_type": "类型2",
"target": [
{
"name": "信息技术",
"value": 0.25
},
{
"name": "智能助手",
"value": 0.45
},
{
"name": "能源",
"value": 0.2
},
{
"name": "医疗",
"value": 0.1
},
{
"name": "综合能力",
"value": 0.1
}
]
},
{
"_key": "54833388",
"_id": "event_line_entity/54833388",
"id": 2,
"event_title": "机器学习",
"event_time": "2022年7月20日",
"event_type": "类型2",
"target": [
{
"name": "信息技术",
"value": 0.45
},
{
"name": "智能助手",
"value": 0.55
},
{
"name": "能源",
"value": 0.4
},
{
"name": "医疗",
"value": 0.3
},
{
"name": "综合能力",
"value": 0.4
}
]
},
{
"_key": "54833599",
"_id": "event_line_entity/54833599",
"id": 3,
"event_title": "预训练语言",
"event_time": "2022年7月26日",
"event_type": "类型2",
"target": [
{
"name": "信息技术",
"value": 0.75
},
{
"name": "智能助手",
"value": 0.85
},
{
"name": "能源",
"value": 0.7
},
{
"name": "医疗",
"value": 0.6
},
{
"name": "综合能力",
"value": 0.7
}
]
},
{
"_key": "54833649",
"_id": "event_line_entity/54833649",
"id": 4,
"event_title": "大语言模型",
"event_time": "2022年8月26日",
"event_type": "类型3",
"target": [
{
"name": "信息技术",
"value": 0.85
},
{
"name": "智能助手",
"value": 0.95
},
{
"name": "能源",
"value": 0.8
},
{
"name": "医疗",
"value": 0.7
},
{
"name": "综合能力",
"value": 0.9
}
]
}
],
"total": 4
},
"error_msg": null
}
\ No newline at end of file
{
"data": [{
"title": "Meta以1000万–1亿美元年薪挖角44名AI科学家(21人来自中国)",
"country": "美国",
"date": "2025年7月",
"achievement": "目标:Llama 5多模态能力对标GPT-4o,秋季发布",
"company": "Meta / Llama 5(研发中)"
}, {
"title": "谷歌资本支出增至850亿美元,新增50万张H100 GPU",
"country": "美国",
"date": "2025年Q2",
"achievement": "Gemini 2.5支持1000K上下文,代码生成效率提升40%",
"company": "Google / Gemini 2.5"
}, {
"title": "美国AI私募投资1091亿美元,占全球43.2%(中国93亿,占3.7%)",
"country": "美国",
"date": "2025年",
"achievement": "GPT-4o推理成本降至$0.07/百万token(降幅280倍)",
"company": "OpenAI、Anthropic、xAI等"
}, {
"title": "Anthropic获亚马逊/谷歌40亿美元投资,用于Claude 3.5训练",
"country": "美国",
"date": "2024年",
"achievement": "Claude 3.5编程任务(SWE)得分全球第一(86.5)",
"company": "Anthropic / Claude 3.5"
}, {
"title": "OpenAI从特斯拉、xAI挖角核心研究员,强化o1模型推理能力",
"country": "美国",
"date": "2024年",
"achievement": "o1-mini复杂规划任务完成率仅23.6%(20步以上问题)",
"company": "OpenAI / o1-mini"
}, {
"title": "微软收购Inflection AI,吸纳70名工程师及CEO,开发MAI-1模型",
"country": "美国",
"date": "2024年3月",
"achievement": "MAI-1数学推理能力超GPT-4(MATH基准87.1分)",
"company": "Microsoft / MAI-1"
}, {
"title": "OpenAI获微软追加100亿美元投资,用于GPT-4/4o研发及算力扩容",
"country": "美国",
"date": "2023–2024年",
"achievement": "GPT-4o多模态响应速度提升2倍,综合性能全球第一(2024 Arena榜)",
"company": "OpenAI / GPT-4系列"
}, {
"title": "DeepSeek V3发布,训练成本557.6万美元(278.8万H800 GPU小时)",
"country": "中国",
"date": "2025年7月",
"achievement": "性价比全球第一(API价格$0.27/百万token),数学任务超Claude3.5",
"company": "DeepSeek / V3"
}, {
"title": "DeepSeek获金沙江创投朱啸虎公开投资意向,估值超100亿美元",
"country": "中国",
"date": "2025年7月",
"achievement": "V3数学任务(MATH-500)90.2分,超GPT-4o(74.6)",
"company": "DeepSeek / V3"
}, {
"title": "阿里云PAI Model Gallery支持一键部署DeepSeek-V3/R1,优化分布式训练效率",
"country": "中国",
"date": "2025年2月",
"achievement": "DeepSeek-V3训练成本仅557万美元(对标Llama 3节约90%+)",
"company": "DeepSeek / V3、R1"
}]
}
\ No newline at end of file
{
"data": [{
"title": "GPT-4.1发布,包括GPT‑4.1、GPT‑4.1 mini和GPT‑4.1 nano,在编码、指令遵循和长上下文处理方面实现了重大突破,首次支持高达1百万tokens的超长上下文窗口",
"country": "美国",
"date": "2025年4月15日",
"company": "OpenAI"
}, {
"title": "GPT-4.5发布,代表了无监督学习扩展方面的一个进步,拥有更广泛的知识库,能更好地理解用户意图,幻觉率显著下降",
"country": "美国",
"date": "2025年2月27日",
"company": "OpenAI"
}, {
"title": "GPT-4o发布,“o”代表“omni”,意为“全能”,是真正的全模态模型,支持图、声、语等多种模态,可进行实时语音对话",
"country": "美国",
"date": "2024年5月14日",
"company": "OpenAI"
}, {
"title": "GPT-4 Turbo发布,具有更快的速度和更低的成本,上下文窗口提升到128K",
"country": "美国",
"date": "2023年11月6日",
"company": "OpenAI"
}, {
"title": "GPT-4V发布,是GPT-4的视觉增强版本",
"country": "美国",
"date": "2023年9月26日",
"company": "OpenAI"
}, {
"title": "GPT-4发布,不仅支持文本,还能处理图像输入,在复杂任务中表现更接近人类水平,同时大幅降低错误率",
"country": "美国",
"date": "2023年3月14日",
"company": "OpenAI"
}, {
"title": "GPT-3.5发布,通过人类反馈强化学习(RLHF)进行微调,使回答更加准确、安全且符合人类价值观",
"country": "美国",
"date": "2022年11月30日",
"company": "OpenAI"
}, {
"title": "GPT-3发布,参数量达1750亿,显著提升了自然语言理解和生成能力,支持175种语言,首次实现少样本学习",
"country": "美国",
"date": "2020年6月",
"company": "OpenAI"
}, {
"title": "GPT-2发布,参数增至15亿,引入零样本学习能力,支持直接通过指令执行任务,如翻译、摘要等",
"country": "美国",
"date": "2019年2月",
"company": "OpenAI"
}, {
"title": "GPT-1发布,首个将Transformer与无监督预训练相结合的模型,开启了大规模语言模型的探索,参数量为1.17亿。",
"country": "美国",
"date": "2018年6月",
"company": "OpenAI"
}]
}
\ No newline at end of file
<template>
<div class="hyper common" v-loading="loading" element-loading-text="正在加载中...">
<div class="hyper-aside">
<!-- <div style="display: flex">
<el-button v-if="isExpand" :icon="ArrowUp" class="aside-title" @click="isShowList"></el-button>
<el-button v-if="!isExpand" :icon="ArrowDown" class="aside-title" @click="isShowList"></el-button>
<el-button :icon="Plus" class="aside-title" @click="toCreate"></el-button>
<el-button :icon="Upload" class="aside-title" @click="onImportClick"></el-button>
<input ref="fileInputRef" type="file" accept=".xls, .xlsx" style="display: none" @change="handleImport" />
</div> -->
<!-- <div class="aside-list" v-if="isExpand">
<div class="list-box">
<div :class="['list', event_graph_id == list.id ? 'active-list' : '']" v-for="(list, index) in asideList" :key="list.id" @click.stop="changeActive(index, list)">
<el-tooltip :content="list.name" :disabled="!list.tooltip">
<span class="list-name" @mouseenter="(e) => isShowTooltip(e, list)">
{{ list.name }}
</span>
</el-tooltip>
<div @click.stop>
<el-popconfirm title="确认删除图谱?" @confirm="handleDelete(list.id)">
<template #reference>
<span class="delete" slot="reference">删除</span>
</template>
</el-popconfirm>
</div>
</div>
</div>
<div class="custom-pagination-box">
<el-pagination class="bottom-pagination" size="small" @current-change="handleCurrentChange" :current-page="pagination.page" :page-size="pagination.page_size" layout="total,prev, pager, next" :total="pagination.total_count">
</el-pagination>
</div>
</div> -->
<!-- <div v-else class="empty-text">暂无数据</div> -->
</div>
<div class="theme-title">{{ curThemeTitle }}</div>
<div class="hyper-graph">
<Map class="map" ref="mapRef" :config="mapConfig" v-show="isShowMap"></Map>
<HullGraphTimeBar class="hull-graph" ref="graphRef" :graphData="graphData" :graphZoomCenter="graphZoomCenter" :eventGraphId="event_graph_id" :eventGraphName="event_graph_name" @handleViewInfo="handleViewInfo" @getEventGraphData="getEventGraphData" @saveMap="saveMap" @showHideMap="showHideMap" @refreshGraph="refreshGraph"/>
<!-- 节点信息 -->
</div>
<DrawerComp style="background: #ffffffcc" :drawerVisible="drawerVisible" :eventId="event_graph_id" @closeDrawer="closeDrawer"></DrawerComp>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onBeforeUnmount,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import {
Plus,
Upload,
ArrowUp,
ArrowDown
} from '@element-plus/icons-vue'
import {
useRoute,
useRouter
} from 'vue-router';
import {
useStore
} from "vuex";
import HullGraphTimeBar from "./components/HullGraphTimeBar.vue";
import DrawerComp from "./components/TempDrawer.vue";
import Map from "./components/map/ManagementMap.vue";
import {
textRange
} from "@/utils/index";
import {
getGraphList,
getEntityList,
getGraphInfo,
getHypermapById
} from "@/api/graphApi";
import { importExcelFileByServer } from "@/utils/excelUtils.js";
const {
appContext
} = getCurrentInstance();
const globalProxy = appContext.config.globalProperties;
const store = useStore();
let route = useRoute();
let router = useRouter();
let activeIndex = ref(0);
let asideList = ref([]); // 侧边列表
let mapConfig = ref(null);
let graphZoomCenter = ref(null);
let graphData = ref({
nodes: [],
edges: [],
groups: []
}); // 图谱数据
let graphRef = ref()
let isGraphShow = ref(true);
let drawerVisible = ref(false);
let event_graph_id = ref(null);
let event_graph_name = ref(null);
let cache_key = ref(null); // 用于清除缓存
let keyWord = ref("");
let loading = ref(false);
let pagination = ref({
page: 1,
page_size: 15,
total_count: 0
})
let mapRef = ref()
let isShowMap = ref(true)
let isExpand = ref(true)
let tableLoading = ref(false)
const curThemeTitle = ref('')
onMounted(() => {
const curTheme = JSON.parse(window.localStorage.getItem('theme'))
changeActive(0,curTheme)
curThemeTitle.value = curTheme.name
handleRefresh();
window.addEventListener("resize", function(){
if (event_graph_id.value){
getEventGraphData(event_graph_id.value);
}
})
});
onBeforeUnmount(() => {
getViewData();
})
// 文件输入实例对象
const fileInputRef = ref(null);
// 导入
const onImportClick = () => {
// 模拟点击元素
// if (fileInputRef.value) {
// // 重置以允许重复选择相同文件
// fileInputRef.value.value = "";
// fileInputRef.value.click();
// }
};
// 点击【导入】触发
const handleImport= async (e) => {
// let dataList = [];
// try {
// tableLoading.value = true;
// // 获取文件对象
// const input = e.target;
// if (!input.files?.length) return;
// const file = input.files[0];
// // 导入文件,由后端解析文件,获取数据
// dataList = await importExcelFileByServer(file);
// } finally {
// tableLoading.value = false;
// }
}
// 获取图谱列表
function getGraphListFun(type) {
loading.value = true;
getGraphList().then((res) => {
if (res.data && res.data.length > 0) {
loading.value = false;
let result = res;
if (result.data && result.data.length) {
asideList.value = result.data.map((item) => ({
...item,
id: item.group_id,
name: item.group_name,
}));
console.log('asideList',result);
cache_key.value = result.cache_key;
pagination.value.total_count = result.total;
if (!type) {
getDefaultGraphData();
}
}
} else {
loading.value = false;
globalProxy.$message.error(res.data.error_msg);
}
}).catch(() => {
loading.value = false;
});
}
// 获取默认的图谱展示
function getDefaultGraphData() {
if (!event_graph_id.value) {
let default_graph = asideList.value[0];
changeActive(0, default_graph);
} else {
getEventGraphData();
}
}
function refreshGraph() {
if (event_graph_id.value){
getEventGraphData(event_graph_id.value);
}
}
// 选中状态切换
function changeActive(index, data) {
const {
group_id,
group_name
} = data
getViewData();
activeIndex.value = index;
event_graph_id.value = group_id;
event_graph_name.value = group_name
getEventGraphData(group_id);
}
const saveMap = (callback)=>{
let data = mapRef.value.saveMapFun();
callback(data); //返回data值
}
function showHideMap(){
isShowMap.value = !isShowMap.value;
}
// 获取图谱数据
function getEventGraphData(id) {
let paramsObj = {
id: id
}
getHypermapById(paramsObj).then((res)=>{
let leftTopLon = res.data.bounds.leftTop.lon;
let leftTopLat = res.data.bounds.leftTop.lat;
let rightBottomLon = res.data.bounds.rightBottom.lon;
let rightBottomLat = res.data.bounds.rightBottom.lat;
let data = mapRef.value.getXYPosition();
let params = {
id: id,
// cur_lt_lon: data.leftTop.lon,
// cur_lt_lat: data.leftTop.lat,
cur_lt_lon: leftTopLon,
cur_lt_lat: leftTopLat,
cur_lt_x: data.leftTop.x,
cur_lt_y: data.leftTop.y,
// cur_rb_lon: data.rightBottom.lon,
// cur_rb_lat: data.rightBottom.lat,
cur_rb_lon: rightBottomLon,
cur_rb_lat: rightBottomLat,
cur_rb_x: data.rightBottom.x,
cur_rb_y: data.rightBottom.y,
}
getGraphInfo(params).then((res)=>{
let result = res;
// 没有记录点位数据,获取初始化的
if (JSON.stringify(result) !== "{}") {
const {
graph
} = result;
graphData.value = graph.data;
// graphZoomCenter.value = graph.zoom_center
graphZoomCenter.value = {
zoom: 1
}
mapConfig.value = graph.zoom_center;
}
}).catch(() => {
// loading.value = false;
});
})
}
// 地图/画布视图--->节点拖拽/缩放
function getViewData() {
let graph = graphRef.value.graph;
if (graph) {
let graph_center = graph.getGraphCenterPoint()
let groups = [];
let nodes = graph.getNodes().map((node) => {
const model = node.getModel();
const {
id,
label,
originLabel,
parent_id,
x,
y,
} = model
// const {x:clientX,y:clientY} = graph.getClientByPoint(x,y)
return {
id,
label,
originLabel,
parent_id,
// x:clientX,
// y:clientY,
x,
y
};
});
let edges = graph.getEdges().map((edge) => {
const {
id,
label,
source,
target,
date,
} = edge.getModel();
return {
id,
label,
source,
target,
date,
};
});
let hulls = graph.getHulls();
for (let key in hulls) {
const {
cfg
} = hulls[key];
groups.push({
group: {
id: cfg.id,
name: cfg.label,
},
ids: cfg.members,
});
}
if (!nodes.length && !edges.length && !groups.length) {
return
}
// let graph_info = {
// data: {
// nodes,
// edges,
// groups,
// },
// zoom_center: {
// zoom: graph_zoom,
// center: graph_center
// }
// };
// let map_info = {
// center: map.getCenter(),
// zoom: map.getZoom(),
// };
// let postData = {
// graph_id: event_graph_id.value,
// graph_data: {
// graph: graph_info,
// map: map_info,
// },
// };
// requestAddTimeSpaceGraphXY(postData).then((res) => {
// if (res.data && res.data.success) {
// // 存储不提示
// return;
// } else {
// globalProxy.$message.error(res.data.error_msg);
// }
// });
}
//
}
// 专题生成页面
function toCreate() {
router.push("createTheme");
}
function isShowList() {
isExpand.value = !isExpand.value
}
function isShowTooltip(e, data) {
const bool = textRange(e.target);
data["tooltip"] = bool;
}
// 节点点击
function handleViewInfo() {
drawerVisible.value = true;
}
// 删除
const handleDelete = (id) => {
// 删除的是当前选中的列表
if (id == event_graph_id.value) {
event_graph_id.value = null;
event_graph_name.value = null
}
// requestDeleteEntityById({
// entity_id: id
// }).then((res) => {
// if (res.data && res.data.success) {
// globalProxy.success("删除成功");
// handleRefresh();
// } else {
// globalProxy.$message.error(res.data.error_msg);
// }
// }).catch(() => {
// globalProxy.$message({
// type: "info",
// message: "已取消删除",
// });
// });
}
// 清除缓存
function handleRefresh() {
getGraphListFun();
// let local_yz_cache_key = localStorage.getItem("yz_graph_cache_key") || "";
// if (local_yz_cache_key) {
// requestDeleteCacheStatus({
// cache_key: local_yz_cache_key
// }).then(
// (res) => {
// if (res.data && res.data.success) {
// getGraphListFun();
// } else {
// globalProxy.$message.error(res.data.error_msg);
// }
// }
// );
// } else {
// getGraphListFun();
// }
}
function closeDrawer() {
drawerVisible.value = false;
}
function handleCurrentChange(page) {
pagination.value.page = page;
getGraphListFun("page");
}
</script>
<style lang="scss" scoped>
.hyper {
height: 100%;
display: flex;
position: relative;
.theme-title{
position: absolute;
right: 100px;
top: 5px;
// width: 200px;
padding: 0 10px;
height: 30px;
line-height: 30px;
font-size: 18px;
z-index: 99999;
background: rgba(1, 20, 35,0.8);
color: #409EFF;
font-weight: bold;
}
.hyper-aside {
position: absolute;
z-index: 1000;
margin: 5px 10px;
width: 250px;
max-height: calc(100% - 80px);
display: flex;
flex-direction: column;
box-shadow: 0 2px 12px 0 #0000001a;
cursor: pointer;
.aside-title {
width: 30px;
background: #000000cc;
color: #ffffff;
border: none;
margin-left: 0;
margin-right: 2px;
}
.aside-list {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
margin-top: 2px;
background: linear-gradient(45deg, black, transparent);
border-radius: 8px;
.list-box {
flex: 1;
}
.custom-pagination-box {
position: relative;
.bottom-pagination {
float: right;
}
}
.list {
padding: 12px;
border-bottom: 1px solid #ccc6;
display: flex;
justify-content: space-between;
font-size: 14px;
.list-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.delete {
margin-left: 14px;
min-width: 50px;
display: inline-block;
color: red;
font-size: 12px;
}
}
.active-list {
background: #409EFF;
}
}
.empty-text {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 18px;
color: #909399;
}
}
.hyper-graph {
flex: 1;
position: relative;
.map {
position: absolute;
height: 100%;
width: 100%;
z-index: 99;
}
.hull-graph {
position: absolute;
width: 100%;
height: 100%;
// background: #ddd;
}
}
}
</style>
<style lang="scss">
.bottom-pagination .btn-prev,
.bottom-pagination .el-pager .is-active,
.bottom-pagination .btn-next
{
background-color: transparent !important;
}
</style>
\ No newline at end of file
<template>
<div class="theme-search">
<div class="header">
<el-select class="header-select" multiple v-model="selectList" :popper-append-to-body="false" placeholder="请选择" @change="handleChange">
<el-option v-for="l in itemList" :key="l.id" :label="l.name" :value="l.id"></el-option>
</el-select>
<el-button type="primary" style="margin-left: 10px" @click="handleSearch">时空检索</el-button>
<el-button type="primary" style="margin-left: 10px" v-if="graphVisible" @click="intelligentAnalysis">智能分析</el-button>
<!-- <el-button style="margin-left: 10px" @click="backToInit">返回初始位置</el-button> -->
<!-- <el-button style="margin-left: 10px" v-if="graphVisible" @click="showHide">图谱显隐</el-button> -->
</div>
<div class="graph" v-if="graphVisible">
<HullGraph ref="hullGraphRef" @changeGraphData="handleChangeGraphData" />
</div>
<Map class="map" ref="mapRef" :mapCenter="mapCenter" v-show="isShowMap"></Map>
<el-dialog class="analysis-dialog" title="智能分析" v-model="analysisDialog" width="50%" :before-close="handleCancel" :close-on-click-modal="false" :modal="false">
<el-input
v-model="textarea"
style="width: 100%"
:autosize="{ minRows: 5, maxRows: 8 }"
type="textarea"
placeholder="请输入分析内容"
/>
<div class="dialog-footer">
<el-button @click="handleCancel">取 消</el-button>
<el-button type="primary" @click="handleConfirm">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import HullGraph from "./components/HullGraph.vue";
import Map from "./components/map/SearchMap.vue";
import {
getGraphList
} from "@/api/graphApi";
import {
useRoute,
useRouter
} from 'vue-router';
import {
Plus,
Minus
} from '@element-plus/icons-vue'
const emit = defineEmits(['changeIndex']);
const {
appContext
} = getCurrentInstance();
const globalProxy = appContext.config.globalProperties;
let route = useRoute();
let router = useRouter();
let mapRef = ref()
let graphData = ref({
nodes: [],
edges: [],
groups: []
})
let mapCenter = ref({});
let isShowMap = ref(true);
let analysisDialog = ref(false)
let textarea = ref("")
onMounted(() => {
getGraphList().then((res) => {
if (res.data && res.data.length > 0) {
let result = res;
if (result.data && result.data.length) {
itemList.value = result.data.map((item) => ({
...item,
id: item.group_id,
name: item.group_name,
description: item.description
}));
}
} else {
globalProxy.$message.error(res.data.error_msg);
}
}).catch(() => {
});
})
let graphVisible = ref(false)
let hullGraphRef = ref(null)
let itemList = ref([])
let selectList = ref([])
function handleChange(params) {}
function handleChangeGraphData(data) {
graphData.value = data;
}
function handleSearch() {
graphVisible.value = false;
nextTick(() => {
graphVisible.value = true;
let data = selectList.value.map(e => parseInt(e))
setTimeout(() => {
if (hullGraphRef.value) {
hullGraphRef.value.getData(data);
}
});
});
}
function intelligentAnalysis() {
analysisDialog.value = true;
}
function handleCancel() {
analysisDialog.value = false;
}
function handleConfirm() {
analysisDialog.value = false;
emit("changeIndex", '/mechanismAnalysis')
}
function showHide() {
if (selectList.value && selectList.value.length > 0) {
hullGraphRef.value.showOrHide();
}
}
function backToInit(params) {
if (hullGraphRef.value) {
hullGraphRef.value.backToInit();
}
}
</script>
<style lang="scss" scoped>
.theme-search {
position: relative;
width: 100%;
height: 100%;
.header {
position: absolute;
z-index: 1000;
position: absolute;
margin: 10px 0 0 10px;
display: flex;
.header-select {
min-width: 200px;
max-width: 300px;
}
}
.map {
position: absolute;
height: 100%;
width: 100%;
z-index: 99;
}
.graph {
position: absolute;
width: 100%;
height: 100%;
}
.analysis-dialog {
.dialog-footer {
margin-top: 10px;
}
}
}
</style>
<template>
<el-dialog v-model="props.info.visible" :modal="false" @close="handleCloseAddRelation">
<el-form :model="relationFormState" ref="relationFormRef" class="form-edge">
<el-form-item class="custom-edge-item">
<div class="left-form-item entity-item">
<el-select v-model="relationFormState.from" filterable remote clearable reserve-keyword placeholder="请输入关键词" :remote-method="remoteMethod" :loading="selectLoading" @change="changeLeftSelect">
<el-option v-for="item in selectOptions" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
</div>
<div class="line-box">
<!-- <el-select v-model="relationFormState.relation_name" placeholder="请选择" clearable @change="changeRelation">
<el-option v-for="item in relationList" :key="item.value" :label="item.label" :value="item.label">
</el-option>
</el-select> -->
<el-input style="margin-top: -40px" v-model="relationFormState.relation_name" placeholder="请输入关系名称" />
<div class="line"></div>
<el-icon class="sort" @click="handleExchange"><Sort /></el-icon>
</div>
<div class="right-form-item entity-item">
<el-select v-model="relationFormState.to" filterable remote clearable reserve-keyword placeholder="请输入关键词" :remote-method="remoteMethod" :loading="selectLoading" @change="changeRightSelect">
<el-option v-for="item in selectOptions" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
</div>
</el-form-item>
</el-form>
<div class="dialog-footer">
<el-button @click="handleCloseAddRelation">取消</el-button>
<el-button type="primary" @click="handleConfirmRelation">确定</el-button>
</div>
</el-dialog>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import {
useRoute,
useRouter
} from 'vue-router';
import {
useStore
} from "vuex";
const emit = defineEmits(['handleCloseAddRelation', 'handleConfirmRelation','handleQuery']);
const {
appContext
} = getCurrentInstance();
const globalProxy = appContext.config.globalProperties;
const store = useStore();
let route = useRoute();
let router = useRouter();
let props = defineProps({
info: {
type: Object,
default: () => {},
},
title: {
type: String,
default: "",
},
})
let relationFormRef = ref()
let visibleVal = ref(false)
let selectLoading = ref(false)
// let selectOptions = reactive([])
let relationList = reactive([])
let showAddRelation = ref(false)
let relationFormState = ref({
from: "",
from_id: "",
relation_name: "",
relation_id: "",
to: "",
to_id: "",
})
const selectOptions = computed(() => {
let arr = props.info.nodes.map(item => {
return {
label: item.label,
value: item.label
}
})
return arr
})
watch(() => props.info, (newValue, oldValue) => {
handler(newValue);
}, { deep: true })
function handler(val) {
const {
visible,
from,
fromId
} = val;
visibleVal.value = visible
relationFormState.value.from = from;
relationFormState.value.from_id = fromId;
}
// 快速创建边关系
function handleCreateRelation() {
showAddRelation.value = true
}
function handleCloseCreateRelation() {
showAddRelation.value = false
}
// 远程检索
function remoteMethod(query) {
// if (query !== "") {
// selectLoading.value = true;
// selectOptions = [];
// }
emit('handleQuery',query)
}
function changeLeftSelect(val) {
if (val) {
let {
entity_id
} = selectOptions.find((f) => f.value == val);
relationFormState.value.from_id = entity_id;
getRelations();
}
}
function changeRightSelect(val) {
// if (val) {
// let {
// entity_id
// } = selectOptions.find(
// (f) => f.value == val
// );
// relationFormState.value.to_id = entity_id;
// getRelations();
// }
}
// 获取两个实体之间的边关系
function getRelations() {
relationFormState.value.relation_name = "";
const {
from_id,
to_id
} = relationFormState.value;
if (from_id && to_id) {
}
}
// 获取关系id
function changeRelation(val) {
if (val) {
let {
value
} = relationList.find((f) => f.label == val);
relationFormState.value.relation_id = value;
}
}
function handleCloseAddRelation() {
// selectOptions = [];
relationFormState.value = {
from: "",
from_id: "",
relation_name: "",
relation_id: "",
to: "",
to_id: "",
};
emit("handleCloseAddRelation");
}
function handleConfirmRelation() {
const {
from,
to,
relation_name
} = relationFormState.value;
if (from && to && relation_name) {
emit("handleConfirmRelation", relationFormState.value);
} else {
globalProxy.$message.error("有未选择项");
}
}
function handleExchange() {
let originObj = JSON.parse(JSON.stringify(relationFormState.value));
relationFormState.value.from = originObj.to;
relationFormState.value.from_id = originObj.to_id;
relationFormState.value.to = originObj.from;
relationFormState.value.to_id = originObj.from_id;
getRelations();
}
</script>
<style lang="scss" scoped>
.dialog-title {
font-size: 16px;
text-align: left;
color: #000000;
}
.form-edge {
margin-top: 24px;
.custom-edge-item {
.line-box {
margin: 0 14px;
position: relative;
>.el-select {
position: absolute;
bottom: 8px;
left: 0;
text-align: center;
width: 100%;
}
.line {
width: 100%;
height: 1px;
background: #ccc;
position: relative;
&::before {
display: inline-block;
content: "";
position: absolute;
right: -6px;
top: -3px;
border: 6px solid #ccc;
width: 0;
height: 0;
border-top-color: transparent;
border-bottom-color: transparent;
border-right-color: transparent;
}
}
.sort {
position: absolute;
bottom: -20px;
left: 50%;
cursor: pointer;
transform: translateX(-50%) rotate(90deg);
}
}
.icon {
margin-left: 14px;
cursor: pointer;
}
.entity-item {
position: relative;
.el-select {
width: 100%;
}
}
:deep(.el-form-item__content) {
display: flex;
align-items: center;
margin-bottom: 24px;
line-height: 24px;
width: 40%;
>div {
flex: 1;
}
}
}
}
</style>
<style>
.el-dialog__header {
text-align: left;
}
</style>
<template>
<el-dialog :title="props.nodeDialogInfo.title" v-model="props.nodeDialogInfo.visible" width="35%" :before-close="handleDialogClose" :close-on-click-modal="false" :modal="false">
<div class="dialog-content">
<div class="content-select">
<el-select v-model="selectedNodeType" placeholder="请选择类型">
<el-option v-for="item in nodeTypeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</div>
<div class="content-input">
<el-input ref="inputRef" v-model="nodeName" placeholder="请输入节点名称"/>
</div>
</div>
<div class="desc-box" v-if="selectedNodeType == '实体'">
<el-input maxlength="1000" show-word-limit type="textarea" :rows="4" v-model="nodeDesc" placeholder="请输入节点描述"/>
</div>
<div class="dialog-footer">
<el-button @click="handleDialogClose" size="small">取 消</el-button>
<el-button type="primary" @click="handleDialogConfirm" size="small">确 定</el-button>
</div>
</el-dialog>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import {
useRoute,
useRouter
} from 'vue-router';
import {
useStore
} from "vuex";
const emit = defineEmits(['handleDialogClose', 'handleDialogEdit', 'handleDialogAdd']);
const {
appContext
} = getCurrentInstance();
const globalProxy = appContext.config.globalProperties;
const store = useStore();
let route = useRoute();
let router = useRouter();
let props = defineProps({
nodeDialogInfo: {
type: Object,
default: {
visible: true, // 弹窗显隐藏
title: "", // 弹窗名
selectNodeId: null, // 选中的节点id,
selectLabel: "", // 选中节点的名字
}
}
})
let inputRef = ref()
let selectedNodeType = ref("实体")
let nodeTypeList = ref([{
value: "实体",
label: "实体"
},{
value: "事件",
label: "事件"
}])
let nodeName = ref("") // 节点名称
const nodeDesc = ref("") // 节点描述
let formStateRef = ref();
let formState = ref({
type: "",
name: ""
})
let columnList = ref([])
let treeData = ref([])
function handleDialogClose() {
formState.value = {};
emit("handleDialogClose");
}
function handleDialogConfirm() {
formState.value = {
type: selectedNodeType.value,
name: nodeName.value,
id: props.nodeDialogInfo.selectNodeId?props.nodeDialogInfo.selectNodeId:''
}
if (props.nodeDialogInfo.selectNodeId) {
//编辑
emit("handleDialogEdit", formState.value);
} else {
//新增
const addFormData = {
name: formState.value.name,
desc: nodeDesc.value
}
emit("handleDialogAdd", addFormData);
}
}
</script>
<style lang="scss" scoped>
.dialog-content {
display: flex;
.content-select {
width: 30%;
}
.content-input {
margin-left: 10px;
width: calc(70% - 10px);
}
}
.desc-box{
margin-top: 20px;
height: 100px;
}
.dialog-footer {
margin-top: 10px;
}
</style>
<template>
<div class="search-entity-container">
<el-select placeholder="请输入关键词" v-model="selectValue" multiple filterable remote reserve-keyword collapse-tags clearable :remote-method="querySearchAsync" v-select-lazy="hanldeLazyMore" popper-class="select-factual-popper-class" @clear="handleClear">
<el-option v-for="(item, index) in entityOptions" :key="item.value" :label="item.label" :value="item">
<el-tooltip popper-class="content-tooltip" :disabled="!item.tooltip">
<div>
<div class="content-container">{{ item.label }}</div>
</div>
<div class="select-content-label" @mouseenter="(e) => isShowSelectTooltip(e, index)">
{{ item.label }}
</div>
</el-tooltip>
</el-option>
</el-select>
<el-button type="primary" @click="handleAddNode">添加</el-button>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import {
debounce,
cloneDeep
} from "lodash";
import {
textRange
} from "@/utils/index";
let props = defineProps({
currentNodeKey: {
type: String,
default: "",
}
})
const emit = defineEmits(['handleAddNode']);
let searchInput = ref(null)
let entityOptions = ref([])
let selectValue = ref([])
let lazyPagination = ref({
page: 1,
page_size: 10,
total_count: 0,
})
watch(
() => searchInput.value,
(newVal, oldVal) => {
handler(newVal);
}, {
deep: true
}
);
watch(
() => props.currentNodeKey,
(newVal, oldVal) => {
handler2(newVal);
}, {
deep: true
}
);
function handler(val) {
if (val) {
resetLazyPagination();
}
}
function handler2(val) {
if (val) {
selectValue.value = [];
handleClear();
getLoadOptions();
}
}
// 模糊搜索
function querySearchAsync(queryString) {
if (queryString) {
searchInput.value = queryString;
getLoadOptions(queryString);
}
}
function getLoadOptions(queryString = "", wait = 1000) {
return debounce(function () {
const {
page,
page_size
} = lazyPagination.value;
}, wait)();
}
// 懒加载
function hanldeLazyMore() {
let max_page = Math.ceil(
lazyPagination.value.total_count / lazyPagination.value.page_size
);
if (lazyPagination.value.page < max_page) {
lazyPagination.value.page += 1;
getLoadOptions(searchInput.value);
}
}
function handleClear() {
searchInput.value = null;
entityOptions.value = [];
selectValue.value = [];
resetLazyPagination();
}
// selecttooltip控制
function isShowSelectTooltip(e, index) {
const bool = textRange(e.target);
entityOptions.value[index]["tooltip"] = bool;
}
// 重置懒加载的分页
function resetLazyPagination() {
lazyPagination.value = {
page: 1,
page_size: 10,
total_count: 0,
};
}
// 添加节点
function handleAddNode() {
if (!selectValue.value.length) {
return;
}
emit("handleAddNode", selectValue.value);
}
// 节点查重
function handleDuplicateCheck(selectArr, nodes) {
let arr = [];
selectArr.forEach((item) => {
let has = nodes.findIndex((f) => f.id == item._id);
if (has == -1) {
const {
_id,
name,
...arg
} = item;
arr.push({
id: _id,
label: name,
...arg,
});
}
});
return arr;
}
</script>
<style lang="less" scoped>
.search-entity-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
.el-select {
width: 100%;
flex: 1;
}
.el-button {
margin-left: 10px;
}
}
</style><style lang="less">
.select-factual-popper-class {
width: 300px;
.select-content-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style>
<template>
<div class="toolbar-container common-toolbar">
<el-button v-if="!isExpand" :icon="Expand" class="aside-title" @click="isShowExpand('true')"></el-button>
<el-button v-if="isExpand" :icon="Fold" class="aside-title" @click="isShowExpand('false')"></el-button>
<div v-if="isExpand" v-for="(item, index) in graphTools" :key="index" class="list" @click="changeActive(item)">
<span>{{ item.name }}</span>
</div>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import {
Expand,
Fold
} from '@element-plus/icons-vue'
const emit = defineEmits(['changeActiveTool']);
let isExpand = ref(true);
let graphTools = ref([
// {
// name: "节点搜索",
// value: 'search'
// },
{
name: "编辑节点",
value: 'edit'
},
// {
// name: "删除节点",
// value:'hide'
// },
{
name: "新增节点",
value: 'add'
},
{
name: "新增关系",
value: 'addEdge'
},
{
name: "图谱详情",
value: 'viewInfo'
},
{
name: "图谱显隐",
value: 'showHide'
},
{
name: "地图显隐",
value: 'showHideMap'
},
{
name: "保存超图参数",
value: 'saveGraph'
},
// {
// name: "保存地图参数",
// value: 'saveMap'
// },
])
function changeActive(item) {
emit("changeActiveTool", item);
}
function isShowExpand(params) {
if (params === "true"){
isExpand.value = true;
} else if(params === "false"){
isExpand.value = false;
}
}
</script>
<style lang="scss" scoped>
.toolbar-container {
display: flex;
background: rgba(0, 0, 0, 0.8);
cursor: pointer;
.aside-title {
width: 30px;
background: rgba(0, 0, 0, 0.8);
color: #ffffff;
border: none;
}
.list {
padding: 0 8px;
margin: auto;
font-size: 14px;
&:first-child {
border-radius: 4px 0 0 4px;
margin-left: 0;
}
&:last-child {
border-radius: 0 4px 4px 0;
margin-right: 0;
}
&:hover {
background: #0b68d2;
color: #fff;
}
}
}
</style>
<template>
<div
class="graph-time-line"
v-loading="graphLoading"
element-loading-text="正在加载中..."
>
<div class="container" ref="containerRef" v-show="isGraphShow"></div>
<div class="node-tooltip" ref="tooltipRef"></div>
<div class="timebar-box" v-if="isShowTimeBar">
<div
:class="['show fs14', showFlag ? 'active' : '']"
@click="changeTimebarShow"
>
时间轴
</div>
</div>
<!-- 图例 -->
<div class="graph-legend">
<div v-for="event in eventList" :key="event.id" class="legend-list">
<span class="event-shape" :style="{ background: event.color }"></span>
<el-tooltip :content="event.label" :disabled="!event.tooltip">
<span class="list-name" @mouseenter="(e) => isShowTooltip(e, event)">
{{ event.label }}
</span>
</el-tooltip>
</div>
</div>
<!-- 右键菜单 -->
<NewRingMenu
class="graph-contextmenu"
v-if="showMenu && showContextMenu"
ref="ringMenuRef"
:targetType="clickMenuTarget"
:style="menuStyle"
>
<template #edge>
<div class="rightMenu-edge" @click="handleRemoveEdge">删除关系</div>
</template>
<template #node>
<div class="rightMenu-edge" @click="handleRemoveNode">删除节点</div>
</template>
</NewRingMenu>
<!-- 工具栏 -->
<ToolBarComp
v-if="showToolbar"
class="graph-tools"
@changeActiveTool="handleNodeToolbar"
></ToolBarComp>
<!-- 新增节点&编辑节点 -->
<PropDialogComp
:nodeDialogInfo="nodeDialogInfo"
@handleDialogClose="handleDialogClose"
@handleDialogAdd="handleDialogAdd"
@handleDialogEdit="handleDialogEdit"
></PropDialogComp>
<!-- 新增关系 -->
<AddEdgeComp
:info="addEdgeInfo"
title="新增关系"
@handleCloseAddRelation="handleCloseAddRelation"
@handleConfirmRelation="handleConfirmRelation"
@handleQuery="handleQuery"
></AddEdgeComp>
<!-- 节点搜索 -->
<el-dialog
title="节点搜索"
v-model="searchNodeDialog.visible"
width="30%"
:before-close="handleCloseSearch"
:close-on-click-modal="false"
:modal="false"
>
<SearchEntityComp
@handleAddNode="handleAddNode"
ref="searchEntityRef"
></SearchEntityComp>
</el-dialog>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance,
} from "vue";
import G6 from "@antv/g6";
import { hexToRgb, textRange } from "@/utils/index";
import ToolBarComp from "./G6/Toolbar.vue";
import PropDialogComp from "././G6/PropDialog.vue";
import AddEdgeComp from "./G6/AddEdge.vue";
import SearchEntityComp from "./G6/SearchEntity.vue";
import NewRingMenu from "./NewRingMenu.vue";
import store from "@/store";
import _ from "lodash";
import {
saveHypergraph,
createEntity,
insertEntityIntoHypergraph,
} from "@/api/graphApi";
import { ElMessage } from "element-plus";
const { appContext } = getCurrentInstance();
const globalProxy = appContext.config.globalProperties;
let props = defineProps({
graphData: {
type: Object,
default: () => {},
},
eventGraphId: {
type: Number,
default: 1,
},
eventGraphName: {
type: String,
default: "",
},
showToolbar: {
type: Boolean,
default: true,
},
showContextMenu: {
type: Boolean,
default: true,
},
graphZoomCenter: {
type: Object,
default: null,
},
});
const emit = defineEmits([
"handleNodeClick",
"handleViewInfo",
"saveMap",
"showHideMap",
"refreshGraph",
]);
watch(
() => props.graphData,
(newValue, oldValue) => {
handler(newValue);
},
{
deep: true,
}
);
let graph = null;
let timeBar = ref(null);
let graphLoading = ref(false);
let showFlag = ref(false);
let copy_nodes = ref([]);
let graph_data = ref({
nodes: [],
edges: [],
groups: [],
});
let eventList = ref([]);
let showMenu = ref(false);
let isGraphShow = ref(true); // 图谱默认是否展示
let menuStyle = ref();
let clickMenuTarget = ref(""); // node | edge
let searchNodeDialog = ref({
visible: false,
});
let nodeDialogInfo = ref({
visible: false, // 弹窗显隐藏
title: "新增节点", // 弹窗名
selectNodeId: null, // 选中的节点id,
selectLabel: "", // 选中节点的名字
});
let addEdgeInfo = ref({
visible: false,
}); // 新增关系
let isShowTimeBar = ref(false);
let containerRef = ref();
let tooltipRef = ref();
let ringMenuRef = ref();
let searchEntityRef = ref();
function handler(val) {
const { nodes } = val;
copy_nodes.value = JSON.parse(JSON.stringify(nodes));
handleFormatterGraphData(val);
}
// 处理超图数据
function handleFormatterGraphData(result) {
if (graph) {
if (timeBar.value) {
graph.removePlugin(timeBar.value);
}
showFlag.value = false;
graph.destroy();
}
isGraphShow.value = true;
let graphData = JSON.parse(JSON.stringify(result));
const { nodes } = graphData;
if (!nodes.length) {
eventList.value = [];
isShowTimeBar.value = false;
return globalProxy.$message.error("暂无数据");
}
graphData.nodes.forEach((node, index) => {
let sourceNode = node.id == props.eventGraphId ? node : null;
let has = graphData.groups.find((g) => node.label == g.group.name);
if (has || sourceNode) {
// 原子事件与中心事件
node.size = 20;
node.style = {
stroke: "#ffae0b",
fill: "#e6a23cc2",
lineWidth: 2,
};
}
});
graphData.edges.forEach((edge, index) => {
if (!edge.id) {
delete edge.id;
}
// if (index % 3 === 0){
// edge.date = "2024-05-12"
// } else if (index % 3 === 1){
// edge.date = "2024-05-13"
// } else {
// edge.date = "2024-05-14"
// }
});
// 若边关系上都存在时间则显示时间轴
let timebar_flag = graphData.edges.every((t) => t.date);
graph_data.value = graphData;
nextTick(() => {
init();
});
if (timebar_flag) {
isShowTimeBar.value = true;
} else {
isShowTimeBar.value = false;
}
}
// 初始化
function init() {
const el = containerRef.value;
const tooltip_el = tooltipRef.value;
const widthValue = window.getComputedStyle(el).width.replace("px", "");
const heightValue = window.getComputedStyle(el).height.replace("px", "");
graph_data.value.nodes.forEach((node) => {
if (!node.originLabel) {
node.originLabel = node.label;
node.label = fittingString(node.label, 120, 12);
}
});
const tooltip = new G6.Tooltip({
container: tooltip_el,
offsetX: 10,
offsetY: 0,
fixToNode: [1, 0.5],
itemTypes: ["node"],
getContent: (e) => {
const outDiv = document.createElement("div");
outDiv.style.maxWidth = "300px";
outDiv.innerHTML = `<span>${e.item.getModel().originLabel}</span>`;
return outDiv;
},
});
// 图谱配置
let option = {
container: el,
widthValue,
heightValue,
modes: {
default: [
"drag-canvas",
"zoom-canvas",
"drag-node",
"brush-select",
{
type: "click-select",
trigger: "ctrl",
selectEdge: true,
},
{
type: "drag-combo",
},
],
},
plugins: [tooltip],
defaultNode: {
size: 20,
style: {
lineWidth: 2,
stroke: "#ffae0b",
fill: "#e6a23cc2",
},
labelCfg: {
// 标签配置属性
position: "bottom", // 标签的属性,标签在元素中的位置
offset: 5,
style: {
fontSize: 16,
stroke: "#409eff",
fill: "#ffffff",
lineWidth: 2,
},
},
},
defaultEdge: {
style: {
stroke: "#3e8d76",
lineWidth: 2,
cursor: "pointer",
endArrow: {
path: G6.Arrow.triangle(8, 5, 0),
d: 0,
fill: "#4f9794b3",
},
},
labelCfg: {
style: {
fontSize: 16,
fill: "#ffffff",
cursor: "pointer",
},
autoRotate: true,
refY: 10,
},
},
nodeStateStyles: {
selected: {
lineWidth: 4,
shadowColor: "#ffae0b",
stroke: "#ffae0b",
fill: "#e6a23cc2",
},
},
};
// 节点是否有xy记录
let flag_xy = copy_nodes.value.every(
(e) => e.hasOwnProperty("x") && e.hasOwnProperty("y")
);
if (!flag_xy) {
option = Object.assign(option, {
layout: {
type: "force",
// center: [-500, -500],
preventOverlap: true,
linkDistance: (d) => {
return 300;
},
nodeStrength: (d) => {
return -10;
},
edgeStrength: (d) => {
return 0.5;
},
},
});
}
const graphNew = new G6.Graph({
...option,
});
graph = graphNew;
graph.data(graph_data.value);
graph.render();
initSize();
// 监听拖拽结束事件,保存新的位置
// graph.on("node:dragend", function (e) {
// const model = e.item.get("model");
// // console.log(graph.getZoom(), 'zoom', graph.getGraphCenterPoint(), 'center');
// });
// 画布拖拽事件
graph.on("canvas:drag", (e) => {});
// 画布缩放事件
// graph.on('wheel', (e) => {});
handleEvent(flag_xy);
}
// graph的事件机制
function handleEvent(flag_xy) {
const nodes = graph.getNodes();
const edges = graph.getEdges();
if (nodes.length) {
nodes.forEach((node) => {
node.hide();
});
edges.forEach((edge) => {
edge.hide();
});
if (flag_xy) {
changeGraphLoading(true);
setTimeout(() => {
let newGroups = createGroups();
createHulls(newGroups);
nodes.forEach((node) => {
node.show();
});
edges.forEach((edge) => {
edge.show();
});
// if (props.graphZoomCenter) {
// const {
// zoom,
// center
// } = props.graphZoomCenter
// graph.zoom(zoom, center)
// }
changeGraphLoading(false);
}, 500);
} else {
let newGroups = createGroups();
createHulls(newGroups);
nodes.forEach((node) => {
node.show();
});
edges.forEach((edge) => {
edge.show();
});
changeGraphLoading(false);
graph.fitCenter();
}
graph.on("afterupdateitem", (e) => {
const hulls = graph.getHulls();
hulls &&
Object.keys(hulls).forEach((hullkey) => {
const hull = hulls[hullkey];
hull.updateData(hull.members);
});
});
graph.on("afterremoveitem", (e) => {
let newGroups = createGroups();
createHulls(newGroups);
});
graph.on("afteradditem", (e) => {
let newGroups = createGroups();
createHulls(newGroups);
});
// 右键菜单
graph.on("contextmenu", (e) => {
const type = e.item?.getType();
clickMenuTarget.value = type;
setMenuPosition(e);
});
// 单击取消选中
graph.on("canvas:click", (e) => {
changeMenuState(false);
});
graph.on("node:click", (e) => {
const { item } = e;
const model = item.getModel();
// emit("handleNodeClick", { model });
});
}
}
function isShowTooltip(e, data) {
const bool = textRange(e.target);
data["tooltip"] = bool;
}
// 生成groups
function createGroups() {
const groups = JSON.parse(JSON.stringify(graph_data.value.groups));
const nodes = graph.getNodes();
groups.forEach((g) => {
const new_ids = [];
nodes.forEach((node) => {
if (g.nodes.includes(node.get("id"))) {
new_ids.push(node.get("id"));
}
});
g.nodes = new_ids;
});
return groups;
}
// 生成hulls
function createHulls(groups) {
let nodes = graph.getNodes();
let edges = graph.getEdges();
nodes.forEach((item) => {
let model = item.getModel();
let has = copy_nodes.value.find((f) => f.id == model.id);
if (has) {
item.updatePosition({
x: has.x,
y: has.y,
});
}
});
edges.forEach((edge) => {
edge.refresh();
});
graph.removeHulls();
eventList.value = [];
groups.forEach((g, index) => {
const { group, nodes } = g;
const color = getColor(index);
eventList.value.push({
id: group.id,
label: group.name,
members: nodes
.map((n) => {
let { id, label } = graph.findById(n).getModel();
return {
id,
label,
color: hexToRgb(color, 0.4),
};
})
.filter((f) => f),
color,
});
graph.createHull({
id: group.id,
label: group.name,
members: nodes,
padding: 20,
style: {
fill: color,
stroke: color,
},
});
});
changeGraphLoading(false);
// graph.zoom(graphZoom);
// // graph.fitCenter();
// graph.focusItems(nodes);
}
function getColor(type) {
switch (type) {
case 0:
return "#1cff64";
case 1:
return "#f05a1e";
case 2:
return "#1f5fe7";
case 3:
return "#ffb412";
case 4:
return "#5900ff";
case 6:
return "#ffe300";
case 7:
return "#f01e1e";
case 8:
return "#000000";
case 9:
return "#e71f6c";
default:
return "#4c2007";
}
}
// 改变loading
function changeGraphLoading(flag) {
graphLoading.value = flag;
}
// 添加时间轴插件
function addPluginTimebar(flag) {
const container = containerRef.value;
let timeWidth =
window.getComputedStyle(container).width.replace("px", "") - 50;
const edges_time = handleDuplicateRemoval(graph_data.value.edges, ["date"]);
const timeData = edges_time
.map((e) => {
return {
date: e.date,
value: 10,
};
})
.sort(function (a, b) {
return new Date(a.date).getTime() - new Date(b.date).getTime();
});
const timeBarG6 = new G6.TimeBar({
x: 0,
y: 0,
width: timeWidth,
height: 120,
putInGraphContainer: true,
// type: "simple",
type: "trend",
filterItemTypes: ["node", "edge"],
shouldIgnore: handleRangeChange,
changeData: flag,
trend: {
data: timeData,
},
tick: {
tickLabelStyle: {
fill: "#000",
opacity: 1,
fontSize: 14,
offset: 10,
},
},
textStyle: {
fill: "#000",
opacity: 1,
textShadow: "none",
stroke: "transparent",
fontSize: 14,
},
slider: {
start: 0,
end: 1,
width: timeWidth - 120,
},
controllerCfg: {
hideTimeTypeController: true,
fill: "#fff",
stroke: "#fff",
opacity: 1,
textStyle: {
fontSize: 14,
},
},
});
if (graph) {
timeBar.value = timeBarG6;
isShowTimeBar.value = true;
showFlag.value = true;
graph.addPlugin(timeBar.value);
}
}
// 时间轴过滤
function handleRangeChange(val, model, { min, max }) {
if (val == "edge") {
const maxTime = new Date(max).valueOf();
const minTime = new Date(min).valueOf();
const currentTime = new Date(model.date).valueOf();
return currentTime >= minTime && currentTime <= maxTime;
} else {
let flag = false;
const maxTime = new Date(max).valueOf();
const minTime = new Date(min).valueOf();
const new_edges = graph_data.value.edges.filter((e) => {
if (e.date) {
const currentTime = new Date(e.date).valueOf();
return currentTime >= minTime && currentTime <= maxTime;
}
});
new_edges.forEach((ne) => {
if (ne.target == model.id || ne.source == model.id) {
flag = true;
}
});
return flag;
}
}
// 时间轴显隐
function changeTimebarShow() {
showFlag.value = !showFlag.value;
if (timeBar.value && !showFlag.value) {
graph.removePlugin(timeBar.value);
timeBar.value = null;
} else if (showFlag.value) {
nextTick(() => {
addPluginTimebar(true);
});
}
}
// 右键菜单
function changeMenuState(flag) {
if (!flag) {
clickMenuTarget.value = "";
}
showMenu.value = flag;
}
function menuClick(type) {
switch (type) {
case "expand":
handleNodeToolbar({
value: type,
});
break;
case "hide":
handleNodeToolbar({
value: type,
});
break;
default:
break;
}
}
// 设置节点菜单位置
function setMenuPosition(e) {
if (!e.item) {
return changeMenuState(false);
}
e.preventDefault();
const { canvasX, canvasY } = e;
graph.setItemState(e.item, "selected", true);
let top = e.item.getType() == "node" ? canvasY - 80 : canvasY - 30;
menuStyle.value = {
left: canvasX + "px",
top: top + "px",
zIndex: 2,
};
changeMenuState(true);
}
const curEdgeInfoNodes = ref(null);
// 节点对应功能处理
function handleNodeToolbar(e) {
const { value, name } = e;
if (value == "showHide") {
isGraphShow.value = !isGraphShow.value;
if (!isGraphShow.value) {
graph.destroy();
graph = null;
showFlag.value = false;
eventList.value = [];
isShowTimeBar.value = false;
} else {
nextTick(() => {
init();
isShowTimeBar.value = true;
});
}
return;
} else if (value == "showHideMap") {
emit("showHideMap");
return;
} else if (value == "search") {
// 搜索添加
searchNodeDialog.value.visible = true;
return;
} else if (value == "hide") {
globalProxy
.$confirm("是否删除选中的节点,清除画布选中?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
let selectedNode = graph.findAllByState("node", "selected");
selectedNode.forEach((node) => {
graph.removeItem(node);
});
changeMenuState(false);
})
.catch(() => {
globalProxy.$message({
type: "info",
message: "已取消删除",
});
});
return;
} else if (value == "viewInfo") {
emit("handleViewInfo");
getGraphJSON();
return;
} else if (value == "saveGraph") {
let zoom_center = {};
emit("saveMap", (val) => {
zoom_center = val;
});
let savedGraph = _.cloneDeep(graph_data.value);
let graph = simplifyData(savedGraph);
let id = props.eventGraphId;
let params = {
hyper_id: id,
graph: {
data: graph,
zoom_center: zoom_center,
},
};
saveHypergraph(params)
.then((res) => {
if (res.code === 1) {
globalProxy.$message({
type: "success",
message: "保存成功",
});
emit("refreshGraph");
}
})
.catch(() => {
globalProxy.$message({
type: "error",
message: "保存失败",
});
});
return;
} else if (value == "add") {
// 新增节点
nodeDialogInfo.value = {
selectNodeId: null,
selectLabel: "",
visible: true, // 弹窗显隐
title: name,
isAdd: true,
};
return;
} else if (value == "edit") {
// 编辑节点
if (graph) {
let selectNodes = graph.findAllByState("node", "selected") || [];
if (!selectNodes.length) {
return globalProxy.$message.error("请选择节点");
} else if (selectNodes.length > 1) {
return globalProxy.$message.error("请选择单个节点");
}
const model = selectNodes[0].getModel();
const { id, label, originLabel } = model;
nodeDialogInfo.value = {
selectNodeId: id,
selectLabel: originLabel,
visible: true,
title: originLabel,
isAdd: false,
};
} else {
globalProxy.$message.warning("请先展示超图");
}
return;
} else if (value == "addEdge") {
if (graph) {
let selectNodes = graph.findAllByState("node", "selected") || [];
if (!selectNodes.length) {
return globalProxy.$message.error("请选择节点");
} else if (selectNodes.length > 1) {
return globalProxy.$message.error("请选择单个节点");
}
console.log("data", props.graphData);
const model = selectNodes[0].getModel();
const { id, label, originLabel } = model;
addEdgeInfo.value = {
visible: true,
from: label,
from_id: id,
nodes: props.graphData.nodes,
};
curEdgeInfoNodes.value = props.graphData.nodes;
} else {
globalProxy.$message.warning("请先展示超图");
}
} else if (value == "saveMap") {
return;
}
}
// 简化数据为规定的格式
function simplifyData(graph) {
graph.edges.forEach((element) => {
delete element.endPoint;
delete element.startPoint;
delete element.style;
delete element.labelCfg;
delete element.sourceNode;
delete element.targetNode;
});
graph.nodes.forEach((element) => {
delete element.labelCfg;
delete element.originLabel;
delete element.size;
delete element.style;
delete element.type;
});
return graph;
}
// 删除边
function handleRemoveEdge() {
globalProxy
.$confirm("确认删除边关系?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
handleDeleteEdge();
changeMenuState(false);
})
.catch(() => {
changeMenuState(false);
globalProxy.$message({
type: "info",
message: "已取消删除",
});
});
}
function handleRemoveNode() {
alert(123);
globalProxy
.$confirm("是否删除选中的节点,清除画布选中?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
let selectedNode = graph.findAllByState("node", "selected");
console.log("selectedNode", selectedNode);
console.log("graph", graph);
// selectedNode.forEach((node) => {
// graph.removeItem(node);
// });
// changeMenuState(false);
})
.catch(() => {
globalProxy.$message({
type: "info",
message: "已取消删除",
});
});
}
function handleDeleteEdge() {
let selectedEdge = graph.findAllByState("edge", "selected") || [];
if (!selectedEdge.length) {
return globalProxy.$message.error("请选择边关系");
} else if (selectedEdge.length > 1) {
return globalProxy.$message.error("请选择单条边关系");
}
const { id, source, target } = selectedEdge[0].getModel();
if (id) {
let deleteIndex = -1;
let savedGraph = _.cloneDeep(graph_data.value);
let graph = simplifyData(savedGraph);
console.log("graph", graph);
graph.edges.forEach((item, index) => {
if (item.id === id) {
deleteIndex = index;
}
});
graph.edges.splice(deleteIndex, 1);
console.log("graph1", graph);
let zoom_center = {};
emit("saveMap", (val) => {
zoom_center = val;
});
let params = {
hyper_id: props.eventGraphId,
graph: {
data: graph,
zoom_center: zoom_center,
},
};
saveHypergraph(params)
.then((res) => {
if (res.code === 1) {
globalProxy.$message({
type: "success",
message: "保存成功",
});
emit("refreshGraph");
}
})
.catch(() => {
globalProxy.$message({
type: "error",
message: "保存失败",
});
});
// let event_ids = [];
// let hulls = graph.getHulls();
// for (let key in hulls) {
// const { cfg } = hulls[key];
// const { members, id } = cfg;
// if (members.includes(source) && members.includes(target)) {
// event_ids.push(id);
// }
// }
// // let postData = {
// // relation_id: id,
// // group_id: props.eventGraphId,
// // event_id: event_ids,
// // };
// // deleteRelation(postData).then((res) => {
// // });
}
}
// 删除实体
function handleDeleteEntity(id) {
// requestDeleteEntity({
// entity_id: id
// }).then((res) => {
// if (res.data) {
// if (showMenu.value) {
// showMenu.value = false;
// }
// globalProxy.$message.success("删除成功");
// graph.removeItem(id);
// }
// }).catch(() => {});
}
// 新增/编辑节点属性弹窗
function handleDialogClose() {
nodeDialogInfo.value = {
visible: false,
title: "新增节点",
selectNodeId: null,
selectLabel: "",
isAdd: false,
};
}
const handleDialogAdd = async (formData) => {
try {
const createParams = {
name: formData.name,
description: formData.desc,
};
let res = await createEntity(createParams);
if (res.code === 1) {
const insertParams = {
hyper_id: props.eventGraphId,
entity_id: res.data.id,
};
try {
const res = await insertEntityIntoHypergraph(insertParams);
console.log("插入超图res", res);
if (res.code === 1) {
emit("refreshGraph");
ElMessage({
message: "新增节点成功",
type: "success",
});
} else {
ElMessage({
message: "新增节点失败,请检查网络或稍候重试!",
});
}
handleDialogClose();
} catch (error) {
console.error("实体插入超图error", error);
ElMessage({
message: "新增节点失败,请检查网络或稍候重试!",
});
handleDialogClose();
}
}
} catch (error) {
console.error("创建实体error", error);
ElMessage({
message: "新增节点失败,请检查网络或稍候重试!",
});
handleDialogClose();
}
};
// function handleDialogAdd(formData) {
// console.log("formData", formData);
// const createParams = {
// name: formData.name,
// description: formData.desc,
// };
// console.log("createParams", createParams);
// createEntity(createParams)
// .then((res) => {
// console.log("res", res);
// if (res.code === 1) {
// const insertParams = {
// hyper_id: props.eventGraphId,
// entity_id: res.data.id,
// };
// insertEntityIntoHypergraph(insertParams)
// .then((res) => {})
// .catch();
// }
// })
// .catch((err) => {
// console.error(err);
// });
// }
function handleDialogEdit(formData) {
console.log("formData", formData);
let savedGraph = _.cloneDeep(graph_data.value);
let graph = simplifyData(savedGraph);
let id = props.eventGraphId;
console.log("id", id);
console.log("graph", graph);
graph.nodes.forEach((item) => {
if (item.id === formData.id) {
item.label = formData.name;
}
});
let zoom_center = {};
emit("saveMap", (val) => {
zoom_center = val;
});
let params = {
hyper_id: id,
graph: {
data: graph,
zoom_center: zoom_center,
},
};
saveHypergraph(params)
.then((res) => {
if (res.code === 1) {
globalProxy.$message({
type: "success",
message: "保存成功",
});
emit("refreshGraph");
}
})
.catch(() => {
globalProxy.$message({
type: "error",
message: "保存失败",
});
});
handleDialogClose();
}
// 新增关系
function handleCloseAddRelation() {
// addEdgeInfo.value = {
// visible: false,
// };
addEdgeInfo.value.visible = false;
}
// 搜索弹窗
function handleCloseSearch() {
searchNodeDialog.value.visible = false;
}
function handleAddNode(arr) {
console.log("arr", arr);
let nodes = arr.filter((f) => !graph.findById(f._id));
if (!nodes.length) {
ElMessage({
message: "节点已存在",
type: "error",
});
}
const { x, y } = graph.getViewPortCenterPoint();
nodes.forEach((n) => {
const { label, _id } = n;
graph.addItem("node", {
x,
y,
label: fittingString(label, 120, 12),
id: _id,
originLabel: label,
});
// graph.fitCenter();
// graph.paint();
});
// let newGroups = createGroups();
// createHulls(newGroups);
// graph.layout();
}
const handleQuery = (e) => {
addEdgeInfo.value.nodes = curEdgeInfoNodes.value.filter((item) => {
return item.label.toLowerCase().includes(e.toLowerCase());
});
};
// 建立节点与节点之间的关系
function handleConfirmRelation(e) {
console.log("e", e);
console.log("graph_data", graph_data.value);
let savedGraph = _.cloneDeep(graph_data.value);
let graph = simplifyData(savedGraph);
let id = props.eventGraphId;
console.log("graph", graph);
const newEdge = {
date: null,
label: e.relation_name,
source: graph.nodes.filter((item) => {
return item.label === e.from;
})[0].id,
target: graph.nodes.filter((item) => {
return item.label === e.to;
})[0].id,
};
graph.edges = [...graph.edges, newEdge];
let zoom_center = {};
emit("saveMap", (val) => {
zoom_center = val;
});
let params = {
hyper_id: id,
graph: {
data: graph,
zoom_center: zoom_center,
},
};
saveHypergraph(params)
.then((res) => {
if (res.code === 1) {
globalProxy.$message({
type: "success",
message: "保存成功",
});
emit("refreshGraph");
}
})
.catch(() => {
globalProxy.$message({
type: "error",
message: "保存失败",
});
});
handleCloseAddRelation();
}
// 融合图谱数据
/**
* arr: 过滤的数据,
* keys: 要根据哪几个字段去过滤
*/
function handleDuplicateRemoval(arr, keys) {
let object = {};
let new_arr = arr.reduce((cur, next) => {
let str = "_";
keys.forEach((s) => {
str += next[s];
});
object[str] ? "" : (object[str] = true && cur.push(next));
return cur;
}, []);
return new_arr;
}
// 设置画布大小自适应
function initSize() {
addEventListener("resize", () => {
const el = graph.value;
if (el) {
const width = getStyle(el, "width").replace("px", "");
const height = getStyle(el, "height").replace("px", "");
graph.value.changeSize(Number(width), Number(height));
graph.value.fitCenter();
}
});
}
// 获取画布数据
function getGraphJSON() {
const { nodes, edges } = graph.save();
store.commit("graph/SET_GRAPH_JSON", {
nodes: nodes,
edges: edges,
group: eventList.value,
});
}
// 文本过长处理
/*
str: label/待处理的字符
maxWidth: 最大展示长度
fontSize: 文字大小
*/
function fittingString(str, maxWidth = 120, fontSize) {
const ellipsis = "...";
const ellipsisLength = G6.Util.getTextSize(ellipsis, fontSize)[0];
let currentWidth = 0;
let res = str;
const pattern = new RegExp("[\u4E00-\u9FA5]");
str.split("").forEach((letter, i) => {
if (currentWidth > maxWidth - ellipsisLength) return;
if (pattern.test(letter)) {
currentWidth += fontSize;
} else {
currentWidth += G6.Util.getLetterWidth(letter, fontSize);
}
if (currentWidth > maxWidth - ellipsisLength) {
res = `${str.substr(0, i)}${ellipsis}`;
}
});
return res;
}
</script>
<style lang="scss" scoped>
.graph-time-line {
.container {
position: absolute;
right: 0;
bottom: 0;
left: 0;
top: 0;
margin: auto;
width: 100%;
height: 100%;
z-index: 999;
// border: 1px solid;
}
.node-tooltip {
width: 80%;
z-index: 999;
position: absolute !important;
}
.graph-legend {
position: absolute;
right: 10px;
top: 10px;
z-index: 999;
cursor: pointer;
.legend-list {
padding: 5px;
display: flex;
align-items: center;
.event-shape {
width: 14px;
height: 14px;
border-radius: 50%;
margin-right: 5px;
opacity: 0.4;
}
.list-name {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.graph-tools {
position: absolute;
top: 5px;
left: 108px;
z-index: 1001;
border-radius: 4px;
// font-family: "FZZZHONGJW--GB1-0";
}
.timebar-box {
position: absolute;
z-index: 999;
right: 0;
bottom: 10px;
display: flex;
justify-content: space-between;
user-select: none;
.timebar {
flex: 1;
}
.show {
// position: absolute;
// right: 0;
width: 20px;
height: 106px;
padding: 10px;
cursor: pointer;
border-radius: 10px 0 0 10px;
display: flex;
align-items: center;
background: #fff;
color: #000;
&.active {
background: #0b68d2;
color: #fff;
}
}
}
:deep(.g6-component-tooltip) {
display: inline-block;
}
:deep(.g6-component-timebar) {
position: absolute !important;
bottom: 10px;
margin-left: 60px;
width: calc(100% - 120px);
border-radius: 10px;
background: rgba(255, 255, 255, 0.8);
}
.graph-contextmenu {
position: absolute;
z-index: 999 !important;
}
.rightMenu-edge {
cursor: pointer;
padding: 7px 15px;
border-radius: 20px;
background: #fff;
border: 1px solid #dcdfe6;
color: #606266;
font-size: 12px;
&:hover {
color: #409eff;
border-color: #c6e2ff;
background-color: #ecf5ff;
}
}
}
</style>
<template>
<div class="table-pagination-box" v-if="tableDataShow">
<el-table class="customTable" :data="tableDataNew" :size="size" :max-height="maxHeight" :border="border" :highlight-current-row="highlightCurrentRow" @sort-change="sortChange" @selection-change="handleSelectionChange" @select="handleSelectSingle" @row-click="handleRowClick" :row-key="rowKey" ref="tableRef">
<el-table-column v-if="selection" type="selection" width="55" :reserve-selection="true">
</el-table-column>
<!-- <el-table-column v-if="tableIndex" label="序号" :width="tableIndexWidth" type="index" :index="tableIndexNumber">
</el-table-column> -->
<el-table-column v-for="column in columnList" :key="column.prop" :show-overflow-tooltip="column.tooltip" :prop="column.prop" :label="column.label" :sortable="column.sort">
</el-table-column>
<el-table-column :label="operationLabel" :width="operationMinWidth" :align="operationAlign" v-if="operationLabelShow">
<template v-slot="scope">
<slot></slot>
</template>
</el-table-column>
</el-table>
<div class="custom-pagination-box" v-if="showPagination">
<el-pagination :current-page="pagination.page" :page-size="pagination.page_size" :total="pagination.total_count" :layout="layout" :page-sizes="pageSizes" :pager-count="pagerCount" @size-change="handleSizeChange" @current-change="handleCurrentChange">
</el-pagination>
</div>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import {
useRoute,
useRouter
} from 'vue-router';
import {
useStore
} from "vuex";
const emit = defineEmits(['handleSizeChange', 'handleCurrentChange', 'sortChange', 'handleSelectSingle', 'handleRowClick', 'handleSelectionChange']);
onMounted(() => {
columnListNew = props.columnList
})
let tableDataShow = ref(true)
let tableDataNew = ref([])
let columnListNew = reactive([])
let props = defineProps({
// 表单
tableData: {
type: Array,
default: () => [],
},
size: {
type: String,
default: "default",
},
maxHeight: {
type: String,
default: "650",
},
border: {
type: Boolean,
default: false,
},
columnList: {
type: Array,
default: () => [],
},
operationLabel: {
type: String,
default: "操作",
},
operationMinWidth: {
type: String,
default: "",
},
operationAlign: {
type: String,
default: "center",
},
// 操作列是否展示
operationLabelShow: {
type: Boolean,
default: true,
},
// rowKey
rowKey: {
type: String,
default: "id",
},
// 选框
selection: {
type: Boolean,
default: false,
},
// 序号
tableIndex: {
type: Boolean,
default: false,
},
tableIndexNumber: {
type: Number,
default: 1,
},
tableIndexWidth: {
type: String,
default: "50",
},
// 高亮行
highlightCurrentRow: {
type: Boolean,
default: false,
},
// 分页
pagination: {
type: Object,
default: () => {
return {
page: 1,
page_size: 10,
total_count: 0,
};
},
},
// 每页显示个数选择器
pageSizes: {
type: Array,
default: () => {
return [5, 10, 20];
},
},
// 页码按钮的数量
pagerCount: {
type: Number,
default: 7
},
layout: {
type: String,
default: "total,sizes,prev,pager,next,jumper",
},
showPagination: {
type: Boolean,
default: true,
},
})
watch(
() => props.tableData,
(newVal, oldVal) => {
tableDataNew.value = newVal;
}, {
immediate: true
}
);
watch(
() => props.columnList,
(newVal, oldVal) => {
columnListNew.value = newVal;
}, {
immediate: true
}
);
function handleSizeChange(size) {
emit("handleSizeChange", size);
}
function handleCurrentChange(page) {
emit("handleCurrentChange", page);
}
function sortChange({
column,
prop,
order
}) {
emit("sortChange", {
column,
prop,
order
});
}
function handleSelectionChange(selection) {
emit("handleSelectionChange", selection);
}
function handleSelectSingle(selection, row) {
emit("handleSelectSingle", {
selection,
row
});
}
function handleRowClick(row, column, event) {
emit("handleRowClick", {
row,
column,
event
});
}
</script>
<style lang="scss" scoped>
.table-pagination-box {
height: 100%;
position: relative;
.el-table {
--el-table-border-color: rgba(204, 204, 204, 0.5);
--el-table-text-color: #fff;
--el-table-header-text-color: #fff;
--el-table-row-hover-bg-color: transparent;
--el-table-current-row-bg-color: transparent;
--el-table-header-bg-color: transparent;
--el-table-bg-color: transparent;
--el-table-tr-bg-color: transparent;
--el-table-expanded-cell-bg-color: transparent;
}
.customTable {
.type-box {
width: 100%;
height: 100px;
max-height: 100px;
img,
audio,
video {
width: 100%;
height: 100%;
}
}
}
.custom-pagination-box {
position: relative;
margin-top: 20px;
float: right;
}
}
</style>
<template>
<div class="btn-steps common-bg">
<el-button type="primary" @click="handleCancelCreate">取消创建</el-button>
<el-button type="primary" v-if="activeSteps" @click="handlePreStep">上一步</el-button>
<el-button v-if="!(activeSteps == maxStepsTotal)" type="primary" @click="handleNextStep">下一步</el-button>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import {
useStore
} from "vuex";
const store = useStore();
const activeSteps = computed(() => store.state.graph.activeSteps);
const maxStepsTotal = computed(() => store.state.maxStepsTotal);
const emit = defineEmits(['handlePreStep','handleNextStep','handleCancelCreate']);
function handlePreStep() {
emit('handlePreStep')
}
function handleNextStep() {
emit('handleNextStep')
}
function handleCancelCreate() {
emit('handleCancelCreate')
}
</script>
<style lang="scss" scoped>
.btn-steps {
text-align: right;
border-top: 1px solid #ccc6;
padding: 10px 8px 14px 8px;
}
</style>
{
"success": true,
"data": {
"text": [
{
"url": null,
"date": "2022-08",
"title": "标题",
"content": "内容"
}
]
},
"error_msg": null
}
\ No newline at end of file
<template>
<div class="hypher-hullGraph" id="hullGraph" v-loading="loading" element-loading-text="搜索中..." element-loading-background="#000">
<div class="container" id="container" ref="containerRef" v-show="isGraphShow"></div>
<div class="node-tooltip" ref="tooltipRef"></div>
<div class="timebar-box" v-if="isShowTimeBar">
<div :class="['show fs14', showFlag ? 'active' : '']" @click="changeTimebarShow">
时间轴
</div>
</div>
<!-- 图例 -->
<div class="graph-legend">
<div v-for="event in eventList" :key="event.id" class="legend-list">
<span class="event-shape" :style="{ background: event.color }"></span>
<el-tooltip :content="event.label" :disabled="!event.tooltip">
<span class="list-name" @mouseenter="(e) => isShowTooltip(e, event)">
{{ event.label }}
</span>
</el-tooltip>
</div>
</div>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import G6 from "@antv/g6";
import {
hexToRgb,
textRange
} from "@/utils/index";
const {
appContext
} = getCurrentInstance();
const globalProxy = appContext.config.globalProperties;
import {
getGraphInfoByIds,
getGraphInfo
} from "@/api/graphApi";
// 定义触发的事件及其数据类型
const emit = defineEmits(['handleNodeClick', 'changeGraphData']);
defineExpose({
getData,
backToInit,
showOrHide
})
let graph = null;
let timeBar = ref(null);
let hullGroup = ref([]);
let graphData = ref(null);
let showGraphdata = ref(null);
let timeData = ref([]);
let startTime = ref("");
let endTime = ref("");
let loading = ref(false);
let showFlag = ref(false);
let isGraphShow = ref(true); // 图谱默认是否展示
let isShowTimeBar = ref(false);
let containerRef = ref();
let tooltipRef = ref();
let copy_nodes = ref([]);
let graph_data = ref({
nodes: [],
edges: [],
groups: [],
});
let eventList = ref([]);
onMounted(() => {
});
async function getData(idList) {
isShowTimeBar.value = false;
if (idList) {
changeGraphLoading(true)
getGraphInfoByIds({
ids: idList
}).then((res) => {
isShowTimeBar.value = true;
// 没有记录点位数据,获取初始化的
if (res) {
// mapCenter.value = {
// center: zoom_center.center //[51, 48],
// zoom: zoom_center.zoom
// }
const {
graph
} = res;
graphData.value = graph.data;
emit("changeGraphData", graph.data);
setTimeout(() => {
handleFormatterGraphData(graph.data);
}, 500);
} else {
changeGraphLoading(false);
globalProxy.$message.error("未查询到结果");
}
}).catch(() => {
changeGraphLoading(false);
});
}
}
// 添加时间轴插件
function addPluginTimebar(flag) {
const container = containerRef.value;
let timeWidth = window.getComputedStyle(container).width.replace("px", "") - 50;
const edges_time = handleDuplicateRemoval(graph_data.value.edges, [
"date",
]);
const timeData = edges_time
.map((e) => {
return {
date: e.date,
value: 10,
};
})
.sort(function (a, b) {
return new Date(a.date).getTime() - new Date(b.date).getTime();
});
const timeBarG6 = new G6.TimeBar({
x: 0,
y: 0,
width: timeWidth,
height: 120,
putInGraphContainer: true,
// type: "simple",
type: "trend",
filterItemTypes: ["node", "edge"],
shouldIgnore: handleRangeChange,
changeData: flag,
trend: {
data: timeData,
},
tick: {
tickLabelStyle: {
fill: "#000",
opacity: 1,
fontSize: 14,
offset: 10,
},
},
textStyle: {
fill: "#000",
opacity: 1,
textShadow: "none",
stroke: "transparent",
fontSize: 14,
},
slider: {
start: 0,
end: 1,
width: timeWidth - 120,
},
controllerCfg: {
hideTimeTypeController: true,
fill: "#fff",
stroke: "#fff",
opacity: 1,
// speed:5,
textStyle: {
fontSize: 14,
},
},
});
if (graph) {
timeBar.value = timeBarG6;
isShowTimeBar.value = true;
showFlag.value = true;
graph.addPlugin(timeBar.value);
}
}
function isShowTooltip(e, data) {
const bool = textRange(e.target);
data["tooltip"] = bool;
}
// 时间轴过滤
function handleRangeChange(val, model, {
min,
max
}) {
if (val == "edge") {
const maxTime = new Date(max).valueOf();
const minTime = new Date(min).valueOf();
const currentTime = new Date(model.date).valueOf();
return currentTime >= minTime && currentTime <= maxTime;
} else {
let flag = false;
const maxTime = new Date(max).valueOf();
const minTime = new Date(min).valueOf();
const new_edges = graph_data.value.edges.filter((e) => {
if (e.date) {
const currentTime = new Date(e.date).valueOf();
return currentTime >= minTime && currentTime <= maxTime;
}
});
new_edges.forEach((ne) => {
if (ne.target == model.id || ne.source == model.id) {
flag = true;
}
});
return flag;
}
}
function changeTimebarShow() {
showFlag.value = !showFlag.value;
if (timeBar.value && !showFlag.value) {
graph.removePlugin(timeBar.value);
timeBar.value = null;
} else if (showFlag.value) {
nextTick(() => {
addPluginTimebar(true);
});
}
}
/**
* arr : 过滤的数据,
* keys : 要根据哪几个字段去过滤
*/
function handleDuplicateRemoval(arr, keys) {
let object = {};
let new_arr = arr.reduce((cur, next) => {
let str = "_";
keys.forEach((s) => {
str += next[s];
});
object[str] ? "" : (object[str] = true && cur.push(next));
return cur;
}, []);
return new_arr;
}
// 处理超图数据
function handleFormatterGraphData(result) {
if (graph) {
if (timeBar.value) {
graph.removePlugin(timeBar.value);
}
showFlag.value = false;
graph.destroy();
}
isGraphShow.value = true;
let graphData = JSON.parse(JSON.stringify(result));
const {
nodes
} = graphData;
if (!nodes.length) {
eventList.value = [];
isShowTimeBar.value = false;
changeGraphLoading(false);
return globalProxy.$message.error("暂无数据");
}
graphData.nodes.forEach((node, index) => {
let sourceNode = node;
let has = graphData.groups.find((g) => node.label == g.group.name);
if (has || sourceNode) {
// 原子事件与中心事件
node.size = 20;
node.style = {
stroke: "#ffae0b",
fill: "#e6a23cc2",
lineWidth: 2,
};
}
});
graphData.edges.forEach((edge) => {
if (!edge.id) {
delete edge.id;
}
// let last2Num = Number(edge.id.split("_")[1]);
// if (last2Num <= 38){
// edge.date = "2024-05-12"
// } else if (last2Num > 38 && last2Num <= 76){
// edge.date = "2024-08-12"
// } else if (last2Num > 76){
// edge.date = "2024-11-12"
// }
});
// 若边关系上都存在时间则显示时间轴
let timebar_flag = graphData.edges.every((t) => t.date);
graph_data.value = graphData;
nextTick(() => {
initNew();
});
if (timebar_flag) {
isShowTimeBar.value = true;
} else {
isShowTimeBar.value = false;
}
}
// 初始化
function initNew() {
const el = containerRef.value;
const tooltip_el = tooltipRef.value;
const widthValue = window.getComputedStyle(el).width.replace("px", "");
const heightValue = window.getComputedStyle(el).height.replace("px", "");
graph_data.value.nodes.forEach((node) => {
if (!node.originLabel) {
node.originLabel = node.label;
node.label = fittingString(node.label, 120, 12);
}
});
// tooltip
const tooltip = new G6.Tooltip({
container: tooltip_el,
offsetX: 10,
offsetY: 0,
fixToNode: [1, 0.5],
itemTypes: ["node"],
getContent: (e) => {
const outDiv = document.createElement("div");
outDiv.style.maxWidth = "300px";
outDiv.innerHTML = `<span>${e.item.getModel().originLabel}</span>`;
return outDiv;
},
});
// 图谱配置
let option = {
container: el,
widthValue,
heightValue,
modes: {
default: [
"drag-canvas",
"zoom-canvas",
"drag-node",
// "tooltip",
"brush-select",
{
type: "click-select",
trigger: "ctrl",
selectEdge: true,
},
{
type: "drag-combo",
},
],
},
plugins: [tooltip],
defaultNode: {
size: 20,
style: {
lineWidth: 2,
stroke: "#ffae0b",
fill: "#e6a23cc2",
},
labelCfg: {
// 标签配置属性
position: "bottom", // 标签的属性,标签在元素中的位置
offset: 5,
style: {
fontSize: 16,
stroke: "#409eff",
fill: "#ffffff",
lineWidth: 2
},
},
},
defaultEdge: {
style: {
stroke: "#3e8d76",
lineWidth: 2,
cursor: "pointer",
endArrow: {
path: G6.Arrow.triangle(8, 5, 0),
d: 0,
fill: "#4f9794b3",
},
},
labelCfg: {
style: {
fontSize: 16,
// fill: "#409eff",
fill: "#ffffff",
cursor: "pointer",
},
autoRotate: true,
refY: 10,
},
},
nodeStateStyles: {
selected: {
lineWidth: 4,
shadowColor: "#ffae0b",
stroke: "#ffae0b",
fill: "#e6a23cc2",
},
},
};
// 节点是否有xy记录
let flag_xy = copy_nodes.value.every(
(e) => e.hasOwnProperty("x") && e.hasOwnProperty("y")
);
if (!flag_xy) {
option = Object.assign(option, {
layout: {
type: "force",
// center: [-500, -500],
preventOverlap: true,
linkDistance: (d) => {
return 300;
},
nodeStrength: (d) => {
return -10;
},
edgeStrength: (d) => {
return 0.5;
},
},
});
}
const graphNew = new G6.Graph({
...option,
});
graph = graphNew;
graph.data(graph_data.value);
graph.render();
handleEvent(flag_xy);
}
// graph的事件机制
function handleEvent(flag_xy) {
// const { nodes } = graph.save();
const nodes = graph.getNodes();
const edges = graph.getEdges();
if (nodes.length) {
nodes.forEach((node) => {
node.hide();
});
edges.forEach((edge) => {
edge.hide();
});
if (flag_xy) {
// graph.zoom(graphZoom);
changeGraphLoading(true);
setTimeout(() => {
let newGroups = createGroups();
createHulls(newGroups);
nodes.forEach((node) => {
node.show();
});
edges.forEach((edge) => {
edge.show();
});
changeGraphLoading(false);
}, 500);
} else {
changeGraphLoading(true);
let newGroups = createGroups();
createHulls(newGroups);
nodes.forEach((node) => {
node.show();
});
edges.forEach((edge) => {
edge.show();
});
graph.fitCenter();
}
graph.on("afterupdateitem", (e) => {
const hulls = graph.getHulls();
hulls &&
Object.keys(hulls).forEach((hullkey) => {
const hull = hulls[hullkey];
hull.updateData(hull.members);
});
});
// });
// }
graph.on("afterremoveitem", (e) => {
let newGroups = createGroups();
createHulls(newGroups);
});
graph.on("afteradditem", (e) => {
let newGroups = createGroups();
createHulls(newGroups);
});
graph.on("node:click", (e) => {
const {
item
} = e;
const model = item.getModel();
// emit("handleNodeClick", { model });
});
}
}
// 文本过长处理
/**
* str: label/待处理的字符
* maxWidth: 最大展示长度
* fontSize: 文字大小
*/
function fittingString(str, maxWidth = 120, fontSize) {
const ellipsis = "...";
const ellipsisLength = G6.Util.getTextSize(ellipsis, fontSize)[0];
let currentWidth = 0;
let res = str;
const pattern = new RegExp("[\u4E00-\u9FA5]");
str.split("").forEach((letter, i) => {
if (currentWidth > maxWidth - ellipsisLength) return;
if (pattern.test(letter)) {
currentWidth += fontSize;
} else {
currentWidth += G6.Util.getLetterWidth(letter, fontSize);
}
if (currentWidth > maxWidth - ellipsisLength) {
res = `${str.substr(0, i)}${ellipsis}`;
}
});
return res;
}
// 生成groups
function createGroups() {
const groups = JSON.parse(JSON.stringify(graph_data.value.groups));
const nodes = graph.getNodes();
groups.forEach((g) => {
const new_ids = [];
nodes.forEach((node) => {
if (g.nodes.includes(node.get("id"))) {
new_ids.push(node.get("id"));
}
});
g.nodes = new_ids;
});
return groups;
}
// hulls
function createHulls(groups) {
graph.removeHulls();
eventList.value = [];
groups.forEach((g, index) => {
const {
group,
nodes
} = g;
const color = getColor(index);
eventList.value.push({
id: group.id,
label: group.name,
members: nodes
.map((n) => {
let {
id,
label
} = graph
.findById(n)
.getModel();
return {
id,
label,
color: hexToRgb(color, 0.4),
};
})
.filter((f) => f),
color,
});
graph.createHull({
id: group.id,
label: group.name,
members: nodes,
padding: 20,
style: {
fill: color,
stroke: color,
// opacity:1
},
});
});
changeGraphLoading(false);
}
function getColor(type) {
switch (type) {
case 0:
return "#1cff64";
case 1:
return "#f05a1e";
case 2:
return "#1f5fe7";
case 3:
return "#ffb412";
case 4:
return "#5900ff";
case 6:
return "#ffe300";
case 7:
return "#f01e1e";
case 8:
return "#000000";
case 9:
return "#e71f6c";
default:
return "#4c2007";
}
}
// 改变loading
function changeGraphLoading(flag) {
loading.value = flag;
}
function backToInit() {
}
function showOrHide() {
isGraphShow.value = !isGraphShow.value;
if (!isGraphShow.value) {
graph.destroy();
graph = null;
showFlag.value = false;
isShowTimeBar.value = false;
} else {
changeGraphLoading(true)
isShowTimeBar.value = true;
showFlag.value = true;
setTimeout(() => {
initNew();
}, 500)
}
}
function handleTimeChange(graphVal, min, max) {
}
function exportData() {
graph.save()
}
</script>
<style lang="scss" scoped>
.hypher-hullGraph {
width: 100%;
height: 100%;
// background: #ddd;
position: relative;
.container {
position: absolute;
right: 0;
bottom: 0;
left: 0;
top: 0;
margin: auto;
width: 100%;
height: 100%;
z-index: 999;
}
.node-tooltip {
width: 80%;
z-index: 999;
position: absolute !important;
}
.timebar-box {
position: absolute;
z-index: 999;
right: 0;
bottom: 10px;
display: flex;
justify-content: space-between;
user-select: none;
.timebar {
flex: 1;
}
.show {
// position: absolute;
// right: 0;
width: 20px;
height: 106px;
padding: 10px;
cursor: pointer;
border-radius: 10px 0 0 10px;
display: flex;
align-items: center;
background: #fff;
color: #000;
&.active {
background: #0b68d2;
color: #fff;
}
}
}
:deep(.g6-component-tooltip) {
display: inline-block;
}
:deep(.g6-component-timebar) {
position: absolute !important;
bottom: 10px;
margin-left: 60px;
width: calc(100% - 120px);
border-radius: 10px;
background: rgba(255, 255, 255, 0.8);
}
.graph-legend {
position: absolute;
right: 10px;
top: 10px;
z-index: 999;
cursor: pointer;
.legend-list {
padding: 5px;
display: flex;
align-items: center;
.event-shape {
width: 14px;
height: 14px;
border-radius: 50%;
margin-right: 5px;
opacity: 0.4;
}
.list-name {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
#timeBar {
position: absolute;
bottom: 10px;
left: 0;
width: calc(100% - 120px);
margin: 0 60px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.8);
}
.show {
position: absolute;
right: 0;
bottom: 10px;
padding: 10px;
width: 20px;
height: 146px;
cursor: pointer;
border-radius: 10px 0 0 10px;
display: flex;
align-items: center;
background: #fff;
color: #000;
z-index: 999;
}
.show.active {
background: #0b68d2;
color: #fff;
}
}
</style>
<template>
<div class="view-chose-map">
<div ref="mapRef" class="map-container" id="hyper-map"></div>
<div id="position-info"></div>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import L from "leaflet";
import "leaflet/dist/leaflet.css";
let props = defineProps({
config: {
type: Object,
default: null,
},
})
watch(() => props.config, (newValue, oldValue) => {
resetZoomCenter(newValue);
}, {
deep: true
})
let map = ref(null)
let layerGroup = ref(null)
onMounted(() => {
setTimeout(() => {
initMap();
})
})
function resetZoomCenter(val) {
if (val) {
const {
center,
zoom
} = val;
map.value.setView([center.lat, center.lon], zoom);
}
}
function getXYPosition(params) {
let bounds = map.value.getBounds();
let southEast = bounds.getSouthEast();
let northWest = bounds.getNorthWest();
return {
leftTop: {
lat: northWest.lat,
lon: northWest.lng,
x: 0,
y: 0
},
rightBottom: {
lat: southEast.lat,
lon: southEast.lng,
x: document.body.clientWidth,
y: document.body.clientHeight - 60
}
}
}
function saveMapFun() {
let zoomLevel = map.value.getZoom();
let center = map.value.getCenter();
let bounds = map.value.getBounds();
let southEast = bounds.getSouthEast();
let northWest = bounds.getNorthWest();
return {
center: {
lon: center.lng,
lat: center.lat
},
zoom: zoomLevel,
bounds: {
leftTop: {
lat: northWest.lat,
lon: northWest.lng,
x: 0,
y: 0
},
rightBottom: {
lat: southEast.lat,
lon: southEast.lng,
x: document.body.clientWidth,
y: document.body.clientHeight - 60
}
},
}
}
// 初始化地图
function initMap() {
if (!map.value) {
map.value = L.map("hyper-map", {
center: [38.134557, 177.891059],
zoom: 3,
minZoom: 1,
maxZoom: 5,
minNativeZoom: 1,
zoomControl: false,
attributionControl: false, // 移除右下角leaflet标识
// worldCopyJump: true
});
} else {
clearMarks();
}
L.tileLayer(window.mapIP).addTo(map.value);
layerGroup.value = L.featureGroup([]);
map.value.addLayer(layerGroup.value);
// 添加鼠标移动事件监听器
map.value.on('mousemove', function(e) {
let latlng = e.latlng; // 获取鼠标位置的经纬度对象
let lat = latlng.lat.toFixed(5); // 格式化纬度为小数点后五位
let lng = latlng.lng.toFixed(5); // 格式化经度为小数点后五位
document.getElementById('position-info').innerHTML = '经度: ' + lng + ', 纬度: ' + lat; // 更新页面元素显示经纬度
});
}
// 清除图层
function clearMarks() {
if (layerGroup.value) layerGroup.value.clearLayers();
}
defineExpose({
saveMapFun,
getXYPosition
})
</script>
<style lang="scss" scoped>
#position-info {
position: relative;
bottom: 30px;
z-index: 1000;
}
.view-chose-map {
width: 100%;
height: 100%;
}
.map-container {
width: 100%;
height: 100%;
// width: 1920px;
// height: 1080px;
}
</style>
<template>
<div class="view-map">
<div id="map" style="height:100%;width:100%"></div>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import L from "leaflet";
import "leaflet/dist/leaflet.css";
const emit = defineEmits(['handleIconClick', 'regionCreated', 'deleteLine']);
let props = defineProps({
mapCenter: {
type: Array
}
})
let map = ref(null)
let zoom = ref(3)
watch(() => props.mapCenter, (newValue, oldValue) => {
map.value.setView(newValue, 6);
})
onMounted(() => {
initMap();
});
// 初始化地图
function initMap() {
if (!map.value) {
map.value = L.map("map", {
center: [30.695833, 121.001944],
zoom: 3,
minZoom: 1,
maxZoom: 5,
minNativeZoom: 1,
zoomControl: false,
attributionControl: false // 移除右下角leaflet标识
});
}
L.tileLayer(window.mapIP).addTo(
map.value
);
}
</script>
<style lang="scss" scoped>
.view-map {
height: 100%;
}
</style>
<template>
<div class="ringMenu-box">
<div v-if="targetType == 'edge'">
<slot name="edge"></slot>
</div>
<div v-else-if="targetType == 'node'">
<slot name="node"></slot>
</div>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
let props = defineProps({
targetType: {
type: String,
default: "node",
},
})
</script>
<style lang="scss" scoped>
</style>
<template>
<el-drawer class="drawer-container" title="详情" v-model="drawerVisible" :before-close="closeDrawer" :modal="false" direction="rtl">
<div class="drawer-content">
<el-radio-group v-model="radio" size="default" @change="changeRadioGroup">
<el-radio-button value="event" border>事件</el-radio-button>
<el-radio-button value="entity" border>实体</el-radio-button>
<el-radio-button value="relation" border>关系</el-radio-button>
</el-radio-group>
<div class="content">
<!-- 事件 -->
<div v-if="radio == 'event'" class="event-content">
<div v-if="!eventListData" class="not-text">暂无数据</div>
<div v-else class="event-content-info">
<div class="event-list" v-for="(event, index) in eventListData[activeBtn]" :key="index">
<div class="event-list-date">{{ event.date }}</div>
<div class="event-list-date">{{ event.title }}</div>
<div class="event-list-info">{{ event.content }}</div>
</div>
</div>
</div>
<!-- 实体 -->
<div class="entity-content" v-if="radio == 'entity'">
<div class="entity-list" v-if="entityList.length">
<div v-for="(entity, index) in entityList" :key="index" :style="{ background: entity.color }">
{{ entity.label }}
</div>
</div>
<div v-else class="not-text">暂无数据</div>
</div>
<!-- 关系 -->
<div class="relation-content" v-if="radio == 'relation'">
<div class="relation-list-box" v-if="relationList.length">
<div v-for="(entity, index) in relationList" :key="index">
{{ entity.label }}
</div>
</div>
<div v-else class="not-text">暂无数据</div>
</div>
</div>
</div>
</el-drawer>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import {
textRange,
hexToRgb
} from "@/utils/index";
import {
useStore
} from "vuex";
import eventlist from "../components/data/eventlistData.json"
import {
searchItem
} from "@/api/graphApi";
const {
appContext
} = getCurrentInstance();
const globalProxy = appContext.config.globalProperties;
let props = defineProps({
drawerVisible: {
type: Boolean,
default: false,
},
eventId: {
type: Number,
default: 1,
},
})
let drawerVisible = ref(false)
watch(() => props.drawerVisible, (newValue, oldValue) => {
handler(newValue);
drawerVisible.value = newValue;
}, {
deep: true
})
function handler(val) {
if (val) {
changeRadioGroup("event");
} else {
radio.value = "event";
}
}
const emit = defineEmits(['closeDrawer']);
const store = useStore();
const graphJSON = computed(() => store.state.graph.graphJSON);
let radio = ref("event");
let activeBtn = ref("");
let eventListData = ref({}); // 包含文本,图片...
let entityList = ref([]);
let relationList = ref([]); // 关系
function changeRadioGroup(radio) {
eventListData.value = [];
entityList.value = [];
relationList.value = [];
switch (radio) {
case "event":
getEventList(props.eventId);
break;
case "entity":
formatterEntity();
break;
case "relation":
getRelation();
break;
default:
break;
}
}
// 获取事件
function getEventList(id) {
// searchItem({
// id: id
// }).then((res) => {
let result = eventlist.data;
if (Array.isArray(result)) {
eventListData.value = null;
return;
}
eventListData.value = result;
let keys = Object.keys(eventListData.value);
activeBtn.value = keys.length ? keys[0] : "text";
// })
}
// 获取实体
function formatterEntity() {
if (graphJSON.value.hasOwnProperty("nodes")) {
const {
group,
nodes
} = graphJSON.value;
let enList = [];
if (group.length) {
let new_arr = JSON.parse(JSON.stringify(group));
new_arr.forEach((list) => {
enList.push(...list.members);
});
// 过滤出不包含在group中的节点
const not_group_nodes = nodes
.filter((f) => enList.findIndex((n) => f.id == n.id) == -1)
.map((m) => {
const {
id,
label,
style
} = m;
return {
id,
label,
color: style.fill,
};
});
entityList.value = enList.concat(not_group_nodes)
}
}
}
// 获取关系
function getRelation() {
const {
nodes,
edges,
group
} = graphJSON.value;
let new_nodes = nodes.map((node) => {
const {
id,
label,
originLabel,
} = node;
return {
id,
label: originLabel ? originLabel : label
};
});
let new_edges = edges.map((edge) => {
const {
id,
label,
source,
target
} = edge;
return {
id,
label,
source,
target,
};
});
let new_group = group.map((group) => {
const {
id,
label,
members
} = group;
let ids = members.map((m) => m.id);
return {
group: ids,
id,
tag: label,
};
});
relationList.value = new_edges;
}
function showToolTip(e, index) {
const bool = textRange(e.target);
entityList[index]["tooltip"] = bool;
}
function closeDrawer() {
emit("closeDrawer");
}
</script>
<style lang="scss" scoped>
.drawer-container {
display: flex;
flex-direction: column;
.drawer-content {
height: 100%;
padding: 10px;
display: flex;
flex-direction: column;
justify-content: space-between;
.content {
flex: 1;
overflow: hidden;
.not-text {
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #ccc;
}
.btn-box {
margin: 10px 0;
display: flex;
>.el-button {
margin-right: 5px;
}
}
.event-content {
height: 100%;
color: #000;
.event-content-info {
height: 100%;
display: flex;
flex-direction: column;
}
.event-content-text {
overflow-y: auto;
border: 1px solid #ccc;
.event-list {
padding: 10px 5px;
color: #606266;
.event-list-info {
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
word-break: break-all;
overflow: hidden;
}
}
}
.event-content-not-text {
overflow-y: auto;
.list-not-text {
width: 100%;
height: 100px;
margin-bottom: 5px;
padding: 0 5px;
}
img,
video,
audio {
width: 100%;
height: 100%;
}
}
}
.entity-content {
height: 100%;
.entity-list {
display: grid;
margin: 10px 0;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-gap: 10px;
cursor: pointer;
color: #000000;
.entity {
padding: 8px;
text-align: center;
height: 32px;
.entity-label {
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.relation-content {
height: 100%;
.relation-list-box {
height: 100%;
margin: 10px 0;
padding-bottom: 10px;
overflow-y: auto;
color: #000;
}
.relation-list {
width: 100%;
height: 120px;
margin-bottom: 8px;
display: flex;
padding: 0 5px;
overflow: hidden;
}
}
}
}
}
</style><style>
.drawer-container .el-drawer__header {
margin-bottom: 0;
}
.drawer-container .el-drawer__wrapper {
top: 60px;
}
:deep(.drawer-container .el-drawer__header) {
font-size: 18px;
margin-bottom: 14px;
}
</style>
<template>
<div class="create-graph-setp" v-loading="treeLoading" element-loading-text="加载中...">
<div class="form-box common-bg">
<el-form :model="formState" label-suffix=":" :rules="rules" label-width="120px" ref="formStateRef" class="form-state">
<el-form-item label="专题名称" prop="graphName">
<el-input placeholder="请输入专题名称" v-model="formState.graphName"></el-input>
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input placeholder="请输入描述" v-model="formState.description"></el-input>
</el-form-item>
</el-form>
</div>
<CreateFooterBtn @handleNextStep="handleNextStep" @handleCancelCreate="handleCancelCreate"></CreateFooterBtn>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import G6 from "@antv/g6";
import CreateFooterBtn from "../components/CreateFooterBtn.vue";
import {
useRoute,
useRouter
} from 'vue-router';
import {
useStore
} from "vuex";
let props = defineProps({
currentStep: {
type: Number,
default: 0,
},
})
const emit = defineEmits(['handleCancelCreate']);
const {
appContext
} = getCurrentInstance();
const globalProxy = appContext.config.globalProperties;
const store = useStore();
let route = useRoute();
let router = useRouter();
const cache_fromState = computed(() => store.state.graph.create_graph_form ?.[0] || {});
let formState = ref({
graphName: "",
description: ""
})
let rules = ref({
graphName: [{
required: true,
message: "请输入专题名称"
}],
})
let graphOptions = ref({
layout: {
type: "force",
preventOverlap: true,
linkDistance: (d) => {
return 100;
},
nodeStrength: (d) => {
return -10;
},
edgeStrength: (d) => {
return 0.5;
},
},
modes: {
default: ["drag-canvas", "zoom-canvas", "drag-node"],
},
defaultNode: {
size: 20,
style: {
lineWidth: 2,
stroke: "#ffae0b",
fill: "#e6a23cc2",
},
labelCfg: {
// 标签配置属性
position: "bottom", // 标签的属性,标签在元素中的位置
offset: 5,
style: {
fontSize: 16,
fill: "#409eff",
},
},
},
defaultEdge: {
style: {
stroke: "#3e8d76",
lineWidth: 2,
cursor: "pointer",
endArrow: {
path: G6.Arrow.triangle(8, 5, 0),
d: 0,
fill: "#4f9794b3",
},
},
labelCfg: {
style: {
fontSize: 16,
fill: "#409eff",
cursor: "pointer",
},
autoRotate: true,
refY: 10,
},
},
})
let treeLoading = ref(false);
let formStateRef = ref();
onMounted(() => {
getCacheFormState();
});
// 获取缓存的数据
function getCacheFormState() {
const {
graph_id = "", ...arg
} = cache_fromState.value;
if (graph_id) {
formState.value = {
graph_id,
...arg,
};
}
}
// 下一步
function handleNextStep() {
formStateRef.value.validate((valid) => {
if (valid) {
let graph_data = {}
handleToNext(formState.value, graph_data);
return
}
});
}
function handleToNext(state, newGraphData) {
const {
...arg
} = state;
store.commit("graph/SET_GRAPH_DATA", {
data: {
...arg
},
currentStep: props.currentStep,
nextStep: 1,
});
}
// 取消
function handleCancelCreate() {
emit('handleCancelCreate')
}
</script>
<style lang="scss" scoped>
.create-graph-setp {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
.form-box {
width: 30%;
padding: 24px;
// height: 80%;
margin: 50px auto;
display: flex;
flex-direction: column;
box-shadow: 0 2px 12px 0 #0000001a;
.form-state {
font-size: 18px;
}
}
.cascader-tips {
margin-top: 8px;
font-size: 14px;
line-height: 20px;
display: inline-block;
.highlight {
cursor: pointer;
}
}
.graph-structure {
flex: 1;
border: 1px solid #ccc;
// background: #ccc;
}
}
</style>
<style>
.common-bg .el-radio__label,
.form-box .el-form-item__label,
.custom-pagination-box .el-pagination>.is-first,
.custom-pagination-box .el-pagination__sizes, .el-pagination__total,
.custom-pagination-box .el-pagination__goto,
.custom-pagination-box .el-pagination__classifier {
color: #ffffffde;
}
</style>
<template>
<div class="step-container">
<div class="step-container-main">
<div class="g6-container"></div>
<div class="relation-container">
<div class="table-container">
<div class="table-container-left">
<el-table class="left-table" ref="singleTableRef" border :header-cell-style="{'text-align':'center'}"
:cell-style="{'text-align':'center'}"
:data="tableData" highlight-current-row @row-click="handleCurrentChange">
<el-table-column type="index" label="序号" width="60" />
<el-table-column property="name" label="名称" />
<el-table-column fixed="right" label="操作" width="60">
<template #default>
<el-button link type="primary" size="small" @click="handleDelete">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="add-panel-left">
<el-select class="select-type" v-model="selectValue" placeholder="" filterable>
<el-option v-for="option in optionsLeft" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
<el-button class="confirm-button" @click="addRelation(selectValue)">添加</el-button>
</div>
</div>
<div class="table-container-right">
<el-table class="right-table" ref="singleTableRightRef" border :header-cell-style="{'text-align':'center'}"
:cell-style="{'text-align':'center'}" :data="tableDataRight" highlight-current-row @row-click="handleCurrentChangeRight">
<el-table-column type="index" label="序号" width="60" />
<el-table-column property="name" label="名称" />
<el-table-column fixed="right" label="操作" width="60">
<template #default>
<el-button link type="primary" size="small" @click.native.stop="handleDeleteRight">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="add-panel-right">
<el-select class="select-type" v-model="selectValueRight" placeholder="" filterable>
<el-option v-for="option in optionsRight" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
<el-button class="confirm-button" @click="addRelationRight(selectValueRight)">添加</el-button>
</div>
</div>
</div>
<div class="relationship-input">
<el-input type="textarea" :rows="2" v-model="relationshipInput"></el-input>
<el-button class="edit-button" @click="editDescribe()">修改</el-button>
</div>
</div>
</div>
<CreateFooterBtn @handlePreStep="handlePreStep" @handleNextStep="handleNextStep" @handleCancelCreate="handleCancelCreate"></CreateFooterBtn>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import {
useRoute,
useRouter
} from 'vue-router';
import {
useStore
} from "vuex";
import CreateFooterBtn from "../components/CreateFooterBtn.vue";
const emit = defineEmits(['handleCancelCreate']);
const {
appContext
} = getCurrentInstance();
const globalProxy = appContext.config.globalProperties;
const store = useStore();
let route = useRoute();
let router = useRouter();
let props = defineProps({
currentStep: {
type: Number,
default: 0,
},
})
const cache_fromState = computed(() => store.state.graph.create_graph_form);
const graph_name = computed(() => store.state.graph.graph_name);
let formState = ref({
radio: "local",
tableData: [],
selectData: {},
pagination: {
page: 1,
page_size: 10,
total_count: 0,
}
})
const currentRow = ref()
const singleTableRef = ref()
const currentRowRight = ref()
const singleTableRightRef = ref()
let relationshipInput = ref("")
let tableData = ref([])
let tableDataRight = ref([])
let itemList = ref([])
let itemListItem = ref([])
let selectValue = ref("")
let optionsLeft = reactive([{
value: 'X.AI',
label: 'X.AI'
}])
let selectValueRight = ref("")
let optionsRight = reactive([{
value: 'X.AI',
label: 'X.AI'
}])
// 生命周期钩子
onMounted(() => {
getCacheFormState();
})
function handleCurrentChange(val) {
relationshipInput.value = "";
currentRow.value = val;
if (val.id === 1) {
tableDataRight.value = [
{
"description": [
"OpenAI是一家全球领先的人工智能研究实验室,由埃隆·马斯克、山姆·奥特曼等多位知名人士于2015年创立。其开发的GPT系列模型,如GPT-4o等,具有强大的自然语言处理能力,能够处理多种模态的信息,如文本、语音和图像。OpenAI的产品广泛应用于多个领域,包括搜索引擎、教育、机器人等。2023年,ChatGPT的推出使OpenAI成为全球关注的焦点。"
],
"id": 4,
"name": "OpenAI"
},
{
"description": [
"Google是全球最大的科技公司之一,在人工智能领域拥有深厚的技术积累和广泛的应用场景。其开发的Gemini等模型在多个领域表现出色,尽管在某些评测中受到一些争议,但仍然是全球AI领域的重要参与者。Google在AI的研究和应用方面投入巨大,涵盖了从搜索引擎到自动驾驶等多个领域。"
],
"id": 5,
"name": "Google"
},
{
"description": [
"Anthropic是一家专注于人工智能研究和开发的初创公司,由Dario Amodei和Daniela Amodei于2021年创立。其开发的Claude系列模型,如Claude 3.7等,在多模态和语言能力方面表现出色,是全球唯一能与GPT-4匹敌的模型。Anthropic注重AI的安全性和伦理,提出了基于AI反馈的强化学习等创新方法。"
],
"id": 6,
"name": "Anthropic"
},
{
"description": [
"X.AI是由埃隆·马斯克创立的人工智能公司,专注于开发高效、智能的AI助手。其目标是通过先进的技术帮助用户更好地管理日程、提高工作效率。X.AI的团队成员来自OpenAI等顶尖机构,具有强大的技术背景和创新能力。"
],
"id": 7,
"name": "X.AI"
},
{
"description": [
"Meta(原Facebook)是全球领先的社交媒体和科技公司,在人工智能领域也有重要布局。其开发的AI模型和工具广泛应用于内容推荐、图像识别等领域。Meta致力于通过AI技术提升用户体验和平台运营效率。"
],
"id": 8,
"name": "Meta"
},
{
"description": [
"微软是全球最大的软件公司之一,在人工智能领域拥有深厚的技术积累和广泛的应用场景。微软与OpenAI合作紧密,共同推动AI技术的发展。微软的AI产品涵盖了从办公软件到云计算等多个领域,致力于通过AI技术提升生产力和用户体验。"
],
"id": 9,
"name": "微软"
},
{
"description": [
"Nous Research是一家专注于人工智能研究的公司,致力于开发先进的AI模型和应用。其研究方向包括自然语言处理、计算机视觉和多模态AI等。Nous Research注重技术创新和实际应用,旨在通过AI技术解决实际问题并推动行业发展。"
],
"id": 10,
"name": "Nous Research"
}
]
} else if (val.id === 2) {
tableDataRight.value = [
{
"description": [
"X.AI是由埃隆·马斯克创立的人工智能公司,专注于开发高效、智能的AI助手。其目标是通过先进的技术帮助用户更好地管理日程、提高工作效率。X.AI的团队成员来自OpenAI等顶尖机构,具有强大的技术背景和创新能力。"
],
"id": 7,
"name": "X.AI"
},
{
"description": [
"Meta(原Facebook)是全球领先的社交媒体和科技公司,在人工智能领域也有重要布局。其开发的AI模型和工具广泛应用于内容推荐、图像识别等领域。Meta致力于通过AI技术提升用户体验和平台运营效率。"
],
"id": 8,
"name": "Meta"
},
{
"description": [
"微软是全球最大的软件公司之一,在人工智能领域拥有深厚的技术积累和广泛的应用场景。微软与OpenAI合作紧密,共同推动AI技术的发展。微软的AI产品涵盖了从办公软件到云计算等多个领域,致力于通过AI技术提升生产力和用户体验。"
],
"id": 9,
"name": "微软"
},
{
"description": [
"Nous Research是一家专注于人工智能研究的公司,致力于开发先进的AI模型和应用。其研究方向包括自然语言处理、计算机视觉和多模态AI等。Nous Research注重技术创新和实际应用,旨在通过AI技术解决实际问题并推动行业发展。"
],
"id": 10,
"name": "Nous Research"
}
]
} else if (val.id === 3) {
tableDataRight.value = [
{
"description": [
"X.AI是由埃隆·马斯克创立的人工智能公司,专注于开发高效、智能的AI助手。其目标是通过先进的技术帮助用户更好地管理日程、提高工作效率。X.AI的团队成员来自OpenAI等顶尖机构,具有强大的技术背景和创新能力。"
],
"id": 7,
"name": "X.AI"
}
]
} else {
tableDataRight.value = [
{
"description": [
"Nous Research是一家专注于人工智能研究的公司,致力于开发先进的AI模型和应用。其研究方向包括自然语言处理、计算机视觉和多模态AI等。Nous Research注重技术创新和实际应用,旨在通过AI技术解决实际问题并推动行业发展。"
],
"id": 10,
"name": "Nous Research"
}
]
}
}
function addRelation(name) {
const isInArray = tableData.value.some(item => item.name === name);
if (isInArray){
globalProxy.$message.error("已有该元素");
} else {
tableData.value.push(
{
"description": [""],
"id": tableData.value.length + 1,
"name": name
}
)
}
}
function addRelationRight(name) {
if (!currentRow.value){
globalProxy.$message.error("请选择后再添加");
} else {
const isInArray = tableDataRight.value.some(item => item.name === name);
if (isInArray){
globalProxy.$message.error("已有该元素");
} else {
tableDataRight.value.push(
{
"description": [""],
"id": tableDataRight.value.length + 1,
"name": name
}
)
}
}
}
function editDescribe() {
if (!currentRow.value){
globalProxy.$message.error("请选择后再添加");
} else {
globalProxy.$message({
message: "修改成功",
type: "success",
});
}
}
function handleDelete(params) {
}
function handleDeleteRight(params) {
}
function handleCurrentChangeRight(val) {
currentRowRight.value = val
let row = JSON.parse(JSON.stringify(currentRow.value))
let rowRight = JSON.parse(JSON.stringify(currentRowRight.value))
relationshipInput.value = row?.name + "→" + rowRight?.name
}
function isEmptyObject(obj) {
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
return false;
}
}
return true;
}
// 获取缓存的数据
const getCacheFormState = () => {
tableData.value = cache_fromState.value[1].selectData.entity;
}
// 取消
function handleCancelCreate() {
emit('handleCancelCreate')
}
// 上一步
function handlePreStep() {
store.commit("graph/SET_ACTIVE_STEP", props.currentStep - 1);
}
// 下一步
function handleNextStep() {
let newSelectData = {};
const {
selectData
} = formState.value;
for (let key in selectData) {
if (selectData[key].length) {
newSelectData[key] = selectData[key];
}
}
formState.value.selectData = newSelectData;
store.commit("graph/SET_SELECT_DATA", {
selectData: formState.value.selectData,
currentStep: props.currentStep,
});
store.commit("graph/SET_GRAPH_DATA", {
data: formState.value,
currentStep: props.currentStep,
nextStep: 3,
});
}
</script>
<style lang="scss" scoped>
.step-container {
display: flex;
overflow-y: auto;
flex-direction: column;
height: 100%;
.step-container-main {
height: 100%;
overflow: auto;
display: flex;
.g6-container {
width: 50%;
}
.relation-container {
width: 50%;
margin: 20px;
overflow: auto;
text-align: left;
.table-container {
display: flex;
.table-container-left {
width: 50%;
.add-panel-left {
display: flex;
margin-top: 10px;
.confirm-button {
margin-left: 10px;
}
}
}
.table-container-right {
margin-left: 20px;
width: calc(50% - 20px);
.left-table,
.right-table {
overflow: auto;
width: 100%;
}
.add-panel-right {
display: flex;
margin-top: 10px;
.confirm-button {
margin-left: 10px;
}
}
}
.el-table {
--el-table-border-color: rgba(204, 204, 204, 0.5);
--el-table-text-color: #fff;
--el-table-header-text-color: #fff;
--el-table-row-hover-bg-color: rgba(0, 0, 0, 0.5);
--el-table-current-row-bg-color: rgba(0, 0, 0, 0.5);
--el-table-header-bg-color: transparent;
--el-table-bg-color: transparent;
--el-table-tr-bg-color: transparent;
--el-table-expanded-cell-bg-color: transparent;
}
}
.table-container-bottom {
margin-top: 10px;
}
.relationship-input {
margin-top: 20px;
display: flex;
.edit-button {
margin-left: 10px;
}
.el-textarea .el-textarea__inner {
width: 240px;
overflow-y: auto;
/* 确保内容超出时可以滚动 */
}
}
}
}
}
/* 每个input都有 */
.input_common {
border: 1px solid rgba(205, 205, 205, 1);
border-radius: 4px;
}
</style><style lang="scss">
.relationship-input .el-textarea .el-textarea__inner {
background-color: transparent;
color: #fff;
max-height: 200px;
/* 设置最大高度 */
}
</style>
<template>
<div class="import-data-step">
<!-- 表单数据 -->
<div class="form-container">
<div class="common-bg form-box">
<el-form ref="formStateRef" :model="formState" label-suffix=":">
<el-row :gutter="24">
<el-col :span="6">
<el-form-item label="数据导入方式">
<el-radio-group v-model="formState.radio" @change="changeRadio">
<el-radio value="local">本地数据</el-radio>
<el-radio value="outer">自行上传</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="18">
<div v-if="formState.radio == 'local'" style="display: flex"></div>
<el-col :span="24" v-else class="action-upload">
<el-button type="primary" @click="handleDownload">
<el-icon style="vertical-align: middle">
<Document />
</el-icon>
下载模板
</el-button>
<el-button type="primary" @click="hanlleUploadVisible">
<el-icon style="vertical-align: middle">
<UploadFilled />
</el-icon>
数据上传
</el-button>
</el-col>
</el-col>
</el-row>
</el-form>
<el-tabs class="tablist" v-model="activeTabName" @tab-click="changeTabs">
<el-tab-pane
v-for="tab in tabsList"
:key="tab.label"
:label="tab.label"
:name="tab.value"
>
<!-- 表格数据 -->
<div class="table-box">
<div class="table-content" v-loading="tableLoading" element-loading-text="正在加载中...">
<TablePaginationComp v-if="isShowTable" class="table-comp" ref="tableCompRef" :tableData="formState.tableData" :columnList="columnList" :border="true" :selection="true" :showPagination="formState.radio == 'local' ? true : false" :pagination="formState.pagination" :maxHeight="formState.radio == 'local' ? '500' : null" @handleSizeChange="handleSizeChange" @handleCurrentChange="handleCurrentChange" @handleSelectionChange="handleSelectionChange" @selectSingle="handleSelectSingle">
<template v-slot="scope">
<el-button type="" link size="small" @click="toViewRow(scope.row)" class="info-button">查看详情</el-button>
</template>
</TablePaginationComp>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
<!-- 数据上传 -->
<el-dialog title="数据上传" v-model="uploadVisible" width="30%" :before-close="handleCancleUpload" :close-on-click-modal="false">
<el-upload drag action="#" ref="uploadRef" multiple :auto-upload="false" :file-list="fileList" :show-file-list="true" :before-upload="handleBeforeUpload" :on-change="changeUpload">
<el-icon class="el-icon-upload">
<Upload />
</el-icon>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
</el-upload>
<div class="dialog-footer">
<el-button @click="handleCancleUpload">取 消</el-button>
<el-button type="primary" @click="handleConfirmUpload">确 定</el-button>
</div>
</el-dialog>
<!-- 数据详情 -->
<el-dialog :title="dialogTitle" v-model="dialogVisible" width="30%" :before-close="handleCloseVisible" :close-on-click-modal="false" custom-class="custom-row-info">
<div class="row-info-content">
<div class="text-info" v-if="rowInfo.title">
{{ rowInfo.name }}
</div>
<div class="text-info" v-if="rowInfo.content">
{{ rowInfo.content[0] }}
</div>
<div class="text-info" v-if="rowInfo.name">
{{ rowInfo.name }}
</div>
<div class="text-info" v-if="rowInfo.description">
{{ rowInfo.description[0] }}
</div>
</div>
</el-dialog>
<CreateFooterBtn @handlePreStep="handlePreStep" @handleNextStep="handleNextStep" @handleCancelCreate="handleCancelCreate"></CreateFooterBtn>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import {
useRoute,
useRouter
} from 'vue-router';
import {
useStore
} from "vuex";
import {
getEntityList,
getItemList
} from "@/api/graphApi";
import TablePaginationComp from "../components/TablePagination.vue";
import CreateFooterBtn from "../components/CreateFooterBtn.vue";
const emit = defineEmits(['handleCancelCreate']);
const {
appContext
} = getCurrentInstance();
const globalProxy = appContext.config.globalProperties;
const store = useStore();
let route = useRoute();
let router = useRouter();
let props = defineProps({
currentStep: {
type: Number,
default: 0,
},
})
const cache_fromState = computed(() => store.state.graph.create_graph_form ?.[1] || {});
const graph_name = computed(() => store.state.graph.graph_name);
let isShowTable = ref(true)
let formStateRef = ref();
let formState = ref({
radio: "local",
tableData: [],
selectData: {},
pagination: {
page: 1,
page_size: 10,
total_count: 0,
}
})
let fileList = ref([])
let tableLoading = ref(false)
let postData = ref({}) // 获取数据的参数
// table表头
let columnList = ref([
{
prop: "name",
label: "标题",
sort: true,
tooltip: true,
},
{
prop: "description",
label: "描述",
sort: true,
tooltip: true,
}
])
let dialogVisible = ref(false)
let rowInfo = ref({}) // 数据详情
let dialogTitle = ref("")
let tableCompRef = ref()
let uploadVisible = ref(false) // 数据上传弹框
let tabsList = ref([
{
label: "实体",
value: "entity",
},
{
label: "事件",
value: "items",
},
{
label: "关系",
value: "relations",
}
])
let activeTabName = ref("entity")
onMounted(() => {
getTableDataEntity();
})
function getTableDataEntity() {
formState.value.tableData = [{
id: 1,
name: "1",
description: "123"
},{
id: 2,
name: "2",
description: "1234"
},{
id: 3,
name: "3",
description: "12345"
},{
id: 4,
name: "4",
description: "123456"
}]
// getEntityList({}).then((res) => {
// if (res && res.data) {
// formState.value.tableData = res.data;
// formState.value.pagination.total_count = res.data.length;
// }
// })
}
function getTableDataItem() {
getItemList().then((res) => {
if (res && res.data) {
formState.value.tableData = res.data;
formState.value.pagination.total_count = res.data.length;
}
})
}
// 导入方式切换
function changeRadio(val) {
formStateRef.value.resetFields();
resetTableSelect()
formState.value = {
radio: val,
tableData: [],
selectData: {},
pagination: {
page: 1,
page_size: 10,
total_count: 0,
}
};
fileList.value = [];
activeTabName.value = "entity";
if (val === "local"){
getTableDataEntity();
}
}
function changeTabs(val) {
if (formState.value.radio === "local"){
if (val.paneName === "entity"){
columnList.value = [{
prop: "name",
label: "标题",
sort: true,
tooltip: true,
},
{
prop: "description",
label: "描述",
sort: true,
tooltip: true,
},
];
getTableDataEntity();
} else if (val.paneName == "news" || val.paneName == "items") {
columnList.value = [{
prop: "title",
label: "标题",
sort: true,
tooltip: true,
},
{
prop: "content",
label: "描述",
sort: true,
tooltip: true,
},
];
getTableDataItem();
}
}
}
function handleBeforeUpload(file) {
return false;
}
function changeUpload(file, list) {
fileList.value = list;
}
// 模板下载
function handleDownload() {
}
// 数据上传弹框
function hanlleUploadVisible() {
uploadVisible.value = true;
}
// 取消上传
function handleCancleUpload() {
uploadVisible.value = false;
fileList.value = [];
}
// 确定上传
function handleConfirmUpload() {
if (!fileList.value.length) {
return globalProxy.$message.error("请上传数据");
}
}
// 获取数据
function getTableData(postData) {
const formData = new FormData();
for (let key in postData) {
formData.append(key, postData[key]);
}
fileList.value.forEach((file) => {
const {
raw
} = file;
formData.append("files", raw);
});
handleCancleUpload();
changeTableLoading(true);
}
// 数据详情
function toViewRow(row) {
rowInfo.value = {
...row,
};
dialogTitle.value = row.title;
dialogVisible.value = true;
}
function handleCloseVisible() {
dialogVisible.value = false;
rowInfo.value = {};
dialogTitle.value = "";
}
function handleSizeChange(size) {
formState.value.pagination.page_size = size;
}
function handleCurrentChange(page) {
formState.value.pagination.page = page;
}
// 选中
function handleSelectionChange(selection) {
// 本地选中的集合
if (formState.value.radio == "local") {
formState.value.selectData[activeTabName.value] = selection;
}
}
// 单选
function handleSelectSingle({
selection,
row
}) {
handleSelectionChange(selection);
}
// 清空选中的数据
function resetTableSelect() {
// 如果有选中的数据,先取消选中
let values = Object.values(formState.value.selectData).flat(Infinity);
if (values.length && tableCompRef.value) {
tableCompRef.value.clearSelection();
}
}
function changeTableLoading(flag) {
tableLoading.value = flag;
}
function isEmptyObject(obj) {
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
return false;
}
}
return true;
}
// 取消
function handleCancelCreate() {
emit('handleCancelCreate')
}
// 上一步
function handlePreStep() {
store.commit("graph/SET_ACTIVE_STEP", props.currentStep - 1);
}
// 下一步
function handleNextStep() {
let isEmpty = isEmptyObject(formState.value.selectData)
if (isEmpty) {
return globalProxy.$message.error("请选择数据");
}
// let new_select_data = {};
// const {
// selectData
// } = formState.value;
// for (let key in selectData) {
// if (selectData[key].length) {
// new_select_data[key] = selectData[key];
// }
// }
// formState.value.selectData = new_select_data;
store.commit("graph/SET_SELECT_DATA", {
selectData: formState.value.selectData,
currentStep: props.currentStep,
});
store.commit("graph/SET_GRAPH_DATA", {
data: formState.value,
currentStep: props.currentStep,
nextStep: 2,
});
}
</script>
<style lang="scss" scoped>
.import-data-step {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
.form-container {
padding: 0 30px;
height: 100%;
.form-box {
height: 100%;
display: flex;
flex-direction: column;
}
}
.action-upload {
margin-bottom: 14px;
text-align: right;
.file-step {
height: 100%;
:deep(.el-form-item__content) {
height: 100%;
margin-left: 0 !important;
display: flex;
line-height: 24px;
justify-content: space-between;
.step {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 14px;
.template {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
.document {
font-size: 100px;
}
}
}
.line {
width: 1px;
background: #ccc;
margin: 0 14px;
}
.upload-file {
display: flex;
flex-direction: column;
.upload-box {
width: 100%;
height: 100%;
display: flex;
overflow-y: auto;
.file-list {
width: 50%;
margin-left: 10px;
}
}
.el-upload {
width: 100%;
height: 100%;
padding: 5px 0;
}
.el-upload-dragger {
flex: 1;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.el-icon-upload {
margin: 0;
}
}
.el-upload-list {
width: 60%;
margin: 0 auto;
}
}
}
}
.upload-reslove {
padding: 0 10px;
.wait {
width: 100%;
display: inline-block;
text-align: center;
}
}
.highlight {
cursor: pointer;
}
}
.col-btn-right {
text-align: right;
margin-left: 20px;
}
.footer-btn {
text-align: center;
}
.table-box {
height: 100%;
display: flex;
flex-direction: column;
.table-content {
flex: 1;
display: flex;
flex-direction: column;
.table-comp {
flex: 1;
}
.info-button {
color: #409eff;
}
}
}
:deep(.custom-row-info) {
.el-dialog__title {
display: inline-block;
padding-right: 14px;
}
}
.row-info-content {
.text-info {
max-height: 400px;
overflow-y: auto;
}
video,
audio,
img {
width: 100%;
}
}
:deep(.el-upload) {
width: 100%;
.el-upload-dragger {
width: 100%;
}
}
:deep(.el-upload-list) {
max-height: 300px;
overflow-y: auto;
}
}
</style>
<style lang="scss">
.tablist .el-tabs__item.is-top {
color: #fff;
}
.tablist .el-tabs__item.is-top.is-active {
color: #409eff;
}
.tablist .el-tabs__item {
padding: 0;
min-width: 80px;
}
</style>
<template>
<div class="create-hyper common">
<el-steps :active="activeSteps" finish-status="success" align-center>
<el-step v-for="(step,index) in stepList" :key="step" :title="step" :status="index == activeSteps ? 'success' : ''"></el-step>
</el-steps>
<div class="steps-content">
<!-- 创建专题 -->
<CreateGraphStep ref="createGraphStepRef" :currentStep="activeSteps" v-if="activeSteps == 0" @handleCancelCreate="handleCancelCreate"></CreateGraphStep>
<!-- 数据导入 -->
<ImportDataStep ref="importDataStepRef" :currentStep="activeSteps" v-if="activeSteps == 1" @handleCancelCreate="handleCancelCreate"></ImportDataStep>
<!-- 定义关系 -->
<DefineRelationsStep ref="importDataStepRef" :currentStep="activeSteps" v-if="activeSteps == 2" @handleCancelCreate="handleCancelCreate"></DefineRelationsStep>
<!-- 专题生成 -->
<div v-if="activeSteps == 3" class="graph-generation">
<div class="icon-success">
<span class="icon el-icon-success"></span>
<div class="success-tips">创建成功!</div>
</div>
<div>
<el-button type="primary" size="small" @click="resetCreate">继续创建</el-button>
<el-button type="primary" size="small" @click="toViewGraph">查看图谱</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted,
watch,
computed,
nextTick,
getCurrentInstance
} from 'vue'
import {
useRoute,
useRouter
} from 'vue-router';
import {
useStore
} from "vuex";
import CreateGraphStep from "./CreateGraphStep.vue";
import ImportDataStep from "./ImportDataStep.vue";
import DefineRelationsStep from "./DefineRelationsStep.vue";
const {
appContext
} = getCurrentInstance();
const globalProxy = appContext.config.globalProperties;
const store = useStore();
let route = useRoute();
let router = useRouter();
const activeSteps = computed(() => store.state.graph.activeSteps);
const stepList = computed(() => store.state.graph.stepList);
onMounted(() => {
});
onUnmounted(() => {
handleCancelCreate("destroy")
})
// 继续创建
function resetCreate() {
clearCache();
}
function clearCache() {
store.commit("graph/SET_CLEAR_CACHE");
}
// 查看图谱
function toViewGraph() {
resetCreate();
// router.push("/themeManage");
router.push("/home");
}
// 删除主题
function handleCancelCreate(type) {
// 图谱未创建完成离开删除创建的主题
if (activeSteps.value !== 3) {
clearCache();
if (type !== "destroy") {
toViewGraph();
}
return
}
}
</script>
<style lang="scss" scoped>
.create-hyper {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
.el-steps {
width: 70%;
margin: 10px auto;
:deep(.el-step__main) {
.el-step__title {
font-weight: normal;
font-size: 14px;
}
}
}
.steps-content {
flex: 1;
overflow-y: auto;
.graph-generation {
text-align: center;
.icon-success {
margin: 14px 0;
.icon {
font-size: 120px;
}
.success-tips {
font-size: 24px;
}
}
.el-button {
margin: 0 14px;
}
}
}
}
</style>
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
{
"name": "vite-project",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "~5.7.2",
"vite": "^6.2.0"
}
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
\ No newline at end of file
export function setupCounter(element: HTMLButtonElement) {
let counter = 0
const setCounter = (count: number) => {
counter = count
element.innerHTML = `count is ${counter}`
}
element.addEventListener('click', () => setCounter(counter + 1))
setCounter(0)
}
import './style.css'
import typescriptLogo from './typescript.svg'
import viteLogo from '/vite.svg'
import { setupCounter } from './counter.ts'
document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
<div>
<a href="https://vite.dev" target="_blank">
<img src="${viteLogo}" class="logo" alt="Vite logo" />
</a>
<a href="https://www.typescriptlang.org/" target="_blank">
<img src="${typescriptLogo}" class="logo vanilla" alt="TypeScript logo" />
</a>
<h1>Vite + TypeScript</h1>
<div class="card">
<button id="counter" type="button"></button>
</div>
<p class="read-the-docs">
Click on the Vite and TypeScript logos to learn more
</p>
</div>
`
setupCounter(document.querySelector<HTMLButtonElement>('#counter')!)
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vanilla:hover {
filter: drop-shadow(0 0 2em #3178c6aa);
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>
\ No newline at end of file
/// <reference types="vite/client" />
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path';
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [vue(), AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'), // 设置 @ 别名指向 src 目录
},
},
server: {
proxy: {
'/api': {
target: 'http://8.140.26.4:10006/tech_hyper', // 目标地址
changeOrigin: true, // 修改请求头的 Origin 字段
// rewrite: (path) => path.replace(/^\/api/, '') // 重写路径
},
}
}
})
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论