@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.
Files changed (86) hide show
  1. package/.eslintignore +3 -0
  2. package/.eslintrc +55 -0
  3. package/.tmp-check.js +47 -0
  4. package/README.md +287 -0
  5. package/app/controller/base.js +37 -0
  6. package/app/controller/project.js +76 -0
  7. package/app/controller/view.js +17 -0
  8. package/app/extend/logger.js +36 -0
  9. package/app/middleware/api-params-verify.js +78 -0
  10. package/app/middleware/api-sign-verify.js +31 -0
  11. package/app/middleware/error-handler.js +36 -0
  12. package/app/middleware/project-handler.js +26 -0
  13. package/app/middleware.js +69 -0
  14. package/app/pages/asserts/custom.css +10 -0
  15. package/app/pages/boot.js +44 -0
  16. package/app/pages/common/curl.js +76 -0
  17. package/app/pages/common/utils.js +2 -0
  18. package/app/pages/dashboard/complex-view/header-view/complex-view/sub-menu/sub-menu.vue +19 -0
  19. package/app/pages/dashboard/complex-view/header-view/header-view.vue +131 -0
  20. package/app/pages/dashboard/complex-view/iframe-view/iframe-view.vue +29 -0
  21. package/app/pages/dashboard/complex-view/schema-view/complex-view/search-pannel/search-pannel.vue +36 -0
  22. package/app/pages/dashboard/complex-view/schema-view/complex-view/table-pannel/table-pannel.vue +125 -0
  23. package/app/pages/dashboard/complex-view/schema-view/components/component-config.js +19 -0
  24. package/app/pages/dashboard/complex-view/schema-view/components/create-form/create-form.vue +119 -0
  25. package/app/pages/dashboard/complex-view/schema-view/components/detail-panel/detail-panel.vue +131 -0
  26. package/app/pages/dashboard/complex-view/schema-view/components/edit-form/edit-form.vue +149 -0
  27. package/app/pages/dashboard/complex-view/schema-view/hook/schema.js +126 -0
  28. package/app/pages/dashboard/complex-view/schema-view/schema-view.vue +111 -0
  29. package/app/pages/dashboard/complex-view/sider-view/complex-view/sub-menu/sub-menu.vue +28 -0
  30. package/app/pages/dashboard/complex-view/sider-view/sider-view.vue +138 -0
  31. package/app/pages/dashboard/dashboard.vue +121 -0
  32. package/app/pages/dashboard/entry.dashboard.js +52 -0
  33. package/app/pages/dashboard/router.js +3 -0
  34. package/app/pages/dashboard/todo/todo.vue +10 -0
  35. package/app/pages/store/index.js +3 -0
  36. package/app/pages/store/menu.js +71 -0
  37. package/app/pages/store/project.js +15 -0
  38. package/app/pages/widgets/header-container/asserts/avatar.png +0 -0
  39. package/app/pages/widgets/header-container/asserts/logo.png +0 -0
  40. package/app/pages/widgets/header-container/header-container.vue +118 -0
  41. package/app/pages/widgets/schema-form/complex-view/input/input.vue +165 -0
  42. package/app/pages/widgets/schema-form/complex-view/input-number/input-number.vue +164 -0
  43. package/app/pages/widgets/schema-form/complex-view/select/select.vue +138 -0
  44. package/app/pages/widgets/schema-form/form-item-config.js +20 -0
  45. package/app/pages/widgets/schema-form/schema-form.vue +154 -0
  46. package/app/pages/widgets/schema-search-bar/complex-view/date-range/date-range.vue +49 -0
  47. package/app/pages/widgets/schema-search-bar/complex-view/dynamic-select/dynamic-select.vue +65 -0
  48. package/app/pages/widgets/schema-search-bar/complex-view/input/input.vue +44 -0
  49. package/app/pages/widgets/schema-search-bar/complex-view/select/select.vue +50 -0
  50. package/app/pages/widgets/schema-search-bar/schema-search-bar.vue +134 -0
  51. package/app/pages/widgets/schema-search-bar/search-item-config.js +27 -0
  52. package/app/pages/widgets/schema-table/schema-table.vue +277 -0
  53. package/app/pages/widgets/sider-container/sider-container.vue +42 -0
  54. package/app/public/static/logo.png +0 -0
  55. package/app/public/static/normalize.css +239 -0
  56. package/app/router/project.js +7 -0
  57. package/app/router/view.js +7 -0
  58. package/app/router-schema/project.js +34 -0
  59. package/app/service/base.js +10 -0
  60. package/app/service/project.js +43 -0
  61. package/app/view/entry.tpl +31 -0
  62. package/app/webpack/config/webpack.base.js +224 -0
  63. package/app/webpack/config/webpack.dev.js +53 -0
  64. package/app/webpack/config/webpack.prod.js +104 -0
  65. package/app/webpack/dev.js +54 -0
  66. package/app/webpack/libs/blank.js +1 -0
  67. package/app/webpack/prod.js +17 -0
  68. package/config/config.default.js +3 -0
  69. package/docs/dashboard-model.js +150 -0
  70. package/elpis-core/env.js +21 -0
  71. package/elpis-core/index.js +82 -0
  72. package/elpis-core/loader/config.js +58 -0
  73. package/elpis-core/loader/controller.js +55 -0
  74. package/elpis-core/loader/extend.js +42 -0
  75. package/elpis-core/loader/middleware.js +50 -0
  76. package/elpis-core/loader/router-schema.js +28 -0
  77. package/elpis-core/loader/router.js +42 -0
  78. package/elpis-core/loader/service.js +51 -0
  79. package/index.js +37 -0
  80. package/logs/application.log +50 -0
  81. package/logs/application.log.2026-06-13 +159 -0
  82. package/model/index.js +119 -0
  83. package/package.json +88 -0
  84. package/scripts/generate-cover.py +93 -0
  85. package/scripts/generate-diagrams.js +80 -0
  86. 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,3 @@
1
+ module.exports = () => {
2
+ // 默认空实现:业务项目未提供 dashboard 路由扩展时不注入额外路由
3
+ }
@@ -0,0 +1,10 @@
1
+ <template>
2
+ <h1>待开发</h1>
3
+ </template>
4
+ <script>
5
+
6
+ </script>
7
+
8
+ <style lang="less" scoped>
9
+
10
+ </style>
@@ -0,0 +1,3 @@
1
+ import { createPinia } from 'pinia'
2
+ const pinia=createPinia()
3
+ export default pinia
@@ -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
+ })
@@ -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>