第一次提交

This commit is contained in:
yans 2025-11-24 14:49:39 +08:00
commit 0a6fff3cf2
74 changed files with 4891 additions and 0 deletions

12
README.md Normal file
View File

@ -0,0 +1,12 @@
### 环境配置
node版本: 22.10.0
pnpm版本: 6.11.0
### 安装依赖
pnpm install
### 启动项目
pnpm run dev

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>雷达⽬标识别迁移部署系统</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

64
package.json Normal file
View File

@ -0,0 +1,64 @@
{
"name": "radar-system",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --open ",
"build": "vite build",
"preview": "vite preview",
"test:unit": "vitest",
"build-only": "vite build",
"lint": "eslint . --fix",
"format": "prettier --write src/"
},
"dependencies": {
"@types/d3": "^7.4.3",
"ant-design-vue": "4.x",
"d3": "^7.9.0",
"echarts": "^5.6.0",
"lodash": "^4.17.21",
"pinia": "^3.0.1",
"screenfull": "^5.1.0",
"socket.io-client": "^4.2.0",
"streamsaver": "^2.0.6",
"swiper": "^12.0.3",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@tsconfig/node22": "^22.0.1",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.14.0",
"@vitejs/plugin-vue": "^5.2.3",
"@vitejs/plugin-vue-jsx": "^4.1.2",
"@vitest/eslint-plugin": "^1.1.39",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.5.0",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.7.0",
"autoprefixer": "^10.4.19",
"dayjs": "^1.11.13",
"eslint": "^9.22.0",
"eslint-plugin-vue": "~10.0.0",
"jiti": "^2.4.2",
"jsdom": "^26.0.0",
"npm-run-all2": "^7.0.2",
"postcss-normalize-charset": "^6.0.1",
"postcss-preset-env": "^9.3.0",
"prettier": "3.5.3",
"sass": "^1.66.1",
"sass-loader": "^13.3.2",
"typescript": "~5.8.0",
"vite": "^6.2.4",
"vite-plugin-vue-devtools": "^7.7.2",
"vitest": "^3.1.1",
"vue-tsc": "^2.2.8"
},
"packageManager": "pnpm@6.11.0",
"engines": {
"node": ">=18.18.2 <= 22.10.0",
"pnpm": ">=6.11.0"
}
}

View File

@ -0,0 +1,18 @@
@font-face {
font-family: '黑体';
src: url('/static/font/MyFont.ttf');
}
@font-face {
font-family: 'Times New Roman';
src: url('/static/font/Times-New-Romance-1.ttf');
}
@font-face {
font-family: '宋体';
src: url('/static/font/ZiTiGuanJiaFangSongTi-2.ttf');
}
* {
line-height: 1.5;
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

45
src/App.vue Normal file
View File

@ -0,0 +1,45 @@
<script setup lang="ts">
import { ref } from "vue";
import defaultJSON from "@/styles/theme/default.json";
import zhCN from "ant-design-vue/es/locale/zh_CN";
import { useRoute } from "vue-router";
const data = {
theme: defaultJSON,
};
const route = useRoute();
const reload = ref<boolean>(false);
</script>
<template>
<a-config-provider :theme="data.theme" :locale="zhCN">
<a-app>
<Suspense>
<RouterView :reload="reload" />
<template #fallback>
<div class="loading-box">
<a-spin />
</div>
</template>
</Suspense>
</a-app>
</a-config-provider>
</template>
<style lang="scss" scoped>
.ant-app {
width: 100%;
height: 100%;
overflow: hidden;
}
.loading-box {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
</style>

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,49 @@
<script lang="ts">
import { defineComponent, reactive } from "vue";
export default defineComponent({
name: "LoginLayout",
});
</script>
<script setup lang="ts">
import { info } from "@/config/index.ts";
const prefix = "login-layout";
</script>
<template>
<div :class="prefix" :style="`background-image: url(${info.bgImg})`">
<div :class="`${prefix}-form`">
<RouterView />
</div>
</div>
</template>
<style lang="scss" scoped>
$prefix: "login-layout";
.#{$prefix} {
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative;
background-size: 100% auto;
&-form {
position: absolute;
left: 384px;
top: 253px;
width: 447px;
height: 300px;
opacity: 1;
background: #fff;
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.16);
border-radius: 16px;
// border-radius: 16px 16px 16px 16px;
}
}
.global-login-page {
width: 100%;
height: 100%;
position: relative;
}
</style>

View File

@ -0,0 +1,85 @@
<script lang="ts">
import { defineComponent, defineAsyncComponent } from "vue";
export default defineComponent({
name: "SystemLayoutContent",
});
</script>
<script setup lang="ts">
// import LoginDrawer from '@/components/Service/Login/index/index.vue'
import ErrorLoading from "@/components/Module/AsyncContent/ErrorLoading.vue";
import { useRoute } from "vue-router";
const asyncContent = defineAsyncComponent({
loader: () => import("@/components/Module/AsyncContent/index.vue"),
delay: 2000,
loadingComponent: ErrorLoading,
errorComponent: ErrorLoading,
timeout: 10000,
});
interface propsType {
contentLoading: boolean;
}
let props = withDefaults(defineProps<propsType>(), {
//
contentLoading: true,
});
const route = useRoute();
const prefix = "_system-layout-content";
</script>
<!-- bgImg
SiderImg-->
<template>
<a-layout-content :class="prefix">
<div class="router-view-box">
<div v-if="props.contentLoading">
<Suspense>
<asyncContent>
<RouterView :key="route.path" />
</asyncContent>
<template #fallback>
<div class="empty-box">
<a-spin />
</div>
</template>
</Suspense>
</div>
</div>
<!-- <LoginDrawer /> -->
</a-layout-content>
</template>
<style lang="scss" scoped>
$prefix: "_system-layout-content";
.#{$prefix} {
width: 100%;
// background-color: #f4f6fc;
width: 100%;
height: calc(100vh - 60px);
overflow: hidden;
.router-view-box {
width: 100%;
height: 100%;
// background-color: #f7f8fa;
overflow: auto;
box-sizing: border-box;
> div {
width: 100%;
height: 100%;
}
.empty-box {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
}
</style>

View File

@ -0,0 +1,58 @@
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "SystemLayoutHeader",
});
</script>
<script setup lang="ts">
import Info from "@/components/Base/Tools/Info/index.vue";
const prefix = "_system-layout-header";
</script>
<template>
<a-layout-header theme="light" :class="prefix">
<div :class="`${prefix}-wrapper`">
<div :class="`${prefix}-wrapper-grid`">
<Info />
</div>
<div :class="`${prefix}-wrapper-grid`">
<!-- <UserBox /> -->
</div>
</div>
</a-layout-header>
</template>
<style lang="scss" scoped>
$prefix: "_system-layout-header";
.#{$prefix} {
padding: 0;
background-color: #fff;
height: 60px;
position: reactive;
z-index: 20;
box-shadow: 0px 2px 4px 0px rgba(133, 99, 244, 0.2);
box-sizing: border-box;
&-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
&-left {
font-size: 16px;
color: #333;
}
&-right {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
}
}
}
</style>

View File

@ -0,0 +1,44 @@
import { h } from 'vue'
import Icon from '@/components/Module/Icon/index.vue'
export const useMain = () => {
// 路由过滤,只显示要显示的路由
const routerFilter = (menus: Array<Record<string, any>>) => {
return menus.filter((item: Record<string, any>) => {
if (item.children) {
item.children = routerFilter(item.children)
}
return !item.meta.hidden
})
}
// 路由格式化
const routerFormat = (arr: Array<Record<string, any>>) => {
return arr.map((item: Record<string, any>) => {
if (item.children && item.children.length) {
item.children = routerFormat(item.children)
}
return {
meta: item.meta,
icon: () =>
h(Icon, {
name: item.meta.icon,
}),
fullPath: item.fullPath,
pid: item.parentId,
key: item.name,
label: item.meta.title || '',
title: item.meta.title || '',
children: item.children?.length ? item.children : '',
type: item.type || null,
}
})
}
return {
routerFilter,
routerFormat
}
}

View File

@ -0,0 +1,240 @@
<script lang="ts">
import { defineComponent } from "vue";
import { computed } from "vue";
export default defineComponent({
name: "SystemLayoutSider",
});
</script>
<script setup lang="ts">
import { reactive, ref, watch, h, onMounted } from "vue";
import type { MenuProps } from "ant-design-vue";
import { router } from "@/router/index.ts";
import UserBox from "@/components/Base/Tools/UserBox/index.vue";
import { useMain } from "@/components/Base/Layout/SystemLayout/Sider/hooks/router.ts";
import { useRouterStore } from "@/stores/modules/async-router.ts";
import type { AppRouter } from "@/router/config/index";
const routerStore = useRouterStore();
const routers = routerStore.ROUTERS;
const { routerFilter, routerFormat } = useMain();
//
let menus = ref<Array<AppRouter>>([]);
//
const createMenus = (routerList: Array<AppRouter>) => {
const validRouter = routerFilter(routerList);
const useRouter = routerFormat(validRouter);
return useRouter;
};
// let menuSelectKeys = computed(() => {
// console.log("router.currentRoute", router.currentRoute.value);
// return [router.currentRoute.value.name];
// });
//
const menuSelectKeys = ref<string[]>([]);
//
const handleClick: MenuProps["onClick"] = (e) => {
const { fullPath } = e.item;
console.log("fullPath", e.item, routers[0].children);
console.log("menuSelectKeys", menuSelectKeys);
if (fullPath != router.currentRoute.value.fullPath) {
router.push(e.item.fullPath as string);
}
};
//
const routerOpenKeys = ref<string[]>([]);
const handleOpenChange = (openKeys: string[]) => {};
//
const initMenus = () => {
//@ts-ignorex
menus.value = createMenus(routers[0].children as AppRouter[]);
};
//
const initMenuSelectkeys = () => {
const curRouter = router.currentRoute.value;
console.log("curRouter", curRouter, routers[0]);
let key = curRouter.name as string;
const { hidden } = curRouter.meta;
if (hidden) {
const routerGroup = curRouter.matched[1];
let target = findRouter([routerGroup], key);
console.log("targetRouter", target);
let isSelect = true;
while (isSelect) {
if (target.meta.hidden) {
if (!target.parent) {
isSelect = false;
} else {
target = target.parent;
}
} else {
key = target.name;
isSelect = false;
}
}
}
menuSelectKeys.value = [key];
};
//
const findRouter = (routerList: Array<Record<string, any>>, key: string): any => {
let router = null;
for (let i = 0; i < routerList.length; i++) {
const item = routerList[i];
if (item.name == key) {
router = item;
break;
}
if (item.children) {
router = findRouter(item.children, key);
if (router) {
break;
}
}
}
return router;
};
//
const initStatus = () => {
const { value } = router.currentRoute;
routerOpenKeys.value = (value.meta.parentId ? [value.meta.parentId] : []) as string[];
};
const init = () => {
initMenus();
initStatus();
initMenuSelectkeys();
};
const collapsed = ref(false);
onMounted(() => {
//@ts-ignorex
init();
});
const prefix = "_system-layout-sider";
</script>
<template>
<a-layout-sider
collapsible
v-model:collapsed="collapsed"
theme="light"
:class="prefix"
:trigger="null"
width="240"
>
<a-menu
v-model:openKeys="routerOpenKeys"
v-model:selectedKeys="menuSelectKeys"
mode="inline"
:multiple="false"
:inline-collapsed="collapsed"
:items="menus"
@click="handleClick"
@openChange="handleOpenChange"
>
</a-menu>
</a-layout-sider>
</template>
<style lang="scss" scoped>
$prefix: "_system-layout-sider";
.#{$prefix} {
max-height: 100vh;
overflow: hidden;
// padding: 10px 10px;
box-sizing: border-box;
position: reactive;
z-index: 10;
box-shadow: 0px 2px 4px 0px rgba(133, 99, 244, 0.2);
border-radius: 0px 0px 16px 16px;
background: rgba(255, 255, 255, 0.6);
border: 1px solid rgba(231, 239, 251, 0.5);
background: rgba(255, 255, 255, 0.6);
:deep(.ant-layout-sider-children) {
display: flex;
align-items: start;
justify-content: start;
flex-direction: column;
.ant-menu-light {
background-color: transparent;
margin: 0;
}
.ant-menu-item-selected {
background-color: #e2eaf2;
}
.ant-menu-root.ant-menu-inline {
border-inline-end: none;
}
}
&-header {
padding-bottom: 15px;
&__row {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
.search-row {
margin-top: 5px;
padding: 5px 15px;
border-radius: 40px;
color: #333;
box-sizing: border-box;
:deep(.ant-input) {
background-color: transparent;
}
}
}
&__collapsed {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
cursor: pointer;
&:hover {
background-color: #f0f0f0;
}
}
}
&-chats {
flex: 1;
width: 100%;
flex-shrink: 0;
align-self: stretch;
}
&-menus {
padding-top: 10px;
width: 100%;
height: 240px;
margin-top: auto;
}
&-footer {
width: 100%;
}
&-marker {
width: 339px;
height: 319px;
position: absolute;
bottom: 0;
left: -121px;
}
}
</style>

View File

