@james-zhang/elpis 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +3 -0
- package/.eslintrc +55 -0
- package/.tmp-check.js +47 -0
- package/README.md +287 -0
- package/app/controller/base.js +37 -0
- package/app/controller/project.js +76 -0
- package/app/controller/view.js +17 -0
- package/app/extend/logger.js +36 -0
- package/app/middleware/api-params-verify.js +78 -0
- package/app/middleware/api-sign-verify.js +31 -0
- package/app/middleware/error-handler.js +36 -0
- package/app/middleware/project-handler.js +26 -0
- package/app/middleware.js +69 -0
- package/app/pages/asserts/custom.css +10 -0
- package/app/pages/boot.js +44 -0
- package/app/pages/common/curl.js +76 -0
- package/app/pages/common/utils.js +2 -0
- package/app/pages/dashboard/complex-view/header-view/complex-view/sub-menu/sub-menu.vue +19 -0
- package/app/pages/dashboard/complex-view/header-view/header-view.vue +131 -0
- package/app/pages/dashboard/complex-view/iframe-view/iframe-view.vue +29 -0
- package/app/pages/dashboard/complex-view/schema-view/complex-view/search-pannel/search-pannel.vue +36 -0
- package/app/pages/dashboard/complex-view/schema-view/complex-view/table-pannel/table-pannel.vue +125 -0
- package/app/pages/dashboard/complex-view/schema-view/components/component-config.js +19 -0
- package/app/pages/dashboard/complex-view/schema-view/components/create-form/create-form.vue +119 -0
- package/app/pages/dashboard/complex-view/schema-view/components/detail-panel/detail-panel.vue +131 -0
- package/app/pages/dashboard/complex-view/schema-view/components/edit-form/edit-form.vue +149 -0
- package/app/pages/dashboard/complex-view/schema-view/hook/schema.js +126 -0
- package/app/pages/dashboard/complex-view/schema-view/schema-view.vue +111 -0
- package/app/pages/dashboard/complex-view/sider-view/complex-view/sub-menu/sub-menu.vue +28 -0
- package/app/pages/dashboard/complex-view/sider-view/sider-view.vue +138 -0
- package/app/pages/dashboard/dashboard.vue +121 -0
- package/app/pages/dashboard/entry.dashboard.js +52 -0
- package/app/pages/dashboard/router.js +3 -0
- package/app/pages/dashboard/todo/todo.vue +10 -0
- package/app/pages/store/index.js +3 -0
- package/app/pages/store/menu.js +71 -0
- package/app/pages/store/project.js +15 -0
- package/app/pages/widgets/header-container/asserts/avatar.png +0 -0
- package/app/pages/widgets/header-container/asserts/logo.png +0 -0
- package/app/pages/widgets/header-container/header-container.vue +118 -0
- package/app/pages/widgets/schema-form/complex-view/input/input.vue +165 -0
- package/app/pages/widgets/schema-form/complex-view/input-number/input-number.vue +164 -0
- package/app/pages/widgets/schema-form/complex-view/select/select.vue +138 -0
- package/app/pages/widgets/schema-form/form-item-config.js +20 -0
- package/app/pages/widgets/schema-form/schema-form.vue +154 -0
- package/app/pages/widgets/schema-search-bar/complex-view/date-range/date-range.vue +49 -0
- package/app/pages/widgets/schema-search-bar/complex-view/dynamic-select/dynamic-select.vue +65 -0
- package/app/pages/widgets/schema-search-bar/complex-view/input/input.vue +44 -0
- package/app/pages/widgets/schema-search-bar/complex-view/select/select.vue +50 -0
- package/app/pages/widgets/schema-search-bar/schema-search-bar.vue +134 -0
- package/app/pages/widgets/schema-search-bar/search-item-config.js +27 -0
- package/app/pages/widgets/schema-table/schema-table.vue +277 -0
- package/app/pages/widgets/sider-container/sider-container.vue +42 -0
- package/app/public/static/logo.png +0 -0
- package/app/public/static/normalize.css +239 -0
- package/app/router/project.js +7 -0
- package/app/router/view.js +7 -0
- package/app/router-schema/project.js +34 -0
- package/app/service/base.js +10 -0
- package/app/service/project.js +43 -0
- package/app/view/entry.tpl +31 -0
- package/app/webpack/config/webpack.base.js +224 -0
- package/app/webpack/config/webpack.dev.js +53 -0
- package/app/webpack/config/webpack.prod.js +104 -0
- package/app/webpack/dev.js +54 -0
- package/app/webpack/libs/blank.js +1 -0
- package/app/webpack/prod.js +17 -0
- package/config/config.default.js +3 -0
- package/docs/dashboard-model.js +150 -0
- package/elpis-core/env.js +21 -0
- package/elpis-core/index.js +82 -0
- package/elpis-core/loader/config.js +58 -0
- package/elpis-core/loader/controller.js +55 -0
- package/elpis-core/loader/extend.js +42 -0
- package/elpis-core/loader/middleware.js +50 -0
- package/elpis-core/loader/router-schema.js +28 -0
- package/elpis-core/loader/router.js +42 -0
- package/elpis-core/loader/service.js +51 -0
- package/index.js +37 -0
- package/logs/application.log +50 -0
- package/logs/application.log.2026-06-13 +159 -0
- package/model/index.js +119 -0
- package/package.json +88 -0
- package/scripts/generate-cover.py +93 -0
- package/scripts/generate-diagrams.js +80 -0
- package/test/controller/project.test.js +218 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<sider-container>
|
|
3
|
+
<!-- 侧边菜单插槽 -->
|
|
4
|
+
<template #menu-content>
|
|
5
|
+
<el-menu
|
|
6
|
+
:default-active="activeKey"
|
|
7
|
+
:ellipsis="false"
|
|
8
|
+
@select="onMenuSelect"
|
|
9
|
+
>
|
|
10
|
+
<!-- 循环渲染菜单项 -->
|
|
11
|
+
<template v-for="item in menuList">
|
|
12
|
+
<sub-menu
|
|
13
|
+
v-if="item.subMenu && item.subMenu.length > 0"
|
|
14
|
+
:menu-item="item"
|
|
15
|
+
:key="'sub-' + item.key"
|
|
16
|
+
/>
|
|
17
|
+
<el-menu-item v-else :key="'item-' + item.key">
|
|
18
|
+
{{ item.name }}
|
|
19
|
+
</el-menu-item>
|
|
20
|
+
</template>
|
|
21
|
+
</el-menu>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<!-- 右侧路由内容插槽 -->
|
|
25
|
+
<template #main-content>
|
|
26
|
+
<router-view />
|
|
27
|
+
</template>
|
|
28
|
+
</sider-container>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<script setup>
|
|
32
|
+
import { ref, watch, onMounted } from 'vue';
|
|
33
|
+
import { useRouter, useRoute } from 'vue-router';
|
|
34
|
+
import { useMenuStore } from '$elpisStore/menu.js';
|
|
35
|
+
import SiderContainer from '$elpisWidgets/sider-container/sider-container';
|
|
36
|
+
import SubMenu from '$elpisPages/dashboard/complex-view/sider-view/complex-view/sub-menu/sub-menu.vue';
|
|
37
|
+
|
|
38
|
+
const router = useRouter();
|
|
39
|
+
const route = useRoute();
|
|
40
|
+
const menuStore = useMenuStore();
|
|
41
|
+
|
|
42
|
+
// 当前激活菜单key
|
|
43
|
+
const activeKey = ref('');
|
|
44
|
+
|
|
45
|
+
const setActiveKey = function () {
|
|
46
|
+
let siderMenuItem = menuStore.findMenuItem({
|
|
47
|
+
key: 'key',
|
|
48
|
+
value: route.query.sider_key
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// 如果首次加载 sider-view,用户未选中左侧菜单,默认高亮第一个菜单但不跳转
|
|
52
|
+
if (!siderMenuItem) {
|
|
53
|
+
const hMenuItem = menuStore.findMenuItem({
|
|
54
|
+
key: 'key',
|
|
55
|
+
value: route.query.key
|
|
56
|
+
});
|
|
57
|
+
if (hMenuItem && hMenuItem.siderConfig && hMenuItem.siderConfig.menu) {
|
|
58
|
+
const siderMenuList = hMenuItem.siderConfig.menu || [];
|
|
59
|
+
siderMenuItem = menuStore.findFirstMenu(siderMenuList);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (siderMenuItem) {
|
|
64
|
+
activeKey.value = siderMenuItem.key;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// 菜单数据源
|
|
69
|
+
const menuList = ref([]);
|
|
70
|
+
const setMenuList = function () {
|
|
71
|
+
const menuItem = menuStore.findMenuItem({
|
|
72
|
+
key: 'key',
|
|
73
|
+
value: route.query.key
|
|
74
|
+
});
|
|
75
|
+
if (menuItem && menuItem.siderConfig && menuItem.siderConfig.menu) {
|
|
76
|
+
menuList.value = menuItem.siderConfig.menu || [];
|
|
77
|
+
} else {
|
|
78
|
+
menuList.value = [];
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
watch(() => route.query.key, () => {
|
|
83
|
+
setMenuList();
|
|
84
|
+
setActiveKey();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
watch(() => menuStore.menuList, () => {
|
|
88
|
+
setMenuList();
|
|
89
|
+
setActiveKey();
|
|
90
|
+
}, { deep: true });
|
|
91
|
+
|
|
92
|
+
onMounted(() => {
|
|
93
|
+
setMenuList();
|
|
94
|
+
setActiveKey();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// 菜单选中跳转事件
|
|
98
|
+
const onMenuSelect = (menuKey) => {
|
|
99
|
+
handleMenuSelect(menuKey);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const handleMenuSelect = function (menuKey) {
|
|
103
|
+
const menuItem = menuStore.findMenuItem({
|
|
104
|
+
key: 'key',
|
|
105
|
+
value: menuKey
|
|
106
|
+
});
|
|
107
|
+
if (!menuItem) {
|
|
108
|
+
console.warn('未找到菜单项', menuKey);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const { moduleType, key, customConfig } = menuItem;
|
|
113
|
+
// 如果是当前页面不处理
|
|
114
|
+
if (key === route.query.sider_key) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 侧边栏菜单项只在 /sider 子路由下切换,避免跳转到不存在的外部 route
|
|
119
|
+
const pathMap = {
|
|
120
|
+
sider: '/sider',
|
|
121
|
+
iframe: '/sider/iframe',
|
|
122
|
+
schema: '/sider/schema',
|
|
123
|
+
custom: '/sider'
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
router.push({
|
|
127
|
+
path: '/view/dashboard'+pathMap[moduleType] || '/view/dashboard/sider',
|
|
128
|
+
query: {
|
|
129
|
+
key: route.query.key,
|
|
130
|
+
sider_key: key,
|
|
131
|
+
proj_key: route.query.proj_key,
|
|
132
|
+
...(moduleType === 'custom' && customConfig?.path ? { custom_path: customConfig.path } : {})
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<style lang="less" scoped></style>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-container class="dashboard-wrapper" :locale="zhCn">
|
|
3
|
+
<header-view :proj-name="projName" @menu-select="onMenuSelect">
|
|
4
|
+
<template #main-content>
|
|
5
|
+
<router-view></router-view>
|
|
6
|
+
</template>
|
|
7
|
+
</header-view>
|
|
8
|
+
</el-container>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup>
|
|
12
|
+
import { ref, onMounted } from 'vue';
|
|
13
|
+
import { useRoute, useRouter } from 'vue-router';
|
|
14
|
+
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
|
15
|
+
import HeaderView from './complex-view/header-view/header-view.vue';
|
|
16
|
+
import $curl from '$elpisCommon/curl.js';
|
|
17
|
+
import { useMenuStore } from '$elpisStore/menu';
|
|
18
|
+
import { useProjectStore } from '$elpisStore/project';
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
const router = useRouter();
|
|
22
|
+
const route = useRoute();
|
|
23
|
+
const projectStore = useProjectStore();
|
|
24
|
+
const menuStore = useMenuStore();
|
|
25
|
+
const projName = ref('');
|
|
26
|
+
|
|
27
|
+
//组件挂载时,请求项目列表和菜单列表
|
|
28
|
+
onMounted(async () => {
|
|
29
|
+
// 如果 URL 没有 proj_key,先加载项目列表,取第一个项目作为默认值
|
|
30
|
+
if (!route.query.proj_key) {
|
|
31
|
+
await getProjectList();
|
|
32
|
+
if (projectStore.projectList.length > 0) {
|
|
33
|
+
const defaultProject = projectStore.projectList[0];
|
|
34
|
+
const homePage = defaultProject.homePage || '/todo';
|
|
35
|
+
const [homePath, queryStr = ''] = homePage.split('?');
|
|
36
|
+
const params = new URLSearchParams(queryStr);
|
|
37
|
+
params.set('proj_key', defaultProject.key);
|
|
38
|
+
await router.replace({
|
|
39
|
+
path: homePath,
|
|
40
|
+
query: Object.fromEntries(params)
|
|
41
|
+
});
|
|
42
|
+
} else {
|
|
43
|
+
console.warn('未获取到项目列表,无法加载 dashboard');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
await getProjectList();
|
|
48
|
+
await getProjectConfig();
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
//请求 /api/project/list接口,并缓存到 projectStore中
|
|
52
|
+
async function getProjectList() {
|
|
53
|
+
const res = await $curl({
|
|
54
|
+
method: 'get',
|
|
55
|
+
url: '/api/project/list',
|
|
56
|
+
query:{
|
|
57
|
+
//动态获取当前项目key,暂时写死
|
|
58
|
+
proj_key:route.query.proj_key
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
if(!res || !res.success || !res.data) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
projectStore.setProjectList(res.data);
|
|
65
|
+
}
|
|
66
|
+
//请求 /api/project接口,并缓存到 menuStore中
|
|
67
|
+
async function getProjectConfig() {
|
|
68
|
+
const res = await $curl({
|
|
69
|
+
method: 'get',
|
|
70
|
+
url: '/api/project',
|
|
71
|
+
query:{
|
|
72
|
+
//动态获取当前项目key,暂时写死
|
|
73
|
+
proj_key:route.query.proj_key
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
if(!res || !res.success || !res.data) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const {name,menu} = res.data;
|
|
80
|
+
projName.value = name;
|
|
81
|
+
menuStore.setMenuList(menu);
|
|
82
|
+
}
|
|
83
|
+
//菜单点击事件
|
|
84
|
+
const onMenuSelect=function(menuItem) {
|
|
85
|
+
if(!menuItem){
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const {moduleType,key,customConfig}=menuItem
|
|
89
|
+
|
|
90
|
+
//如果是当前页面不处理
|
|
91
|
+
if(key===route.query.key){
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
const pathMap={
|
|
95
|
+
sider:'/sider',
|
|
96
|
+
iframe:'/iframe',
|
|
97
|
+
schema:'/schema',
|
|
98
|
+
custom:customConfig?.path
|
|
99
|
+
}
|
|
100
|
+
router.push({
|
|
101
|
+
path: '/view/dashboard'+pathMap[moduleType],
|
|
102
|
+
query: {
|
|
103
|
+
key: key,
|
|
104
|
+
proj_key:route.query.proj_key
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
<style lang="less" scoped>
|
|
114
|
+
.dashboard-wrapper {
|
|
115
|
+
height: 100%;
|
|
116
|
+
}
|
|
117
|
+
:deep(.el-main){
|
|
118
|
+
padding: 0;
|
|
119
|
+
}
|
|
120
|
+
</style>
|
|
121
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import boot from "$elpisPages/boot";
|
|
2
|
+
import Dashboard from './dashboard.vue'
|
|
3
|
+
import businessDashboardRouterConfig from '$businessDashboardRouterConfig'
|
|
4
|
+
|
|
5
|
+
const routes=[]
|
|
6
|
+
|
|
7
|
+
//头部菜单路由
|
|
8
|
+
routes.push({
|
|
9
|
+
path: '/view/dashboard/iframe',
|
|
10
|
+
component: () => import('./complex-view/iframe-view/iframe-view.vue')
|
|
11
|
+
})
|
|
12
|
+
routes.push({
|
|
13
|
+
path: '/view/dashboard/schema',
|
|
14
|
+
component: () => import('./complex-view/schema-view/schema-view.vue')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const siderRoutes=[{
|
|
18
|
+
path: 'iframe',
|
|
19
|
+
component: () => import('./complex-view/iframe-view/iframe-view.vue')
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
path: 'schema',
|
|
23
|
+
component: () => import('./complex-view/schema-view/schema-view.vue')
|
|
24
|
+
},
|
|
25
|
+
//custom 自定义路由
|
|
26
|
+
{
|
|
27
|
+
path: 'todo',
|
|
28
|
+
component: () => import('./todo/todo.vue')
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
//业务拓展路由
|
|
33
|
+
if(typeof businessDashboardRouterConfig === 'function'){
|
|
34
|
+
businessDashboardRouterConfig({routes,siderRoutes})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
//侧边栏菜单路由
|
|
39
|
+
routes.push({
|
|
40
|
+
path: '/view/dashboard/sider',
|
|
41
|
+
component: () => import('./complex-view/sider-view/sider-view.vue'),
|
|
42
|
+
children: siderRoutes
|
|
43
|
+
})
|
|
44
|
+
//侧边栏兜底策略·默认路由
|
|
45
|
+
routes.push({
|
|
46
|
+
path: '/view/dashboard/sider/:chapters+',
|
|
47
|
+
component: () => import('./complex-view/iframe-view/iframe-view.vue')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
boot(Dashboard, {
|
|
51
|
+
routes
|
|
52
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { defineStore } from 'pinia';
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
export const useMenuStore = defineStore('menu',() => {
|
|
5
|
+
//菜单列表
|
|
6
|
+
const menuList = ref([]);
|
|
7
|
+
//设置菜单列表
|
|
8
|
+
const setMenuList = (list) => {
|
|
9
|
+
menuList.value = list;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 查找菜单项
|
|
13
|
+
* @param key 搜索字段
|
|
14
|
+
* @param value 搜索值
|
|
15
|
+
* @param mList 要搜索的菜单列表
|
|
16
|
+
*/
|
|
17
|
+
const findMenuItem=function({key,value},mList=menuList.value){
|
|
18
|
+
for(let i=0;i<mList.length;i++){
|
|
19
|
+
const menuItem=mList[i]
|
|
20
|
+
if(!menuItem[key]){
|
|
21
|
+
continue
|
|
22
|
+
}
|
|
23
|
+
const {menuType,moduleType}=menuItem
|
|
24
|
+
|
|
25
|
+
if(menuItem[key]==value){
|
|
26
|
+
return menuItem
|
|
27
|
+
}
|
|
28
|
+
if(menuType === 'group' && menuItem.subMenu){
|
|
29
|
+
const mItem=findMenuItem({
|
|
30
|
+
key,
|
|
31
|
+
value,
|
|
32
|
+
}, menuItem.subMenu)
|
|
33
|
+
if(mItem){
|
|
34
|
+
return mItem
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if(moduleType === 'sider' && menuItem.siderConfig && menuItem.siderConfig.menu){
|
|
38
|
+
const mItem=findMenuItem({
|
|
39
|
+
key,
|
|
40
|
+
value,
|
|
41
|
+
}, menuItem.siderConfig.menu)
|
|
42
|
+
if(mItem){
|
|
43
|
+
return mItem
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 找出第一个菜单目录
|
|
51
|
+
* @param mList 要搜索的菜单列表
|
|
52
|
+
*/
|
|
53
|
+
const findFirstMenu=function(mList=menuList.value){
|
|
54
|
+
if(!mList || !mList[0]){
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
let firstMenuItem=mList[0]
|
|
58
|
+
if(firstMenuItem.subMenu){
|
|
59
|
+
firstMenuItem=findFirstMenu(firstMenuItem.subMenu)
|
|
60
|
+
}
|
|
61
|
+
return firstMenuItem
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
menuList,
|
|
67
|
+
setMenuList,
|
|
68
|
+
findMenuItem,
|
|
69
|
+
findFirstMenu
|
|
70
|
+
}
|
|
71
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineStore } from 'pinia';
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
export const useProjectStore = defineStore('project',() => {
|
|
5
|
+
//项目列表
|
|
6
|
+
const projectList = ref([]);
|
|
7
|
+
//设置项目列表
|
|
8
|
+
const setProjectList = (list) => {
|
|
9
|
+
projectList.value = list;
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
projectList,
|
|
13
|
+
setProjectList
|
|
14
|
+
}
|
|
15
|
+
})
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-container class="header-container">
|
|
3
|
+
<el-header class="header">
|
|
4
|
+
<el-row type="flex" align="middle" class="header-row">
|
|
5
|
+
<!-- 左上方 title -->
|
|
6
|
+
<el-row type="flex" align="middle" class="title-panel">
|
|
7
|
+
<img src="./asserts/logo.png" class="logo"/>
|
|
8
|
+
<el-row class="text">{{title}}</el-row>
|
|
9
|
+
</el-row>
|
|
10
|
+
|
|
11
|
+
<!-- 插槽:菜单区域 -->
|
|
12
|
+
<slot name="menu-content"></slot>
|
|
13
|
+
|
|
14
|
+
<!-- 右上方区域 -->
|
|
15
|
+
<el-row type="flex" align="middle" justify="end" class="setting-panel">
|
|
16
|
+
<!-- 插槽:设置区域 -->
|
|
17
|
+
<slot name="setting-content"></slot>
|
|
18
|
+
<img src="./asserts/avatar.png" class="avatar" />
|
|
19
|
+
<el-dropdown @command="handleUserCommand">
|
|
20
|
+
<span class="username">
|
|
21
|
+
{{userName}}<i class="el-icon-arrow-down el-icon--right"></i>
|
|
22
|
+
</span>
|
|
23
|
+
<template #dropdown>
|
|
24
|
+
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
|
25
|
+
</template>
|
|
26
|
+
</el-dropdown>
|
|
27
|
+
</el-row>
|
|
28
|
+
</el-row>
|
|
29
|
+
</el-header>
|
|
30
|
+
<el-main class="main-container">
|
|
31
|
+
<!-- 外部拓展填充区域 -->
|
|
32
|
+
<slot name="main-content"></slot>
|
|
33
|
+
</el-main>
|
|
34
|
+
</el-container>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script setup>
|
|
38
|
+
import { ref } from 'vue'
|
|
39
|
+
|
|
40
|
+
const userName = ref('张三')
|
|
41
|
+
|
|
42
|
+
const handleUserCommand = (command) => {
|
|
43
|
+
if (command === 'logout') {
|
|
44
|
+
// 退出登录逻辑
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
defineProps({
|
|
49
|
+
title: {
|
|
50
|
+
type: String,
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<style lang="less" scoped>
|
|
56
|
+
.header-container {
|
|
57
|
+
height: 100%;
|
|
58
|
+
min-width: 1000px;
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
|
|
61
|
+
.header {
|
|
62
|
+
max-height: 120px;
|
|
63
|
+
border-bottom: 1px solid #E8E8E8;
|
|
64
|
+
|
|
65
|
+
.header-row {
|
|
66
|
+
height: 60px;
|
|
67
|
+
padding: 0 20px;
|
|
68
|
+
|
|
69
|
+
.title-panel {
|
|
70
|
+
width: 180px;
|
|
71
|
+
min-width: 180px;
|
|
72
|
+
|
|
73
|
+
.logo {
|
|
74
|
+
margin-right: 10px;
|
|
75
|
+
width: 25px;
|
|
76
|
+
height: 25px;
|
|
77
|
+
border-radius: 50%;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.text {
|
|
81
|
+
font-size: 15px;
|
|
82
|
+
font-weight: 500;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.setting-panel {
|
|
87
|
+
margin-left: auto;
|
|
88
|
+
min-width: 180px;
|
|
89
|
+
|
|
90
|
+
.avatar {
|
|
91
|
+
margin-right: 12px;
|
|
92
|
+
width: 30px;
|
|
93
|
+
height: 30px;
|
|
94
|
+
border-radius: 50%;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.username {
|
|
98
|
+
font-size: 16px;
|
|
99
|
+
font-weight: 500;
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
height: 60px;
|
|
102
|
+
line-height: 60px;
|
|
103
|
+
outline: none;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.main-container {
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-direction: column;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
:deep(.el-header) {
|
|
116
|
+
padding: 0;
|
|
117
|
+
}
|
|
118
|
+
</style>
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- 整行容器:标签 + 输入框左右布局 -->
|
|
3
|
+
<el-row
|
|
4
|
+
type="flex"
|
|
5
|
+
align="middle"
|
|
6
|
+
class="form-item"
|
|
7
|
+
>
|
|
8
|
+
<!-- 左侧标签区域 -->
|
|
9
|
+
<el-row
|
|
10
|
+
class="item-label"
|
|
11
|
+
justify="end"
|
|
12
|
+
>
|
|
13
|
+
<!-- 必填红色星号:schema里required为true才展示 -->
|
|
14
|
+
<el-row
|
|
15
|
+
v-if="schema.option?.required"
|
|
16
|
+
type="flex"
|
|
17
|
+
class="required"
|
|
18
|
+
>
|
|
19
|
+
*
|
|
20
|
+
</el-row>
|
|
21
|
+
<!-- 字段中文名称 -->
|
|
22
|
+
{{ schema.label }}
|
|
23
|
+
</el-row>
|
|
24
|
+
|
|
25
|
+
<!-- 右侧输入框区域 -->
|
|
26
|
+
<el-row class="item-value">
|
|
27
|
+
<el-input
|
|
28
|
+
v-bind="schema.option"
|
|
29
|
+
v-model="dtoValue"
|
|
30
|
+
:placeholder="placeholder"
|
|
31
|
+
class="component"
|
|
32
|
+
:class="validTips?'valid-border':''"
|
|
33
|
+
@focus="onFocus"
|
|
34
|
+
@blur="onBlur"
|
|
35
|
+
/>
|
|
36
|
+
<div
|
|
37
|
+
v-if="validTips"
|
|
38
|
+
class="valid-tips"
|
|
39
|
+
>
|
|
40
|
+
{{ validTips }}
|
|
41
|
+
</div>
|
|
42
|
+
</el-row>
|
|
43
|
+
</el-row>
|
|
44
|
+
</template>
|
|
45
|
+
<script setup>
|
|
46
|
+
import { ref, toRefs, watch, inject, onMounted } from 'vue'
|
|
47
|
+
const ajv = inject('ajv')
|
|
48
|
+
|
|
49
|
+
// 接收父组件传入参数
|
|
50
|
+
const props = defineProps({
|
|
51
|
+
schemaKey: String, // 当前字段key(属性名,如productName)
|
|
52
|
+
schema: Object, // 当前字段完整schema配置
|
|
53
|
+
model: Object, // 当前字段的表单值
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// 解构props,toRefs保证响应式不丢失
|
|
57
|
+
const schemaKey = props.schemaKey
|
|
58
|
+
const { schema, model } = toRefs(props)
|
|
59
|
+
|
|
60
|
+
const name=ref('input')
|
|
61
|
+
const dtoValue=ref()
|
|
62
|
+
const placeholder=ref('')
|
|
63
|
+
const validTips=ref(null) // 校验错误信息
|
|
64
|
+
|
|
65
|
+
const initData=()=>{
|
|
66
|
+
dtoValue.value=model.value ?? schema.value.option?.default
|
|
67
|
+
validTips.value=null
|
|
68
|
+
|
|
69
|
+
const {
|
|
70
|
+
minLength,
|
|
71
|
+
maxLength,
|
|
72
|
+
pattern,
|
|
73
|
+
}=schema.value
|
|
74
|
+
|
|
75
|
+
const ruleList=[]
|
|
76
|
+
if(schema.value.option?.placeholder){
|
|
77
|
+
ruleList.push(schema.value.option.placeholder)
|
|
78
|
+
}else if(minLength){
|
|
79
|
+
ruleList.push(`最小长度应为 ${minLength}`)
|
|
80
|
+
}
|
|
81
|
+
if(maxLength){
|
|
82
|
+
ruleList.push(`最大长度应为 ${maxLength}`)
|
|
83
|
+
}
|
|
84
|
+
if(pattern){
|
|
85
|
+
ruleList.push(`格式不正确;${pattern}`)
|
|
86
|
+
}
|
|
87
|
+
placeholder.value=ruleList.join('|')
|
|
88
|
+
}
|
|
89
|
+
onMounted(()=>{
|
|
90
|
+
initData()
|
|
91
|
+
})
|
|
92
|
+
watch([schema,model],()=>{
|
|
93
|
+
initData()
|
|
94
|
+
})
|
|
95
|
+
// 对外暴露:表单校验方法
|
|
96
|
+
const validate = () => {
|
|
97
|
+
// 清空上一轮校验错误提示
|
|
98
|
+
validTips.value = null;
|
|
99
|
+
// 解构取出schema里的type字段
|
|
100
|
+
const { type } = schema.value;
|
|
101
|
+
|
|
102
|
+
// 1、必填项校验
|
|
103
|
+
if (schema.value.option?.required && !dtoValue.value) {
|
|
104
|
+
validTips.value = '不能为空';
|
|
105
|
+
return false; // 校验失败,终止后续逻辑
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 2、输入有内容时,使用AJV库校验JSON Schema规则
|
|
109
|
+
if (dtoValue.value) {
|
|
110
|
+
// 编译当前字段的schema校验规则,生成校验函数
|
|
111
|
+
const validate = ajv.compile(schema.value);
|
|
112
|
+
// 执行校验:传入当前输入值
|
|
113
|
+
const valid = validate(dtoValue.value);
|
|
114
|
+
|
|
115
|
+
// 校验不通过 并且 存在错误详情
|
|
116
|
+
if (!valid && validate.errors && validate.errors[0]) {
|
|
117
|
+
// 取出第一条错误的关键字、参数
|
|
118
|
+
const { keyword, params } = validate.errors[0];
|
|
119
|
+
|
|
120
|
+
// 根据不同错误类型,自定义中文提示文案(把AJV默认英文提示转为中文)
|
|
121
|
+
if (keyword === 'type') {
|
|
122
|
+
validTips.value = `类型必须为 ${type}`;
|
|
123
|
+
} else if (keyword === 'maxLength') {
|
|
124
|
+
validTips.value = `最大长度应为 ${params.limit}`;
|
|
125
|
+
} else if (keyword === 'minLength') {
|
|
126
|
+
validTips.value = `最小长度应为 ${params.limit}`;
|
|
127
|
+
} else if (keyword === 'pattern') {
|
|
128
|
+
validTips.value = '格式不正确';
|
|
129
|
+
} else {
|
|
130
|
+
// 其余未知错误兜底文案
|
|
131
|
+
console.log(validate.errors[0]);
|
|
132
|
+
validTips.value = '不符合要求';
|
|
133
|
+
}
|
|
134
|
+
return false; // 格式校验失败
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 所有校验全部通过
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
// 获得焦点时,清空校验错误提示
|
|
142
|
+
const onFocus = () => {
|
|
143
|
+
validTips.value = null;
|
|
144
|
+
}
|
|
145
|
+
// 失去焦点时,校验当前输入值
|
|
146
|
+
const onBlur = () => {
|
|
147
|
+
validate()
|
|
148
|
+
}
|
|
149
|
+
// 对外暴露:获取当前组件表单值
|
|
150
|
+
const getValue = () => {
|
|
151
|
+
return dtoValue.value !== undefined ? {
|
|
152
|
+
[schemaKey]:dtoValue.value
|
|
153
|
+
}:{}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 暴露方法给上层表单组件调用(全局表单提交校验、取值)
|
|
157
|
+
defineExpose({
|
|
158
|
+
name,
|
|
159
|
+
validate,
|
|
160
|
+
getValue
|
|
161
|
+
})
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
<style lang="less">
|
|
165
|
+
</style>
|