第一次提交
This commit is contained in:
commit
0a6fff3cf2
12
README.md
Normal file
12
README.md
Normal file
@ -0,0 +1,12 @@
|
||||
### 环境配置
|
||||
|
||||
node版本: 22.10.0
|
||||
pnpm版本: 6.11.0
|
||||
|
||||
### 安装依赖
|
||||
|
||||
pnpm install
|
||||
|
||||
### 启动项目
|
||||
|
||||
pnpm run dev
|
||||
13
index.html
Normal file
13
index.html
Normal 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
64
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
18
public/css/tinymceEdit--wirte.css
Normal file
18
public/css/tinymceEdit--wirte.css
Normal 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
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
45
src/App.vue
Normal file
45
src/App.vue
Normal 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
BIN
src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
49
src/components/Base/Layout/LoginLayout/index.vue
Normal file
49
src/components/Base/Layout/LoginLayout/index.vue
Normal 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>
|
||||
85
src/components/Base/Layout/SystemLayout/Content/index.vue
Normal file
85
src/components/Base/Layout/SystemLayout/Content/index.vue
Normal 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>
|
||||
58
src/components/Base/Layout/SystemLayout/Header/index.vue
Normal file
58
src/components/Base/Layout/SystemLayout/Header/index.vue
Normal 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>
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
240
src/components/Base/Layout/SystemLayout/Sider/index.vue
Normal file
240
src/components/Base/Layout/SystemLayout/Sider/index.vue
Normal 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>
|
||||
45
src/components/Base/Layout/SystemLayout/index.vue
Normal file
45
src/components/Base/Layout/SystemLayout/index.vue
Normal 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>
|
||||
12
src/components/Base/Layout/ViewLayout/index.vue
Normal file
12
src/components/Base/Layout/ViewLayout/index.vue
Normal 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>
|
||||
56
src/components/Base/Tools/Info/index.vue
Normal file
56
src/components/Base/Tools/Info/index.vue
Normal 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>
|
||||
81
src/components/Base/Tools/Menus/index.vue
Normal file
81
src/components/Base/Tools/Menus/index.vue
Normal 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>
|
||||
36
src/components/Base/Tools/UserBox/hooks/index.ts
Normal file
36
src/components/Base/Tools/UserBox/hooks/index.ts
Normal 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
|
||||
}
|
||||
}
|
||||
71
src/components/Base/Tools/UserBox/index.vue
Normal file
71
src/components/Base/Tools/UserBox/index.vue
Normal 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>
|
||||
21
src/components/Module/AsyncContent/ErrorLoading.vue
Normal file
21
src/components/Module/AsyncContent/ErrorLoading.vue
Normal 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>
|
||||
14
src/components/Module/AsyncContent/index.vue
Normal file
14
src/components/Module/AsyncContent/index.vue
Normal 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>
|
||||
80
src/components/Module/Icon/components/SvgIcon.vue
Normal file
80
src/components/Module/Icon/components/SvgIcon.vue
Normal 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>
|
||||
92
src/components/Module/Icon/components/icon.ts
Normal file
92
src/components/Module/Icon/components/icon.ts
Normal 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
|
||||
// };
|
||||
// }
|
||||
73
src/components/Module/Icon/index.vue
Normal file
73
src/components/Module/Icon/index.vue
Normal 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>
|
||||
171
src/components/Module/Modal/components/ModalHeader.vue
Normal file
171
src/components/Module/Modal/components/ModalHeader.vue
Normal 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>
|
||||
187
src/components/Module/Modal/index.vue
Normal file
187
src/components/Module/Modal/index.vue
Normal 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>
|
||||
11
src/components/__tests__/HelloWorld.spec.ts
Normal file
11
src/components/__tests__/HelloWorld.spec.ts
Normal 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
8
src/config/index.ts
Normal 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
115
src/core/lazy_use.ts
Normal 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
8
src/directive/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import type { App } from 'vue'
|
||||
|
||||
|
||||
function directive(app: App<Element>) {
|
||||
// 注册指令
|
||||
}
|
||||
|
||||
export default directive;
|
||||
41
src/main.ts
Normal file
41
src/main.ts
Normal 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
29
src/router/base/index.ts
Normal 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: []
|
||||
},
|
||||
]
|
||||
61
src/router/config/index.ts
Normal file
61
src/router/config/index.ts
Normal 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
70
src/router/func/index.ts
Normal 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
|
||||
}
|
||||
20
src/router/guard/before.ts
Normal file
20
src/router/guard/before.ts
Normal 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
90
src/router/guard/index.ts
Normal 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
17
src/router/index.ts
Normal 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)
|
||||
}
|
||||
|
||||
22
src/router/modules/routerComponents.ts
Normal file
22
src/router/modules/routerComponents.ts
Normal 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')),
|
||||
}
|
||||
|
||||
54
src/router/modules/staticRouter.ts
Normal file
54
src/router/modules/staticRouter.ts
Normal 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
12
src/stores/counter.ts
Normal 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
10
src/stores/index.ts
Normal 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 }
|
||||
54
src/stores/modules/async-router.ts
Normal file
54
src/stores/modules/async-router.ts
Normal 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)
|
||||
}
|
||||
6
src/stores/modules/mutation-types.ts
Normal file
6
src/stores/modules/mutation-types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
export const ACCESS_TOKEN = 'token'
|
||||
export const ACCESS_EXPIRES = 'expires'
|
||||
export const APP_VERSION = 'app_version'
|
||||
|
||||
|
||||
78
src/stores/modules/socket.ts
Normal file
78
src/stores/modules/socket.ts
Normal 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)
|
||||
}
|
||||
22
src/stores/modules/system.ts
Normal file
22
src/stores/modules/system.ts
Normal 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
103
src/stores/modules/user.ts
Normal 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
30
src/styles/index.css
Normal 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
34
src/styles/index.scss
Normal 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; /* 保持指针样式 */
|
||||
}
|
||||
30
src/styles/theme/default.json
Normal file
30
src/styles/theme/default.json
Normal 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
37
src/types/global.d.ts
vendored
Normal 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
37
src/types/module.d.ts
vendored
Normal 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';
|
||||
0
src/utils/EvenStream/index.ts
Normal file
0
src/utils/EvenStream/index.ts
Normal file
187
src/utils/download/index.ts
Normal file
187
src/utils/download/index.ts
Normal 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
106
src/utils/request/index.ts
Normal 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;
|
||||
}
|
||||
43
src/utils/request/types/index.ts
Normal file
43
src/utils/request/types/index.ts
Normal 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
19
src/utils/socket/index.ts
Normal 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
48
src/utils/text/index.ts
Normal 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;
|
||||
};
|
||||
21
src/utils/transition/file.ts
Normal file
21
src/utils/transition/file.ts
Normal 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
64
src/utils/util.ts
Normal 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
23
src/utils/valid.ts
Normal 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)
|
||||
}
|
||||
30
src/views/exception/404.vue
Normal file
30
src/views/exception/404.vue
Normal 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
13
src/views/index/index.vue
Normal 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>
|
||||
88
src/views/knowledge/components/Chat/chat.vue
Normal file
88
src/views/knowledge/components/Chat/chat.vue
Normal 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>
|
||||
342
src/views/knowledge/hooks/chart.ts
Normal file
342
src/views/knowledge/hooks/chart.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
40
src/views/knowledge/hooks/utils.ts
Normal file
40
src/views/knowledge/hooks/utils.ts
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
312
src/views/knowledge/index.vue
Normal file
312
src/views/knowledge/index.vue
Normal 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>
|
||||
356
src/views/knowledge/services/chart.ts
Normal file
356
src/views/knowledge/services/chart.ts
Normal 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
|
||||
}
|
||||
}
|
||||
361
src/views/knowledge/services/db.ts
Normal file
361
src/views/knowledge/services/db.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
9
src/views/knowledge/services/index.ts
Normal file
9
src/views/knowledge/services/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { useOperateServices } from '@/views/knowledge/services/db.ts'
|
||||
|
||||
export const useServices = () => {
|
||||
const dbServices = useOperateServices()
|
||||
|
||||
return {
|
||||
db: dbServices
|
||||
}
|
||||
}
|
||||
49
src/views/knowledge/services/mockData.ts
Normal file
49
src/views/knowledge/services/mockData.ts
Normal 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
|
||||
}
|
||||
65
src/views/knowledge/test.json
Normal file
65
src/views/knowledge/test.json
Normal 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": " \n陕西重型汽车有限公司",
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 745,
|
||||
"properties": {
|
||||
"summary": "陕西重型汽车有限公司是一家专注于重型汽车制造的企业。",
|
||||
"last_updated": "2025-11-17T14:40:01.388976",
|
||||
"name": "陕西重型汽车有限公司",
|
||||
"created_at": "2025-11-17T13:54:19.372583",
|
||||
"origin_text": " \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
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
37
tsconfig.json
Normal file
37
tsconfig.json
Normal 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
10
tsconfig.node.json
Normal 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
101
vite.config.ts
Normal 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: {
|
||||
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user