@ -0,0 +1,45 @@
<script setup lang="ts">
import { reactive, computed } from "vue";
import LayoutHeader from "./Header/index.vue";
import LayoutSider from "./Sider/index.vue";
import LayoutContent from "./Content/index.vue";
import { router } from "@/router/index.ts";
import { inject } from "vue";
const data = reactive({
loading: true,
});
</script>
<template>
<a-layout class="system-layout">
<layout-header></layout-header>
<a-layout>
<layout-sider v-model:contentLoading="data.loading"> </layout-sider>
<layout-content v-model:contentLoading="data.loading"> </layout-content>
</a-layout>
</a-layout>
</template>
<style lang="scss" scoped>
.system-layout {
width: 100%;
height: 100vh;
position: relative;
background: linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5));
background-size: cover;
:deep(.ant-layout) {
background: transparent;
}
&_header {
}
&_content {
// padding-top: 80px;
box-sizing: border-box;
height: 100%;
width: 100%;
// position: relative;
// z-index: 0;
}
}
</style>

View File

@ -0,0 +1,12 @@
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'ViewLayout',
})
</script>
<script setup lang="ts"></script>
<template>
<RouterView />
</template>

View File

@ -0,0 +1,56 @@
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "SystemInfo",
});
</script>
<script setup lang="ts">
import { info } from "@/config/index.ts";
const prefix = "_info-box";
</script>
<template>
<div :class="prefix">
<div :class="`${prefix}-icon`">
<img :class="`${prefix}-icon-img`" :src="info.logo" />
</div>
<div :class="`${prefix}-title`">
{{ info.title }}
</div>
</div>
</template>
<style lang="scss" scoped>
$prefix: "_info-box";
.#{$prefix} {
display: flex;
align-items: center;
justify-content: center;
&-icon {
margin-right: 10px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
&-img {
width: 100%;
height: 100%;
}
}
&-title {
font-family:
Source Han Sans CN,
Source Han Sans CN;
font-weight: 500;
font-size: 20px;
color: #1d2129;
line-height: 28px;
text-align: left;
font-style: normal;
text-transform: none;
}
}
</style>

View File

@ -0,0 +1,81 @@
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "SystemLayoutSider",
});
</script>
<script setup lang="ts">
import Icon from "@/components/Module/Icon/index.vue";
import { ref } from "vue";
interface propsType {
menus: Array<{
name: string;
path: string;
key: string;
icon: string;
}>;
}
let props = withDefaults(defineProps<propsType>(), {
//
menus: () => [],
});
const emits = defineEmits(["click"]);
//
const onHandle = (item: { path: string; key: string }) => {
emits("click", item);
};
const prefix = "_system-menus";
</script>
<template>
<div :class="prefix">
<button :class="`${prefix}-row`" v-for="item in props.menus" @click="onHandle(item)">
<div :class="`${prefix}-row__icon`">
<Icon :name="item.icon" />
</div>
<div :class="`${prefix}-row__label`">{{ item.name }}</div>
</button>
</div>
</template>
<style lang="scss" scoped>
$prefix: "_system-menus";
.#{$prefix} {
width: 100%;
display: flex;
align-items: start;
justify-content: start;
flex-direction: column;
&-row {
width: 100%;
height: 40px;
padding: 5px;
border-radius: 12px;
background-color: transparent;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: start;
gap: 10px;
cursor: pointer;
&:hover {
background-color: #333;
}
&__icon {
margin-right: 8px;
font-size: 16px;
}
&__label {
font-size: 14px;
font-weight: 500;
}
}
}
</style>

View File

@ -0,0 +1,36 @@
export const useMain = () => {
const menus = [
{
name: '设置',
path:'/toolsSet',
key: 'toolsSet',
icon: 'briefCase'
},
{
name: '已归档对话',
path:'/workspace/knowledge',
key: 'knowledge',
icon: 'book'
},
{
name: '管理员面板',
path:'/fileManagementSet',
key: 'fileManagementSet',
icon: 'file'
},
]
const operateMenus = [
{
name: '退出登录',
path:'/login',
key: 'login',
icon: 'briefCase'
}
]
return {
menus,
operateMenus
}
}

View File

@ -0,0 +1,71 @@
<script lang="ts">
import { defineComponent, ref, inject } from "vue";
export default defineComponent({
name: "UserCompBox",
});
</script>
<script lang="ts" setup>
import { useMain } from "@/components/Base/Tools/UserBox/hooks/index.ts";
import Menus from "@/components/Base/Tools/Menus/index.vue";
import Icon from "@/components/Module/Icon/index.vue";
const { menus, operateMenus } = useMain();
const prefix = "_user-box";
</script>
<template>
<a-Dropdown>
<template #overlay>
<div :class="`${prefix}-drop`">
<div :class="`${prefix}-drop-row`">
<Menus :menus="menus" />
</div>
<div :class="`${prefix}-drop-row`">
<Menus :menus="operateMenus" />
</div>
</div>
</template>
<button :class="`${prefix}-avator`">
<icon name="userLine" />
</button>
</a-Dropdown>
</template>
<style lang="scss" scoped>
$prefix: "_user-box";
.#{$prefix} {
&-avator {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
background-color: #333;
}
}
&-drop {
width: 200px;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
border-radius: 6px;
background-color: #fff;
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.16);
&-row {
margin-bottom: 10px;
&:last-child {
padding-top: 10px;
margin-bottom: 0;
border-top: 1px solid #333;
}
}
}
}
</style>

View File

@ -0,0 +1,21 @@
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'asyncErrLoadingBox',
})
</script>
<script setup lang="ts"></script>
<template>
<div class="global-comp-loading-box">
<a-spin />
</div>
</template>
<style lang="scss" scoped>
.global-comp-loading-box {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,14 @@
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'asyncContentBox',
})
</script>
<script setup lang="ts"></script>
<template>
<slot></slot>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,80 @@
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "SvgIcon",
});
</script>
<script lang="ts" setup>
import { computed } from "vue";
import { getIconContent } from "@/components/Module/Icon/components/icon.ts";
const props = defineProps({
prefix: {
type: String,
default: "icon",
},
name: {
type: String,
required: true,
},
color: {
type: String,
default: "currentColor",
},
className: {
type: String,
default: "",
},
stroke: {
type: String,
default: "currentColor",
},
});
function processSvgForInline(svg: string, fillColor: string) {
let processed = svg.replace(/<\?xml.*?\?>/, "");
if (fillColor) {
processed = processed.replace("<svg", `<svg fill="${fillColor || "currentColor"}" `);
}
return processed;
}
let svgContent: string | null = "";
const processedSvg = computed(() => {
svgContent = getIconContent(props.name);
return svgContent ? processSvgForInline(svgContent, props.color) : "";
});
</script>
<template>
<div class="_svg-container" :style="`--color: ${color ?? 'inherit'};`">
<template v-if="processedSvg">
<div :class="`_svg-icon ${className || ''}`" v-html="processedSvg"></div>
</template>
<template v-else-if="name">
<div :class="`_svg-icon ${className || ''}`">{{ name }}</div>
</template>
</div>
</template>
<style lang="scss" scoped>
._svg-container {
display: flex;
align-items: center;
justify-content: center;
font-size: inherit;
}
._svg-icon {
display: flex;
align-items: center;
justify-content: center;
width: 1em;
height: 1em;
vertical-align: -0.1em;
fill: currentColor;
overflow: hidden;
font-size: inherit;
}
</style>

View File

@ -0,0 +1,92 @@
// Icon Store - For efficient SVG icon imports (Vue3 Composition API)
import { ref } from 'vue';
type IconMap = {
[key: string]: string;
}
// 响应式状态
const availableIcons = ref<IconMap>({});
const iconContents = ref<IconMap>({});
const iconCache = new Map();
// 使用Vite的import.meta.glob动态导入
export const initialize = async () => {
const iconModules = import.meta.glob('/src/assets/icons/*.svg', {
eager: true,
as: 'raw'
});
Object.entries(iconModules).forEach(([path, content]: [string, string]) => {
const name = path?.split('/')?.pop()?.replace('.svg', '') as string;
iconContents.value[name] = content;
availableIcons.value[name] = `data:image/svg+xml;base64,${btoa(content)}`;
});
return Object.keys(availableIcons).length;
};
// 获取图标URL
export const getIconUrl = (name: string) => {
if (iconCache.has(name)) return iconCache.get(name);
const url = availableIcons.value[name];
if (url) iconCache.set(name, url);
return url;
};
// 获取原始SVG内容
export const getIconContent = (name: string) => {
return iconContents.value[name] || null;
};
initialize()
// return {
// availableIcons: ref(availableIcons),
// iconContents: ref(iconContents),
// initialize,
// getIconUrl,
// getIconContent
// };
// 初始化图标存储
// export function useIconStore() {
// // 使用Vite的import.meta.glob动态导入
// const initialize = async () => {
// const iconModules = import.meta.glob('/src/lib/assets/icons/*.svg', {
// eager: true,
// as: 'raw'
// });
// Object.entries(iconModules).forEach(([path, content]: [string, string]) => {
// const name = path?.split('/')?.pop()?.replace('.svg', '') as string;
// iconContents.value[name] = content;
// availableIcons.value[name] = `data:image/svg+xml;base64,${btoa(content)}`;
// });
// return Object.keys(availableIcons).length;
// };
// // 获取图标URL
// const getIconUrl = (name: string) => {
// if (iconCache.has(name)) return iconCache.get(name);
// const url = availableIcons.value[name];
// if (url) iconCache.set(name, url);
// return url;
// };
// // 获取原始SVG内容
// const getIconContent = (name: string) => {
// return iconContents.value[name] || null;
// };
// return {
// availableIcons: ref(availableIcons),
// iconContents: ref(iconContents),
// initialize,
// getIconUrl,
// getIconContent
// };
// }

View File

@ -0,0 +1,73 @@
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "CommonIcon",
});
</script>
<script lang="ts" setup>
import * as Icons from "@ant-design/icons-vue";
import SvgIcon from "./components/SvgIcon.vue";
// const { name, color } = defineProps(['name', 'color'])
interface propsType {
name?: string;
color?: string;
}
let props = withDefaults(defineProps<propsType>(), {
name: "",
color: "inherit",
});
const keys = Object.keys(Icons);
const rendIcon = (name: string) => {
const isDefault = keys.find((item) => {
return item == name;
});
return isDefault;
};
</script>
<template>
<!-- {{ props.color }} -->
<div
v-if="rendIcon(props.name)"
class="_global-icon"
:style="{
'--color': props.color ? props.color : 'inherit',
}"
>
<component :is="props.name"></component>
</div>
<div
v-else
class="_global-icon"
:style="{
'--color': props.color ? props.color : 'inherit',
}"
>
<svg-icon :name="props.name" v-if="props.name" :color="props.color" class="_global-svg-icon" />
</div>
</template>
<style lang="scss" scoped>
._global-icon {
color: var(--color);
display: flex;
align-items: center;
justify-content: center;
._global-svg-icon {
display: flex;
align-items: center;
justify-content: center;
color: inherit;
font-size: inherit;
use {
fill: currentColor;
}
}
}
</style>

View File

