commit 0a6fff3cf2e8bd23613ba08b4791753dc4c70ea5 Author: yans <498418533@qq.com> Date: Mon Nov 24 14:49:39 2025 +0800 第一次提交 diff --git a/README.md b/README.md new file mode 100644 index 0000000..534a03f --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +### 环境配置 + +node版本: 22.10.0 +pnpm版本: 6.11.0 + +### 安装依赖 + +pnpm install + +### 启动项目 + +pnpm run dev diff --git a/index.html b/index.html new file mode 100644 index 0000000..3a0bd42 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + 雷达⽬标识别迁移部署系统 + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..a71421e --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/public/css/tinymceEdit--wirte.css b/public/css/tinymceEdit--wirte.css new file mode 100644 index 0000000..48aec65 --- /dev/null +++ b/public/css/tinymceEdit--wirte.css @@ -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; +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..46c2933 Binary files /dev/null and b/public/favicon.ico differ diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..986629a --- /dev/null +++ b/src/App.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..46c2933 Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/components/Base/Layout/LoginLayout/index.vue b/src/components/Base/Layout/LoginLayout/index.vue new file mode 100644 index 0000000..c282c51 --- /dev/null +++ b/src/components/Base/Layout/LoginLayout/index.vue @@ -0,0 +1,49 @@ + + + + + + + diff --git a/src/components/Base/Layout/SystemLayout/Content/index.vue b/src/components/Base/Layout/SystemLayout/Content/index.vue new file mode 100644 index 0000000..21bf243 --- /dev/null +++ b/src/components/Base/Layout/SystemLayout/Content/index.vue @@ -0,0 +1,85 @@ + + + + + + + + diff --git a/src/components/Base/Layout/SystemLayout/Header/index.vue b/src/components/Base/Layout/SystemLayout/Header/index.vue new file mode 100644 index 0000000..7458f03 --- /dev/null +++ b/src/components/Base/Layout/SystemLayout/Header/index.vue @@ -0,0 +1,58 @@ + + + + + + + diff --git a/src/components/Base/Layout/SystemLayout/Sider/hooks/router.ts b/src/components/Base/Layout/SystemLayout/Sider/hooks/router.ts new file mode 100644 index 0000000..b00b489 --- /dev/null +++ b/src/components/Base/Layout/SystemLayout/Sider/hooks/router.ts @@ -0,0 +1,44 @@ +import { h } from 'vue' +import Icon from '@/components/Module/Icon/index.vue' + + +export const useMain = () => { + // 路由过滤,只显示要显示的路由 + const routerFilter = (menus: Array>) => { + return menus.filter((item: Record) => { + if (item.children) { + item.children = routerFilter(item.children) + } + + return !item.meta.hidden + }) + } + + // 路由格式化 + const routerFormat = (arr: Array>) => { + return arr.map((item: Record) => { + 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 + } +} diff --git a/src/components/Base/Layout/SystemLayout/Sider/index.vue b/src/components/Base/Layout/SystemLayout/Sider/index.vue new file mode 100644 index 0000000..86e2185 --- /dev/null +++ b/src/components/Base/Layout/SystemLayout/Sider/index.vue @@ -0,0 +1,240 @@ + + + + + + + diff --git a/src/components/Base/Layout/SystemLayout/index.vue b/src/components/Base/Layout/SystemLayout/index.vue new file mode 100644 index 0000000..5d69aab --- /dev/null +++ b/src/components/Base/Layout/SystemLayout/index.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/src/components/Base/Layout/ViewLayout/index.vue b/src/components/Base/Layout/ViewLayout/index.vue new file mode 100644 index 0000000..3084ee8 --- /dev/null +++ b/src/components/Base/Layout/ViewLayout/index.vue @@ -0,0 +1,12 @@ + + + + + diff --git a/src/components/Base/Tools/Info/index.vue b/src/components/Base/Tools/Info/index.vue new file mode 100644 index 0000000..7bf4ae7 --- /dev/null +++ b/src/components/Base/Tools/Info/index.vue @@ -0,0 +1,56 @@ + + + + + + + diff --git a/src/components/Base/Tools/Menus/index.vue b/src/components/Base/Tools/Menus/index.vue new file mode 100644 index 0000000..9d82de0 --- /dev/null +++ b/src/components/Base/Tools/Menus/index.vue @@ -0,0 +1,81 @@ + + + + + + + diff --git a/src/components/Base/Tools/UserBox/hooks/index.ts b/src/components/Base/Tools/UserBox/hooks/index.ts new file mode 100644 index 0000000..de4dc6d --- /dev/null +++ b/src/components/Base/Tools/UserBox/hooks/index.ts @@ -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 + } +} diff --git a/src/components/Base/Tools/UserBox/index.vue b/src/components/Base/Tools/UserBox/index.vue new file mode 100644 index 0000000..f09d5c4 --- /dev/null +++ b/src/components/Base/Tools/UserBox/index.vue @@ -0,0 +1,71 @@ + + + + + + + diff --git a/src/components/Module/AsyncContent/ErrorLoading.vue b/src/components/Module/AsyncContent/ErrorLoading.vue new file mode 100644 index 0000000..5bf6e0f --- /dev/null +++ b/src/components/Module/AsyncContent/ErrorLoading.vue @@ -0,0 +1,21 @@ + + + + + + + diff --git a/src/components/Module/AsyncContent/index.vue b/src/components/Module/AsyncContent/index.vue new file mode 100644 index 0000000..8693f4b --- /dev/null +++ b/src/components/Module/AsyncContent/index.vue @@ -0,0 +1,14 @@ + + + + + + + diff --git a/src/components/Module/Icon/components/SvgIcon.vue b/src/components/Module/Icon/components/SvgIcon.vue new file mode 100644 index 0000000..fb45c66 --- /dev/null +++ b/src/components/Module/Icon/components/SvgIcon.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/src/components/Module/Icon/components/icon.ts b/src/components/Module/Icon/components/icon.ts new file mode 100644 index 0000000..db1dde4 --- /dev/null +++ b/src/components/Module/Icon/components/icon.ts @@ -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({}); +const iconContents = ref({}); +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 +// }; +// } diff --git a/src/components/Module/Icon/index.vue b/src/components/Module/Icon/index.vue new file mode 100644 index 0000000..397dcdf --- /dev/null +++ b/src/components/Module/Icon/index.vue @@ -0,0 +1,73 @@ + + + + + + + diff --git a/src/components/Module/Modal/components/ModalHeader.vue b/src/components/Module/Modal/components/ModalHeader.vue new file mode 100644 index 0000000..3813b7e --- /dev/null +++ b/src/components/Module/Modal/components/ModalHeader.vue @@ -0,0 +1,171 @@ + + + + + + + diff --git a/src/components/Module/Modal/index.vue b/src/components/Module/Modal/index.vue new file mode 100644 index 0000000..1eb79f2 --- /dev/null +++ b/src/components/Module/Modal/index.vue @@ -0,0 +1,187 @@ + + + + + + + diff --git a/src/components/__tests__/HelloWorld.spec.ts b/src/components/__tests__/HelloWorld.spec.ts new file mode 100644 index 0000000..2533202 --- /dev/null +++ b/src/components/__tests__/HelloWorld.spec.ts @@ -0,0 +1,11 @@ +import { describe, it, expect } from 'vitest' + +import { mount } from '@vue/test-utils' +import HelloWorld from '../HelloWorld.vue' + +describe('HelloWorld', () => { + it('renders properly', () => { + const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } }) + expect(wrapper.text()).toContain('Hello Vitest') + }) +}) diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..6bd8909 --- /dev/null +++ b/src/config/index.ts @@ -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: '' +} diff --git a/src/core/lazy_use.ts b/src/core/lazy_use.ts new file mode 100644 index 0000000..90a4e7e --- /dev/null +++ b/src/core/lazy_use.ts @@ -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) { + 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) { + + +} + diff --git a/src/directive/index.ts b/src/directive/index.ts new file mode 100644 index 0000000..c3043ae --- /dev/null +++ b/src/directive/index.ts @@ -0,0 +1,8 @@ +import type { App } from 'vue' + + +function directive(app: App) { + // 注册指令 +} + +export default directive; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..88e34a7 --- /dev/null +++ b/src/main.ts @@ -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') diff --git a/src/router/base/index.ts b/src/router/base/index.ts new file mode 100644 index 0000000..aa192ad --- /dev/null +++ b/src/router/base/index.ts @@ -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: [] + }, +] diff --git a/src/router/config/index.ts b/src/router/config/index.ts new file mode 100644 index 0000000..b90d0ab --- /dev/null +++ b/src/router/config/index.ts @@ -0,0 +1,61 @@ +import type { + RouteRecordRaw, + RouteMeta +} from 'vue-router' + +import type { defineComponent } from 'vue' + + +export type Component = ReturnType | (() => Promise) | (() => Promise) + +// @ts-expect-error +export interface AppRouter extends Omit { + 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 // 全路径 +// } diff --git a/src/router/func/index.ts b/src/router/func/index.ts new file mode 100644 index 0000000..c70b1f2 --- /dev/null +++ b/src/router/func/index.ts @@ -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 => { + 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 +} diff --git a/src/router/guard/before.ts b/src/router/guard/before.ts new file mode 100644 index 0000000..3dd24e7 --- /dev/null +++ b/src/router/guard/before.ts @@ -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(), + ]) +} diff --git a/src/router/guard/index.ts b/src/router/guard/index.ts new file mode 100644 index 0000000..465f13c --- /dev/null +++ b/src/router/guard/index.ts @@ -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() + // } + } + // } + }) +} diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..4d8be1a --- /dev/null +++ b/src/router/index.ts @@ -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) { + app.use(router) +} + diff --git a/src/router/modules/routerComponents.ts b/src/router/modules/routerComponents.ts new file mode 100644 index 0000000..f624d94 --- /dev/null +++ b/src/router/modules/routerComponents.ts @@ -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')), +} + diff --git a/src/router/modules/staticRouter.ts b/src/router/modules/staticRouter.ts new file mode 100644 index 0000000..e24fe75 --- /dev/null +++ b/src/router/modules/staticRouter.ts @@ -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, + }, + } +] + diff --git a/src/stores/counter.ts b/src/stores/counter.ts new file mode 100644 index 0000000..b6757ba --- /dev/null +++ b/src/stores/counter.ts @@ -0,0 +1,12 @@ +import { ref, computed } from 'vue' +import { defineStore } from 'pinia' + +export const useCounterStore = defineStore('counter', () => { + const count = ref(0) + const doubleCount = computed(() => count.value * 2) + function increment() { + count.value++ + } + + return { count, doubleCount, increment } +}) diff --git a/src/stores/index.ts b/src/stores/index.ts new file mode 100644 index 0000000..cd30903 --- /dev/null +++ b/src/stores/index.ts @@ -0,0 +1,10 @@ +import type { App } from 'vue' +import { createPinia } from 'pinia' + +const store = createPinia() + +export function setupStore(app: App) { + app.use(store) +} + +export { store } \ No newline at end of file diff --git a/src/stores/modules/async-router.ts b/src/stores/modules/async-router.ts new file mode 100644 index 0000000..ca10bfe --- /dev/null +++ b/src/stores/modules/async-router.ts @@ -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 { + 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) +} diff --git a/src/stores/modules/mutation-types.ts b/src/stores/modules/mutation-types.ts new file mode 100644 index 0000000..01daca2 --- /dev/null +++ b/src/stores/modules/mutation-types.ts @@ -0,0 +1,6 @@ + +export const ACCESS_TOKEN = 'token' +export const ACCESS_EXPIRES = 'expires' +export const APP_VERSION = 'app_version' + + diff --git a/src/stores/modules/socket.ts b/src/stores/modules/socket.ts new file mode 100644 index 0000000..54f9e07 --- /dev/null +++ b/src/stores/modules/socket.ts @@ -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) +} diff --git a/src/stores/modules/system.ts b/src/stores/modules/system.ts new file mode 100644 index 0000000..afd8fea --- /dev/null +++ b/src/stores/modules/system.ts @@ -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) +} diff --git a/src/stores/modules/user.ts b/src/stores/modules/user.ts new file mode 100644 index 0000000..7922a1e --- /dev/null +++ b/src/stores/modules/user.ts @@ -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) +} diff --git a/src/styles/index.css b/src/styles/index.css new file mode 100644 index 0000000..bbc22d8 --- /dev/null +++ b/src/styles/index.css @@ -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; + /* 保持指针样式 */ +} diff --git a/src/styles/index.scss b/src/styles/index.scss new file mode 100644 index 0000000..a44ae04 --- /dev/null +++ b/src/styles/index.scss @@ -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; /* 保持指针样式 */ +} diff --git a/src/styles/theme/default.json b/src/styles/theme/default.json new file mode 100644 index 0000000..abf96df --- /dev/null +++ b/src/styles/theme/default.json @@ -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": {} + } +} diff --git a/src/types/global.d.ts b/src/types/global.d.ts new file mode 100644 index 0000000..688a850 --- /dev/null +++ b/src/types/global.d.ts @@ -0,0 +1,37 @@ +import type { DefineComponent } from 'vue' + + +declare global { + type ExtraObj = { + [key?: string]: any + } + type Recordable = Record + + 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/*'; + + + diff --git a/src/types/module.d.ts b/src/types/module.d.ts new file mode 100644 index 0000000..5468342 --- /dev/null +++ b/src/types/module.d.ts @@ -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'; diff --git a/src/utils/EvenStream/index.ts b/src/utils/EvenStream/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/download/index.ts b/src/utils/download/index.ts new file mode 100644 index 0000000..97c2d48 --- /dev/null +++ b/src/utils/download/index.ts @@ -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() }); +} + diff --git a/src/utils/request/index.ts b/src/utils/request/index.ts new file mode 100644 index 0000000..5cb5bc5 --- /dev/null +++ b/src/utils/request/index.ts @@ -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 => { + 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; +} diff --git a/src/utils/request/types/index.ts b/src/utils/request/types/index.ts new file mode 100644 index 0000000..8ad4ad7 --- /dev/null +++ b/src/utils/request/types/index.ts @@ -0,0 +1,43 @@ +export interface Config { + base?: string, // 前缀 + url: string // 请求地址 + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' + data?: string | Record // 请求体 + dataRaw?: boolean // 请求体是否为原始数据 + params?: string | Record | URLSearchParams | string[][] // 请求参数 + headers?: HeadersInit | undefined // 请求头 + controller?: AbortController // 请求控制器 + getRaw?: boolean // 是否需要返回原始数据 +} + +// 请求头 +export interface RequestHead { + [key: string]: string +} + +// 标准响应 +export interface ResponseData { + code: number + data: T + [key: string]: any +} + +// 错误响应 +export interface ResponseError { + 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 +} + + + + diff --git a/src/utils/socket/index.ts b/src/utils/socket/index.ts new file mode 100644 index 0000000..90b0dc0 --- /dev/null +++ b/src/utils/socket/index.ts @@ -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 +} + diff --git a/src/utils/text/index.ts b/src/utils/text/index.ts new file mode 100644 index 0000000..8f42f2f --- /dev/null +++ b/src/utils/text/index.ts @@ -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; +}; diff --git a/src/utils/transition/file.ts b/src/utils/transition/file.ts new file mode 100644 index 0000000..51effca --- /dev/null +++ b/src/utils/transition/file.ts @@ -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] : ''}` +} diff --git a/src/utils/util.ts b/src/utils/util.ts new file mode 100644 index 0000000..19842cc --- /dev/null +++ b/src/utils/util.ts @@ -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 +} + + + + + + + + + + + + diff --git a/src/utils/valid.ts b/src/utils/valid.ts new file mode 100644 index 0000000..c30fc51 --- /dev/null +++ b/src/utils/valid.ts @@ -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) +} diff --git a/src/views/exception/404.vue b/src/views/exception/404.vue new file mode 100644 index 0000000..23c0bd8 --- /dev/null +++ b/src/views/exception/404.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/src/views/index/index.vue b/src/views/index/index.vue new file mode 100644 index 0000000..37e4804 --- /dev/null +++ b/src/views/index/index.vue @@ -0,0 +1,13 @@ + + + + + diff --git a/src/views/knowledge/components/Chat/chat.vue b/src/views/knowledge/components/Chat/chat.vue new file mode 100644 index 0000000..fec54d0 --- /dev/null +++ b/src/views/knowledge/components/Chat/chat.vue @@ -0,0 +1,88 @@ + + + + + + + diff --git a/src/views/knowledge/hooks/chart.ts b/src/views/knowledge/hooks/chart.ts new file mode 100644 index 0000000..3790039 --- /dev/null +++ b/src/views/knowledge/hooks/chart.ts @@ -0,0 +1,342 @@ +import { ref } from "vue"; + +// 关系图逻辑 +export const useChartHooks = () => { + const option = ref({ + title: { + text: "", + }, + tooltip: { + formatter: (params: Record) => { + // params.data包含节点的数据信息 + if (params.data && params.data.raw) { + // 显示指定字段,这里假设我们要显示raw对象中的特定字段 + // 可以根据实际需求调整要显示的字段 + const rawData = params.data.raw; + let result = ''; + + // 显示ID(作为唯一标识) + if (rawData.id) { + result += `ID: ${rawData.id}
`; + } + + // 显示名称 + if (rawData.properties?.name || rawData.name) { + const displayName = rawData.properties?.name || rawData.name; + result += `名称: ${displayName}
`; + } + + + + // 可以添加更多需要显示的字段 + // 例如: + // if (rawData.properties?.description) { + // result += `描述: ${rawData.properties.description}
`; + // } + + 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) => { + 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) => { + return { + raw: data, + source: data.parentIndex, + target: data.index, + value: data.type, + itemStyle: data?.itemStyle || null + } + } + + // 构造节点数据 + const createPointData = (data: Record, options?: Record) => { + return { + raw: data, + id: data.id, + name: data.name, + draggable: options?.draggable || false, + itemStyle: data?.itemStyle || null + } + } + + // 类型 + // const createType = (data: Record) => { + // const colorMap: Record = { + // 夫妻: "#009800", + // 战友: "#4592FF", + // 亲戚: "#3592F", + // } + + // return { + // name: data.type, + // itemStyle: { + // normal: { + // color: colorMap[data.type] || "#009800", + // } + // }, + // } + // } + + + + return { + option, + createLink, + createPointData, + }; +}; diff --git a/src/views/knowledge/hooks/utils.ts b/src/views/knowledge/hooks/utils.ts new file mode 100644 index 0000000..348d7fd --- /dev/null +++ b/src/views/knowledge/hooks/utils.ts @@ -0,0 +1,40 @@ +export const useUtils = () => { + + // 存储已生成的颜色,确保不重复 + const usedColors = new Set(); + + // 生成随机颜色 + 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 + } + +} diff --git a/src/views/knowledge/index.vue b/src/views/knowledge/index.vue new file mode 100644 index 0000000..af1acb9 --- /dev/null +++ b/src/views/knowledge/index.vue @@ -0,0 +1,312 @@ + + + + + diff --git a/src/views/knowledge/services/chart.ts b/src/views/knowledge/services/chart.ts new file mode 100644 index 0000000..5bb2698 --- /dev/null +++ b/src/views/knowledge/services/chart.ts @@ -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() + +/** + * 生成随机且不重复的颜色 + * @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>(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>(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([]) + + // 节点集合 + const nodes = ref[]>([]) + + // 节点字典集合 + 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[]>([]) + + // 数据规整 + const chartConfig = { + chatData, + relationMap: relationMap, + typesMap, + nodes, + relations + } + + // 选中的类型 + const selectTypes = ref([]) + + // 选中的关系 + const selectRelations = ref([]) + + // 切换选中类型 + 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) => { + if (typesMap.value.includes(data.labels)) return + typesMap.value.push(data.labels) + } + + // 处理数据 + const handleData = (data: Record) => { + const { nodes: rawNodes, relationships } = data + rawNodes.forEach((node: Record) => { + 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) => { + // 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) => { + return (source || nodes.value).findIndex((item:Record) => 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 = { + '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) => { + 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 + } +} diff --git a/src/views/knowledge/services/db.ts b/src/views/knowledge/services/db.ts new file mode 100644 index 0000000..113f622 --- /dev/null +++ b/src/views/knowledge/services/db.ts @@ -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>({ + 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) => { + const { nodes, relationships } = data + createCategoriesMap(nodes) + createNodeMap(nodes) + createRelationMap(relationships) + relationships.forEach((item: Record) => { + 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) => { + const isRoots = roots.find((rootNode: Record) => rootNode.id === item.id) + if (isRoots) return true + else { + return item.show + } + }) + return useNodes + }) + + // 可视的关系 + const showRelations = computed(() => { + const useNode = showNodes.value + const useRelations: Record[] = [] + useNode?.map((item: Record) => { + 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) => { + return item.name == category + }) + return i + } + + + console.log('categories', categories) + + // const link = () + const useNodes: Record[] = [] + showNodes.value.forEach((node: Record) => { + 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[] = [] + showRelations.value.forEach((relation: Record) => { + 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 = showNodes.value) => { + return source.findIndex((item: Record) => item.id === id) + } + + // 找到主节点 + const getMainNodes = (data: Record) => { + const { nodes, relationships } = data + let roots = JSON.parse(JSON.stringify(nodes)) + relationships.forEach((item: Record) => { + const end = item.end_node_id + roots = roots.filter((item: Record) => { + return item.id !== end + }) + }) + return roots + } + + // 构建nodemap + const createNodeMap = (arr: Record[]) => { + nodesMap.clear() + arr.forEach((item: Record) => { + 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[]) => { + categoriesMap.clear() + arr.forEach((item: Record) => { + if (item.label) { + if (!categoriesMap.has(item.label)) { + categoriesMap.set(item.label, { + color: createColor() + }) + } + } + }) + } + + // 构建关系map + const createRelationMap = (arr: Record[]) => { + relationsMap.clear() + arr.forEach((item: Record) => { + 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) => 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) => { + 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) => { + 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 + } + } + +} diff --git a/src/views/knowledge/services/index.ts b/src/views/knowledge/services/index.ts new file mode 100644 index 0000000..9d27587 --- /dev/null +++ b/src/views/knowledge/services/index.ts @@ -0,0 +1,9 @@ +import { useOperateServices } from '@/views/knowledge/services/db.ts' + +export const useServices = () => { + const dbServices = useOperateServices() + + return { + db: dbServices + } +} diff --git a/src/views/knowledge/services/mockData.ts b/src/views/knowledge/services/mockData.ts new file mode 100644 index 0000000..3123b44 --- /dev/null +++ b/src/views/knowledge/services/mockData.ts @@ -0,0 +1,49 @@ +export const createData = () => { + const size = 3 // 每层级节点个数 + const level = 3 // 层级 + + + const data: Record = { + 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 +} diff --git a/src/views/knowledge/test.json b/src/views/knowledge/test.json new file mode 100644 index 0000000..57b6654 --- /dev/null +++ b/src/views/knowledge/test.json @@ -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\"}" + ] + } + }, + ] +} \ No newline at end of file diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0b1e6e0 --- /dev/null +++ b/tsconfig.json @@ -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" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..4138c35 --- /dev/null +++ b/vite.config.ts @@ -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: { + + }, + }, + } +})