@ -0,0 +1,171 @@
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "ModalHeader",
});
</script>
<script setup lang="ts">
import { reactive, defineEmits, withDefaults } from "vue";
import { useRoute } from "vue-router";
import Icon from "@/components/Module/Icon/index.vue";
const emits = defineEmits(["onSubmit", "onSubmitAndCreate", "onCancel"]);
interface propsType {
addPermission?: string[] | undefined; //
showIcon?: boolean;
title?: string | null | undefined;
showSubmit?: boolean;
showSubmitAndCreate?: boolean;
loading?: boolean;
// icon
}
const props = withDefaults(defineProps<propsType>(), {
//
addPermission: undefined, //
showSubmit: true,
showSubmitAndCreate: true,
loading: false,
});
//
const route = useRoute();
const pageInfo = reactive({
name: route.meta.title,
icon: route.meta.icon,
});
const typeObj: ExtraObj = {
add: "新增",
edit: "编辑",
};
console.log("typeObj", typeObj, props.type);
const onSubmit = () => {
emits("onSubmit");
};
const onSubmitAndCreate = () => {
emits("onSubmitAndCreate");
};
const onCancel = () => {
emits("onCancel");
};
</script>
<template>
<a-page-header class="_modal-header">
<template #title>
<div class="page-header-title-bar">
<slot name="title">
<slot name="title-start-slot"></slot>
<div class="page-header-title">
<!-- <div class="icon" v-if="showIcon">
<Icon :name="route.meta.icon" />
</div> -->
<div class="type">
{{ typeObj[$attrs.type] || "" }}
</div>
<div class="title">{{ props.title || pageInfo.name }}</div>
</div>
<slot name="title-end-slot"></slot>
</slot>
</div>
</template>
<template #extra>
<!-- 前插槽 -->
<slot name="btn-start-slot"></slot>
<a-button
type="primary"
class="main-color"
@click="onSubmit"
v-if="showSubmit"
:loading="props.loading"
>提交</a-button
>
<a-button
type="primary"
class="main-color"
@click="onSubmitAndCreate"
:loading="props.loading"
v-if="showSubmitAndCreate"
>提交并继续新建</a-button
>
<a-button @click="onCancel">{{ $attrs.cancelText || "取消" }}</a-button>
<!-- 后插槽 -->
<slot name="btn-end-slot"></slot>
</template>
</a-page-header>
</template>
<style lang="scss" scoped>
._modal-header {
padding: 0;
height: 54px;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid rgba(229, 233, 242, 1);
.page-header-title-bar {
display: flex;
align-items: center;
padding-left: 28px;
position: relative;
&::after {
content: " ";
position: absolute;
left: 0px;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 32px;
// background: $primary-color-1;
}
.page-header-title {
display: flex;
align-items: center;
.type {
margin-right: 4px;
@include module-title-font;
}
.title {
@include module-title-font;
}
}
}
:deep(.ant-page-header-heading-extra) {
padding-left: 24px;
}
}
.ant-page-header {
// background-color: red !important;
// background-color: red;
:deep(.ant-page-header-heading) {
height: 100%;
width: 100%;
padding-right: 24px;
box-sizing: border-box;
margin: 0px;
}
:deep(.ant-page-header-extra) {
margin: 0px;
display: flex;
align-items: center;
}
}
:deep(.ant-btn) {
padding: 7px 16px;
height: 38px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
</style>

View File

@ -0,0 +1,187 @@
<script lang="ts">
import { defineComponent, defineAsyncComponent } from "vue";
export default defineComponent({
name: "CommonModal",
});
</script>
<script lang="ts" setup>
import { ref, watch } from "vue";
import Icon from "@/components/Module/Icon/index.vue";
const emits = defineEmits(["update:open"]);
const asyncContent = defineAsyncComponent({
loader: () => import("@/components/Module/AsyncContent/index.vue"),
delay: 2000,
});
interface propsType {
open: boolean;
title?: string | null;
zIndex?: number | string;
loading?: boolean;
containDom?: any; // dom
showConfirm?: boolean;
showCancel?: boolean;
confirmText?: string;
cancelText?: string;
onConfirm?: null | (() => void | Promise<void>);
onCancel?: null | (() => void | Promise<void>);
}
let props = withDefaults(defineProps<propsType>(), {
//
open: false,
title: null,
zIndex: 1000,
loading: false,
confirmText: "确定",
cancelText: "取消",
onConfirm: null,
onCancel: null,
showConfirm: true,
showCancel: true,
});
const useOpen = ref<boolean>(props.open);
const onConfirmHandle = () => {
if (props.onConfirm) {
props.onConfirm();
} else {
emits("update:open", false);
useOpen.value = false;
}
};
const onCancel = () => {
if (props.onCancel) {
props.onCancel();
} else {
emits("update:open", false);
useOpen.value = false;
}
};
watch(
() => props.open,
(val) => {
useOpen.value = val;
},
);
let globalModalRef = ref<HTMLElement>();
const prefix = "_global-modal";
</script>
<template>
<div ref="globalModalRef" :class="prefix">
<a-modal
:zIndex="props.zIndex"
v-bind="$attrs"
:getContainer="() => props.containDom || globalModalRef"
:closable="false"
v-model:open="useOpen"
:footer="null"
@cancel="onCancel"
>
<slot name="header">
<div :class="`${prefix}-head`">
<div :class="`${prefix}-head-label`">
{{ props.title }}
</div>
<button :class="`${prefix}-head-close`" @click="onCancel">
<Icon name="close" />
</button>
</div>
</slot>
<Suspense>
<asyncContent>
<a-spin :spinning="props.loading">
<div :class="`${prefix}-container`">
<div class="modal-content-center">
<asyncContent>
<slot></slot>
</asyncContent>
</div>
</div>
</a-spin>
<slot name="footer">
<div :class="`${prefix}-footer`">
<div :class="`${prefix}-footer__grid`" v-if="props.showCancel">
<a-button @click="onCancel">{{ cancelText }}</a-button>
</div>
<div :class="`${prefix}-footer__grid`" v-if="props.showConfirm">
<a-button type="primary" @click="onConfirmHandle" :loading="props.loading">{{
confirmText
}}</a-button>
</div>
</div>
</slot>
</asyncContent>
<template #fallback>
<div class="modal-content load-box">
<a-spin />
</div>
</template>
</Suspense>
</a-modal>
</div>
</template>
<style lang="scss" scoped>
$prefix: "_global-modal";
.#{$prefix} {
&-head {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-radius: 16px 16px 0 0;
background-color: #d9d9d9;
margin-bottom: 20px;
&-close {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 500;
color: #333;
}
}
&-container {
padding: 0 16px;
width: 100%;
flex: 1;
align-self: stretch;
}
&-footer {
padding: 12px 16px;
width: 100%;
display: flex;
align-items: center;
justify-content: end;
gap: 20px;
}
:deep(.ant-modal-content) {
padding: 0;
.ant-modal-body {
padding: 0;
display: flex;
align-items: start;
justify-content: start;
flex-direction: column;
}
}
:deep(.ant-spin-nested-loading) {
width: 100%;
}
}
</style>

View File

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

8
src/config/index.ts Normal file
View File

@ -0,0 +1,8 @@
// import logo from '@/assets/images/logo/logo.png'
// import authBg from '@/assets/images/auth/auth-bg.jpg'
export const info = {
logo: '',
title: "",
bgImg: ''
}

115
src/core/lazy_use.ts Normal file
View File

@ -0,0 +1,115 @@
import type { App } from 'vue'
import {
App as antApp,
Button,
Tree,
ConfigProvider,
Layout,
Menu,
PageHeader,
Dropdown,
Drawer,
Space,
Switch,
Input,
Form,
Popover,
Row,
Col,
Select,
Checkbox,
Modal,
Tag,
AutoComplete,
Upload,
Radio,
Tabs,
Timeline,
Carousel,
Spin,
List,
Tooltip,
Card,
DatePicker,
Divider,
message,
InputNumber,
TreeSelect,
Steps,
Pagination,
Popconfirm,
Cascader,
Badge,
Empty,
Progress,
Affix,
Descriptions,
Slider,
Table
} from 'ant-design-vue'
import { router } from '@/router/index.ts'
const [ messageApi, contextHolder] = message.useMessage();
// 注册全局组件
export function loadComponent(app: App<Element>) {
app
.use(ConfigProvider)
.use(Layout)
.use(Checkbox)
.use(Menu)
.use(Button)
.use(Tree)
.use(antApp)
.use(PageHeader)
.use(Dropdown)
.use(Drawer)
.use(Space)
.use(Switch)
.use(Input)
.use(Form)
.use(Popover)
.use(Row)
.use(Col)
.use(Select)
.use(Table)
.use(Modal)
.use(Tag)
.use(AutoComplete)
.use(Upload)
.use(Radio)
.use(Tabs)
.use(Timeline)
.use(Carousel)
.use(Spin)
.use(List)
.use(Tooltip)
.use(Card)
.use(DatePicker)
.use(Divider)
.use(List)
.use(InputNumber)
.use(TreeSelect)
// .use(hevueImgPreview)
.use(Steps)
.use(Pagination)
.use(Popconfirm)
.use(Cascader)
.use(Badge)
.use(Empty)
.use(Progress)
.use(Affix)
.use(Descriptions)
.use(Slider)
}
// 注册全局方法
export function loadFunc (app: App<Element>) {
}

8
src/directive/index.ts Normal file
View File

@ -0,0 +1,8 @@
import type { App } from 'vue'
function directive(app: App<Element>) {
// 注册指令
}
export default directive;

41
src/main.ts Normal file
View File

@ -0,0 +1,41 @@
// import './assets/main.css'
import '@/styles/index.css'
import 'ant-design-vue/dist/reset.css'
import { createApp } from 'vue'
import { setupStore } from '@/stores'
import { router, setupRouter } from '@/router'
import { setRouteGuard } from '@/router/guard/index'
import { loadComponent, loadFunc } from '@/core/lazy_use'
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import 'swiper/css/effect-fade';
import 'swiper/css/autoplay';
import * as Icons from '@ant-design/icons-vue'
import App from './App.vue'
// import router from './router'
const app = createApp(App)
loadComponent(app)
loadFunc(app)
setupStore(app)
setupRouter(app)
setRouteGuard(router)
const icons:any = Icons
for (const i in icons) {
app.component(i, icons[i])
}
app.mount('#app')

29
src/router/base/index.ts Normal file
View File

@ -0,0 +1,29 @@
import type { AppRouter } from '@/router/config/index'
export const baseRouter: AppRouter= {
name: 'Base',
path: '',
meta: {
title: '首页',
hidden: false,
hiddenInMenu: true,
},
component: 'BaseLayout',
children: []
}
export const globalRouter = [
{
name: 'LoginView',
path: '/login',
redirect: '/login/index',
icon: 'HomeOutlined',
hidden: false,
meta: {
icon: 'HomeOutlined',
title: '用户登录',
},
component: () => import('@/components/Base/Layout/LoginLayout/index.vue'),
children: []
},
]

View File

@ -0,0 +1,61 @@
import type {
RouteRecordRaw,
RouteMeta
} from 'vue-router'
import type { defineComponent } from 'vue'
export type Component<T = any> = ReturnType<typeof defineComponent> | (() => Promise<typeof import('*.vue')>) | (() => Promise<T>)
// @ts-expect-error
export interface AppRouter extends Omit<RouteRecordRaw, 'meta'> {
keepAlive?: boolean
visible?: boolean
icon?: string
name?: string
key?: string
sort?: number
parent?: AppRouter | null
parentId?: number | string
menuId?: number | string
meta: RouteMeta
component?: Component | string
components?: Component
componentName?: string
children?: AppRouter[]
props?: Recordable,
path?: string,
fullPath?: string,
hidden?: boolean,
type?: string | null,
typeName?: string | null,
menuName?: string | null
menuType?: string | null
}
// export interface AppRouter {
// keepAlive?: boolean, // 缓存
// component?: RawRouteComponent | string | null | undefined | any, // 组件
// children?: AppRouter[], // 子路由
// path?: string, // 路径
// redirect?: string, // 重定向
// isFrame?: number, // 外链
// target?: string // 外链打开方式 新标签打开还是当前页面打开
// key?: string,
// hidden: boolean, // 是否隐藏
// title?: string // 标题
// // visible?: boolean //
// // icon?: string // 图标
// name: string // 路由name
// sort?: number // 排序
// parentId?: number // 父节点
// // component?: Component | string
// // components?: Component
// // componentName?: string //
// hideChildrenInMenu?: boolean, // 隐藏下级节点
// meta?: RouteMeta, // 信息
// // props?: Recordable
// // fullPath?: string // 全路径
// }

70
src/router/func/index.ts Normal file
View File

@ -0,0 +1,70 @@
import { layouts, staticRouters } from "@/router/modules/routerComponents"
import type { AppRouter } from '@/router/config/index'
import { validURL } from "@/utils/valid"
import type { Component } from "vue"
import { uuid } from '@/utils/util'
const constantRouterComponents: Component = {
...layouts,
...staticRouters
}
const menuTypeFormat = (type?: string) => {
if (!type) return null
const menuTypeObj: ExtraObj = {
'G': 'group', // 分组
'F': 'button', // 按钮
'C': 'menu', // 菜单
'M': 'catalogue', // 目录
}
return menuTypeObj[type] || null
}
// 构造路由
export const generator = async (routerMap: AppRouter[] | ExtraObj[], parent:AppRouter | null): Promise<AppRouter[]> => {
const newRouter = await Promise.all(routerMap.map(async (item: AppRouter | ExtraObj) => {
const { title, hideChildren, hiddenHeaderContent, hidden, icon, isFrame, target } = item.meta || {}
const currentRouter: AppRouter = {
path: item.path,
key: item.key,
fullPath: (parent ? `${parent?.fullPath ?? ''}${item.path?.includes('/') ? item.path : '/' + item.path}` : `${item.path}`),
// 路由名称,建议唯一
name: item.menuId || item.name || item.key || uuid(),
// 该路由对应页面的 组件(动态加载)
component: constantRouterComponents[item.component] || (() => import(`@/views/${item.component}`)),
// hideChildrenInMenu: item.hideChildrenInMenu,
// meta: 页面标题, 菜单图标, 页面权限(供指令权限用,可去掉)
hidden: item.hidden,
parent: parent,
parentId: item.parentId ?? parent?.name,
icon: item?.meta?.icon || item.icon as string | undefined,
meta: {
icon: item?.meta?.icon || item.icon,
isFrame: isFrame,
title: title || item.menuName,
hideChildrenInMenu: hideChildren,
hiddenHeaderContent: hiddenHeaderContent,
// 目前只能通过判断path的http链接来判断是否外链
target: (validURL(item.path) ? '_blank' : ''),
permission: item.name,
hidden: hidden
},
type: item.type || menuTypeFormat(item.menuType),
typeName: item.typeName || null,
redirect: item.redirect
}
if (item.component && !constantRouterComponents[item.component]) {
currentRouter.path = `${parent?.path ?? ''}/${item.path}`
}
// 是否设置了隐藏子菜单
// 是否有子菜单并递归处理并将父path传入
if (item.children && item.children.length > 0) {
// Recursion
currentRouter.children = await generator(item.children, currentRouter)
}
return currentRouter
}))
return newRouter
}

View File

@ -0,0 +1,20 @@
import { useSocketStoreWithOut } from '@/stores/modules/socket'
const socketStore = useSocketStoreWithOut()
// 初始化socket
export const initSocket = () => {
if (!socketStore.socket) {
socketStore.initSocket()
}
}
// 初始化
export const init = async () => {
return Promise.all([
await initSocket(),
])
}

90
src/router/guard/index.ts Normal file
View File

@ -0,0 +1,90 @@
import type { Router, RouteRecordRaw, RouteRecordName } from 'vue-router'
import { useUserStore } from '@/stores/modules/user'
import { useRouterStoreWithOut } from '@/stores/modules/async-router'
import type { AppRouter } from '@/router/config/index'
import { APP_VERSION } from '@/stores/modules/mutation-types'
import { info } from '@/config/index.ts'
import { init as beforeInit } from '@/router/guard/before.ts'
const allowName = ['Login', 'LoginView', '404']
const defaultRouter = '/'
export function _window(key: string) {
if (window.hasOwnProperty('admin_config')) {
//@ts-ignore
return window.admin_config[key]
}
return null
}
// 版本校验
const checkVersion = () => {
}
export const setRouteGuard = (router:Router) => {
router.beforeEach(async (to, from, next) => {
window.document.title = `${to.meta.title as string} | ${info.title}`
console.log('from', from)
// beforeInit()
// 校验版本
// checkVersion()
const userStore = useUserStore()
const token = userStore.getToken()
console.log('getToken', token)
// token失效走退出
// if (token && !userStore.validToken()) {
// // userStore.logout()
// }
// 无token
// if(!token || token === '') {
// console.log(to.name && allowName.includes(to.name as string))
// if (to.name && allowName.includes(to.name as string)) {
// next()
// } else {
// // next({ path: '/login/index', query: { redirect: to.fullPath } })
// next()
// }
// } else {
console.log('toROuter', to)
const routerStore = useRouterStoreWithOut()
const nowRouter = routerStore.ROUTERS
// 无角色, 重新初始化路由
// if (!userStore.permissions?.length || (userStore.permissions?.length && !nowRouter.length))
// if (!(userStore.permissions?.length && nowRouter.length)){
// 结构化路由
if (!(nowRouter.length)){
await routerStore.loadRouter().then(res => {
const addRoters:AppRouter[] = res
console.log('addRoters', addRoters)
// routerStore.setRouter(addRoters)
addRoters.map((route)=> {
router.addRoute(route as RouteRecordRaw)
})
})
next({ path: to.fullPath, replace: true, query: to.query })
} else {
// const userInfo = userStore.USERINFO ? JSON.parse(userStore.USERINFO) : {}
// console.log('userInfo', userInfo)
// if ((token || token !== '') && !userInfo.sysUserId) {
// userStore.getUserInfo()
// }
next()
// if (to.path == '/') {
// next({ path: defaultRouter, replace: true, query: to.query })
// } else {
// next()
// }
}
// }
})
}

17
src/router/index.ts Normal file
View File

@ -0,0 +1,17 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import { globalRouter } from './base/index'
import type { App } from 'vue'
export const router = createRouter({
history: createWebHashHistory(),
routes: [
...globalRouter
],
})
export function setupRouter(app: App<Element>) {
app.use(router)
}

View File

@ -0,0 +1,22 @@
import { defineAsyncComponent, markRaw } from 'vue'
export const layouts = {
BaseLayout: () => import('@/components/Base/Layout/SystemLayout/index.vue'),
ViewLayout: () => import('@/components/Base/Layout/ViewLayout/index.vue'),
LoginLayout: () => import('@/components/Base/Layout/LoginLayout/index.vue'),
}
export const staticRouters = {
// Index: () => import('@/views/index/index.vue'),
// Test: () => import('@/views/test/index.vue'),
NotFound: defineAsyncComponent(() => import('@/views/exception/404.vue')),
KnowledgeChart: defineAsyncComponent(() => import('@/views/knowledge/index.vue')),
// DataManage: defineAsyncComponent(() => import('@/views/dataManage/index/index.vue')),
// ModelReasoning: defineAsyncComponent(() => import('@/views/modelReasoning/index/index.vue')),
// ModelTraining: defineAsyncComponent(() => import('@/views/modelTraining/index/index.vue')),
// ModelTrainingDetail: defineAsyncComponent(() => import('@/views/modelTraining/detail/index.vue')),
// ModelManage: defineAsyncComponent(() => import('@/views/setting/model/index/index.vue')),
}

View File

@ -0,0 +1,54 @@
export const staticRouter = [
{
path: '/',
meta: {
hidden: true,
},
redirect: '/knowledge-chart'
},
]
export const exceptionRouter = [
{
path: '/404',
name: '404',
component: 'NotFound',
meta: {
icon: '',
title: '',
hidden: true,
},
},
{
path: '/test',
name: 'test',
component: 'Test',
meta: {
icon: '',
title: '',
hidden: true,
},
},
{
name: 'KnowledgeChart',
path: '/knowledge-chart',
meta: {
icon: 'data',
title: '知识图谱',
hidden: false,
},
component: 'KnowledgeChart',
},
{
path: '/:pathMatch(.*)',
redirect: '/404',
hidden: true,
meta: {
icon: '',
title: '',
hidden: true,
},
}
]

12
src/stores/counter.ts Normal file
View File

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

10
src/stores/index.ts Normal file
View File

@ -0,0 +1,10 @@
import type { App } from 'vue'
import { createPinia } from 'pinia'
const store = createPinia()
export function setupStore(app: App<Element>) {
app.use(store)
}
export { store }

View File

@ -0,0 +1,54 @@
import { defineStore } from "pinia"
import { store } from '@/stores'
import { staticRouter, exceptionRouter } from '@/router/modules/staticRouter'
import type { AppRouter } from '@/router/config/index'
import { generator } from '@/router/func/index'
import { baseRouter } from '@/router/base/index'
import { handleTree } from "@/utils/util"
import { Row } from "ant-design-vue"
interface router {
routerArr: AppRouter[]
asyncRouter: ExtraObj[]
}
export const useRouterStore = defineStore('router', {
state: (): router => ({
routerArr: [],
asyncRouter: []
}),
getters: {
ROUTERS(state):AppRouter[] {
return state.routerArr
},
ASYNCROUTERS(state): ExtraObj[] {
return state.asyncRouter
},
},
actions: {
setRouter(data: AppRouter[] ) {
this.routerArr = data
},
async loadRouter(): Promise<AppRouter[]> {
let routers:AppRouter[] = []
return new Promise(async (resolve) => {
// 组装
const menuNav: ExtraObj[] = []
baseRouter.children = staticRouter.concat() as AppRouter[]
menuNav.push(baseRouter)
menuNav.push(...exceptionRouter)
// menuNav.push(...globalRouter)
routers = await generator(menuNav, null)
console.log('routers', routers)
this.routerArr = routers
resolve(routers)
})
}
},
})
export const useRouterStoreWithOut = () => {
return useRouterStore(store)
}

View File

@ -0,0 +1,6 @@
export const ACCESS_TOKEN = 'token'
export const ACCESS_EXPIRES = 'expires'
export const APP_VERSION = 'app_version'

View File

@ -0,0 +1,78 @@
import { defineStore, acceptHMRUpdate } from "pinia"
import { store } from '@/stores'
import { socketInit } from '@/utils/socket/index.ts'
import type { Socket } from "socket.io-client"
interface UserState {
socket: Socket | null,
connected:any,
reconnectAttempts: number,
}
export const useSocketStore = defineStore('socket', {
state: (): UserState => ({
socket: null,
connected: false,
reconnectAttempts: 0,
}),
getters: {
SOCKET(state) {
return state.socket
},
isConnected(state) {
return state.connected
},
attempts(state) {
return state.reconnectAttempts
}
},
actions: {
// 获取token
initSocket() {
console.log('?initSocket')
const socket = socketInit(true)
this.$patch((state) => {
state.socket = socket
})
// ===== 事件监听 =====
socket.on('connect', () => {
this.connected = true
console.log('[Socket] 已连接socket id:', socket.id)
})
socket.on('disconnect', (reason) => {
this.connected = false
console.log(`[Socket] 已断开,原因: ${reason}`)
})
// 重连中,每次尝试都会触发
socket.io.on('reconnect_attempt', (attempt: number) => {
this.reconnectAttempts = attempt
console.log(`[Socket] 重连尝试 #${attempt}`)
})
// 重连成功
socket.io.on('reconnect', (attempt: number) => {
console.log(`[Socket] 重连成功,尝试次数: ${attempt}`)
})
// 重连失败
socket.io.on('reconnect_failed', () => {
console.warn('[Socket] 重连失败,放弃继续重连')
})
},
// 清除仓库数据
cleanStore() {
this.$reset()
}
},
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useSocketStore, import.meta.hot))
}
export const useSocketStoreWithOut = () => {
return useSocketStore(store)
}

View File

@ -0,0 +1,22 @@
import { defineStore } from "pinia"
import { store } from '@/stores'
interface info {
}
export const systemStore = defineStore('system', {
state: (): info => ({
}),
getters: {
},
actions: {
},
})
export const systemStoreWithOut = () => {
return systemStore(store)
}

103
src/stores/modules/user.ts Normal file
View File

@ -0,0 +1,103 @@
import { defineStore, acceptHMRUpdate } from "pinia"
import { store } from '@/stores'
import { ACCESS_TOKEN, ACCESS_EXPIRES, USER_INFO } from '@/stores/modules/mutation-types'
// import { loginApi } from '@/apis/auths/index'
import { notification } from 'ant-design-vue';
import { useRouterStoreWithOut } from './async-router'
interface LoginForm {
email: string;
password: string;
}
interface UserState {
token: string | null
role: string | null
permissions: {
[key: string]: ExtraObj
},
userInfo?: ExtraObj,
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
token: '',
role: null,
permissions: {},
userInfo: {},
}),
getters: {
ROLE(state) {
return state.role
},
TOKEN(): string {
return this.getToken()
},
USERINFO(state) {
return state.userInfo
},
},
actions: {
// 获取token
getToken() {
const raw = localStorage.getItem(ACCESS_TOKEN) || ''
return raw
},
// 设置token
setToken(token: string) {
console.log('ACCESS_TOKEN', ACCESS_TOKEN)
localStorage.setItem(ACCESS_TOKEN, token || '')
},
// 设置用户数据
setUserInfo(data: ExtraObj | null) {
this.$patch((state) => {
if (data) {
state.userInfo = data
state.permissions = data.permissions
state.role = data.role
} else {
state.userInfo = {}
state.permissions = {}
state.role = null
state.userInfo = {}
}
})
},
// 登录
async login(data: LoginForm) {
// let res: ExtraObj
// res = await loginApi(data)
// .catch((err) => {
// notification.error({
// message: '登录失败',
// description: err.message,
// })
// return err
// })
// if (res.code == 200) {
// this.setToken(res.data.token)
// this.setUserInfo(res.data)
// this.getUserInfo()
// }
// return res
},
// 获取用户信息
async getUserInfo() {
},
// 退出
logout() {
},
},
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot))
}
export const useUserStoreWithOut = () => {
return useUserStore(store)
}

30
src/styles/index.css Normal file
View File

@ -0,0 +1,30 @@
@charset "UTF-8";
#app {
padding: 0;
margin: 0;
max-width: 100%;
width: 100%;
height: 100vh;
}
p {
margin-top: 0;
margin-bottom: 0;
}
button {
margin: 0;
/* 清除外边距 */
padding: 0;
/* 清除内边距 */
border: none;
/* 去除边框 */
background: none;
/* 清除背景色 */
font: inherit;
/* 继承父元素字体 */
color: inherit;
/* 继承父元素文字颜色 */
cursor: pointer;
/* 保持指针样式 */
}

34
src/styles/index.scss Normal file
View File

@ -0,0 +1,34 @@
html,
body,
p {
}
#app {
padding: 0;
margin: 0;
max-width: 100%;
width: 100%;
height: 100vh;
}
p {
margin-top: 0;
margin-bottom: 0;
}
a,
a:hover,
a:visited,
a:link,
a:active {
}
button {
margin: 0; /* 清除外边距 */
padding: 0; /* 清除内边距 */
border: none; /* 去除边框 */
background: none; /* 清除背景色 */
font: inherit; /* 继承父元素字体 */
color: inherit; /* 继承父元素文字颜色 */
cursor: pointer; /* 保持指针样式 */
}

View File

@ -0,0 +1,30 @@
{
"token": {
"fontSize": 16,
"wireframe": false,
"fontSizeXL": 20,
"fontSizeHeading1": 36,
"fontSizeHeading2": 20,
"fontSizeHeading3": 16,
"fontSizeHeading4": 14,
"fontSizeHeading5": 12,
"lineHeight": 1.5,
"sizeStep": 4,
"borderRadiusLG": 16
},
"components": {
"Dropdown": {
"borderRadiusLG": 6,
"fontSize": 14,
"fontSizeSM": 12,
"lineWidthBold": 2
},
"Menu": {
"zIndexPopup": 1050,
"dropdownWidth": 260,
"radiusItem": 6
},
"Pagination": {},
"Cascader": {}
}
}

37
src/types/global.d.ts vendored Normal file
View File

@ -0,0 +1,37 @@
import type { DefineComponent } from 'vue'
declare global {
type ExtraObj = {
[key?: string]: any
}
type Recordable<T = any> = Record<string, T>
declare module '*.csv' {
const value: any;
export default value;
}
}
declare module '*.jpg' {
const value: any;
export default value;
}
declare module '*.png' {
const value: any;
export default value;
}
declare module '*.vue' {
const component: DefineComponent<{}, {}, any>
export default component
}
declare module '@components/*';

37
src/types/module.d.ts vendored Normal file
View File

@ -0,0 +1,37 @@
declare module '*.vue' {
import { DefineComponent } from 'vue'
const Component: DefineComponent<{}, {}, any>
export default Component
}
// declare module 'ant-design-vue/es/locale/*' {
// import { Locale } from 'ant-design-vue/types/locale-provider'
// const locale: Locale & ReadonlyRecordable
// export default locale as Locale & ReadonlyRecordable
// }
declare module 'virtual:*' {
const result: any
export default result
}
declare module 'lodash' {
const content: any
export = content
}
declare module '*.ts' {
const content: any
export = content
}
declare module 'hevue-img-preview' {
const content: any
export = content
}
declare module 'fabric-with-erasing' {
const content: any
export = content
}
declare module '@ant-design/icons-vue';

View File

187
src/utils/download/index.ts Normal file
View File

@ -0,0 +1,187 @@
// import { getEnv } from '@/lib/utils/env/index.ts'
import { request } from '@/utils/request/index.ts';
// 创建一个a标签并做点击下载事件
export function downloadFile(hrefUrl:any, fileName:any){
console.log('downloadFile',hrefUrl,fileName);
const a = document.createElement('a')
a.href = hrefUrl
a.target = '_blank' // 新窗口打开 20250320 yjc 新增
a.download = fileName // 下载后文件名
document.body.appendChild(a)
a.click() // 点击下载
document.body.removeChild(a) // 下载完成移除元素
}
// 封装blob对象
function dataURLToBlob(base64Str:any, mimeTypeStr:any) {
const bstr = window.atob(base64Str); // 解码 base-64 编码的字符串base-64 编码使用方法是 btoa()
let length = bstr.length;
const u8arr = new Uint8Array(length); // 创建初始化为0的包含length个元素的无符号整型数组
while (length--) {
u8arr[length] = bstr.charCodeAt(length); // 返回在指定的位置的字符的 Unicode 编码
}
return new Blob([u8arr], { type: mimeTypeStr }); // 返回一个blob对象
}
// 后端返回base64公共导出
export function downloadFileByBase64(base64Str:any, mimeTypeStr:any, fileName:any){
const myBlob = dataURLToBlob(base64Str, mimeTypeStr)
const myUrl = window.URL.createObjectURL(myBlob)
downloadFile(myUrl, fileName)
}
// 后端返回文件流公共导出
export function downloadFileByFileFlow(blobData:any, mimeTypeStr:any, fileName:any) {
const blob = new Blob([blobData], { type: mimeTypeStr })
const hrefUrl = window.URL.createObjectURL(blob) // 创建下载的链接
downloadFile(hrefUrl, fileName);
}
//将base64转换为文件
export function base64Tofile(dataurl:any,fileName:any){
console.log(dataurl,fileName);
// debugger;
let arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
const blob =new Blob([u8arr], { type: mime });
// @ts-ignore
blob.lastModifiedDate = new Date();
// @ts-ignore
blob.name = fileName;
console.log(blob,'blob');
download(blob)
// window.location.replace(blobUrl)
return blob;
}
export function download(downfile:any, fileName?: string) {
const tmpLink = document.createElement("a");
const objectUrl = URL.createObjectURL(downfile);
tmpLink.href = objectUrl;
tmpLink.target = '_blank' // 新窗口打开 20250320 yjc 新增
tmpLink.download = fileName || downfile.name;
document.body.appendChild(tmpLink);
tmpLink.click();
document.body.removeChild(tmpLink);
URL.revokeObjectURL(objectUrl);
}
// 生成唯一的uuid
export function getUUID(randomLength:any) {
function S4() {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
}
return ("p"+S4() + S4() + S4() + S4() + S4() + S4() + S4() + S4())
}
export function downloadWav(base64Data:any,name:any) {
// const tmpLink = document.createElement("a");
// const objectUrl = URL.createObjectURL(downfile);
// tmpLink.href = objectUrl;
// tmpLink.download = downfile.name;
// document.body.appendChild(tmpLink);
// tmpLink.click();
// document.body.removeChild(tmpLink);
// URL.revokeObjectURL(objectUrl);
const downloadLink = document.createElement('a');
downloadLink.href = base64Data; // 这里的 base64Data 是之前转换得到的 Base64 字符串
downloadLink.download = name; // 设置下载文件的名称
downloadLink.click(); // 模拟点击下载链接
}
export function saveDatatoJson(data:any, filename:any) {
if (!data) {
console.error('Console.save: No data')
return;
}
if (!filename) filename = 'console.json'
if (typeof data === "object") {
data = JSON.stringify(data, undefined, 4)
}
const blob = new Blob([data], {
type: 'text/json'
}),
e = document.createEvent('MouseEvents'),
a = document.createElement('a')
a.download = filename
a.href = window.URL.createObjectURL(blob)
a.dataset.downloadurl = ['text/json', a.download, a.href].join(':')
e.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null)
a.dispatchEvent(e)
}
// file 文件转 base64
export function fileToBase64(file:any) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
resolve(reader.result);
};
reader.onerror = (error) => {
reject(error);
};
});
}
// file 文件转换成 字节流
export function fileToBytes(file:any) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = () => {
resolve(reader.result);
};
reader.onerror = (error) => {
reject(error);
};
});
}
// base64转文件
export function base64ToFile(base64Data: string, fileName: string, mimeType?: string): File {
// 从base64字符串中提取数据部分和MIME类型
let dataType = '';
let base64Content = '';
if (base64Data.includes(';base64,')) {
const parts = base64Data.split(';base64,');
dataType = parts[0].split(':')[1];
base64Content = parts[1];
} else {
// 如果base64字符串不包含MIME信息则使用传入的mimeType
dataType = mimeType || 'application/octet-stream';
base64Content = base64Data;
}
// 解码base64
const byteCharacters = atob(base64Content);
const byteArrays = [];
// 将字符串转换为字节数组
for (let i = 0; i < byteCharacters.length; i++) {
byteArrays.push(byteCharacters.charCodeAt(i));
}
// 创建Uint8Array
const uint8Array = new Uint8Array(byteArrays);
// 创建Blob对象
const blob = new Blob([uint8Array], { type: dataType });
// 创建File对象
return new File([blob], fileName, { type: dataType, lastModified: new Date().getTime() });
}

106
src/utils/request/index.ts Normal file
View File

@ -0,0 +1,106 @@
import { Config, RequestHead, ResponseData, ResponseError, RequestParams } from '@/utils/request/types/index.ts'
import { message } from "ant-design-vue";
// 默认配置
const createDefaultConfig = (): {
headers: RequestHead
} => {
const defaultConfig = {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${localStorage.token}`
},
}
return defaultConfig
}
// 构造数据
const genData = (data: any, dataRaw: boolean) => {
if (dataRaw) {
return data instanceof FormData || data instanceof URLSearchParams
? data
: new URLSearchParams(data);
}
return JSON.stringify(data);
};
// 构造返回
const genResult = (result: ExtraObj, status: 'success' | 'error'): ResponseData | ResponseError => {
if (result.code) {
return {
...result,
code: result.code,
data: result.data
}
} else {
if (status == 'success') {
return {
code: 200,
data: result
}
} else {
let error
if ('detail' in result) {
error = result.detail
} else {
error = result
}
return {
code: 500,
message: error,
_raw: result
}
}
}
}
// 构造请求参数
const createRuestParams = (config: Config) => {
const { method, data, dataRaw, headers, controller } = config;
const requetParams: RequestParams = {
method,
headers: headers as HeadersInit ?? createDefaultConfig().headers,
body: data instanceof URLSearchParams ? data : genData(data, dataRaw || false),
signal: controller?.signal
};
return requetParams
}
// 请求
export const request = async (config: Config): Promise<ResponseData | ResponseError> => {
const { url, base, params, getRaw } = config;
// 路径参数格式化
const urlParams = params ? new URLSearchParams(params) : null
const extra = `${urlParams ? '?' + urlParams.toString() : ''}`
const requestUrl = `${base || import.meta.env.VITE_BASE_API}${url}${extra}`
const requestParams = createRuestParams(config)
// 请求逻辑封装
const res = await fetch(requestUrl, requestParams).then(async (res) => {
// 是否需要返回原始数据
const result = getRaw ? res : await res.json();
if (!res.ok) throw result
if (result?.code && result?.code !== 200) {
throw result
}
return getRaw ? result : genResult(result, 'success')
}).catch((err) => {
if (err.name == 'AbortError') {
return Promise.reject(err)
}
let error
if ('code' in err) {
error = err
} else {
error = genResult(err, 'error')
}
if (error.message) {
message.error(error.message)
}
return Promise.reject(error)
});
return res;
}

View File

@ -0,0 +1,43 @@
export interface Config {
base?: string, // 前缀
url: string // 请求地址
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
data?: string | Record<string, any> // 请求体
dataRaw?: boolean // 请求体是否为原始数据
params?: string | Record<string, any> | URLSearchParams | string[][] // 请求参数
headers?: HeadersInit | undefined // 请求头
controller?: AbortController // 请求控制器
getRaw?: boolean // 是否需要返回原始数据
}
// 请求头
export interface RequestHead {
[key: string]: string
}
// 标准响应
export interface ResponseData<T = any> {
code: number
data: T
[key: string]: any
}
// 错误响应
export interface ResponseError<T = any> {
code: number
message?: string
_raw: T
}
// 请求参数
export interface RequestParams extends RequestInit {
signal?: AbortSignal
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
headers: HeadersInit
body?: BodyInit | null
[key: string]: any
}

19
src/utils/socket/index.ts Normal file
View File

@ -0,0 +1,19 @@
import { io } from 'socket.io-client';
import { Socket } from 'socket.io-client/build/esm/socket'
// 设置socket链接
export const socketInit = (enableWebsocket: boolean) => {
// const socketUrl = import.meta.env.VITE_SOCKET_URL;
const _socket: Socket = io(undefined, {
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
randomizationFactor: 0.5,
path: '/ws/socket.io',
transports: enableWebsocket ? ['websocket'] : ['polling', 'websocket'],
auth: { token: localStorage.token }
});
console.log('_socket', _socket)
return _socket
}

48
src/utils/text/index.ts Normal file
View File

@ -0,0 +1,48 @@
// 复制文本
/**
*
* @param text
* @returns
*/
export const copyToClipboard = async (text: string) => {
let result = false;
if (!navigator.clipboard) {
const textArea = document.createElement('textarea');
textArea.value = text;
// Avoid scrolling to bottom
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.position = 'fixed';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
const msg = successful ? 'successful' : 'unsuccessful';
console.log('Fallback: Copying text command was ' + msg);
result = true;
} catch (err) {
console.error('Fallback: Oops, unable to copy', err);
}
document.body.removeChild(textArea);
return result;
}
result = await navigator.clipboard
.writeText(text)
.then(() => {
console.log('Async: Copying to clipboard was successful!');
return true;
})
.catch((error) => {
console.error('Async: Could not copy text: ', error);
return false;
});
return result;
};

View File

@ -0,0 +1,21 @@
// 文件格式转换
export const sizeFormat = (
number: number | string,
start: number = 0,
max: number = 999,
showUnit: boolean = true,
decimal: number = 2
) => {
const unit = ['b', 'Kb', 'Mb', 'Gb', 'Tb']
let limitUnitIndex = Math.min(max, unit.length - 1)
let unitIndex = start
let useNumber = Number(number)
console.log('useNumber', useNumber)
while (useNumber >= 1024 && unitIndex < limitUnitIndex) {
useNumber /= 1024
unitIndex++
}
console.log('useNumber', useNumber)
return `${Math.ceil(useNumber).toFixed(decimal)}${showUnit ? unit[unitIndex] : ''}`
}

64
src/utils/util.ts Normal file
View File

@ -0,0 +1,64 @@
export function uuid() {
let d = Date.now()
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
d += performance.now() // use high-precision timer if available
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (d + Math.random() * 16) % 16 | 0
d = Math.floor(d / 16)
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
})
}
/**
*
* @param {*} data
* @param {string} id id字段 'id'
* @param {string} parentId 'parentId'
* @param {*} children 'children'
* @param {string | number} rootId Id 0
*/
export function handleTree(data: ExtraObj[], id?: string, parentId?: string, children?: string, rootId?: number | string) {
const reNameId = id || 'id'
const reNameParentId = parentId || 'parentId'
const reNameChildren = children || 'children'
const reNameRootId =
rootId ||
Math.min.apply(
Math,
data.map(item => {
return item[reNameParentId]
})
) ||
0
// 对源数据深度克隆
const cloneData = JSON.parse(JSON.stringify(data))
// 循环所有项
const treeData = cloneData.filter((father: ExtraObj) => {
var branchArr = cloneData.filter((child: ExtraObj) => {
// 返回每一项的子级数组
return father[reNameId] === child[reNameParentId]
})
if (branchArr.length > 0) {
father[reNameChildren] = branchArr
} else {
father[reNameChildren] = ''
}
// 返回第一层
return father[reNameParentId] == reNameRootId
})
return treeData !== '' ? treeData : data
}

23
src/utils/valid.ts Normal file
View File

@ -0,0 +1,23 @@
// 网址校验
export const httpReg = /^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(([A-Za-z0-9-~]+)\.)+([A-Za-z0-9-~/])+$/
export function validURL (url?:string) {
const reg = /^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(([A-Za-z0-9-~]+)\.)+([A-Za-z0-9-~/])+$/
return reg.test(url||'')
}
// 手机校验
export const phoneReg = /^1[3456789]\d{9}$/
export const MobReg = /^((0\d{2,3})-)?(\d{7,8})$/
export function validPhone (phone: string) {
return phoneReg.test(phone) || MobReg.test(phone)
}
// 邮箱校验
export const mailReg = /^[a-zA-Z0-9]+([._\\-]?[a-zA-Z0-9]+)*@[a-zA-Z0-9]+([._\\-]?[a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/
export function validMail (mail: string) {
return mailReg.test(mail)
}

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
// import { defaultDevImg } from '@/config/baseInfo'
</script>
<template>
<div class="dev-box">
<div class="dev-img">
404
<!-- <img :src="defaultDevImg" alt="" class="image" /> -->
</div>
</div>
</template>
<style lang="scss" scoped>
.dev-box {
width: 100%;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
.dev-img {
width: 600px;
height: 400px;
.image {
width: 100%;
height: 100%;
}
}
}
</style>

13
src/views/index/index.vue Normal file
View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import { ref, inject, defineComponent, nextTick, computed, onUnmounted, onMounted } from "vue";
onMounted(() => {});
onUnmounted(() => {});
</script>
<template>
<div class=""></div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,88 @@
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "Chat",
});
</script>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, markRaw } from "vue";
import * as echarts from "echarts";
const emits = defineEmits(["init"]);
interface PropsType {
options: Record<string, any>;
}
const props = withDefaults(defineProps<PropsType>(), {
options: () => ({}),
});
//
let containerRef = ref();
// echat
let chartRef: echarts.ECharts | null = null;
//
const resize = () => {
chartRef?.resize();
};
//
const addListen = () => {
window.addEventListener("resize", resize);
};
//
const removeListen = () => {
window.removeEventListener("resize", resize);
};
//
const initChart = () => {
chartRef?.clear();
chartRef?.setOption(props.options);
};
//
const init = () => {
chartRef = markRaw(echarts.init(containerRef.value));
emits("init", chartRef);
initChart();
addListen();
};
watch(
() => props.options,
() => {
initChart();
},
);
onMounted(() => {
init();
});
onUnmounted(() => {
removeListen();
chartRef?.dispose();
});
const prefix = "circle-chat";
</script>
<template>
<div :class="prefix">
<div ref="containerRef" style="width: 100%; height: 100%"></div>
</div>
</template>
<style lang="scss" scoped>
$prefix: "circle-chat";
.#{$prefix} {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,342 @@
import { ref } from "vue";
// 关系图逻辑
export const useChartHooks = () => {
const option = ref({
title: {
text: "",
},
tooltip: {
formatter: (params: Record<string, any>) => {
// params.data包含节点的数据信息
if (params.data && params.data.raw) {
// 显示指定字段这里假设我们要显示raw对象中的特定字段
// 可以根据实际需求调整要显示的字段
const rawData = params.data.raw;
let result = '';
// 显示ID作为唯一标识
if (rawData.id) {
result += `ID: ${rawData.id}<br/>`;
}
// 显示名称
if (rawData.properties?.name || rawData.name) {
const displayName = rawData.properties?.name || rawData.name;
result += `名称: ${displayName}<br/>`;
}
// 可以添加更多需要显示的字段
// 例如:
// if (rawData.properties?.description) {
// result += `描述: ${rawData.properties.description}<br/>`;
// }
return result;
}
// 回退到默认显示
return params.name || '未知节点';
},
},
animation: false, // 完全关闭动画
animationDuration: 0, // 动画时长设为0
animationDurationUpdate: 0, // 更新动画时长设为0
animationEasingUpdate: "linear", // 使用线性缓动
force: {
repulsion: 1000,
gravity: 0.1, // 添加重力防止节点飞散
friction: 0.6, // 添加摩擦力
layoutAnimation: false, // 关闭力导向布局动画
},
// animationDurationUpdate: 100,
// animationEasingUpdate: "quinticInOut",
label: {
normal: {
show: true,
textStyle: {
fontSize: 12,
},
},
},
legend: {
x: "center",
show: false,
// data: ["夫妻", "战友", "亲戚"],
},
series: [
{
type: "graph",
layout: "force",
symbolSize: 60,
focusNodeAdjacency: true,
roam: true,
categories: [
{
name: "夫妻",
// itemStyle: {
// normal: {
// color: "#009800",
// },
// },
},
{
name: "战友",
// itemStyle: {
// normal: {
// color: "#4592FF",
// },
// },
},
{
name: "亲戚",
// itemStyle: {
// normal: {
// color: "#3592F",
// },
// },
},
],
label: {
normal: {
show: true,
textStyle: {
fontSize: 12,
},
formatter: (e: Record<string, any>) => {
const name = e.data.raw.properties.name
return name;
}
},
},
force: {
repulsion: 1000,
},
edgeSymbolSize: [4, 50],
edgeLabel: {
normal: {
show: true,
textStyle: {
fontSize: 10,
},
formatter: "{c}",
},
},
data: [
// {
// name: "徐贱云",
// draggable: true,
// },
// {
// name: "冯可梁",
// category: 1,
// draggable: true,
// },
// {
// name: "邓志荣",
// category: 1,
// draggable: true,
// },
// {
// name: "李荣庆",
// category: 1,
// draggable: true,
// },
// {
// name: "郑志勇",
// category: 1,
// draggable: true,
// },
// {
// name: "赵英杰",
// category: 1,
// draggable: true,
// },
// {
// name: "王承军",
// category: 1,
// draggable: true,
// },
// {
// name: "陈卫东",
// category: 1,
// draggable: true,
// },
// {
// name: "邹劲松",
// category: 1,
// draggable: true,
// },
// {
// name: "赵成",
// category: 1,
// draggable: true,
// },
// {
// name: "陈现忠",
// category: 1,
// draggable: true,
// },
// {
// name: "陶泳",
// category: 1,
// draggable: true,
// },
// {
// name: "王德福",
// category: 1,
// draggable: true,
// },
],
links: [
// {
// source: 0,
// target: 1,
// value: "asd",
// },
// {
// source: 0,
// target: 2,
// value: "子女",
// },
// {
// source: 0,
// target: 3,
// value: "夫妻",
// },
// {
// source: 0,
// target: 4,
// value: "父母",
// },
// {
// source: 1,
// target: 2,
// value: "表亲",
// },
// {
// source: 0,
// target: 5,
// value: "朋友",
// },
// {
// source: 4,
// target: 5,
// value: "朋友",
// },
// {
// source: 2,
// target: 8,
// value: "叔叔",
// },
// {
// source: 0,
// target: 12,
// value: "朋友",
// },
// {
// source: 6,
// target: 11,
// value: "爱人",
// },
// {
// source: 6,
// target: 3,
// value: "朋友",
// },
// {
// source: 7,
// target: 5,
// value: "朋友",
// },
// {
// source: 9,
// target: 10,
// value: "朋友",
// },
// {
// source: 3,
// target: 10,
// value: "朋友",
// },
// {
// source: 2,
// target: 11,
// value: "同学",
// },
],
lineStyle: {
normal: {
opacity: 0.9,
width: 1,
curveness: 0,
},
},
},
],
});
/**
* @desc
* @auth yjc
* @created 2025-11-17 15:25
* @since v1.0.0
* @param {string} id -
* @param {string}
* @returns {}
*
* @example
* functionName()
*
* @modified 2025-11-17 15:25
* @desc
*/
const createLink = (data: Record<string, any>) => {
return {
raw: data,
source: data.parentIndex,
target: data.index,
value: data.type,
itemStyle: data?.itemStyle || null
}
}
// 构造节点数据
const createPointData = (data: Record<string, any>, options?: Record<string, any>) => {
return {
raw: data,
id: data.id,
name: data.name,
draggable: options?.draggable || false,
itemStyle: data?.itemStyle || null
}
}
// 类型
// const createType = (data: Record<string, any>) => {
// const colorMap: Record<string, any> = {
// 夫妻: "#009800",
// 战友: "#4592FF",
// 亲戚: "#3592F",
// }
// return {
// name: data.type,
// itemStyle: {
// normal: {
// color: colorMap[data.type] || "#009800",
// }
// },
// }
// }
return {
option,
createLink,
createPointData,
};
};

View File

@ -0,0 +1,40 @@
export const useUtils = () => {
// 存储已生成的颜色,确保不重复
const usedColors = new Set<string>();
// 生成随机颜色
const generateColor = (): string => {
// 生成更亮的颜色,避免太暗
const r = Math.floor(Math.random() * 155) + 100
const g = Math.floor(Math.random() * 155) + 100
const b = Math.floor(Math.random() * 155) + 100
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
}
// 生成颜色
const createColor = () => {
let color: string
let attempts = 0
// 尝试生成不重复的颜色最多尝试100次
do {
color = generateColor()
attempts++
} while (usedColors.has(color) && attempts < 100)
// 如果尝试次数过多,清空已使用的颜色重新开始
if (attempts >= 100) {
usedColors.clear()
color = generateColor()
}
usedColors.add(color)
return color
}
return {
createColor
}
}

View File

@ -0,0 +1,312 @@
<script setup lang="ts">
import { ref, inject, defineComponent, nextTick, computed, onUnmounted, onMounted } from "vue";
import Chat from "@/views/knowledge/components/Chat/chat.vue";
import { useServices } from "@/views/knowledge/services/index";
import dayjs from "dayjs";
import { Input, Select, SelectOption } from "ant-design-vue";
import _ from "lodash";
const services = useServices();
const echartRef = ref<any>(null);
const currentNode = ref({});
const showRelationData = ref([]);
const echartInit = (chart: any) => {
echartRef.value = chart;
echartRef.value.on("click", function (params: any) {
console.log("params", params);
const nodeId = params.data.id;
services.db.api.onPointClick(nodeId);
// onPointClick(params.data.id);
// console.log("params", params);
// // console.log("params", params);
// const nodeId = params.data.id;
// const nodes = services.chart.api.getNodeRelationById(nodeId);
// currentNode.value = params.data.raw;
// showRelationData.value = nodes;
// console.log("nodes", showRelationData.value);
});
};
const showData = computed(() => {
// console.log('useNode', showOptions)
// return {};
const curServer = "db";
return services[curServer].state.showOptions.value;
// return {};
});
const onChange = _.debounce((id: string) => {
console.log(id, "id");
services.db.api.onSearch(id);
}, 1000);
const prefix = "knowledge-chat-page";
onMounted(() => {
services.db.api.getData();
services.db.api.getQuery();
});
onUnmounted(() => {});
</script>
<template>
<div :class="prefix">
<div class="filter-box">
<!-- <div class="filter-wrapper">
<label>实体</label>
<a-button
:type="services.chart.state.selectTypes.value.includes(item) ? 'primary' : 'default'"
v-for="item in services.chart.state.typesMap.value"
@click="services.chart.api.toggleSelectType(item)"
>
{{ item }}</a-button
>
</div>
<div class="filter-wrapper">
<label>关系</label>
<a-button
:type="services.chart.state.selectRelations.value.includes(item) ? 'primary' : 'default'"
v-for="item in services.chart.state.relationsMap.value"
@click="services.chart.api.toggleRelation(item)"
>
{{ item }}</a-button
>
</div> -->
</div>
<div class="chat-wrapper">
<div class="chat-box">
<Chat :options="showData" @init="echartInit" />
</div>
<!-- 消息窗口 -->
<div class="chat-info-box" v-if="showRelationData.length">
<!-- 当前 -->
<div class="info-box">
<a-tag color="warning"> 当前节点</a-tag>
</div>
<div class="relation-node">
<div class="node-wrapper">
<div class="label">节点名称</div>
<a-tag color="#108ee9">{{ currentNode.properties.name }}</a-tag>
</div>
<div class="node-wrapper">
<div class="label">总结</div>
<a-tooltip>
<template #title>{{ currentNode.properties.summary }}</template>
<div class="node-desc">{{ currentNode.properties.summary }}</div>
</a-tooltip>
</div>
<div class="node-wrapper">
<div class="label">切片</div>
<a-tooltip>
<template #title>{{ currentNode.properties.origin_text }}</template>
<div class="node-desc">{{ currentNode.properties.origin_text }}</div>
</a-tooltip>
</div>
<div class="node-wrapper">
<div class="label">创建时间</div>
{{ dayjs(currentNode.properties.created_at).format("YYYY-MM-DD hh:mm:ss") }}
</div>
<div class="node-wrapper">
<div class="label">更新时间</div>
{{ dayjs(currentNode.properties.last_updated).format("YYYY-MM-DD hh:mm:ss") }}
</div>
</div>
<!-- 关联 -->
<div class="info-box">
<a-tag color="error"> 关联节点</a-tag>
</div>
<div v-for="item in showRelationData" class="relation-node">
<div class="node-wrapper">
<div class="label">节点名称</div>
<a-tag color="#108ee9">{{ item.node.properties.name }}</a-tag>
</div>
<div class="node-wrapper">
<div class="label">关系</div>
<a-tag color="success">{{ item.type }}</a-tag>
</div>
<div class="node-wrapper">
<div class="label">总结</div>
<a-tooltip>
<template #title>{{ item.node.properties.summary }}</template>
<div class="node-desc">{{ item.node.properties.summary }}</div>
</a-tooltip>
</div>
<div class="node-wrapper">
<div class="label">切片</div>
<a-tooltip>
<template #title>{{ item.node.properties.origin_text }}</template>
<div class="node-desc">{{ item.node.properties.origin_text }}</div>
</a-tooltip>
</div>
<div class="node-wrapper">
<div class="label">创建时间</div>
{{ dayjs(item.node.properties.created_at).format("YYYY-MM-DD hh:mm:ss") }}
</div>
<div class="node-wrapper">
<div class="label">更新时间</div>
{{ dayjs(item.node.properties.last_updated).format("YYYY-MM-DD hh:mm:ss") }}
</div>
</div>
</div>
<!-- 工具窗口 -->
<div class="chat-search-box">
<Select
v-model:value="services.db.state.searchValue.value"
show-search
:filter-option="services.db.api.optionsFilter"
@change="onChange"
style="width: 400px"
>
<SelectOption
v-for="item in services.db.state.graphData.value.nodes"
:value="item.id"
:data-raw="item"
>{{ item.properties.name }}</SelectOption
>
</Select>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
$prefix: "knowledge-chat-page";
.#{$prefix} {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.filter-section {
padding: 16px;
background: #f5f5f5;
border-bottom: 1px solid #e8e8e8;
max-height: 300px;
overflow-y: auto;
.filter-group {
margin-bottom: 16px;
h3 {
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
color: #333;
}
.filter-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
.filter-button {
margin-bottom: 8px;
}
}
}
}
.chart-section {
flex: 1;
overflow: hidden;
position: relative;
}
}
.chat-wrapper {
display: flex;
align-items: flex-start;
justify-content: start;
width: 100%;
height: 100%;
position: relative;
.chat-box {
flex: 1;
height: 100%;
}
.chat-info-box {
font-size: 14px;
position: absolute;
top: 20px;
left: 20px;
height: fit-content;
max-height: 800px;
width: 300px;
border-radius: 6px;
overflow: auto;
overflow-x: hidden;
background-color: rgba(202, 230, 238, 0.3);
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.16);
display: flex;
align-items: start;
flex-direction: column;
gap: 10px;
padding: 10px;
box-sizing: border-box;
.info-box {
}
}
.relation-node {
padding-left: 20px;
padding-bottom: 10px;
border-bottom: 1px solid rgb(51, 51, 51, 0.1);
margin-bottom: 10px;
&:last-child {
border-bottom: none;
}
.node-wrapper {
display: flex;
align-items: start;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid rgb(51, 51, 51, 0.1);
&:last-child {
border-bottom: none;
}
.label {
width: 100px;
flex-shrink: 0;
}
.node-desc {
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
word-break: break-all;
white-space: normal;
-webkit-line-clamp: 2;
display: -webkit-box;
}
}
}
.chat-search-box {
position: absolute;
top: 20px;
right: 20px;
}
}
//
@media (max-width: 768px) {
.#{$prefix} {
.filter-section {
padding: 12px;
.filter-group {
h3 {
font-size: 13px;
}
}
}
}
}
</style>

View File

@ -0,0 +1,356 @@
import { useChartHooks } from '@/views/knowledge/hooks/chart.ts'
import { computed, ref } from 'vue'
import { router } from "@/router/index.ts";
import { createData } from './mockData'
// 存储已生成的颜色,确保不重复
const usedColors = new Set<string>()
/**
*
* @returns
*/
const generateRandomColor = (): string => {
// 生成随机颜色
const generateColor = (): string => {
// 生成更亮的颜色,避免太暗
const r = Math.floor(Math.random() * 155) + 100
const g = Math.floor(Math.random() * 155) + 100
const b = Math.floor(Math.random() * 155) + 100
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
}
let color: string
let attempts = 0
// 尝试生成不重复的颜色最多尝试100次
do {
color = generateColor()
attempts++
} while (usedColors.has(color) && attempts < 100)
// 如果尝试次数过多,清空已使用的颜色重新开始
if (attempts >= 100) {
usedColors.clear()
color = generateColor()
}
usedColors.add(color)
return color
}
/**
*
*/
const relationColors = ref<Map<string, string>>(new Map())
/**
*
*/
const getRelationColor = (type: string): string => {
if (!relationColors.value.has(type)) {
relationColors.value.set(type, generateRandomColor())
}
return relationColors.value.get(type) || '#000000'
}
/**
*
*/
const nodeColors = ref<Map<string, string>>(new Map())
/**
*
*/
const getNodeColor = (type: string): string => {
if (!nodeColors.value.has(type)) {
nodeColors.value.set(type, generateRandomColor())
}
return nodeColors.value.get(type) || '#000000'
}
export const useChartServices = () => {
const { option, createPointData, createLink} = useChartHooks()
// 图表原始数据
const chatData = ref({})
// 类型字典集合
const typesMap = ref<string[]>([])
// 节点集合
const nodes = ref<Record<string, any>[]>([])
// 节点字典集合
const nodesMap = computed(() => {
const dataMap = new Map()
nodes.value.forEach((node) => {
if (!dataMap.has(node.id)) {
dataMap.set(node.id, node)
}
})
return dataMap
})
// 关系字典集合
const relationMap = computed(() => {
const dataMap = new Map()
console.log('relationMap', relations.value)
relations.value.forEach((relation) => {
if (!dataMap.has(relation.id)) {
dataMap.set(relation.raw.id, relation)
}
})
return dataMap
})
// 关系节点集合
const relations = ref<Record<string, any>[]>([])
// 数据规整
const chartConfig = {
chatData,
relationMap: relationMap,
typesMap,
nodes,
relations
}
// 选中的类型
const selectTypes = ref<string[]>([])
// 选中的关系
const selectRelations = ref<string[]>([])
// 切换选中类型
const toggleSelectType = (type: string) => {
const i = selectTypes.value.indexOf(type)
if (i > -1) {
selectTypes.value.splice(i, 1)
} else {
selectTypes.value.push(type)
}
}
// 切换选中关系
const toggleRelation = (relation: string) => {
const i = selectRelations.value.indexOf(relation)
if (i > -1) {
selectRelations.value.splice(i, 1)
} else {
selectRelations.value.push(relation)
}
}
// 重置数据
const resetData = () => {
chatData.value = {}
relations.value = []
typesMap.value = []
nodes.value = []
// 重置颜色映射,以便下次生成新的颜色
relationColors.value.clear()
nodeColors.value.clear()
}
// 新增类型,已存在则跳过
const addTypeMap = (data: Record<string, any>) => {
if (typesMap.value.includes(data.labels)) return
typesMap.value.push(data.labels)
}
// 处理数据
const handleData = (data: Record<string, any>) => {
const { nodes: rawNodes, relationships } = data
rawNodes.forEach((node: Record<string, any>) => {
addTypeMap(node)
// 获取节点类型对应的颜色
const nodeColor = getNodeColor(node.labels)
const nodeInfo = createPointData({
...node,
type: node.labels,
name: node.id,
itemStyle: {
color: nodeColor
},
// name: node.properties.name
})
nodes.value.push(nodeInfo)
})
relationships.forEach((relation: Record<string, any>) => {
// addRelationMap(relation)
// 获取关系类型对应的颜色
const relationColor = getRelationColor(relation.type)
const link = createLink({
...relation,
parentIndex: getNodeIndex(relation.start_node_id),
index: getNodeIndex(relation.end_node_id),
type: relation.type,
lineStyle: {
color: relationColor,
width: 2 // 关系线条宽度
},
itemStyle: {
color: relationColor
}
})
relations.value.push(link)
})
}
// 获取节点索引
const getNodeIndex = (id: string, source?: Record<string, any>) => {
return (source || nodes.value).findIndex((item:Record<string, any>) => item.id === id)
}
// 显示用的数据
const showData = computed(() => {
// 生成带有颜色的分类配置
// const categoriesWithColor = relationsMap.value.map(item => ({
// name: item,
// itemStyle: {
// normal: {
// color: getRelationColor(item)
// }
// }
// }))
// 为每个节点添加category索引使其与对应关系类型关联
const nodesWithCategory = nodes.value.map(node => ({
...node,
// category: relationsMap.value.indexOf(node.type) !== -1 ?
// relationsMap.value.indexOf(node.type) : 0
}))
const useData = {
...option.value,
series: [
{
...option.value.series[0],
// categories: categoriesWithColor,
data: nodesWithCategory,
links: relations.value,
}
]
}
console.log('useData', useData)
return useData
})
const getDataPath = () => {
const { id } = router.currentRoute.value.query;
const jsonMap: Record<string, any> = {
'1': 'http://222.190.139.186:10001/public/demo/knowledge-chat/neo4j_export2.json',
'2': 'http://222.190.139.186:10001/public/demo/knowledge-chat/neo4j_export.json'
}
return jsonMap[id as string] || jsonMap[2]
}
const getMockData = () => {
const data = createData()
return data
}
const getData = async () => {
// const path = getDataPath()
// const res = await fetch(path)
// const mockData = await res.json()
const mockData = getMockData()
console.log('mockData', mockData)
chatData.value = mockData
handleData(chatData.value)
return mockData
}
// 获取节点关系
const getNodeRelationById = (id: string) => {
console.log('relations', relationMap.value)
const relationNode = []
relations.value.forEach((item: Record<string, any>) => {
if (item.raw.end_node_id == id || item.raw.start_node_id == id) {
let targetNodeId = null
// 如果是起始点, 相关点是终点
if (item.raw.start_node_id === id) {
targetNodeId = item.raw.end_node_id
} else {
// 如果是终点, 相关点是起始点
targetNodeId = item.raw.start_node_id
}
const nodeInfo = getNodeById(targetNodeId)
if (!nodeInfo) return
const params = {
type: item.raw.type,
node: nodeInfo.raw,
relation: item.raw
}
relationNode.push(params)
}
// 如果是起始点
// 如果是终点
// if (item.start_node_id === id) {
// targets.push(item)
// } else if (item.end_node_id === id) {
// ends.push(item)
// }
})
return relationNode
}
// 获取节点信息
const getNodeById = (id: string) => {
if (!id) return
return nodesMap.value.get(id)
}
const searchValue = ref('')
// 显示节点
const onShowNodeEven = () => {
}
// 隐藏节点
const onHideNodeEven = () => {}
const state = {
option,
showData,
chartConfig,
relationColors,
nodeColors,
searchValue
}
const api = {
getData,
handleData,
// addRelationMap,
addTypeMap,
resetData,
toggleSelectType,
toggleRelation,
getNodeById,
getNodeRelationById,
onShowNodeEven,
onHideNodeEven
}
return {
state,
api
}
}

View File

@ -0,0 +1,361 @@
import { ref, computed } from 'vue'
import { createData } from './mockData'
import { useChartHooks } from '@/views/knowledge/hooks/chart.ts'
import { router } from "@/router/index.ts";
import { useUtils } from '@/views/knowledge/hooks/utils.ts'
export const useOperateServices = () => {
const { createColor } = useUtils()
const { option, createPointData, createLink} = useChartHooks()
const colors = ['#5470C6', '#91CC75', '#EE6666', '#73C0DE', '#3BA272', '#FC8452', '#9A60B4', '#EA7CC3']
// 图表数据
const graphData = ref<Record<string, any>>({
roots: [],
nodes: [],
relationships: []
})
// 节点映射
const nodesMap = new Map()
// 关系映射
const relationsMap = new Map()
// 分类映射
const categoriesMap = new Map()
// 获取数据
const getData = () => {
const data = createData()
const roots = getMainNodes(data)
handleData(data)
graphData.value = {
roots: roots,
nodes: Array.from(nodesMap.values()),
relationships: Array.from(relationsMap.values())
}
console.log('graphData', graphData.value)
}
// 更新数据
const updateData = () => {
graphData.value = {
...graphData.value,
nodes: Array.from(nodesMap.values()),
relationships: Array.from(relationsMap.values())
}
}
// 处理数据
const handleData = (data: Record<string, any>) => {
const { nodes, relationships } = data
createCategoriesMap(nodes)
createNodeMap(nodes)
createRelationMap(relationships)
relationships.forEach((item: Record<string, any>) => {
const start = item.start_node_id
const end = item.end_node_id
if (nodesMap.has(start)) {
let target = nodesMap.get(start)
if (!target.childrenLinks) target.childrenLinks = []
target.childrenLinks.push(relationsMap.get(item.id))
}
if (nodesMap.has(end)) {
let target = nodesMap.get(end)
if (!target.parentLinks) target.parentLinks = []
target.parentLinks.push(relationsMap.get(item.id))
}
})
return {
nodes,
relationships
}
}
// 可视的节点
const showNodes = computed(() => {
const { roots, nodes } = graphData.value
const useNodes = nodes.filter((item: Record<string, any>) => {
const isRoots = roots.find((rootNode: Record<string, any>) => rootNode.id === item.id)
if (isRoots) return true
else {
return item.show
}
})
return useNodes
})
// 可视的关系
const showRelations = computed(() => {
const useNode = showNodes.value
const useRelations: Record<string, any>[] = []
useNode?.map((item: Record<string, any>) => {
if (item.parentLinks) {
useRelations.push(...item.parentLinks)
}
})
return useRelations
})
// 可视的echart配置
const showOptions = computed(() => {
const categories = Array.from(categoriesMap, ([key, value]) => {
return {
name: key,
itemStyle: {
color: value.color
}
}
})
const getCategoriesIndex = (category: string) => {
const i = categories.findIndex((item: Record<string, any>) => {
return item.name == category
})
return i
}
console.log('categories', categories)
// const link = ()
const useNodes: Record<string, any>[] = []
showNodes.value.forEach((node: Record<string, any>) => {
const color = categoriesMap.get(node.label)?.color
console.log('color', categoriesMap, color, node.label)
const nodeInfo = createPointData({
...node,
itemStyle: {
color: color || '#333'
},
type: node.labels,
name: node.id,
})
useNodes.push(nodeInfo)
})
const useRelations: Record<string, any>[] = []
showRelations.value.forEach((relation: Record<string, any>) => {
const link = createLink({
...relation,
parentIndex: getNodeIndex(relation.start_node_id, useNodes),
index: getNodeIndex(relation.end_node_id, useNodes),
type: relation.type,
lineStyle: {
width: 2 // 关系线条宽度
},
})
useRelations.push(link)
})
const useOptions = {
...option.value,
series: [
{
...option.value.series[0],
// categories: categoriesWithColor,
data: useNodes,
links: useRelations
}
]
}
return useOptions
})
// 获取节点索引
const getNodeIndex = (id: string, source: Record<string, any> = showNodes.value) => {
return source.findIndex((item: Record<string, any>) => item.id === id)
}
// 找到主节点
const getMainNodes = (data: Record<string, any>) => {
const { nodes, relationships } = data
let roots = JSON.parse(JSON.stringify(nodes))
relationships.forEach((item: Record<string, any>) => {
const end = item.end_node_id
roots = roots.filter((item: Record<string, any>) => {
return item.id !== end
})
})
return roots
}
// 构建nodemap
const createNodeMap = (arr: Record<string, any>[]) => {
nodesMap.clear()
arr.forEach((item: Record<string, any>) => {
const raw = {
...item,
show: false
}
if (nodesMap.has(item.id)) {
// nodesMap.get(item.id).push(raw)
} else {
nodesMap.set(item.id, raw)
}
})
}
// 构建分类categoriesMap
const createCategoriesMap = (arr: Record<string, any>[]) => {
categoriesMap.clear()
arr.forEach((item: Record<string, any>) => {
if (item.label) {
if (!categoriesMap.has(item.label)) {
categoriesMap.set(item.label, {
color: createColor()
})
}
}
})
}
// 构建关系map
const createRelationMap = (arr: Record<string, any>[]) => {
relationsMap.clear()
arr.forEach((item: Record<string, any>) => {
if (relationsMap.has(item.id)) {
// relationsMap.get(item.id).push(item)
} else {
relationsMap.set(item.id, item)
}
})
}
// 根据id检索到对应的主节点
const getMainNodeById = (id: string) => {
const node = nodesMap.get(id)
const roots = []
const links = [...node.parentLinks || []]
if (!links.length) return [node]
while(links.length) {
const link = links.shift()
const start = link.start_node_id
const preNode = nodesMap.get(start)
console.log('preNode', preNode, nodesMap)
if (preNode?.parentLinks?.length) {
links.push(...preNode.parentLinks)
} else {
roots.push(preNode)
}
}
return roots
}
// echart关系图节点点击
const onPointClick = (id: string) => {
const { roots } = graphData.value
const isRoots = roots.find((rootNode: Record<string, any>) => rootNode.id === id)
if (isRoots) {
onToggleMainPoint(id)
} else {
onTogglePoint(id)
}
console.log('categoriesMap', categoriesMap)
updateData()
}
// 点击节点
const onTogglePoint = (id: string) => {
const node = nodesMap.get(id)
if (node) {
// node.show = !node.show
const childrenLinks = node?.childrenLinks || []
console.log('childrenLinks', childrenLinks)
childrenLinks.forEach((item: Record<string, any>) => {
const { end_node_id } = item
const cnode = nodesMap.get(end_node_id)
const type = !cnode.show
setNodeTypeInLink(end_node_id, type)
})
// node.show = !node.show
// setNodeTypeInLink(id, !node.show)
}
}
// 点击主节点,将当前节点下的所有子节点都显示或者隐藏出来
const onToggleMainPoint = (id: string) => {
const node = nodesMap.get(id)
if (node) {
setNodeTypeInLink(id, !node.show)
}
// 将当前节点下的所有子节点都显示或者隐藏出来
}
// 链式更新数据可视信息
const setNodeTypeInLink = (id: string, type: boolean) => {
const node = nodesMap.get(id)
if (node) {
// 如果布尔值一致,不处理
// if (node.show != type) {
node.show = type
// }
const childrenLinks = node?.childrenLinks || []
childrenLinks.forEach((item: Record<string, any>) => {
const { end_node_id } = item
setNodeTypeInLink(end_node_id, type)
})
}
}
// 关键字
const searchValue = ref('')
// 选项过滤
const optionsFilter = (input: string, option: any) => {
const raw = option['data-raw']
const id = raw.id
const name = raw.properties.name
return id.includes(input) || name.includes(input)
}
// 搜索
const onSearch = (id: string) => {
const roots = getMainNodeById(id)
console.log('roots', roots)
roots.forEach(item => {
console.log('item', item)
// if (item.show)
setNodeTypeInLink(item.id, true)
})
updateData()
}
const getQuery = () => {
const { id } = router.currentRoute.value.query;
if (id) {
onSearch(id as string)
}
}
return {
state: {
graphData,
nodesMap,
relationsMap,
showOptions,
searchValue,
},
api: {
getData,
onPointClick,
optionsFilter,
// onMainSelect,
// onSelectPoint,
onSearch,
getQuery
}
}
}

View File

@ -0,0 +1,9 @@
import { useOperateServices } from '@/views/knowledge/services/db.ts'
export const useServices = () => {
const dbServices = useOperateServices()
return {
db: dbServices
}
}

View File

@ -0,0 +1,49 @@
export const createData = () => {
const size = 3 // 每层级节点个数
const level = 3 // 层级
const data: Record<string, any> = {
nodes: [],
relationships: []
}
for(let i = 1; i < size; i++) {
data.nodes.push(createNode(`${i}`))
for(let j = 0; j< level; j++) {
data.nodes.push(createNode(`${i}${j}`))
data.relationships.push(createRelations(`${i}${j}`, size))
for(let l = 0; l < level; l++) {
data.nodes.push(createNode(`${i}${j}${l}`))
data.relationships.push(createRelations(`${i}${j}${l}`, size))
}
}
}
return data
}
const createNode = (index: number | string) => {
return {
id: index,
label: `index-${index}`,
properties: {
name: `节点${index}`,
}
}
}
const createRelations = (index: string, max: number) => {
const len = index.length
const start = index.slice(0, len - 1)
const end = index
const params = {
id: `r${index}`,
"type": "组成", // 关系类型
"start_node_id": start, // 起始节点id
"end_node_id": end,
properties: {
}
}
return params
}

View File

@ -0,0 +1,65 @@
{
nodes: [
{
"id": 743, // id
"properties": { //
"summary": "陕西重型汽车有限公司是一家专注于重型汽车制造的企业。",
"last_updated": "2025-11-17T14:40:01.388976",
"name": "陕西重型汽车有限公司",
"created_at": "2025-11-17T13:54:19.372583",
"origin_text": "![](https://cdn-mineru.openxlab.org.cn/result/2025-11-17/602d0d5b-7444-416b-8dc8-705209d7867c/7ef8bd59a75b969b71d628cb9c0d428519a9a97c977ace15dcaca26050cee920.jpg) \n陕西重型汽车有限公司",
}
},
{
"id": 745,
"properties": {
"summary": "陕西重型汽车有限公司是一家专注于重型汽车制造的企业。",
"last_updated": "2025-11-17T14:40:01.388976",
"name": "陕西重型汽车有限公司",
"created_at": "2025-11-17T13:54:19.372583",
"origin_text": "![](https://cdn-mineru.openxlab.org.cn/result/2025-11-17/602d0d5b-7444-416b-8dc8-705209d7867c/7ef8bd59a75b969b71d628cb9c0d428519a9a97c977ace15dcaca26050cee920.jpg) \n陕西重型汽车有限公司",
}
},
],
relations: [
{
"id": 1159748372303709713, // id
"type": "组成", //
"start_node_id": 1553, // id
"end_node_id": 1529, // id
"properties": { //
"last_updated": "2025-11-17T14:20:21.706039",
"fact": "高压油管 组成 共轨管",
"update_count": 1,
"created_at": "2025-11-17T14:20:21.706039",
"fact_timeline": [
"{\"value\": \"高压油管 组成 共轨管\", \"timestamp\": \"2025-11-17T14:20:21.706039\", \"source\": \"text_extraction\"}"
]
}
},
{
"id": 1152992972862653996,
"type": "组成",
"start_node_id": 1580,
"end_node_id": 1581,
"properties": {
"last_updated": "2025-11-17T14:21:59.940095",
"关系": "包含",
"组成_timeline": [
"{\"value\": \"线束接头是其一部分\", \"timestamp\": \"2025-11-17T15:12:23.222218\", \"source\": \"text_extraction\"}"
],
"fact": "线束和传感器接头 组成 线束接头,组成:线束接头是其一部分",
"组成": "线束接头是其一部分",
"update_count": 1,
"created_at": "2025-11-17T14:21:59.940095",
"fact_timeline": [
"{\"value\": \"线束和传感器接头 组成 线束接头,关系:包含\", \"timestamp\": \"2025-11-17T14:21:59.940095\", \"source\": \"text_extraction\"}",
"{\"value\": \"线束和传感器接头 组成 线束接头,组成:线束接头是其一部分\", \"timestamp\": \"2025-11-17T15:12:23.222218\", \"source\": \"text_extraction\"}"
],
"关系_timeline": [
"{\"value\": \"包含\", \"timestamp\": \"2025-11-17T14:21:59.940095\", \"source\": \"text_extraction\"}"
]
}
},
]
}

1
src/vite-env.d.ts vendored Normal file
View File

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

37
tsconfig.json Normal file
View File

@ -0,0 +1,37 @@
{
"compilerOptions": {
"noEmitOnError": false,
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2021", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"baseUrl": "./",
"paths": {
"@": ["src"],
"@/*": ["src/*"]
},
"types": ["vite/client"], // Vite
"typeRoots": ["./node_modules/@types", "./src/types"] //
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.vue",
"src/**/*.tsx",
"src/**/*"
],
"references": [{ "path": "./tsconfig.node.json" }]
}

10
tsconfig.node.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

101
vite.config.ts Normal file
View File

@ -0,0 +1,101 @@
import { fileURLToPath, URL } from 'node:url'
import { resolve } from 'node:path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
import postcssPresetEnv from 'postcss-preset-env'
import postcssNormalizeCharset from 'postcss-normalize-charset'
import autoprefixer from 'autoprefixer'
function pathResolve(dir: string) {
return resolve(__dirname, dir)
}
const URL_USE = ''
// https://vite.dev/config/
export default defineConfig({
base: './',
server: {
host: true,
port: 3019, // 指定端口
strictPort: false, // 如果端口已被占用则退出
proxy: {
'/api': {
target: URL_USE,
changeOrigin: true, // 修改请求的 origin
rewrite: (path: string) => path, // 可选:重写路径
},
'/ollama': {
target: URL_USE,
changeOrigin: true, // 修改请求的 origin
rewrite: (path: string) => path, // 可选:重写路径
},
'/openai': {
target: URL_USE,
changeOrigin: true, // 修改请求的 origin
rewrite: (path: string) => path, // 可选:重写路径
},
'/ws': {
target: URL_USE,
changeOrigin: true, // 修改请求的 origin
ws: true,
rewrite: (path: string) => path, // 可选:重写路径
},
},
},
plugins: [
vue(),
vueJsx(),
vueDevTools(),
],
resolve: {
alias: [
{
find: /\@\//,
replacement: `${pathResolve('src')}/`,
},
]
},
css: {
postcss: {
plugins:[
postcssPresetEnv(),
postcssNormalizeCharset({
add: true,
}),
autoprefixer({
//css兼容前缀
overrideBrowserslist: [
'Android 4.1',
'ios 7.1',
'Chrome >31',
'not ie <=11', //不考虑IE浏览器
'ff >= 30', //仅新版本用'ff >= 30
'>1%', //全球统计有超过1%的使用了使用'> 1%'
'last 2 version', //所有主流浏览器最近2个版本
],
grid: true, //开启grid布局的兼容浏览器IE除外其它都能兼容grid可以关闭开启
}),
// px2rem({
// rootValue: 19.2, // UI设计稿的宽度/10
// // rootValue : 37.5,
// unitPrecision: 3, // 转rem精确到小数点多少位
// propList: ['*'], // 需要转换的属性 *表示所有
// selectorBlackList: ['ignore'], // 不进行px转换的选择器
// replace: true, // 是否直接更换属性值,而不添加备用属性
// mediaQuery: false, // 是否在媒体查询的css代码中也进行转换
// minPixelValue: 0, // 设置要替换的最小像素值
// exclude: /node_modules/i // 排除node_modules文件夹下的文件
// })
]
},
preprocessorOptions: {
scss: {
},
},
}
})