Merge pull request #301 from kailong321200875/master

This commit is contained in:
Archer
2023-08-06 11:14:15 +08:00
committed by GitHub
163 changed files with 8933 additions and 5893 deletions

View File

@@ -2,7 +2,7 @@
NODE_ENV=development
# 接口前缀
VITE_API_BASEPATH=base
VITE_API_BASE_PATH=base
# 打包路径
VITE_BASE_PATH=/

View File

@@ -2,7 +2,7 @@
NODE_ENV=production
# 接口前缀
VITE_API_BASEPATH=dev
VITE_API_BASE_PATH=dev
# 打包路径
VITE_BASE_PATH=/dist-dev/

View File

@@ -2,7 +2,7 @@
NODE_ENV=production
# 接口前缀
VITE_API_BASEPATH=pro
VITE_API_BASE_PATH=pro
# 打包路径
VITE_BASE_PATH=/vue-element-plus-admin/

View File

@@ -2,7 +2,7 @@
NODE_ENV=production
# 接口前缀
VITE_API_BASEPATH=pro
VITE_API_BASE_PATH=pro
# 打包路径
VITE_BASE_PATH=/

View File

@@ -2,7 +2,7 @@
NODE_ENV=production
# 接口前缀
VITE_API_BASEPATH=test
VITE_API_BASE_PATH=test
# 打包路径
VITE_BASE_PATH=/dist-test/

View File

@@ -24,6 +24,7 @@ jobs:
- name: Automerge
uses: 'pascalgn/automerge-action@v0.14.3'
env:
BASE_BRANCHES: 'release'
GITHUB_TOKEN: '${{ secrets.TOKEN }}'
MERGE_LABELS: ''
MERGE_FILTER_AUTHOR: 'kailong321200875'

View File

@@ -1,7 +1,7 @@
on:
push:
branches:
- master
- release
name: Release

0
.husky/commit-msg Normal file → Executable file
View File

0
.husky/pre-commit Normal file → Executable file
View File

219
mock/department/index.ts Normal file
View File

@@ -0,0 +1,219 @@
import config from '@/config/axios/config'
import { MockMethod } from 'vite-plugin-mock'
import { toAnyString } from '@/utils'
import Mock from 'mockjs'
const { code } = config
const departmentList: any = []
const citys = ['厦门总公司', '北京分公司', '上海分公司', '福州分公司', '深圳分公司', '杭州分公司']
for (let i = 0; i < 5; i++) {
departmentList.push({
// 部门名称
departmentName: citys[i],
id: toAnyString(),
createTime: '@datetime',
// 状态
status: Mock.Random.integer(0, 1),
// 备注
remark: '@cword(10, 15)',
children: [
{
// 部门名称
departmentName: '研发部',
createTime: '@datetime',
// 状态
status: Mock.Random.integer(0, 1),
id: toAnyString(),
remark: '@cword(10, 15)'
},
{
// 部门名称
departmentName: '产品部',
createTime: '@datetime',
// 状态
status: Mock.Random.integer(0, 1),
id: toAnyString(),
remark: '@cword(10, 15)'
},
{
// 部门名称
departmentName: '运营部',
createTime: '@datetime',
// 状态
status: Mock.Random.integer(0, 1),
id: toAnyString(),
remark: '@cword(10, 15)'
},
{
// 部门名称
departmentName: '市场部',
createTime: '@datetime',
// 状态
status: Mock.Random.integer(0, 1),
id: toAnyString(),
remark: '@cword(10, 15)'
},
{
// 部门名称
departmentName: '销售部',
createTime: '@datetime',
// 状态
status: Mock.Random.integer(0, 1),
id: toAnyString(),
remark: '@cword(10, 15)'
},
{
// 部门名称
departmentName: '客服部',
createTime: '@datetime',
// 状态
status: Mock.Random.integer(0, 1),
id: toAnyString(),
remark: '@cword(10, 15)'
}
]
})
}
export default [
// 列表接口
{
url: '/department/list',
method: 'get',
response: () => {
return {
data: {
code: code,
data: {
list: departmentList
}
}
}
}
},
{
url: '/department/table/list',
method: 'get',
response: () => {
return {
data: {
code: code,
data: {
list: departmentList,
total: 5
}
}
}
}
},
{
url: '/department/users',
method: 'get',
timeout: 1000,
response: ({ query }) => {
const { pageSize } = query
// 根据pageSize来创建数据
const mockList: any = []
for (let i = 0; i < pageSize; i++) {
mockList.push(
Mock.mock({
// 用户名
username: '@cname',
// 账号
account: '@first',
// 邮箱
email: '@EMAIL',
// 创建时间
createTime: '@datetime',
// 角色
role: '@first',
// 用户id
id: toAnyString()
})
)
}
return {
data: {
code: code,
data: {
total: 100,
list: mockList
}
}
}
}
},
// 保存接口
{
url: '/department/user/save',
method: 'post',
timeout: 1000,
response: () => {
return {
data: {
code: code,
data: 'success'
}
}
}
},
// 删除接口
{
url: '/department/user/delete',
method: 'post',
response: ({ body }) => {
const ids = body.ids
if (!ids) {
return {
code: '500',
message: '请选择需要删除的数据'
}
} else {
return {
data: {
code: code,
data: 'success'
}
}
}
}
},
// 保存接口
{
url: '/department/save',
method: 'post',
timeout: 1000,
response: () => {
return {
data: {
code: code,
data: 'success'
}
}
}
},
// 删除接口
{
url: '/department/delete',
method: 'post',
response: ({ body }) => {
const ids = body.ids
if (!ids) {
return {
code: '500',
message: '请选择需要删除的数据'
}
} else {
return {
data: {
code: code,
data: 'success'
}
}
}
}
}
] as MockMethod[]

250
mock/menu/index.ts Normal file
View File

@@ -0,0 +1,250 @@
import config from '@/config/axios/config'
import { MockMethod } from 'vite-plugin-mock'
import Mock from 'mockjs'
const { code } = config
const timeout = 1000
export default [
// 列表接口
{
url: '/menu/list',
method: 'get',
timeout,
response: () => {
return {
data: {
code: code,
data: {
list: [
{
path: '/dashboard',
component: '#',
redirect: '/dashboard/analysis',
name: 'Dashboard',
status: Mock.Random.integer(0, 1),
id: 1,
meta: {
title: '首页',
icon: 'ant-design:dashboard-filled',
alwaysShow: true
},
children: [
{
path: 'analysis',
component: 'views/Dashboard/Analysis',
name: 'Analysis',
status: Mock.Random.integer(0, 1),
id: 2,
meta: {
title: '分析页',
noCache: true
}
},
{
path: 'workplace',
component: 'views/Dashboard/Workplace',
name: 'Workplace',
status: Mock.Random.integer(0, 1),
id: 3,
meta: {
title: '工作台',
noCache: true
}
}
]
},
{
path: '/external-link',
component: '#',
meta: {
title: '文档',
icon: 'clarity:document-solid'
},
name: 'ExternalLink',
status: Mock.Random.integer(0, 1),
id: 4,
children: [
{
path: 'https://element-plus-admin-doc.cn/',
name: 'DocumentLink',
status: Mock.Random.integer(0, 1),
id: 5,
meta: {
title: '文档'
}
}
]
},
{
path: '/level',
component: '#',
redirect: '/level/menu1/menu1-1/menu1-1-1',
name: 'Level',
status: Mock.Random.integer(0, 1),
id: 6,
meta: {
title: '菜单',
icon: 'carbon:skill-level-advanced'
},
children: [
{
path: 'menu1',
name: 'Menu1',
component: '##',
status: Mock.Random.integer(0, 1),
id: 7,
redirect: '/level/menu1/menu1-1/menu1-1-1',
meta: {
title: '菜单1'
},
children: [
{
path: 'menu1-1',
name: 'Menu11',
component: '##',
status: Mock.Random.integer(0, 1),
id: 8,
redirect: '/level/menu1/menu1-1/menu1-1-1',
meta: {
title: '菜单1-1',
alwaysShow: true
},
children: [
{
path: 'menu1-1-1',
name: 'Menu111',
component: 'views/Level/Menu111',
status: Mock.Random.integer(0, 1),
id: 9,
permission: ['edit', 'add', 'delete'],
meta: {
title: '菜单1-1-1'
}
}
]
},
{
path: 'menu1-2',
name: 'Menu12',
component: 'views/Level/Menu12',
status: Mock.Random.integer(0, 1),
id: 10,
permission: ['edit', 'add', 'delete'],
meta: {
title: '菜单1-2'
}
}
]
},
{
path: 'menu2',
name: 'Menu2Demo',
component: 'views/Level/Menu2',
status: Mock.Random.integer(0, 1),
id: 11,
permission: ['edit', 'add', 'delete'],
meta: {
title: '菜单2'
}
}
]
},
{
path: '/example',
component: '#',
redirect: '/example/example-dialog',
name: 'Example',
status: Mock.Random.integer(0, 1),
id: 12,
meta: {
title: '综合示例',
icon: 'ep:management',
alwaysShow: true
},
children: [
{
path: 'example-dialog',
component: 'views/Example/Dialog/ExampleDialog',
name: 'ExampleDialog',
status: Mock.Random.integer(0, 1),
id: 13,
permission: ['edit', 'add', 'delete'],
meta: {
title: '综合示例-弹窗',
permission: ['edit', 'add']
}
},
{
path: 'example-page',
component: 'views/Example/Page/ExamplePage',
name: 'ExamplePage',
status: Mock.Random.integer(0, 1),
id: 14,
permission: ['edit', 'add', 'delete'],
meta: {
title: '综合示例-页面',
permission: ['edit', 'add']
}
},
{
path: 'example-add',
component: 'views/Example/Page/ExampleAdd',
name: 'ExampleAdd',
status: Mock.Random.integer(0, 1),
id: 15,
permission: ['edit', 'add', 'delete'],
meta: {
title: '综合示例-新增',
noTagsView: true,
noCache: true,
hidden: true,
showMainRoute: true,
activeMenu: '/example/example-page',
permission: ['delete', 'add']
}
},
{
path: 'example-edit',
component: 'views/Example/Page/ExampleEdit',
name: 'ExampleEdit',
status: Mock.Random.integer(0, 1),
id: 16,
permission: ['edit', 'add', 'delete'],
meta: {
title: '综合示例-编辑',
noTagsView: true,
noCache: true,
hidden: true,
showMainRoute: true,
activeMenu: '/example/example-page',
permission: ['delete', 'add']
}
},
{
path: 'example-detail',
component: 'views/Example/Page/ExampleDetail',
name: 'ExampleDetail',
status: Mock.Random.integer(0, 1),
id: 17,
permission: ['edit', 'add', 'delete'],
meta: {
title: '综合示例-详情',
noTagsView: true,
noCache: true,
hidden: true,
showMainRoute: true,
activeMenu: '/example/example-page',
permission: ['delete', 'edit']
}
}
]
}
]
}
}
}
}
}
] as MockMethod[]

View File

@@ -1,5 +1,7 @@
import config from '@/config/axios/config'
import { MockMethod } from 'vite-plugin-mock'
import Mock from 'mockjs'
import { toAnyString } from '@/utils'
const { code } = config
@@ -105,14 +107,6 @@ const adminList = [
meta: {
title: 'UseForm'
}
},
{
path: 'ref-form',
component: 'views/Components/Form/RefForm',
name: 'RefForm',
meta: {
title: 'RefForm'
}
}
]
},
@@ -143,13 +137,29 @@ const adminList = [
}
},
{
path: 'ref-table',
component: 'views/Components/Table/RefTable',
name: 'RefTable',
path: 'tree-table',
component: 'views/Components/Table/TreeTable',
name: 'TreeTable',
meta: {
title: 'RefTable'
title: 'TreeTable'
}
},
{
path: 'table-image-preview',
component: 'views/Components/Table/TableImagePreview',
name: 'TableImagePreview',
meta: {
title: 'router.PicturePreview'
}
}
// {
// path: 'ref-table',
// component: 'views/Components/Table/RefTable',
// name: 'RefTable',
// meta: {
// title: 'RefTable'
// }
// }
]
},
{
@@ -259,14 +269,6 @@ const adminList = [
meta: {
title: 'router.inputPassword'
}
},
{
path: 'sticky',
component: 'views/Components/Sticky',
name: 'Sticky',
meta: {
title: 'router.sticky'
}
}
]
},
@@ -290,13 +292,21 @@ const adminList = [
}
},
{
path: 'useCrudSchemas',
component: 'views/hooks/useCrudSchemas',
name: 'UseCrudSchemas',
path: 'useOpenTab',
component: 'views/hooks/useOpenTab',
name: 'UseOpenTab',
meta: {
title: 'useCrudSchemas'
title: 'useOpenTab'
}
}
// {
// path: 'useCrudSchemas',
// component: 'views/hooks/useCrudSchemas',
// name: 'UseCrudSchemas',
// meta: {
// title: 'useCrudSchemas'
// }
// }
]
},
{
@@ -462,6 +472,59 @@ const adminList = [
}
}
]
},
{
path: '/authorization',
component: '#',
redirect: '/authorization/user',
name: 'Authorization',
meta: {
title: 'router.authorization',
icon: 'eos-icons:role-binding',
alwaysShow: true
},
children: [
{
path: 'department',
component: 'views/Authorization/Department/Department',
name: 'Department',
meta: {
title: 'router.department'
}
},
{
path: 'user',
component: 'views/Authorization/User/User',
name: 'User',
meta: {
title: 'router.user'
}
},
{
path: 'menu',
component: 'views/Authorization/Menu/Menu',
name: 'Menu',
meta: {
title: 'router.menuManagement'
}
},
{
path: 'role',
component: 'views/Authorization/Role/Role',
name: 'Role',
meta: {
title: 'router.role'
}
},
{
path: 'test',
component: 'views/Authorization/Test/Test',
name: 'Test',
meta: {
title: 'router.permission'
}
}
]
}
]
@@ -481,6 +544,8 @@ const testList: string[] = [
'/components/table',
'/components/table/default-table',
'/components/table/use-table',
'/components/table/tree-table',
'/components/table/table-image-preview',
'/components/table/ref-table',
'/components/editor-demo',
'/components/editor-demo/editor',
@@ -498,7 +563,8 @@ const testList: string[] = [
'/Components/Sticky',
'/hooks',
'/hooks/useWatermark',
'/hooks/useCrudSchemas',
'/hooks/useOpenTab',
// '/hooks/useCrudSchemas',
'/level',
'/level/menu1',
'/level/menu1/menu1-1',
@@ -511,12 +577,441 @@ const testList: string[] = [
'/example/example-add',
'/example/example-edit',
'/example/example-detail',
'/authorization',
'/authorization/department',
'/authorization/user',
'/authorization/role',
'/authorization/menu',
'/authorization/test',
'/error',
'/error/404-demo',
'/error/403-demo',
'/error/500-demo'
]
const List: any[] = []
const roleNames = ['超级管理员', '管理员', '普通用户', '游客']
const menus = [
[
{
path: '/dashboard',
component: '#',
redirect: '/dashboard/analysis',
name: 'Dashboard',
status: Mock.Random.integer(0, 1),
id: 1,
meta: {
title: '首页',
icon: 'ant-design:dashboard-filled',
alwaysShow: true
},
children: [
{
path: 'analysis',
component: 'views/Dashboard/Analysis',
name: 'Analysis',
status: Mock.Random.integer(0, 1),
id: 2,
meta: {
title: '分析页',
noCache: true
}
},
{
path: 'workplace',
component: 'views/Dashboard/Workplace',
name: 'Workplace',
status: Mock.Random.integer(0, 1),
id: 3,
meta: {
title: '工作台',
noCache: true
}
}
]
},
{
path: '/external-link',
component: '#',
meta: {
title: '文档',
icon: 'clarity:document-solid'
},
name: 'ExternalLink',
status: Mock.Random.integer(0, 1),
id: 4,
children: [
{
path: 'https://element-plus-admin-doc.cn/',
name: 'DocumentLink',
status: Mock.Random.integer(0, 1),
id: 5,
meta: {
title: '文档'
}
}
]
},
{
path: '/level',
component: '#',
redirect: '/level/menu1/menu1-1/menu1-1-1',
name: 'Level',
status: Mock.Random.integer(0, 1),
id: 6,
meta: {
title: '菜单',
icon: 'carbon:skill-level-advanced'
},
children: [
{
path: 'menu1',
name: 'Menu1',
component: '##',
status: Mock.Random.integer(0, 1),
id: 7,
redirect: '/level/menu1/menu1-1/menu1-1-1',
meta: {
title: '菜单1'
},
children: [
{
path: 'menu1-1',
name: 'Menu11',
component: '##',
status: Mock.Random.integer(0, 1),
id: 8,
redirect: '/level/menu1/menu1-1/menu1-1-1',
meta: {
title: '菜单1-1',
alwaysShow: true
},
children: [
{
path: 'menu1-1-1',
name: 'Menu111',
component: 'views/Level/Menu111',
status: Mock.Random.integer(0, 1),
id: 9,
permission: ['edit', 'add', 'delete'],
meta: {
title: '菜单1-1-1',
permission: ['edit', 'add', 'delete']
}
}
]
},
{
path: 'menu1-2',
name: 'Menu12',
component: 'views/Level/Menu12',
status: Mock.Random.integer(0, 1),
id: 10,
permission: ['edit', 'add', 'delete'],
meta: {
title: '菜单1-2',
permission: ['edit', 'add', 'delete']
}
}
]
},
{
path: 'menu2',
name: 'Menu2Demo',
component: 'views/Level/Menu2',
status: Mock.Random.integer(0, 1),
id: 11,
permission: ['edit', 'add', 'delete'],
meta: {
title: '菜单2',
permission: ['edit', 'add', 'delete']
}
}
]
},
{
path: '/example',
component: '#',
redirect: '/example/example-dialog',
name: 'Example',
status: Mock.Random.integer(0, 1),
id: 12,
meta: {
title: '综合示例',
icon: 'ep:management',
alwaysShow: true
},
children: [
{
path: 'example-dialog',
component: 'views/Example/Dialog/ExampleDialog',
name: 'ExampleDialog',
status: Mock.Random.integer(0, 1),
id: 13,
permission: ['edit', 'add', 'delete'],
meta: {
title: '综合示例-弹窗',
permission: ['edit', 'add', 'delete']
}
},
{
path: 'example-page',
component: 'views/Example/Page/ExamplePage',
name: 'ExamplePage',
status: Mock.Random.integer(0, 1),
id: 14,
permission: ['edit', 'add', 'delete'],
meta: {
title: '综合示例-页面',
permission: ['edit', 'add', 'delete']
}
},
{
path: 'example-add',
component: 'views/Example/Page/ExampleAdd',
name: 'ExampleAdd',
status: Mock.Random.integer(0, 1),
id: 15,
permission: ['edit', 'add', 'delete'],
meta: {
title: '综合示例-新增',
noTagsView: true,
noCache: true,
hidden: true,
showMainRoute: true,
activeMenu: '/example/example-page',
permission: ['edit', 'add', 'delete']
}
},
{
path: 'example-edit',
component: 'views/Example/Page/ExampleEdit',
name: 'ExampleEdit',
status: Mock.Random.integer(0, 1),
id: 16,
permission: ['edit', 'add', 'delete'],
meta: {
title: '综合示例-编辑',
noTagsView: true,
noCache: true,
hidden: true,
showMainRoute: true,
activeMenu: '/example/example-page',
permission: ['edit', 'add', 'delete']
}
},
{
path: 'example-detail',
component: 'views/Example/Page/ExampleDetail',
name: 'ExampleDetail',
status: Mock.Random.integer(0, 1),
id: 17,
permission: ['edit', 'add', 'delete'],
meta: {
title: '综合示例-详情',
noTagsView: true,
noCache: true,
hidden: true,
showMainRoute: true,
activeMenu: '/example/example-page',
permission: ['edit', 'add', 'delete']
}
}
]
}
],
[
{
path: '/dashboard',
component: '#',
redirect: '/dashboard/analysis',
name: 'Dashboard',
status: Mock.Random.integer(0, 1),
id: 1,
meta: {
title: '首页',
icon: 'ant-design:dashboard-filled',
alwaysShow: true
},
children: [
{
path: 'analysis',
component: 'views/Dashboard/Analysis',
name: 'Analysis',
status: Mock.Random.integer(0, 1),
id: 2,
meta: {
title: '分析页',
noCache: true
}
},
{
path: 'workplace',
component: 'views/Dashboard/Workplace',
name: 'Workplace',
status: Mock.Random.integer(0, 1),
id: 3,
meta: {
title: '工作台',
noCache: true
}
}
]
}
],
[
{
path: '/external-link',
component: '#',
meta: {
title: '文档',
icon: 'clarity:document-solid'
},
name: 'ExternalLink',
status: Mock.Random.integer(0, 1),
id: 4,
children: [
{
path: 'https://element-plus-admin-doc.cn/',
name: 'DocumentLink',
status: Mock.Random.integer(0, 1),
id: 5,
meta: {
title: '文档'
}
}
]
},
{
path: '/level',
component: '#',
redirect: '/level/menu1/menu1-1/menu1-1-1',
name: 'Level',
status: Mock.Random.integer(0, 1),
id: 6,
meta: {
title: '菜单',
icon: 'carbon:skill-level-advanced'
},
children: [
{
path: 'menu1',
name: 'Menu1',
component: '##',
status: Mock.Random.integer(0, 1),
id: 7,
redirect: '/level/menu1/menu1-1/menu1-1-1',
meta: {
title: '菜单1'
},
children: [
{
path: 'menu1-1',
name: 'Menu11',
component: '##',
status: Mock.Random.integer(0, 1),
id: 8,
redirect: '/level/menu1/menu1-1/menu1-1-1',
meta: {
title: '菜单1-1',
alwaysShow: true
},
children: [
{
path: 'menu1-1-1',
name: 'Menu111',
component: 'views/Level/Menu111',
status: Mock.Random.integer(0, 1),
id: 9,
permission: ['edit', 'add', 'delete'],
meta: {
title: '菜单1-1-1',
permission: ['edit', 'add', 'delete']
}
}
]
},
{
path: 'menu1-2',
name: 'Menu12',
component: 'views/Level/Menu12',
status: Mock.Random.integer(0, 1),
id: 10,
permission: ['edit', 'add', 'delete'],
meta: {
title: '菜单1-2',
permission: ['edit', 'add', 'delete']
}
}
]
},
{
path: 'menu2',
name: 'Menu2Demo',
component: 'views/Level/Menu2',
status: Mock.Random.integer(0, 1),
id: 11,
permission: ['edit', 'add', 'delete'],
meta: {
title: '菜单2',
permission: ['edit', 'add', 'delete']
}
}
]
}
],
[
{
path: '/example',
component: '#',
redirect: '/example/example-dialog',
name: 'Example',
status: Mock.Random.integer(0, 1),
id: 12,
meta: {
title: '综合示例',
icon: 'ep:management',
alwaysShow: true
},
children: [
{
path: 'example-detail',
component: 'views/Example/Page/ExampleDetail',
name: 'ExampleDetail',
status: Mock.Random.integer(0, 1),
id: 17,
permission: ['edit', 'add', 'delete'],
meta: {
title: '综合示例-详情',
noTagsView: true,
noCache: true,
hidden: true,
showMainRoute: true,
activeMenu: '/example/example-page',
permission: ['edit', 'add', 'delete']
}
}
]
}
]
]
for (let i = 0; i < 4; i++) {
List.push(
Mock.mock({
id: toAnyString(),
// timestamp: +Mock.Random.date('T'),
roleName: roleNames[i],
role: '@first',
status: Mock.Random.integer(0, 1),
createTime: '@datetime',
remark: '@cword(10, 15)',
menu: menus[i]
})
)
}
export default [
// 列表接口
{
@@ -532,5 +1027,21 @@ export default [
}
}
}
},
{
url: '/role/table',
method: 'get',
timeout,
response: () => {
return {
data: {
code: code,
data: {
list: List,
total: 4
}
}
}
}
}
] as MockMethod[]

View File

@@ -12,7 +12,7 @@ const count = 100
const baseContent =
'<p>I am testing data, I am testing data.</p><p><img src="https://wpimg.wallstcn.com/4c69009c-0fd4-4153-b112-6cb53d1cf943"></p>'
let List: {
interface ListProps {
id: string
author: string
title: string
@@ -20,7 +20,21 @@ let List: {
importance: number
display_time: string
pageviews: number
}[] = []
image_uri: string
}
interface TreeListProps {
id: string
author: string
title: string
content: string
importance: number
display_time: string
pageviews: number
children: TreeListProps[]
}
let List: ListProps[] = []
for (let i = 0; i < count; i++) {
List.push(
@@ -32,13 +46,120 @@ for (let i = 0; i < count; i++) {
content: baseContent,
importance: '@integer(1, 3)',
display_time: '@datetime',
pageviews: '@integer(300, 5000)'
pageviews: '@integer(300, 5000)',
image_uri: Mock.Random.image('@integer(300, 5000)x@integer(300, 5000)')
})
)
}
const treeList: TreeListProps[] = []
for (let i = 0; i < count; i++) {
treeList.push(
Mock.mock({
id: toAnyString(),
// timestamp: +Mock.Random.date('T'),
author: '@first',
title: '@title(5, 10)',
content: baseContent,
importance: '@integer(1, 3)',
display_time: '@datetime',
pageviews: '@integer(300, 5000)',
children: [
{
id: toAnyString(),
// timestamp: +Mock.Random.date('T'),
author: '@first',
title: '@title(5, 10)',
content: baseContent,
importance: '@integer(1, 3)',
display_time: '@datetime',
pageviews: '@integer(300, 5000)',
children: [
{
id: toAnyString(),
// timestamp: +Mock.Random.date('T'),
author: '@first',
title: '@title(5, 10)',
content: baseContent,
importance: '@integer(1, 3)',
display_time: '@datetime',
pageviews: '@integer(300, 5000)'
},
{
id: toAnyString(),
// timestamp: +Mock.Random.date('T'),
author: '@first',
title: '@title(5, 10)',
content: baseContent,
importance: '@integer(1, 3)',
display_time: '@datetime',
pageviews: '@integer(300, 5000)'
}
]
},
{
id: toAnyString(),
// timestamp: +Mock.Random.date('T'),
author: '@first',
title: '@title(5, 10)',
content: baseContent,
importance: '@integer(1, 3)',
display_time: '@datetime',
pageviews: '@integer(300, 5000)'
},
{
id: toAnyString(),
// timestamp: +Mock.Random.date('T'),
author: '@first',
title: '@title(5, 10)',
content: baseContent,
importance: '@integer(1, 3)',
display_time: '@datetime',
pageviews: '@integer(300, 5000)'
},
{
id: toAnyString(),
// timestamp: +Mock.Random.date('T'),
author: '@first',
title: '@title(5, 10)',
content: baseContent,
importance: '@integer(1, 3)',
display_time: '@datetime',
pageviews: '@integer(300, 5000)'
}
]
// image_uri
})
)
}
export default [
// 树形列表接口
{
url: '/example/treeList',
method: 'get',
timeout,
response: ({ query }) => {
const { title, pageIndex, pageSize } = query
const mockList = treeList.filter((item) => {
if (title && item.title.indexOf(title) < 0) return false
return true
})
const pageList = mockList.filter(
(_, index) => index < pageSize * pageIndex && index >= pageSize * (pageIndex - 1)
)
return {
data: {
code: code,
data: {
total: mockList.length,
list: pageList
}
}
}
}
},
// 列表接口
{
url: '/example/list',

View File

@@ -1,16 +1,16 @@
{
"name": "vue-element-plus-admin",
"version": "1.9.9",
"version": "2.0。0",
"description": "一套基于vue3、element-plus、typesScript、vite4的后台集成方案。",
"author": "Archer <502431556@qq.com>",
"private": false,
"scripts": {
"i": "pnpm install",
"dev": "vite --mode base",
"ts:check": "vue-tsc --noEmit",
"ts:check": "vue-tsc --noEmit --skipLibCheck",
"build:pro": "vite build --mode pro",
"build:gitee": "vite build --mode gitee",
"build:dev": "npm run ts:check && vite build --mode dev",
"build:dev": "vite build --mode dev",
"build:test": "npm run ts:check && vite build --mode test",
"serve:pro": "vite preview --mode pro",
"serve:dev": "vite preview --mode dev",
@@ -26,88 +26,90 @@
"p": "plop"
},
"dependencies": {
"@iconify/iconify": "^3.1.0",
"@vueuse/core": "^10.1.2",
"@iconify/iconify": "^3.1.1",
"@iconify/vue": "^4.1.1",
"@vueuse/core": "^10.2.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^3.0.0",
"@zxcvbn-ts/core": "^3.0.3",
"animate.css": "^4.1.1",
"axios": "^1.4.0",
"dayjs": "^1.11.7",
"echarts": "^5.4.2",
"dayjs": "^1.11.9",
"echarts": "^5.4.3",
"echarts-wordcloud": "^2.1.0",
"element-plus": "2.3.4",
"element-plus": "^2.3.8",
"intro.js": "^7.0.1",
"lodash-es": "^4.17.21",
"mitt": "^3.0.0",
"mitt": "^3.0.1",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"pinia": "^2.0.36",
"pinia": "^2.1.4",
"pinia-plugin-persist": "^1.0.0",
"qrcode": "^1.5.3",
"qs": "^6.11.1",
"url": "^0.11.0",
"vue": "3.2.47",
"qs": "^6.11.2",
"sortablejs": "^1.15.0",
"url": "^0.11.1",
"vue": "3.3.4",
"vue-i18n": "9.2.2",
"vue-router": "^4.1.6",
"vue-types": "^5.0.2",
"web-storage-cache": "^1.1.1"
"vue-router": "^4.2.4",
"vue-types": "^5.1.0"
},
"devDependencies": {
"@commitlint/cli": "^17.6.3",
"@commitlint/config-conventional": "^17.6.3",
"@iconify/json": "^2.2.62",
"@intlify/unplugin-vue-i18n": "^0.10.0",
"@commitlint/cli": "^17.6.7",
"@commitlint/config-conventional": "^17.6.7",
"@iconify/json": "^2.2.92",
"@intlify/unplugin-vue-i18n": "^0.12.2",
"@purge-icons/generated": "^0.9.0",
"@types/intro.js": "^5.1.1",
"@types/lodash-es": "^4.17.7",
"@types/node": "^20.1.1",
"@types/lodash-es": "^4.17.8",
"@types/node": "^20.4.2",
"@types/nprogress": "^0.2.0",
"@types/qrcode": "^1.5.0",
"@types/qrcode": "^1.5.1",
"@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "^5.59.5",
"@typescript-eslint/parser": "^5.59.5",
"@unocss/transformer-variant-group": "^0.51.12",
"@vitejs/plugin-legacy": "^4.0.3",
"@vitejs/plugin-vue": "^4.2.1",
"@types/sortablejs": "^1.15.1",
"@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0",
"@unocss/transformer-variant-group": "^0.53.5",
"@vitejs/plugin-legacy": "^4.1.0",
"@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "^3.0.1",
"@vue-macros/volar": "^0.9.8",
"@vue-macros/volar": "^0.12.2",
"autoprefixer": "^10.4.14",
"consola": "^3.1.0",
"eslint": "^8.40.0",
"consola": "^3.2.3",
"eslint": "^8.45.0",
"eslint-config-prettier": "^8.8.0",
"eslint-define-config": "^1.20.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.11.1",
"eslint-define-config": "^1.21.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.15.1",
"husky": "^8.0.3",
"less": "^4.1.3",
"lint-staged": "^13.2.2",
"lint-staged": "^13.2.3",
"plop": "^3.1.2",
"postcss": "^8.4.23",
"postcss": "^8.4.26",
"postcss-html": "^1.5.0",
"postcss-less": "^6.0.0",
"prettier": "^2.8.8",
"rimraf": "^5.0.0",
"rollup": "^3.21.5",
"stylelint": "^15.6.1",
"prettier": "^3.0.0",
"rimraf": "^5.0.1",
"rollup": "^3.26.3",
"stylelint": "^15.10.1",
"stylelint-config-html": "^1.1.0",
"stylelint-config-prettier": "^9.0.5",
"stylelint-config-recommended": "^12.0.0",
"stylelint-config-standard": "^33.0.0",
"stylelint-config-recommended": "^13.0.0",
"stylelint-config-standard": "^34.0.0",
"stylelint-order": "^6.0.3",
"terser": "^5.17.2",
"typescript": "5.0.4",
"unocss": "^0.51.12",
"unplugin-vue-define-options": "^1.3.5",
"vite": "4.3.5",
"terser": "^5.19.1",
"typescript": "5.1.6",
"unocss": "^0.53.5",
"unplugin-vue-define-options": "^1.3.11",
"vite": "4.4.4",
"vite-plugin-ejs": "^1.6.4",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-mock": "^3.0.0",
"vite-plugin-mock": "2.9.6",
"vite-plugin-progress": "^0.0.7",
"vite-plugin-purge-icons": "^0.9.2",
"vite-plugin-style-import": "2.0.0",
"vite-plugin-svg-icons": "^2.0.1",
"vue-tsc": "^1.6.4"
"vue-tsc": "^1.8.5"
},
"engines": {
"node": ">= 14.18.0"

View File

@@ -4,7 +4,7 @@ import { useAppStore } from '@/store/modules/app'
import { ConfigGlobal } from '@/components/ConfigGlobal'
import { isDark } from '@/utils/is'
import { useDesign } from '@/hooks/web/useDesign'
import { useCache } from '@/hooks/web/useCache'
import { useStorage } from '@/hooks/web/useStorage'
const { getPrefixCls } = useDesign()
@@ -16,12 +16,12 @@ const currentSize = computed(() => appStore.getCurrentSize)
const greyMode = computed(() => appStore.getGreyMode)
const { wsCache } = useCache()
const { getStorage } = useStorage()
// 根据浏览器当前主题设置系统主题色
const setDefaultTheme = () => {
if (wsCache.get('isDark') !== null) {
appStore.setIsDark(wsCache.get('isDark'))
if (getStorage('isDark') !== null) {
appStore.setIsDark(getStorage('isDark'))
return
}
const isDarkTheme = isDark()

View File

@@ -1,11 +1,11 @@
import request from '@/config/axios'
// 获取所有字典
export const getDictApi = (): Promise<IResponse> => {
export const getDictApi = () => {
return request.get({ url: '/dict/list' })
}
// 模拟获取某个字典
export const getDictOneApi = async (): Promise<IResponse> => {
export const getDictOneApi = async () => {
return request.get({ url: '/dict/one' })
}

View File

@@ -0,0 +1,30 @@
import request from '@/config/axios'
import { DepartmentListResponse, DepartmentUserParams, DepartmentUserResponse } from './types'
export const getDepartmentApi = () => {
return request.get<DepartmentListResponse>({ url: '/department/list' })
}
export const getUserByIdApi = (params: DepartmentUserParams) => {
return request.get<DepartmentUserResponse>({ url: '/department/users', params })
}
export const deleteUserByIdApi = (ids: string[] | number[]) => {
return request.post({ url: '/department/user/delete', data: { ids } })
}
export const saveUserApi = (data: any) => {
return request.post({ url: '/department/user/save', data })
}
export const saveDepartmentApi = (data: any) => {
return request.post({ url: '/department/save', data })
}
export const deleteDepartmentApi = (ids: string[] | number[]) => {
return request.post({ url: '/department/delete', data: { ids } })
}
export const getDepartmentTableApi = (params: any) => {
return request.get({ url: '/department/table/list', params })
}

View File

@@ -0,0 +1,32 @@
export interface DepartmentItem {
id: string
departmentName: string
children?: DepartmentItem[]
}
export interface DepartmentListResponse {
list: DepartmentItem[]
}
export interface DepartmentUserParams {
pageSize: number
pageIndex: number
id: string
username?: string
account?: string
}
export interface DepartmentUserItem {
id: string
username: string
account: string
email: string
createTime: string
role: string
department: DepartmentItem
}
export interface DepartmentUserResponse {
list: DepartmentUserItem[]
total: number
}

5
src/api/menu/index.ts Normal file
View File

@@ -0,0 +1,5 @@
import request from '@/config/axios'
export const getMenuListApi = () => {
return request.get({ url: '/menu/list' })
}

5
src/api/role/index.ts Normal file
View File

@@ -0,0 +1,5 @@
import request from '@/config/axios'
export const getRoleListApi = () => {
return request.get({ url: '/role/table' })
}

View File

@@ -1,10 +1,14 @@
import request from '@/config/axios'
import type { TableData } from './types'
export const getTableListApi = (params: any): Promise<IResponse> => {
export const getTableListApi = (params: any) => {
return request.get({ url: '/example/list', params })
}
export const getTreeTableListApi = (params: any) => {
return request.get({ url: '/example/treeList', params })
}
export const saveTableApi = (data: Partial<TableData>): Promise<IResponse> => {
return request.post({ url: '/example/save', data })
}

View File

@@ -1,3 +1,5 @@
import ConfigGlobal from './src/ConfigGlobal.vue'
export type { ConfigGlobalTypes } from './src/types'
export { ConfigGlobal }

View File

@@ -1,20 +1,19 @@
<script setup lang="ts">
import { provide, computed, watch, onMounted } from 'vue'
import { propTypes } from '@/utils/propTypes'
import { ElConfigProvider } from 'element-plus'
import { ComponentSize, ElConfigProvider } from 'element-plus'
import { useLocaleStore } from '@/store/modules/locale'
import { useWindowSize } from '@vueuse/core'
import { useAppStore } from '@/store/modules/app'
import { setCssVar } from '@/utils'
import { useDesign } from '@/hooks/web/useDesign'
import { ElementPlusSize } from '@/types/elementPlus'
const { variables } = useDesign()
const appStore = useAppStore()
const props = defineProps({
size: propTypes.oneOf<ElementPlusSize>(['default', 'small', 'large']).def('default')
size: propTypes.oneOf<ComponentSize>(['default', 'small', 'large']).def('default')
})
provide('configGlobal', props)

View File

@@ -0,0 +1,5 @@
import { ComponentSize } from 'element-plus'
export interface ConfigGlobalTypes {
size?: ComponentSize
}

View File

@@ -1,11 +1,7 @@
<script setup lang="ts">
import { ElCard, ElButton } from 'element-plus'
import { ElCard } from 'element-plus'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
import { ref, onMounted, defineEmits } from 'vue'
import { Sticky } from '@/components/Sticky'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
const { getPrefixCls } = useDesign()
@@ -15,45 +11,15 @@ defineProps({
title: propTypes.string.def(''),
message: propTypes.string.def('')
})
const emit = defineEmits(['back'])
const offset = ref(85)
const contentDetailWrap = ref()
onMounted(() => {
offset.value = contentDetailWrap.value.getBoundingClientRect().top
})
</script>
<template>
<div :class="[`${prefixCls}-container`, 'relative bg-[#fff]']" ref="contentDetailWrap">
<Sticky :offset="offset">
<div
:class="[
`${prefixCls}-header`,
'flex b-b-1 h-50px items-center text-center bg-white pr-10px'
]"
>
<div :class="[`${prefixCls}-header__back`, 'flex pl-10px pr-10px ']">
<el-button @click="emit('back')">
<Icon icon="ep:arrow-left" class="mr-5px" />
{{ t('common.back') }}
</el-button>
</div>
<div :class="[`${prefixCls}-header__title`, 'flex flex-1 justify-center']">
<slot name="title">
<label class="text-16px font-700">{{ title }}</label>
</slot>
</div>
<div :class="[`${prefixCls}-header__right`, 'flex pl-10px pr-10px']">
<slot name="right"></slot>
</div>
<div :class="[`${prefixCls}-container`, 'relative']">
<ElCard :class="[`${prefixCls}-body`, 'mb-20px']" shadow="never">
<div class="mb-20px pb-20px" style="border-bottom: 1px solid var(--el-border-color)">
<slot name="header"></slot>
</div>
</Sticky>
<div style="padding: var(--app-content-padding)">
<ElCard :class="[`${prefixCls}-body`, 'mb-20px']" shadow="never">
<div>
<slot></slot>
</div>
</ElCard>
</div>
<slot></slot>
</ElCard>
</div>
</template>

View File

@@ -2,6 +2,8 @@ import ContextMenu from './src/ContextMenu.vue'
import { ElDropdown } from 'element-plus'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
export type { ContextMenuSchema } from './src/types'
export interface ContextMenuExpose {
elDropdownMenuRef: ComponentRef<typeof ElDropdown>
tagItem: RouteLocationNormalizedLoaded

View File

@@ -4,7 +4,7 @@ import { PropType, ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useDesign } from '@/hooks/web/useDesign'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { contextMenuSchema } from '../../../types/contextMenu'
import { ContextMenuSchema } from './types'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('context-menu')
@@ -15,7 +15,7 @@ const emit = defineEmits(['visibleChange'])
const props = defineProps({
schema: {
type: Array as PropType<contextMenuSchema[]>,
type: Array as PropType<ContextMenuSchema[]>,
default: () => []
},
trigger: {
@@ -28,7 +28,7 @@ const props = defineProps({
}
})
const command = (item: contextMenuSchema) => {
const command = (item: ContextMenuSchema) => {
item.command && item.command(item)
}

View File

@@ -0,0 +1,7 @@
export interface ContextMenuSchema {
disabled?: boolean
divided?: boolean
icon?: string
label: string
command?: (item: ContextMenuSchema) => void
}

View File

@@ -1,3 +1,5 @@
import Descriptions from './src/Descriptions.vue'
export type { DescriptionsSchema } from './src/types'
export { Descriptions }

View File

@@ -1,134 +1,144 @@
<script setup lang="ts">
<script lang="tsx">
import { ElCollapseTransition, ElDescriptions, ElDescriptionsItem, ElTooltip } from 'element-plus'
import { useDesign } from '@/hooks/web/useDesign'
import { propTypes } from '@/utils/propTypes'
import { ref, unref, PropType, computed, useAttrs, useSlots } from 'vue'
import { ref, unref, PropType, computed, defineComponent } from 'vue'
import { useAppStore } from '@/store/modules/app'
import { DescriptionsSchema } from '@/types/descriptions'
import { DescriptionsSchema } from './types'
import { Icon } from '@/components/Icon'
import { get } from 'lodash-es'
const appStore = useAppStore()
const mobile = computed(() => appStore.getMobile)
const attrs = useAttrs()
const slots = useSlots()
const props = defineProps({
title: propTypes.string.def(''),
message: propTypes.string.def(''),
collapse: propTypes.bool.def(true),
schema: {
type: Array as PropType<DescriptionsSchema[]>,
default: () => []
},
data: {
type: Object as PropType<any>,
default: () => ({})
}
})
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('descriptions')
const getBindValue = computed(() => {
const delArr: string[] = ['title', 'message', 'collapse', 'schema', 'data', 'class']
const obj = { ...attrs, ...props }
for (const key in obj) {
if (delArr.indexOf(key) !== -1) {
delete obj[key]
export default defineComponent({
name: 'Descriptions',
props: {
title: propTypes.string.def(''),
message: propTypes.string.def(''),
collapse: propTypes.bool.def(true),
border: propTypes.bool.def(true),
column: propTypes.number.def(2),
size: propTypes.oneOf(['large', 'default', 'small']).def('default'),
direction: propTypes.oneOf(['horizontal', 'vertical']).def('horizontal'),
extra: propTypes.string.def(''),
schema: {
type: Array as PropType<DescriptionsSchema[]>,
default: () => []
},
data: {
type: Object as PropType<any>,
default: () => ({})
}
}
return obj
})
},
setup(props, { slots, attrs }) {
const getBindValue = computed((): any => {
const delArr: string[] = ['title', 'message', 'collapse', 'schema', 'data', 'class']
const obj = { ...attrs, ...props }
for (const key in obj) {
if (delArr.indexOf(key) !== -1) {
delete obj[key]
}
}
if (unref(mobile)) {
obj.direction = 'vertical'
}
return obj
})
const getBindItemValue = (item: DescriptionsSchema) => {
const delArr: string[] = ['field']
const obj = { ...item }
for (const key in obj) {
if (delArr.indexOf(key) !== -1) {
delete obj[key]
const getBindItemValue = (item: DescriptionsSchema) => {
const delArr: string[] = ['field']
const obj = { ...item }
for (const key in obj) {
if (delArr.indexOf(key) !== -1) {
delete obj[key]
}
}
return obj
}
}
return obj
}
// 折叠
const show = ref(true)
// 折叠
const show = ref(true)
const toggleClick = () => {
if (props.collapse) {
show.value = !unref(show)
}
}
</script>
const toggleClick = () => {
if (props.collapse) {
show.value = !unref(show)
}
}
<template>
<div
:class="[
prefixCls,
'bg-[var(--el-color-white)] dark:bg-[var(--el-bg-color)] dark:border-[var(--el-border-color)] dark:border-1px'
]"
>
<div
v-if="title"
:class="[
`${prefixCls}-header`,
'h-50px flex justify-between items-center b-b-1 border-solid border-[var(--tags-view-border-color)] px-10px cursor-pointer dark:border-[var(--el-border-color)]'
]"
@click="toggleClick"
>
<div :class="[`${prefixCls}-header__title`, 'relative font-18px font-bold ml-10px']">
<div class="flex items-center">
{{ title }}
<ElTooltip v-if="message" :content="message" placement="right">
<Icon icon="ep:warning" class="ml-5px" />
</ElTooltip>
</div>
</div>
<Icon v-if="collapse" :icon="show ? 'ep:arrow-down' : 'ep:arrow-up'" />
</div>
<ElCollapseTransition>
<div v-show="show" :class="[`${prefixCls}-content`, 'p-10px']">
<ElDescriptions
:column="2"
border
:direction="mobile ? 'vertical' : 'horizontal'"
v-bind="getBindValue"
return () => {
return (
<div
class={[
prefixCls,
'bg-[var(--el-color-white)] dark:bg-[var(--el-bg-color)] dark:border-[var(--el-border-color)] dark:border-1px'
]}
>
<template v-if="slots['extra']" #extra>
<slot name="extra"></slot>
</template>
<ElDescriptionsItem
v-for="item in schema"
:key="item.field"
v-bind="getBindItemValue(item)"
>
<template #label>
<slot
:name="`${item.field}-label`"
:row="{
label: item.label
}"
>{{ item.label }}</slot
>
</template>
{props.title ? (
<div
class={[
`${prefixCls}-header`,
'relative h-50px flex justify-between items-center layout-border__bottom px-10px cursor-pointer'
]}
onClick={toggleClick}
>
<div class={[`${prefixCls}-header__title`, 'relative font-18px font-bold ml-10px']}>
<div class="flex items-center">
{props.title}
{props.message ? (
<ElTooltip content={props.message} placement="right">
<Icon icon="bi:question-circle-fill" class="ml-5px" size={14} />
</ElTooltip>
) : null}
</div>
</div>
{props.collapse ? <Icon icon={show.value ? 'ep:arrow-down' : 'ep:arrow-up'} /> : null}
</div>
) : null}
<template #default>
<slot :name="item.field" :row="data">{{ data[item.field] }}</slot>
</template>
</ElDescriptionsItem>
</ElDescriptions>
</div>
</ElCollapseTransition>
</div>
</template>
<ElCollapseTransition>
<div v-show={unref(show)} class={[`${prefixCls}-content`]}>
<ElDescriptions {...unref(getBindValue)}>
{{
extra: () => (slots['extra'] ? slots['extra']() : props.extra),
default: () => {
return props.schema.map((item) => {
return (
<ElDescriptionsItem key={item.field} {...getBindItemValue(item)}>
{{
label: () => (item.slots?.label ? item.slots?.label(item) : item.label),
default: () =>
item.slots?.default
? item.slots?.default(props.data)
: get(props.data, item.field)
}}
</ElDescriptionsItem>
)
})
}
}}
</ElDescriptions>
</div>
</ElCollapseTransition>
</div>
)
}
}
})
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-descriptions';
:deep(.@{elNamespace}-descriptions__header) {
display: none !important;
}
.@{prefix-cls}-header {
&__title {
&::after {

View File

@@ -8,4 +8,8 @@ export interface DescriptionsSchema {
labelAlign?: 'left' | 'center' | 'right'
className?: string
labelClassName?: string
slots?: {
default?: (...args: any[]) => JSX.Element | null
label?: (...args: any[]) => JSX.Element | null
}
}

View File

@@ -10,7 +10,7 @@ const props = defineProps({
modelValue: propTypes.bool.def(false),
title: propTypes.string.def('Dialog'),
fullscreen: propTypes.bool.def(true),
maxHeight: propTypes.oneOfType([String, Number]).def('500px')
maxHeight: propTypes.oneOfType([String, Number]).def('400px')
})
const getBindValue = computed(() => {
@@ -50,7 +50,6 @@ watch(
)
const dialogStyle = computed(() => {
console.log(unref(dialogHeight))
return {
height: unref(dialogHeight)
}
@@ -64,20 +63,34 @@ const dialogStyle = computed(() => {
destroy-on-close
lock-scroll
draggable
top="0"
:close-on-click-modal="false"
:show-close="false"
>
<template #header>
<div class="flex justify-between">
<template #header="{ close }">
<div class="flex justify-between items-center h-54px pl-15px pr-15px relative">
<slot name="title">
{{ title }}
</slot>
<Icon
v-if="fullscreen"
class="mr-18px cursor-pointer is-hover mt-2px z-10"
:icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'"
color="var(--el-color-info)"
@click="toggleFull"
/>
<div
class="h-54px flex justify-between items-center absolute top-[50%] right-15px translate-y-[-50%]"
>
<Icon
v-if="fullscreen"
class="cursor-pointer is-hover !h-54px mr-10px"
:icon="isFullscreen ? 'radix-icons:exit-full-screen' : 'radix-icons:enter-full-screen'"
color="var(--el-color-info)"
hover-color="var(--el-color-primary)"
@click="toggleFull"
/>
<Icon
class="cursor-pointer is-hover !h-54px"
icon="ep:close"
hover-color="var(--el-color-primary)"
color="var(--el-color-info)"
@click="close"
/>
</div>
</div>
</template>
@@ -92,28 +105,28 @@ const dialogStyle = computed(() => {
</template>
<style lang="less">
.@{elNamespace}-dialog__header {
margin-right: 0 !important;
border-bottom: 1px solid var(--tags-view-border-color);
.@{elNamespace}-overlay-dialog {
display: flex;
justify-content: center;
align-items: center;
}
.@{elNamespace}-dialog__footer {
border-top: 1px solid var(--tags-view-border-color);
}
.is-hover {
&:hover {
color: var(--el-color-primary) !important;
}
}
.dark {
.@{elNamespace}-dialog__header {
.@{elNamespace}-dialog {
margin: 0 !important;
&__header {
margin-right: 0 !important;
border-bottom: 1px solid var(--el-border-color);
padding: 0;
height: 54px;
}
.@{elNamespace}-dialog__footer {
&__body {
padding: 15px !important;
}
&__footer {
border-top: 1px solid var(--el-border-color);
}
&__headerbtn {
top: 0;
}
}
</style>

View File

@@ -99,7 +99,6 @@ const handleChange = (editor: IDomEditor) => {
// 组件销毁时,及时销毁编辑器
onBeforeUnmount(() => {
const editor = unref(editorRef.value)
if (editor === null) return
// 销毁,并移除 editor
editor?.destroy()
@@ -116,12 +115,12 @@ defineExpose({
</script>
<template>
<div class="border-1 border-solid border-[var(--tags-view-border-color)] z-99">
<div class="border-1 border-solid border-[var(--el-border-color)] z-10">
<!-- 工具栏 -->
<Toolbar
:editor="editorRef"
:editorId="editorId"
class="b-b-1 border-solid border-[var(--tags-view-border-color)]"
class="border-0 b-b-1 border-solid border-[var(--el-border-color)]"
/>
<!-- 编辑器 -->
<Editor

View File

@@ -1,15 +1,48 @@
import Form from './src/Form.vue'
import { ElForm } from 'element-plus'
import { FormSchema, FormSetPropsType } from '@/types/form'
import type { FormSchema, FormSetProps } from './src/types'
export type {
ComponentNameEnum,
ComponentName,
InputComponentProps,
AutocompleteComponentProps,
InputNumberComponentProps,
SelectOption,
SelectComponentProps,
SelectV2ComponentProps,
CascaderComponentProps,
SwitchComponentProps,
RateComponentProps,
ColorPickerComponentProps,
TransferComponentProps,
RadioOption,
RadioGroupComponentProps,
RadioButtonComponentProps,
CheckboxOption,
CheckboxGroupComponentProps,
DividerComponentProps,
DatePickerComponentProps,
DateTimePickerComponentProps,
TimePickerComponentProps,
TimeSelectComponentProps,
ColProps,
FormSetProps,
FormItemProps,
FormSchema,
FormProps,
PlaceholderModel,
InputPasswordComponentProps,
TreeSelectComponentProps
} from './src/types'
export interface FormExpose {
setValues: (data: Recordable) => void
setProps: (props: Recordable) => void
delSchema: (field: string) => void
addSchema: (formSchema: FormSchema, index?: number) => void
setSchema: (schemaProps: FormSetPropsType[]) => void
setSchema: (schemaProps: FormSetProps[]) => void
formModel: Recordable
getElFormRef: () => ComponentRef<typeof ElForm>
getComponentExpose: (field: string) => any
getFormItemExpose: (field: string) => any
}
export { Form }

View File

@@ -1,302 +0,0 @@
<script lang="tsx">
import { PropType, defineComponent, ref, computed, unref, watch, onMounted } from 'vue'
import { ElForm, ElFormItem, ElRow, ElCol, ElTooltip } from 'element-plus'
import { componentMap } from './componentMap'
import { propTypes } from '@/utils/propTypes'
import { getSlot } from '@/utils/tsxHelper'
import {
setTextPlaceholder,
setGridProp,
setComponentProps,
setItemComponentSlots,
initModel,
setFormItemSlots
} from './helper'
import { useRenderSelect } from './components/useRenderSelect'
import { useRenderRadio } from './components/useRenderRadio'
import { useRenderCheckbox } from './components/useRenderCheckbox'
import { useDesign } from '@/hooks/web/useDesign'
import { findIndex } from '@/utils'
import { set } from 'lodash-es'
import { FormProps } from './types'
import { Icon } from '@/components/Icon'
import { FormSchema, FormSetPropsType } from '@/types/form'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('form')
export default defineComponent({
name: 'Form',
props: {
// 生成Form的布局结构数组
schema: {
type: Array as PropType<FormSchema[]>,
default: () => []
},
// 是否需要栅格布局
isCol: propTypes.bool.def(true),
// 表单数据对象
model: {
type: Object as PropType<Recordable>,
default: () => ({})
},
// 是否自动设置placeholder
autoSetPlaceholder: propTypes.bool.def(true),
// 是否自定义内容
isCustom: propTypes.bool.def(false),
// 表单label宽度
labelWidth: propTypes.oneOfType([String, Number]).def('auto')
},
emits: ['register'],
setup(props, { slots, expose, emit }) {
// element form 实例
const elFormRef = ref<ComponentRef<typeof ElForm>>()
// useForm传入的props
const outsideProps = ref<FormProps>({})
const mergeProps = ref<FormProps>({})
const getProps = computed(() => {
const propsObj = { ...props }
Object.assign(propsObj, unref(mergeProps))
return propsObj
})
// 表单数据
const formModel = ref<Recordable>({})
onMounted(() => {
emit('register', unref(elFormRef)?.$parent, unref(elFormRef))
})
// 对表单赋值
const setValues = (data: Recordable = {}) => {
formModel.value = Object.assign(unref(formModel), data)
}
const setProps = (props: FormProps = {}) => {
mergeProps.value = Object.assign(unref(mergeProps), props)
outsideProps.value = props
}
const delSchema = (field: string) => {
const { schema } = unref(getProps)
const index = findIndex(schema, (v: FormSchema) => v.field === field)
if (index > -1) {
schema.splice(index, 1)
}
}
const addSchema = (formSchema: FormSchema, index?: number) => {
const { schema } = unref(getProps)
if (index !== void 0) {
schema.splice(index, 0, formSchema)
return
}
schema.push(formSchema)
}
const setSchema = (schemaProps: FormSetPropsType[]) => {
const { schema } = unref(getProps)
for (const v of schema) {
for (const item of schemaProps) {
if (v.field === item.field) {
set(v, item.path, item.value)
}
}
}
}
const getElFormRef = (): ComponentRef<typeof ElForm> => {
return unref(elFormRef) as ComponentRef<typeof ElForm>
}
expose({
setValues,
formModel,
setProps,
delSchema,
addSchema,
setSchema,
getElFormRef
})
// 监听表单结构化数组重新生成formModel
watch(
() => unref(getProps).schema,
(schema = []) => {
formModel.value = initModel(schema, unref(formModel))
},
{
immediate: true,
deep: true
}
)
// 渲染包裹标签,是否使用栅格布局
const renderWrap = () => {
const { isCol } = unref(getProps)
const content = isCol ? (
<ElRow gutter={20}>{renderFormItemWrap()}</ElRow>
) : (
renderFormItemWrap()
)
return content
}
// 是否要渲染el-col
const renderFormItemWrap = () => {
// hidden属性表示隐藏不做渲染
const { schema = [], isCol } = unref(getProps)
return schema
.filter((v) => !v.hidden)
.map((item) => {
// 如果是 Divider 组件,需要自己占用一行
const isDivider = item.component === 'Divider'
const Com = componentMap['Divider'] as ReturnType<typeof defineComponent>
return isDivider ? (
<Com {...{ contentPosition: 'left', ...item.componentProps }}>{item?.label}</Com>
) : isCol ? (
// 如果需要栅格,需要包裹 ElCol
<ElCol {...setGridProp(item.colProps)}>{renderFormItem(item)}</ElCol>
) : (
renderFormItem(item)
)
})
}
// 渲染formItem
const renderFormItem = (item: FormSchema) => {
// 单独给只有options属性的组件做判断
const notRenderOptions = ['SelectV2', 'Cascader', 'Transfer']
const componentSlots = (item?.componentProps as any)?.slots || {}
const slotsMap: Recordable = {
...setItemComponentSlots(unref(formModel), componentSlots)
}
if (
item?.component !== 'SelectV2' &&
item?.component !== 'Cascader' &&
item?.componentProps?.options
) {
slotsMap.default = () => renderOptions(item)
}
const formItemSlots: Recordable = setFormItemSlots(slots, item.field)
// 如果有 labelMessage自动使用插槽渲染
if (item?.labelMessage) {
formItemSlots.label = () => {
return (
<>
<span>{item.label}</span>
<ElTooltip placement="right" raw-content>
{{
content: () => <span v-html={item.labelMessage}></span>,
default: () => (
<Icon
icon="ep:warning"
size={16}
color="var(--el-color-primary)"
class="ml-2px relative top-1px"
></Icon>
)
}}
</ElTooltip>
</>
)
}
}
return (
<ElFormItem {...(item.formItemProps || {})} prop={item.field} label={item.label || ''}>
{{
...formItemSlots,
default: () => {
const Com = componentMap[item.component as string] as ReturnType<
typeof defineComponent
>
const { autoSetPlaceholder } = unref(getProps)
return slots[item.field] ? (
getSlot(slots, item.field, formModel.value)
) : (
<Com
vModel={formModel.value[item.field]}
{...(autoSetPlaceholder && setTextPlaceholder(item))}
{...setComponentProps(item)}
style={item.componentProps?.style}
{...(notRenderOptions.includes(item?.component as string) &&
item?.componentProps?.options
? { options: item?.componentProps?.options || [] }
: {})}
>
{{ ...slotsMap }}
</Com>
)
}
}}
</ElFormItem>
)
}
// 渲染options
const renderOptions = (item: FormSchema) => {
switch (item.component) {
case 'Select':
const { renderSelectOptions } = useRenderSelect(slots)
return renderSelectOptions(item)
case 'Radio':
case 'RadioButton':
const { renderRadioOptions } = useRenderRadio()
return renderRadioOptions(item)
case 'Checkbox':
case 'CheckboxButton':
const { renderCheckboxOptions } = useRenderCheckbox()
return renderCheckboxOptions(item)
default:
break
}
}
// 过滤传入Form组件的属性
const getFormBindValue = () => {
// 避免在标签上出现多余的属性
const delKeys = ['schema', 'isCol', 'autoSetPlaceholder', 'isCustom', 'model']
const props = { ...unref(getProps) }
for (const key in props) {
if (delKeys.indexOf(key) !== -1) {
delete props[key]
}
}
return props
}
return () => (
<ElForm
ref={elFormRef}
{...getFormBindValue()}
model={props.isCustom ? props.model : formModel}
class={prefixCls}
>
{{
// 如果需要自定义,就什么都不渲染,而是提供默认插槽
default: () => {
const { isCustom } = unref(getProps)
return isCustom ? getSlot(slots, 'default') : renderWrap()
}
}}
</ElForm>
)
}
})
</script>
<style lang="less" scoped>
.@{elNamespace}-form.@{namespace}-form .@{elNamespace}-row {
margin-right: 0 !important;
margin-left: 0 !important;
}
</style>

View File

@@ -1,7 +1,15 @@
<script lang="tsx">
import { PropType, defineComponent, ref, computed, unref, watch, onMounted } from 'vue'
import { ElForm, ElFormItem, ElRow, ElCol, ElTooltip } from 'element-plus'
import { componentMap } from './componentMap'
import {
ElForm,
ElFormItem,
ElRow,
ElCol,
FormRules,
ComponentSize
// FormItemProp
} from 'element-plus'
import { componentMap } from './helper/componentMap'
import { propTypes } from '@/utils/propTypes'
import { getSlot } from '@/utils/tsxHelper'
import {
@@ -9,19 +17,27 @@ import {
setGridProp,
setComponentProps,
setItemComponentSlots,
initModel,
setFormItemSlots
initModel
} from './helper'
import { useRenderSelect } from './components/useRenderSelect'
import { useRenderRadio } from './components/useRenderRadio'
import { useRenderCheckbox } from './components/useRenderCheckbox'
import { useDesign } from '@/hooks/web/useDesign'
import { findIndex } from '@/utils'
import { set } from 'lodash-es'
import { get, set } from 'lodash-es'
import { FormProps } from './types'
import { Icon } from '@/components/Icon'
import { FormSchema, FormSetPropsType } from '@/types/form'
import { ComponentNameEnum, SelectComponentProps } from '@/types/components.d'
import {
FormSchema,
FormSetProps,
ComponentNameEnum,
SelectComponentProps,
RadioGroupComponentProps,
CheckboxGroupComponentProps
} from './types'
const { renderSelectOptions } = useRenderSelect()
const { renderRadioOptions } = useRenderRadio()
const { renderCheckboxOptions } = useRenderCheckbox()
const { getPrefixCls } = useDesign()
@@ -39,7 +55,7 @@ export default defineComponent({
isCol: propTypes.bool.def(true),
// 表单数据对象
model: {
type: Object as PropType<Recordable>,
type: Object as PropType<any>,
default: () => ({})
},
// 是否自动设置placeholder
@@ -47,7 +63,30 @@ export default defineComponent({
// 是否自定义内容
isCustom: propTypes.bool.def(false),
// 表单label宽度
labelWidth: propTypes.oneOfType([String, Number]).def('auto')
labelWidth: propTypes.oneOfType([String, Number]).def('auto'),
rules: {
type: Object as PropType<FormRules>,
default: () => ({})
},
labelPosition: propTypes.oneOf(['left', 'right', 'top']).def('right'),
labelSuffix: propTypes.string.def(''),
hideRequiredAsterisk: propTypes.bool.def(false),
requireAsteriskPosition: propTypes.oneOf(['left', 'right']).def('left'),
showMessage: propTypes.bool.def(true),
inlineMessage: propTypes.bool.def(false),
statusIcon: propTypes.bool.def(false),
validateOnRuleChange: propTypes.bool.def(true),
size: {
type: String as PropType<ComponentSize>,
default: undefined
},
disabled: propTypes.bool.def(false),
scrollToError: propTypes.bool.def(false),
scrollToErrorOffset: propTypes.oneOfType([Boolean, Object]).def(undefined)
// onValidate: {
// type: Function as PropType<(prop: FormItemProp, isValid: boolean, message: string) => void>,
// default: () => {}
// }
},
emits: ['register'],
setup(props, { slots, expose, emit }) {
@@ -65,8 +104,14 @@ export default defineComponent({
return propsObj
})
// 存储表单实例
const formComponents = ref({})
// 存储form-item实例
const formItemComponents = ref({})
// 表单数据
const formModel = ref<Recordable>({})
const formModel = ref<Recordable>(props.model)
onMounted(() => {
emit('register', unref(elFormRef)?.$parent, unref(elFormRef))
@@ -79,6 +124,7 @@ export default defineComponent({
const setProps = (props: FormProps = {}) => {
mergeProps.value = Object.assign(unref(mergeProps), props)
// @ts-ignore
outsideProps.value = props
}
@@ -100,7 +146,7 @@ export default defineComponent({
schema.push(formSchema)
}
const setSchema = (schemaProps: FormSetPropsType[]) => {
const setSchema = (schemaProps: FormSetProps[]) => {
const { schema } = unref(getProps)
for (const v of schema) {
for (const item of schemaProps) {
@@ -111,8 +157,42 @@ export default defineComponent({
}
}
const getElFormRef = (): ComponentRef<typeof ElForm> => {
return unref(elFormRef) as ComponentRef<typeof ElForm>
const getOptions = async (fn: Function, item: FormSchema) => {
const options = await fn()
setSchema([
{
field: item.field,
path:
item.component === ComponentNameEnum.TREE_SELECT
? 'componentProps.data'
: 'componentProps.options',
value: options
}
])
}
/**
* @description: 获取表单组件实例
* @param filed 表单字段
*/
const getComponentExpose = (filed: string) => {
return unref(formComponents)[filed]
}
/**
* @description: 获取formItem实例
* @param filed 表单字段
*/
const getFormItemExpose = (filed: string) => {
return unref(formItemComponents)[filed]
}
const setComponentRefMap = (ref: any, filed: string) => {
formComponents.value[filed] = ref
}
const setFormItemRefMap = (ref: any, filed: string) => {
formItemComponents.value[filed] = ref
}
expose({
@@ -122,7 +202,8 @@ export default defineComponent({
delSchema,
addSchema,
setSchema,
getElFormRef
getComponentExpose,
getFormItemExpose
})
// 监听表单结构化数组重新生成formModel
@@ -154,7 +235,7 @@ export default defineComponent({
const { schema = [], isCol } = unref(getProps)
return schema
.filter((v) => !v.hidden)
.filter((v) => !v.remove)
.map((item) => {
// 如果是 Divider 组件,需要自己占用一行
const isDivider = item.component === 'Divider'
@@ -172,101 +253,121 @@ export default defineComponent({
// 渲染formItem
const renderFormItem = (item: FormSchema) => {
// 单独给只有options属性的组件做判断
// const notRenderOptions = ['SelectV2', 'Cascader', 'Transfer']
const componentSlots = (item?.componentProps as any)?.slots || {}
const slotsMap: Recordable = {
...setItemComponentSlots(unref(formModel), componentSlots)
// 如果有optionApi优先使用optionApi
if (item.optionApi) {
// 内部自动调用接口,不影响其它渲染
getOptions(item.optionApi, item)
}
// 如果是select组件并且没有自定义模板自动渲染options
if (item.component === ComponentNameEnum.SELECT) {
slotsMap.default = !componentSlots.default
? () => renderOptions(item)
: () => {
return componentSlots.default(
unref((item?.componentProps as SelectComponentProps)?.options)
)
const formItemSlots: Recordable = {
default: () => {
if (item?.formItemProps?.slots?.default) {
return item?.formItemProps?.slots?.default(formModel.value)
} else {
const Com = componentMap[item.component as string] as ReturnType<typeof defineComponent>
const { autoSetPlaceholder } = unref(getProps)
const componentSlots = (item?.componentProps as any)?.slots || {}
const slotsMap: Recordable = {
...setItemComponentSlots(componentSlots)
}
// // 如果是select组件并且没有自定义模板自动渲染options
if (item.component === ComponentNameEnum.SELECT) {
slotsMap.default = !componentSlots.default
? () => renderSelectOptions(item)
: () => {
return componentSlots.default(
unref((item?.componentProps as SelectComponentProps)?.options)
)
}
}
}
// if (
// item?.component !== 'SelectV2' &&
// item?.component !== 'Cascader' &&
// item?.componentProps?.options
// ) {
// slotsMap.default = () => renderOptions(item)
// }
// const formItemSlots: Recordable = setFormItemSlots(slots, item.field)
// 如果有 labelMessage自动使用插槽渲染
// if (item?.labelMessage) {
// formItemSlots.label = () => {
// return (
// <>
// <span>{item.label}</span>
// <ElTooltip placement="right" raw-content>
// {{
// content: () => <span v-html={item.labelMessage}></span>,
// default: () => (
// <Icon
// icon="ep:warning"
// size={16}
// color="var(--el-color-primary)"
// class="ml-2px relative top-1px"
// ></Icon>
// )
// }}
// </ElTooltip>
// </>
// )
// }
// }
return (
<ElFormItem {...(item.formItemProps || {})} prop={item.field} label={item.label || ''}>
{{
default: () => {
const Com = componentMap[item.component as string] as ReturnType<
typeof defineComponent
>
// 虚拟列表
if (item.component === ComponentNameEnum.SELECT_V2 && componentSlots.default) {
slotsMap.default = ({ item }) => {
return componentSlots.default(item)
}
}
const { autoSetPlaceholder } = unref(getProps)
// 单选框组和按钮样式
if (
item.component === ComponentNameEnum.RADIO_GROUP ||
item.component === ComponentNameEnum.RADIO_BUTTON
) {
slotsMap.default = !componentSlots.default
? () => renderRadioOptions(item)
: () => {
return componentSlots.default(
unref((item?.componentProps as CheckboxGroupComponentProps)?.options)
)
}
}
return slots[item.field] ? (
getSlot(slots, item.field, formModel.value)
) : (
// 多选框组和按钮样式
if (
item.component === ComponentNameEnum.CHECKBOX_GROUP ||
item.component === ComponentNameEnum.CHECKBOX_BUTTON
) {
slotsMap.default = !componentSlots.default
? () => renderCheckboxOptions(item)
: () => {
return componentSlots.default(
unref((item?.componentProps as RadioGroupComponentProps)?.options)
)
}
}
const Comp = () => {
// 如果field是多层路径需要转换成对象
const itemVal = computed({
get: () => {
return get(formModel.value, item.field)
},
set: (val) => {
set(formModel.value, item.field, val)
}
})
return (
<Com
vModel={formModel.value[item.field]}
vModel={itemVal.value}
ref={(el: any) => setComponentRefMap(el, item.field)}
{...(autoSetPlaceholder && setTextPlaceholder(item))}
{...setComponentProps(item)}
style={item.componentProps?.style}
style={item.componentProps?.style || {}}
>
{{ ...slotsMap }}
</Com>
)
}
}}
return <>{Comp()}</>
}
}
}
if (item?.formItemProps?.slots?.label) {
formItemSlots.label = (...args: any[]) => {
return (item?.formItemProps?.slots as any)?.label(...args)
}
}
if (item?.formItemProps?.slots?.error) {
formItemSlots.error = (...args: any[]) => {
return (item?.formItemProps?.slots as any)?.error(...args)
}
}
return (
<ElFormItem
v-show={!item.hidden}
ref={(el: any) => setFormItemRefMap(el, item.field)}
{...(item.formItemProps || {})}
prop={item.field}
label={item.label || ''}
>
{formItemSlots}
</ElFormItem>
)
}
// 渲染options
const renderOptions = (item: FormSchema) => {
switch (item.component) {
case ComponentNameEnum.SELECT:
const { renderSelectOptions } = useRenderSelect(slots)
return renderSelectOptions(item)
case 'Radio':
case 'RadioButton':
const { renderRadioOptions } = useRenderRadio()
return renderRadioOptions(item)
case 'Checkbox':
case 'CheckboxButton':
const { renderCheckboxOptions } = useRenderCheckbox()
return renderCheckboxOptions(item)
default:
break
}
}
// 过滤传入Form组件的属性
const getFormBindValue = () => {
// 避免在标签上出现多余的属性
@@ -277,14 +378,14 @@ export default defineComponent({
delete props[key]
}
}
return props
return props as FormProps
}
return () => (
<ElForm
ref={elFormRef}
{...getFormBindValue()}
model={props.isCustom ? props.model : formModel}
model={unref(getProps).isCustom ? unref(getProps).model : formModel}
class={prefixCls}
>
{{

View File

@@ -1,19 +1,25 @@
import { FormSchema } from '@/types/form'
import { FormSchema, ComponentNameEnum, CheckboxGroupComponentProps } from '../types'
import { ElCheckbox, ElCheckboxButton } from 'element-plus'
import { defineComponent } from 'vue'
export const useRenderCheckbox = () => {
const renderCheckboxOptions = (item: FormSchema) => {
// 如果有别名,就取别名
const labelAlias = item?.componentProps?.optionsAlias?.labelField
const valueAlias = item?.componentProps?.optionsAlias?.valueField
const Com = (item.component === 'Checkbox' ? ElCheckbox : ElCheckboxButton) as ReturnType<
typeof defineComponent
>
return item?.componentProps?.options?.map((option) => {
const componentProps = item?.componentProps as CheckboxGroupComponentProps
const valueAlias = componentProps?.props?.value || 'value'
const labelAlias = componentProps?.props?.label || 'label'
const disabledAlias = componentProps?.props?.disabled || 'disabled'
const Com = (
item.component === ComponentNameEnum.CHECKBOX_GROUP ? ElCheckbox : ElCheckboxButton
) as ReturnType<typeof defineComponent>
return componentProps?.options?.map((option) => {
const { value, ...other } = option
return (
<Com {...other} label={option[valueAlias || 'value']}>
<Com
{...other}
disabled={option[disabledAlias || 'disabled']}
label={option[valueAlias || 'value']}
>
{option[labelAlias || 'label']}
</Com>
)

View File

@@ -1,19 +1,25 @@
import { FormSchema } from '@/types/form'
import { FormSchema, ComponentNameEnum, RadioGroupComponentProps } from '../types'
import { ElRadio, ElRadioButton } from 'element-plus'
import { defineComponent } from 'vue'
export const useRenderRadio = () => {
const renderRadioOptions = (item: FormSchema) => {
// 如果有别名,就取别名
const labelAlias = item?.componentProps?.optionsAlias?.labelField
const valueAlias = item?.componentProps?.optionsAlias?.valueField
const Com = (item.component === 'Radio' ? ElRadio : ElRadioButton) as ReturnType<
typeof defineComponent
>
return item?.componentProps?.options?.map((option) => {
const componentProps = item?.componentProps as RadioGroupComponentProps
const valueAlias = componentProps?.props?.value || 'value'
const labelAlias = componentProps?.props?.label || 'label'
const disabledAlias = componentProps?.props?.disabled || 'disabled'
const Com = (
item.component === ComponentNameEnum.RADIO_GROUP ? ElRadio : ElRadioButton
) as ReturnType<typeof defineComponent>
return componentProps?.options?.map((option) => {
const { value, ...other } = option
return (
<Com {...other} label={option[valueAlias || 'value']}>
<Com
{...other}
disabled={option[disabledAlias || 'disabled']}
label={option[valueAlias || 'value']}
>
{option[labelAlias || 'label']}
</Com>
)

View File

@@ -1,22 +1,20 @@
import { ElOption, ElOptionGroup } from 'element-plus'
import { getSlot } from '@/utils/tsxHelper'
import { Slots } from 'vue'
import { FormSchema } from '@/types/form'
import { SelectComponentProps, SelectOption } from '@/types/components'
import { FormSchema, SelectComponentProps, SelectOption } from '../types'
export const useRenderSelect = (slots: Slots) => {
export const useRenderSelect = () => {
// 渲染 select options
const renderSelectOptions = (item: FormSchema) => {
const componentsProps = item.componentProps as SelectComponentProps
const optionGroupDefaultSlot = componentsProps.slots?.optionGroupDefault
const componentsProps = item?.componentProps as SelectComponentProps
const optionGroupDefaultSlot = componentsProps?.slots?.optionGroupDefault
// 如果有别名,就取别名
const labelAlias = componentsProps?.labelAlias
const labelAlias = componentsProps?.props?.label
const keyAlias = componentsProps?.props?.key
return componentsProps?.options?.map((option) => {
if (option?.options?.length) {
return optionGroupDefaultSlot ? (
optionGroupDefaultSlot(option)
) : (
<ElOptionGroup label={option[labelAlias || 'label']}>
<ElOptionGroup label={option[labelAlias || 'label']} key={option[keyAlias || 'key']}>
{{
default: () =>
option?.options?.map((v) => {
@@ -35,17 +33,17 @@ export const useRenderSelect = (slots: Slots) => {
const renderSelectOptionItem = (item: FormSchema, option: SelectOption) => {
// 如果有别名,就取别名
const componentsProps = item.componentProps as SelectComponentProps
const labelAlias = componentsProps?.labelAlias
const valueAlias = componentsProps?.valueAlias
const labelAlias = componentsProps?.props?.label
const valueAlias = componentsProps?.props?.value
const keyAlias = componentsProps?.props?.key
const optionDefaultSlot = componentsProps.slots?.optionDefault
const { label, value, ...other } = option
return (
<ElOption
{...other}
label={labelAlias ? option[labelAlias] : label}
value={valueAlias ? option[valueAlias] : value}
{...option}
key={option[keyAlias || 'key']}
label={option[labelAlias || 'label']}
value={option[valueAlias || 'value']}
>
{{
default: () => (optionDefaultSlot ? optionDefaultSlot(option) : undefined)

View File

@@ -16,15 +16,18 @@ import {
ElTimeSelect,
ElTransfer,
ElAutocomplete,
ElDivider
ElDivider,
ElTreeSelect,
ElUpload
} from 'element-plus'
import { InputPassword } from '@/components/InputPassword'
import { Editor } from '@/components/Editor'
import { ComponentName } from '@/types/components'
import { ComponentName } from '../types'
const componentMap: Recordable<Component, ComponentName> = {
Radio: ElRadioGroup,
Checkbox: ElCheckboxGroup,
RadioGroup: ElRadioGroup,
RadioButton: ElRadioGroup,
CheckboxGroup: ElCheckboxGroup,
CheckboxButton: ElCheckboxGroup,
Input: ElInput,
Autocomplete: ElAutocomplete,
@@ -41,9 +44,10 @@ const componentMap: Recordable<Component, ComponentName> = {
Divider: ElDivider,
TimeSelect: ElTimeSelect,
SelectV2: ElSelectV2,
RadioButton: ElRadioGroup,
InputPassword: InputPassword,
Editor: Editor
Editor: Editor,
TreeSelect: ElTreeSelect,
Upload: ElUpload
}
export { componentMap }

View File

@@ -1,10 +1,8 @@
import { useI18n } from '@/hooks/web/useI18n'
import { unref, type Slots } from 'vue'
import { getSlot } from '@/utils/tsxHelper'
import { PlaceholderMoel } from './types'
import { FormSchema } from '@/types/form'
import { ColProps } from '@/types/components'
import { PlaceholderModel, FormSchema, ComponentNameEnum, ColProps } from '../types'
import { isFunction } from '@/utils/is'
import { firstUpperCase, humpToDash } from '@/utils'
import { set, get } from 'lodash-es'
const { t } = useI18n()
@@ -14,20 +12,32 @@ const { t } = useI18n()
* @returns
* @description placeholder
*/
export const setTextPlaceholder = (schema: FormSchema): PlaceholderMoel => {
const textMap = ['Input', 'Autocomplete', 'InputNumber', 'InputPassword']
const selectMap = ['Select', 'TimePicker', 'DatePicker', 'TimeSelect', 'TimeSelect']
if (textMap.includes(schema?.component as string)) {
export const setTextPlaceholder = (schema: FormSchema): PlaceholderModel => {
const textMap = [
ComponentNameEnum.INPUT,
ComponentNameEnum.AUTOCOMPLETE,
ComponentNameEnum.INPUT_NUMBER,
ComponentNameEnum.INPUT_PASSWORD
]
const selectMap = [
ComponentNameEnum.SELECT,
ComponentNameEnum.TIME_PICKER,
ComponentNameEnum.DATE_PICKER,
ComponentNameEnum.TIME_SELECT,
ComponentNameEnum.SELECT_V2
]
if (textMap.includes(schema?.component as ComponentNameEnum)) {
return {
placeholder: t('common.inputText')
}
}
if (selectMap.includes(schema?.component as string)) {
if (selectMap.includes(schema?.component as ComponentNameEnum)) {
// 一些范围选择器
const twoTextMap = ['datetimerange', 'daterange', 'monthrange', 'datetimerange', 'daterange']
if (
twoTextMap.includes(
(schema?.componentProps?.type || schema?.componentProps?.isRange) as string
((schema?.componentProps as any)?.type ||
(schema?.componentProps as any)?.isRange) as string
)
) {
return {
@@ -74,14 +84,30 @@ export const setGridProp = (col: ColProps = {}): ColProps => {
*/
export const setComponentProps = (item: FormSchema): Recordable => {
// const notNeedClearable = ['ColorPicker']
// 拆分事件并组合
const onEvents = (item?.componentProps as any)?.on || {}
const newOnEvents: Recordable = {}
for (const key in onEvents) {
if (onEvents[key]) {
newOnEvents[`on${firstUpperCase(key)}`] = (...args: any[]) => {
onEvents[key](...args)
}
}
}
const componentProps: Recordable = {
clearable: true,
...item.componentProps
...item.componentProps,
...newOnEvents
}
// 需要删除额外的属性
if (componentProps.slots) {
delete componentProps.slots
}
if (componentProps.on) {
delete componentProps.on
}
return componentProps
}
@@ -90,16 +116,16 @@ export const setComponentProps = (item: FormSchema): Recordable => {
* @param formModel
* @param slotsProps
*/
export const setItemComponentSlots = (formModel: any, slotsProps: Recordable = {}): Recordable => {
export const setItemComponentSlots = (slotsProps: Recordable = {}): Recordable => {
const slotObj: Recordable = {}
for (const key in slotsProps) {
if (slotsProps[key]) {
if (isFunction(slotsProps[key])) {
slotObj[key] = () => {
return slotsProps[key]?.(formModel)
slotObj[humpToDash(key)] = (...args: any[]) => {
return slotsProps[key]?.(...args)
}
} else {
slotObj[key] = () => {
slotObj[humpToDash(key)] = () => {
return slotsProps[key]
}
}
@@ -118,34 +144,19 @@ export const setItemComponentSlots = (formModel: any, slotsProps: Recordable = {
export const initModel = (schema: FormSchema[], formModel: Recordable) => {
const model: Recordable = { ...formModel }
schema.map((v) => {
// 如果是hidden就删除对应的值
if (v.hidden) {
if (v.remove) {
delete model[v.field]
} else if (v.component && v.component !== 'Divider') {
const hasField = Reflect.has(model, v.field)
} else if (v.component !== 'Divider') {
// const hasField = Reflect.has(model, v.field)
const hasField = get(model, v.field)
// 如果先前已经有值存在,则不进行重新赋值,而是采用现有的值
model[v.field] = hasField ? model[v.field] : v.value !== void 0 ? v.value : ''
set(
model,
v.field,
hasField !== void 0 ? get(model, v.field) : v.value !== void 0 ? v.value : undefined
)
// model[v.field] = hasField ? model[v.field] : v.value !== void 0 ? v.value : undefined
}
})
return model
}
/**
* @param slots
* @param field
* @returns FormIiem插槽
*/
export const setFormItemSlots = (slots: Slots, field: string): Recordable => {
const slotObj: Recordable = {}
if (slots[`${field}-error`]) {
slotObj['error'] = (data: Recordable) => {
return getSlot(slots, `${field}-error`, data)
}
}
if (slots[`${field}-label`]) {
slotObj['label'] = (data: Recordable) => {
return getSlot(slots, `${field}-label`, data)
}
}
return slotObj
}

View File

@@ -1,17 +0,0 @@
import { FormSchema } from '@/types/form'
export interface PlaceholderMoel {
placeholder?: string
startPlaceholder?: string
endPlaceholder?: string
rangeSeparator?: string
}
export type FormProps = {
schema?: FormSchema[]
isCol?: boolean
model?: Recordable
autoSetPlaceholder?: boolean
isCustom?: boolean
labelWidth?: string | number
} & Recordable

View File

@@ -0,0 +1,663 @@
import {
AutocompleteProps,
InputNumberProps,
CascaderProps,
CascaderNode,
CascaderValue,
SwitchProps,
ComponentSize,
InputProps,
RateProps,
ColorPickerProps,
TransferProps,
RadioGroupProps,
RadioButtonProps,
CheckboxGroupProps,
DividerProps,
DatePickerProps,
FormItemProps as ElFormItemProps,
FormProps as ElFormProps,
ISelectProps,
UploadProps
} from 'element-plus'
import { IEditorConfig } from '@wangeditor/editor'
import { CSSProperties } from 'vue'
export interface PlaceholderModel {
placeholder?: string
startPlaceholder?: string
endPlaceholder?: string
rangeSeparator?: string
}
export enum ComponentNameEnum {
RADIO_GROUP = 'RadioGroup',
RADIO_BUTTON = 'RadioButton',
CHECKBOX_GROUP = 'CheckboxGroup',
CHECKBOX_BUTTON = 'CheckboxButton',
INPUT = 'Input',
AUTOCOMPLETE = 'Autocomplete',
INPUT_NUMBER = 'InputNumber',
SELECT = 'Select',
CASCADER = 'Cascader',
SWITCH = 'Switch',
SLIDER = 'Slider',
TIME_PICKER = 'TimePicker',
DATE_PICKER = 'DatePicker',
RATE = 'Rate',
COLOR_PICKER = 'ColorPicker',
TRANSFER = 'Transfer',
DIVIDER = 'Divider',
TIME_SELECT = 'TimeSelect',
SELECT_V2 = 'SelectV2',
INPUT_PASSWORD = 'InputPassword',
EDITOR = 'Editor',
TREE_SELECT = 'TreeSelect',
UPLOAD = 'Upload'
}
type CamelCaseComponentName = keyof typeof ComponentNameEnum extends infer K
? K extends string
? K extends `${infer A}_${infer B}`
? `${Capitalize<Lowercase<A>>}${Capitalize<Lowercase<B>>}`
: Capitalize<Lowercase<K>>
: never
: never
export type ComponentName = CamelCaseComponentName
export interface InputPasswordComponentProps {
strength?: boolean
style?: CSSProperties
}
export interface InputComponentProps extends Partial<InputProps> {
rows?: number
on?: {
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
change?: (value: string | number) => void
clear?: () => void
input?: (value: string | number) => void
}
slots?: {
prefix?: (...args: any[]) => JSX.Element | null
suffix?: (...args: any[]) => JSX.Element | null
prepend?: (...args: any[]) => JSX.Element | null
append?: (...args: any[]) => JSX.Element | null
}
style?: CSSProperties
}
export interface AutocompleteComponentProps extends Partial<AutocompleteProps> {
on?: {
select?: (item: any) => void
change?: (value: string | number) => void
}
slots?: {
default?: (...args: any[]) => JSX.Element | null
prefix?: (...args: any[]) => JSX.Element | null
suffix?: (...args: any[]) => JSX.Element | null
prepend?: (...args: any[]) => JSX.Element | null
append?: (...args: any[]) => JSX.Element | null
}
style?: CSSProperties
}
export interface InputNumberComponentProps extends Partial<InputNumberProps> {
on?: {
change?: (currentValue: number | undefined, oldValue: number | undefined) => void
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
}
style?: CSSProperties
}
export interface SelectOption {
label?: string
disabled?: boolean
value?: any
key?: string | number
options?: SelectOption[]
[key: string]: any
}
export interface SelectComponentProps extends Omit<Partial<ISelectProps>, 'options'> {
/**
* 数据源的字段别名
*/
props?: {
key?: string
value?: string
label?: string
children?: string
}
on?: {
change?: (value: string | number | boolean | Object) => void
visibleChange?: (visible: boolean) => void
removeTag?: (tag: any) => void
clear?: () => void
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
}
slots?: {
default?: (options: SelectOption[]) => JSX.Element[] | null
optionGroupDefault?: (item: SelectOption) => JSX.Element
optionDefault?: (option: SelectOption) => JSX.Element | null
prefix?: (...args: any[]) => JSX.Element | null
empty?: (...args: any[]) => JSX.Element | null
}
options?: SelectOption[]
style?: CSSProperties
}
export interface SelectV2ComponentProps {
multiple?: boolean
disabled?: boolean
valueKey?: string
size?: ComponentSize
clearable?: boolean
clearIcon?: string | JSX.Element | null
collapseTags?: boolean
multipleLimit?: number
name?: string
effect?: string
autocomplete?: string
placeholder?: string
filterable?: boolean
allowCreate?: boolean
reserveKeyword?: boolean
noDataText?: string
popperClass?: string
teleported?: boolean
persistent?: boolean
popperOptions?: any
automaticDropdown?: boolean
height?: number
scrollbarAlwaysOn?: boolean
remote?: boolean
remoteMethod?: (query: string) => void
validateEvent?: boolean
placement?: AutocompleteProps['placement']
collapseTagsTooltip?: boolean
on?: {
change?: (value: string | number | boolean | Object) => void
visibleChange?: (visible: boolean) => void
removeTag?: (tag: any) => void
clear?: () => void
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
}
options?: SelectOption[]
slots?: {
default?: (option: SelectOption) => JSX.Element | null
}
style?: CSSProperties
}
export interface CascaderComponentProps {
options?: Record<string, unknown>[]
props?: CascaderProps
size?: ComponentSize
placeholder?: string
disabled?: boolean
clearable?: boolean
showAllLevels?: boolean
collapseTags?: boolean
collapseTagsTooltip?: boolean
separator?: string
filterable?: boolean
filterMethod?: (node: CascaderNode, keyword: string) => boolean
debounce?: number
beforeFilter?: (value: string) => boolean
popperClass?: string
teleported?: boolean
tagType?: ElementPlusInfoType
validateEvent?: boolean
on?: {
change?: (value: CascaderValue) => void
expandChange?: (value: CascaderValue) => void
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
visibleChange?: (value: boolean) => void
removeTag?: (value: CascaderNode['valueByOption']) => void
}
slots?: {
default?: (...args: any[]) => JSX.Element | null
empty?: (...args: any[]) => JSX.Element | null
}
style?: CSSProperties
}
export interface SwitchComponentProps extends Partial<SwitchProps> {
on?: {
change?: (value: boolean | string | number) => void
}
style?: CSSProperties
}
export interface RateComponentProps extends Partial<RateProps> {
on?: {
change?: (value: number) => void
}
style?: CSSProperties
}
export interface ColorPickerComponentProps extends Partial<ColorPickerProps> {
on?: {
change?: (value: string) => void
activeChange?: (value: string) => void
}
style?: CSSProperties
}
export interface TransferComponentProps extends Partial<TransferProps> {
on?: {
change?: (
value: number | string,
direction: 'left' | 'right',
movedKeys: string[] | number[]
) => void
leftCheckChange?: (value: any[]) => void
rightCheckChange?: (value: any[]) => void
}
slots?: {
default?: (...args: any[]) => JSX.Element | null
leftFooter?: (...args: any[]) => JSX.Element | null
rightFooter?: (...args: any[]) => JSX.Element | null
}
style?: CSSProperties
}
export interface RadioOption {
label?: string
value?: string | number | boolean
disabled?: boolean
border?: boolean
size?: ComponentSize
name?: string
[key: string]: any
}
export interface RadioGroupComponentProps extends Partial<RadioGroupProps> {
options?: RadioOption[]
/**
* 数据源的字段别名
*/
props?: {
label?: string
value?: string
disabled?: string
}
on?: {
change?: (value: string | number | boolean) => void
}
slots?: {
default?: (...args: any[]) => JSX.Element[] | null
}
style?: CSSProperties
}
export interface RadioButtonComponentProps extends Partial<RadioButtonProps> {
options?: RadioOption[]
/**
* 数据源的字段别名
*/
props?: {
label?: string
value?: string
disabled?: string
}
on?: {
change?: (value: string | number | boolean) => void
}
slots?: {
default?: (...args: any[]) => JSX.Element[] | null
}
style?: CSSProperties
}
export interface CheckboxOption {
label?: string
value?: string | number | boolean
disabled?: boolean
trueLabel?: string | number
falseLabel?: string | number
border?: boolean
size?: ComponentSize
name?: string
checked?: boolean
indeterminate?: boolean
validateEvent?: boolean
tabindex?: number | string
id?: string
controls?: boolean
[key: string]: any
}
export interface CheckboxGroupComponentProps extends Partial<CheckboxGroupProps> {
options?: CheckboxOption[]
/**
* 数据源的字段别名
*/
props?: {
label?: string
value?: string
disabled?: string
}
on?: {
change?: (value: string | number | boolean) => void
}
slots?: {
default?: (...args: any[]) => JSX.Element[] | null
}
style?: CSSProperties
}
export interface DividerComponentProps extends Partial<DividerProps> {
on?: {
change?: (value: number) => void
input?: (value: number) => void
}
style?: CSSProperties
}
export interface DatePickerComponentProps extends Partial<DatePickerProps> {
on?: {
change?: (value: string | Date | number | string[]) => void
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
calendarChange?: (val: [Date, Date]) => void
panelChange?: (date, mode, view) => void
visibleChange?: (visibility: boolean) => void
}
slots?: {
default?: (...args: any[]) => JSX.Element | null
rangeSeparator?: (...args: any[]) => JSX.Element | null
}
style?: CSSProperties
}
export interface DateTimePickerComponentProps {
readonly?: boolean
disabled?: boolean
editable?: boolean
clearable?: boolean
size?: ComponentSize
placeholder?: string
startPlaceholder?: string
endPlaceholder?: string
timeArrowControl?: boolean
type?: 'year' | 'month' | 'date' | 'datetime' | 'datetimerange' | 'daterange' | 'week'
format?: string
popperClass?: string
rangeSeparator?: string
defaultValue?: Date | [Date, Date]
defaultTime?: Date | [Date, Date]
valueFormat?: string
id?: string
name?: string
unlinkPanels?: boolean
prefixIcon?: string | JSX.Element
clearIcon?: string | JSX.Element
shortcuts?: Array<{ text: string; value: Date | Function }>
disabledDate?: (date: Date) => boolean
cellClassName?: string | ((date: Date) => string | undefined)
teleported?: boolean
on?: {
change?: (value: string | Date | number | string[]) => void
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
calendarChange?: (val: [Date, Date]) => void
visibleChange?: (visibility: boolean) => void
}
slots?: {
default?: (...args: any[]) => JSX.Element | null
rangeSeparator?: (...args: any[]) => JSX.Element | null
}
style?: CSSProperties
}
export interface TimePickerComponentProps {
readonly?: boolean
disabled?: boolean
editable?: boolean
clearable?: boolean
size?: ComponentSize
placeholder?: string
startPlaceholder?: string
endPlaceholder?: string
isRange?: boolean
arrowControl?: boolean
popperClass?: string
rangeSeparator?: string
format?: string
defaultValue?: Date | [Date, Date]
id?: string
name?: string
label?: string
prefixIcon?: string | JSX.Element
clearIcon?: string | JSX.Element
disabledHours?: (role: string, comparingDate?: any) => number[]
disabledMinutes?: (hour: number, role: string, comparingDate?: any) => number[]
disabledSeconds?: (hour: number, minute: number, role: string, comparingDate?: any) => number[]
teleported?: boolean
tabindex?: number | string
on?: {
change: (
val: number | string | Date | [number, number] | [string, string] | [Date, Date]
) => void
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
visibleChange?: (visibility: boolean) => void
}
style?: CSSProperties
}
export interface TimeSelectComponentProps {
disabled?: boolean
editable?: boolean
clearable?: boolean
size?: ComponentSize
placeholder?: string
name?: string
effect?: string
prefixIcon?: string | JSX.Element
clearIcon?: string | JSX.Element
start?: string
end?: string
step?: string
minTime?: string
maxTime?: string
format?: string
on?: {
change?: (val: string) => void
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
}
style?: CSSProperties
}
export interface EditorComponentProps {
editorConfig?: IEditorConfig
style?: CSSProperties
}
export interface ColProps {
span?: number
xs?: number
sm?: number
md?: number
lg?: number
xl?: number
tag?: string
}
export interface FormSetProps {
field: string
path: string
value: any
}
export interface FormItemProps extends Partial<ElFormItemProps> {
style?: CSSProperties
slots?: {
default?: (...args: any[]) => JSX.Element | null
label?: (...args: any[]) => JSX.Element | null
error?: (...args: any[]) => JSX.Element | null
}
}
export interface UploadComponentProps extends Partial<UploadProps> {
slots?: {
default?: (...args: any[]) => JSX.Element | null
trigger?: (...args: any[]) => JSX.Element | null
tip?: (...args: any[]) => JSX.Element | null
file?: (...args: any[]) => JSX.Element | null
}
style?: CSSProperties
}
export interface TreeSelectComponentProps
extends Omit<Partial<SelectComponentProps>, 'props' | 'on' | 'slots'> {
data?: any[]
emptyText?: string
nodeKey?: string
props?: {
children?: string
label?: string | ((...args: any[]) => string)
disabled?: string | ((...args: any[]) => string)
isLeaf?: string | ((...args: any[]) => string)
class?: string | ((...args: any[]) => string)
}
renderAfterExpand?: boolean
load?: (...args: any[]) => Promise<any>
renderContent?: (...args: any[]) => JSX.Element | null
highlightCurrent?: boolean
defaultExpandAll?: boolean
expandOnClickNode?: boolean
checkOnClickNode?: boolean
autoExpandParent?: boolean
defaultExpandedKeys?: any[]
showCheckbox?: boolean
checkStrictly?: boolean
defaultCheckedKeys?: any[]
currentNodeKey?: string | number
filterNodeMethod?: (...args: any[]) => boolean
accordion?: boolean
indent?: number
icon?: string | ((...args: any[]) => JSX.Element | null)
lazy?: boolean
draggable?: boolean
allowDrag?: (...args: any[]) => boolean
allowDrop?: (...args: any[]) => boolean
on?: {
change?: (value: string | number | boolean | Object) => void
visibleChange?: (visible: boolean) => void
removeTag?: (tag: any) => void
clear?: () => void
blur?: (event: FocusEvent) => void
focus?: (event: FocusEvent) => void
nodeClick?: (...args: any[]) => void
nodeContextMenu?: (...args: any[]) => void
checkChange?: (...args: any[]) => void
check?: (...args: any[]) => void
currentChange?: (...args: any[]) => void
nodeExpand?: (...args: any[]) => void
nodeCollapse?: (...args: any[]) => void
nodeDragStart?: (...args: any[]) => void
nodeDragEnter?: (...args: any[]) => void
nodeDragLeave?: (...args: any[]) => void
nodeDragOver?: (...args: any[]) => void
nodeDragEnd?: (...args: any[]) => void
nodeDrop?: (...args: any[]) => void
}
slots?: {
default?: (...args: any[]) => JSX.Element | null
optionGroupDefault?: (item: SelectOption) => JSX.Element
optionDefault?: (option: SelectOption) => JSX.Element | null
prefix?: (...args: any[]) => JSX.Element | null
empty?: (...args: any[]) => JSX.Element | null
}
style?: CSSProperties
}
export interface FormSchema {
/**
* 唯一标识
*/
field: string
/**
* 标题
*/
label?: string
/**
* col组件属性
*/
colProps?: ColProps
/**
* 表单组件属性具体可以查看element-plus文档
*/
componentProps?:
| InputComponentProps
| AutocompleteComponentProps
| InputNumberComponentProps
| SelectComponentProps
| SelectV2ComponentProps
| CascaderComponentProps
| SwitchComponentProps
| RateComponentProps
| ColorPickerComponentProps
| TransferComponentProps
| RadioGroupComponentProps
| RadioButtonComponentProps
| DividerComponentProps
| DatePickerComponentProps
| DateTimePickerComponentProps
| TimePickerComponentProps
| InputPasswordComponentProps
| TreeSelectComponentProps
| UploadComponentProps
| any
/**
* formItem组件属性具体可以查看element-plus文档
*/
formItemProps?: FormItemProps
/**
* 渲染的组件名称
*/
component?: ComponentName
/**
* 初始值
*/
value?: any
/**
* 是否隐藏如果为true会连同值一同删除类似v-if
*/
remove?: boolean
/**
* 样式隐藏不会把值一同删掉类似v-show
*/
hidden?: boolean
/**
* @returns 远程加载下拉项
*/
optionApi?: any
}
export interface FormProps extends Partial<ElFormProps> {
schema?: FormSchema[]
isCol?: boolean
model?: Recordable
autoSetPlaceholder?: boolean
isCustom?: boolean
[key: string]: any
}

View File

@@ -1,3 +1,5 @@
import Icon from './src/Icon.vue'
export type { IconTypes } from './src/types'
export { Icon }

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { computed, unref, ref, watch, nextTick } from 'vue'
import { computed, unref } from 'vue'
import { ElIcon } from 'element-plus'
import { propTypes } from '@/utils/propTypes'
import Iconify from '@purge-icons/generated'
import { useDesign } from '@/hooks/web/useDesign'
import { Icon } from '@iconify/vue'
const { getPrefixCls } = useDesign()
@@ -15,11 +15,10 @@ const props = defineProps({
// icon color
color: propTypes.string,
// icon size
size: propTypes.number.def(16)
size: propTypes.number.def(16),
hoverColor: propTypes.string
})
const elRef = ref<ElRef>(null)
const isLocal = computed(() => props.icon.startsWith('svg-icon:'))
const symbolId = computed(() => {
@@ -33,36 +32,6 @@ const getIconifyStyle = computed(() => {
color
}
})
const updateIcon = async (icon: string) => {
if (unref(isLocal)) return
const el = unref(elRef)
if (!el) return
await nextTick()
if (!icon) return
const svg = Iconify.renderSVG(icon, {})
if (svg) {
el.textContent = ''
el.appendChild(svg)
} else {
const span = document.createElement('span')
span.className = 'iconify'
span.dataset.icon = icon
el.textContent = ''
el.appendChild(span)
}
}
watch(
() => props.icon,
(icon: string) => {
updateIcon(icon)
}
)
</script>
<template>
@@ -71,8 +40,20 @@ watch(
<use :xlink:href="symbolId" />
</svg>
<span v-else ref="elRef" :class="$attrs.class" :style="getIconifyStyle">
<span class="iconify" :data-icon="symbolId"></span>
</span>
<Icon v-else :icon="icon" :style="getIconifyStyle" />
</ElIcon>
</template>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-icon';
.@{prefix-cls},
.iconify {
&:hover {
:deep(svg) {
// stylelint-disable-next-line
color: v-bind(hoverColor) !important;
}
}
}
</style>

View File

@@ -2,4 +2,5 @@ export interface IconTypes {
size?: number
color?: string
icon: string
hoverColor?: string
}

View File

@@ -12,7 +12,7 @@ export function createImageViewer(options: ImageViewerProps) {
initialIndex = 0,
infinite = true,
hideOnClickModal = false,
appendToBody = false,
teleported = false,
zIndex = 2000,
show = true
} = options
@@ -23,7 +23,7 @@ export function createImageViewer(options: ImageViewerProps) {
propsData.initialIndex = initialIndex
propsData.infinite = infinite
propsData.hideOnClickModal = hideOnClickModal
propsData.appendToBody = appendToBody
propsData.teleported = teleported
propsData.zIndex = zIndex
propsData.show = show

View File

@@ -12,7 +12,7 @@ const props = defineProps({
initialIndex: propTypes.number.def(0),
infinite: propTypes.bool.def(true),
hideOnClickModal: propTypes.bool.def(false),
appendToBody: propTypes.bool.def(false),
teleported: propTypes.bool.def(false),
show: propTypes.bool.def(false)
})

View File

@@ -4,6 +4,6 @@ export interface ImageViewerProps {
initialIndex?: number
infinite?: boolean
hideOnClickModal?: boolean
appendToBody?: boolean
teleported?: boolean
show?: boolean
}

View File

@@ -1,3 +1,5 @@
import Infotip from './src/Infotip.vue'
export type { InfoTipSchema } from './src/types'
export { Infotip }

View File

@@ -3,7 +3,7 @@ import { PropType } from 'vue'
import { Highlight } from '@/components/Highlight'
import { useDesign } from '@/hooks/web/useDesign'
import { propTypes } from '@/utils/propTypes'
import { TipSchema } from '@/types/infoTip'
import { InfoTipSchema } from './types'
const { getPrefixCls } = useDesign()
@@ -12,7 +12,7 @@ const prefixCls = getPrefixCls('infotip')
defineProps({
title: propTypes.string.def(''),
schema: {
type: Array as PropType<Array<string | TipSchema>>,
type: Array as PropType<Array<string | InfoTipSchema>>,
required: true,
default: () => []
},

View File

@@ -1,4 +1,4 @@
export interface TipSchema {
export interface InfoTipSchema {
label: string
keys?: string[]
}

View File

@@ -32,10 +32,6 @@ const emit = defineEmits(['update:modelValue'])
// 设置input的type属性
const textType = ref<'password' | 'text'>('password')
const changeTextType = () => {
textType.value = unref(textType) === 'text' ? 'password' : 'text'
}
// 输入框的值
const valueRef = ref(props.modelValue)
@@ -53,19 +49,11 @@ const getPasswordStrength = computed(() => {
const zxcvbnRef = zxcvbn(unref(valueRef)) as ZxcvbnResult
return value ? zxcvbnRef.score : -1
})
const getIconName = computed(() =>
unref(textType) === 'password' ? 'ant-design:eye-invisible-outlined' : 'ant-design:eye-outlined'
)
</script>
<template>
<div :class="[prefixCls, `${prefixCls}--${configGlobal?.size}`]">
<ElInput v-bind="$attrs" v-model="valueRef" :type="textType">
<template #suffix>
<Icon class="el-input__icon cursor-pointer" :icon="getIconName" @click="changeTextType" />
</template>
</ElInput>
<ElInput v-bind="$attrs" v-model="valueRef" showPassword :type="textType" />
<div
v-if="strength"
:class="`${prefixCls}__bar`"
@@ -116,7 +104,9 @@ const getIconName = computed(() =>
height: inherit;
background-color: transparent;
border-radius: inherit;
transition: width 0.5s ease-in-out, background 0.25s;
transition:
width 0.5s ease-in-out,
background 0.25s;
&[data-score='0'] {
width: 20%;

View File

@@ -1,3 +1,5 @@
import LocaleDropdown from './src/LocaleDropdown.vue'
export type { Language, LocaleDropdownType } from './src/types'
export { LocaleDropdown }

View File

@@ -7,7 +7,6 @@ import { useRenderMenuItem } from './components/useRenderMenuItem'
import { useRouter } from 'vue-router'
import { isUrl } from '@/utils/is'
import { useDesign } from '@/hooks/web/useDesign'
import { LayoutType } from '@/types/layout'
const { getPrefixCls } = useDesign()
@@ -124,15 +123,15 @@ export default defineComponent({
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-menu';
.is-active--after {
position: absolute;
top: 0;
right: 0;
width: 4px;
height: 100%;
background-color: var(--el-color-primary);
content: '';
}
// .is-active--after {
// position: absolute;
// top: 0;
// right: 0;
// width: 4px;
// height: 100%;
// background-color: var(--el-color-primary);
// content: '';
// }
.@{prefix-cls} {
position: relative;
@@ -182,9 +181,9 @@ export default defineComponent({
.@{elNamespace}-menu-item.is-active {
position: relative;
&:after {
.is-active--after;
}
// &:after {
// .is-active--after;
// }
}
// 设置子菜单的背景颜色
@@ -205,9 +204,9 @@ export default defineComponent({
position: relative;
background-color: var(--left-menu-collapse-bg-active-color) !important;
&:after {
.is-active--after;
}
// &:after {
// .is-active--after;
// }
}
}
@@ -255,15 +254,15 @@ export default defineComponent({
<style lang="less">
@prefix-cls: ~'@{namespace}-menu-popper';
.is-active--after {
position: absolute;
top: 0;
right: 0;
width: 4px;
height: 100%;
background-color: var(--el-color-primary);
content: '';
}
// .is-active--after {
// position: absolute;
// top: 0;
// right: 0;
// width: 4px;
// height: 100%;
// background-color: var(--el-color-primary);
// content: '';
// }
.@{prefix-cls}--vertical,
.@{prefix-cls}--horizontal {
@@ -292,9 +291,9 @@ export default defineComponent({
background-color: var(--left-menu-bg-active-color) !important;
}
&:after {
.is-active--after;
}
// &:after {
// .is-active--after;
// }
}
}
</style>

View File

@@ -0,0 +1,4 @@
import Permission from './src/Permission.vue'
import { hasPermi } from './src/utils'
export { Permission, hasPermi }

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes'
import { computed, unref } from 'vue'
import { useRouter } from 'vue-router'
const { currentRoute } = useRouter()
const props = defineProps({
permission: propTypes.string.def()
})
const currentPermission = computed(() => {
return unref(currentRoute)?.meta?.permission || []
})
const hasPermission = computed(() => {
const permission = unref(props.permission)
if (!permission) {
return true
}
return unref(currentPermission).includes(permission)
})
</script>
<template>
<template v-if="hasPermission">
<slot></slot>
</template>
</template>

View File

@@ -0,0 +1,14 @@
import { useI18n } from '@/hooks/web/useI18n'
import router from '@/router'
export const hasPermi = (value: string) => {
const { t } = useI18n()
const permission = (router.currentRoute.value.meta.permission || []) as string[]
if (!value) {
throw new Error(t('permission.hasPermission'))
}
if (permission.includes(value)) {
return true
}
return false
}

View File

@@ -1,3 +1,5 @@
import Qrcode from './src/Qrcode.vue'
export type { QrcodeLogo } from './src/types'
export { Qrcode }

View File

@@ -6,7 +6,7 @@ import { cloneDeep } from 'lodash-es'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
import { isString } from '@/utils/is'
import { QrcodeLogo } from '@/types/qrcode'
import { QrcodeLogo } from '@/components/Qrcode'
const props = defineProps({
// img 或者 canvas,img不支持logo嵌套

View File

@@ -1,3 +1,15 @@
import { FormSchema, FormSetProps } from '../Form'
import Search from './src/Search.vue'
export type { SearchProps } from './src/types'
export interface SearchExpose {
setValues: (data: Recordable) => void
setProps: (props: Recordable) => void
delSchema: (field: string) => void
addSchema: (formSchema: FormSchema, index?: number) => void
setSchema: (schemaProps: FormSetProps[]) => void
formModel: Recordable
}
export { Search }

View File

@@ -1,15 +1,15 @@
<script setup lang="ts">
import { Form } from '@/components/Form'
import { PropType, computed, unref, ref } from 'vue'
<script setup lang="tsx">
import { Form, FormSchema, FormSetProps } from '@/components/Form'
import { PropType, computed, unref, ref, watch, onMounted } from 'vue'
import { propTypes } from '@/utils/propTypes'
import { ElButton } from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n'
import { useForm } from '@/hooks/web/useForm'
import { findIndex } from '@/utils'
import { cloneDeep } from 'lodash-es'
import { FormSchema } from '@/types/form'
const { t } = useI18n()
import { cloneDeep, set } from 'lodash-es'
import { initModel } from '@/components/Form/src/helper'
import ActionButton from './components/ActionButton.vue'
import { SearchProps } from './types'
import { FormItemProp } from 'element-plus'
import { isObject, isEmptyVal } from '@/utils/is'
const props = defineProps({
// 生成Form的布局结构数组
@@ -24,41 +24,72 @@ const props = defineProps({
// 操作按钮风格位置
layout: propTypes.string.validate((v: string) => ['inline', 'bottom'].includes(v)).def('inline'),
// 底部按钮的对齐方式
buttomPosition: propTypes.string
buttonPosition: propTypes.string
.validate((v: string) => ['left', 'center', 'right'].includes(v))
.def('center'),
showSearch: propTypes.bool.def(true),
showReset: propTypes.bool.def(true),
// 是否显示伸缩
expand: propTypes.bool.def(false),
showExpand: propTypes.bool.def(false),
// 伸缩的界限字段
expandField: propTypes.string.def(''),
inline: propTypes.bool.def(true),
// 是否去除空值项
removeNoValueItem: propTypes.bool.def(true),
model: {
type: Object as PropType<Recordable>,
default: () => ({})
}
},
searchLoading: propTypes.bool.def(false),
resetLoading: propTypes.bool.def(false)
})
const emit = defineEmits(['search', 'reset'])
const emit = defineEmits(['search', 'reset', 'register', 'validate'])
const visible = ref(true)
// 表单数据
const formModel = ref<Recordable>(props.model)
const newSchema = computed(() => {
let schema: FormSchema[] = cloneDeep(props.schema)
if (props.expand && props.expandField && !unref(visible)) {
const index = findIndex(schema, (v: FormSchema) => v.field === props.expandField)
if (index > -1) {
const length = schema.length
schema.splice(index + 1, length)
}
const propsComputed = unref(getProps)
let schema: FormSchema[] = cloneDeep(propsComputed.schema)
if (propsComputed.showExpand && propsComputed.expandField && !unref(visible)) {
const index = findIndex(schema, (v: FormSchema) => v.field === propsComputed.expandField)
schema.map((v, i) => {
if (i >= index) {
v.hidden = true
} else {
v.hidden = false
}
return v
})
}
if (props.layout === 'inline') {
if (propsComputed.layout === 'inline') {
schema = schema.concat([
{
field: 'action',
formItemProps: {
labelWidth: '0px'
labelWidth: '0px',
slots: {
default: () => {
return (
<div>
<ActionButton
showSearch={propsComputed.showSearch}
showReset={propsComputed.showReset}
showExpand={propsComputed.showExpand}
searchLoading={propsComputed.searchLoading}
resetLoading={propsComputed.resetLoading}
visible={visible.value}
onExpand={setVisible}
onReset={reset}
onSearch={search}
/>
</div>
)
}
}
}
}
])
@@ -66,81 +97,167 @@ const newSchema = computed(() => {
return schema
})
const { register, elFormRef, methods } = useForm({
model: props.model || {}
const { formRegister, formMethods } = useForm()
const { getElFormExpose, getFormData, getFormExpose } = formMethods
// useSearch传入的props
const outsideProps = ref<SearchProps>({})
const mergeProps = ref<SearchProps>({})
const getProps = computed(() => {
const propsObj = { ...props }
Object.assign(propsObj, unref(mergeProps))
return propsObj
})
const setProps = (props: SearchProps = {}) => {
mergeProps.value = Object.assign(unref(mergeProps), props)
// @ts-ignore
outsideProps.value = props
}
// 监听表单结构化数组重新生成formModel
watch(
() => unref(newSchema),
async (schema = []) => {
formModel.value = initModel(schema, unref(formModel))
},
{
immediate: true,
deep: true
}
)
const filterModel = async () => {
const model = await getFormData()
if (unref(getProps).removeNoValueItem) {
// 使用reduce过滤空值并返回一个新对象
return Object.keys(model).reduce((prev, next) => {
const value = model[next]
if (!isEmptyVal(value)) {
if (isObject(value)) {
if (Object.keys(value).length > 0) {
prev[next] = value
}
} else {
prev[next] = value
}
}
return prev
}, {})
}
return model
}
const search = async () => {
await unref(elFormRef)?.validate(async (isValid) => {
const elFormExpose = await getElFormExpose()
await elFormExpose?.validate(async (isValid) => {
if (isValid) {
const { getFormData } = methods
const model = await getFormData()
const model = await filterModel()
emit('search', model)
}
})
}
const reset = async () => {
unref(elFormRef)?.resetFields()
const { getFormData } = methods
const model = await getFormData()
const elFormExpose = await getElFormExpose()
elFormExpose?.resetFields()
const model = await filterModel()
emit('reset', model)
}
const bottonButtonStyle = computed(() => {
const bottomButtonStyle = computed(() => {
return {
textAlign: props.buttomPosition as unknown as 'left' | 'center' | 'right'
textAlign: unref(getProps).buttonPosition as unknown as 'left' | 'center' | 'right'
}
})
const setVisible = () => {
unref(elFormRef)?.resetFields()
const setVisible = async () => {
visible.value = !unref(visible)
}
const setSchema = (schemaProps: FormSetProps[]) => {
const { schema } = unref(getProps)
for (const v of schema) {
for (const item of schemaProps) {
if (v.field === item.field) {
set(v, item.path, item.value)
}
}
}
}
// 对表单赋值
const setValues = async (data: Recordable = {}) => {
formModel.value = Object.assign(props.model, unref(formModel), data)
const formExpose = await getFormExpose()
formExpose?.setValues(data)
}
const delSchema = (field: string) => {
const { schema } = unref(getProps)
const index = findIndex(schema, (v: FormSchema) => v.field === field)
if (index > -1) {
schema.splice(index, 1)
}
}
const addSchema = (formSchema: FormSchema, index?: number) => {
const { schema } = unref(getProps)
if (index !== void 0) {
schema.splice(index, 0, formSchema)
return
}
schema.push(formSchema)
}
const defaultExpose = {
getElFormExpose,
setProps,
setSchema,
setValues,
delSchema,
addSchema
}
onMounted(() => {
emit('register', defaultExpose)
})
defineExpose(defaultExpose)
const onFormValidate = (prop: FormItemProp, isValid: boolean, message: string) => {
emit('validate', prop, isValid, message)
}
</script>
<template>
<Form
:model="formModel"
:is-custom="false"
:label-width="labelWidth"
:label-width="getProps.labelWidth"
hide-required-asterisk
:inline="inline"
:is-col="isCol"
:inline="getProps.inline"
:is-col="getProps.isCol"
:schema="newSchema"
@register="register"
>
<template #action>
<div v-if="layout === 'inline'">
<ElButton v-if="showSearch" type="primary" @click="search">
<Icon icon="ep:search" class="mr-5px" />
{{ t('common.query') }}
</ElButton>
<ElButton v-if="showReset" @click="reset">
<Icon icon="ep:refresh-right" class="mr-5px" />
{{ t('common.reset') }}
</ElButton>
<ElButton v-if="expand" text @click="setVisible">
{{ t(visible ? 'common.shrink' : 'common.expand') }}
<Icon :icon="visible ? 'ant-design:up-outlined' : 'ant-design:down-outlined'" />
</ElButton>
</div>
</template>
</Form>
@register="formRegister"
@validate="onFormValidate"
/>
<template v-if="layout === 'bottom'">
<div :style="bottonButtonStyle">
<ElButton v-if="showSearch" type="primary" @click="search">
<Icon icon="ep:search" class="mr-5px" />
{{ t('common.query') }}
</ElButton>
<ElButton v-if="showReset" @click="reset">
<Icon icon="ep:refresh-right" class="mr-5px" />
{{ t('common.reset') }}
</ElButton>
<ElButton v-if="expand" text @click="setVisible">
{{ t(visible ? 'common.shrink' : 'common.expand') }}
<Icon :icon="visible ? 'ant-design:up-outlined' : 'ant-design:down-outlined'" />
</ElButton>
<div :style="bottomButtonStyle">
<ActionButton
:show-reset="getProps.showReset"
:show-search="getProps.showSearch"
:show-expand="getProps.showExpand"
:search-loading="getProps.searchLoading"
:reset-loading="getProps.resetLoading"
@expand="setVisible"
@reset="reset"
@search="search"
/>
</div>
</template>
</template>

View File

@@ -0,0 +1,59 @@
<script setup lang="ts">
import { ElButton } from 'element-plus'
import { useIcon } from '@/hooks/web/useIcon'
import { propTypes } from '@/utils/propTypes'
import { useI18n } from '@/hooks/web/useI18n'
const emit = defineEmits(['search', 'reset', 'expand'])
const { t } = useI18n()
defineProps({
showSearch: propTypes.bool.def(true),
showReset: propTypes.bool.def(true),
showExpand: propTypes.bool.def(false),
visible: propTypes.bool.def(true),
searchLoading: propTypes.bool.def(false),
resetLoading: propTypes.bool.def(false)
})
const onSearch = () => {
emit('search')
}
const onReset = () => {
emit('reset')
}
const onExpand = () => {
emit('expand')
}
</script>
<template>
<ElButton
v-if="showSearch"
type="primary"
:loading="searchLoading"
:icon="useIcon({ icon: 'ep:search' })"
@click="onSearch"
>
{{ t('common.query') }}
</ElButton>
<ElButton
v-if="showReset"
:loading="resetLoading"
:icon="useIcon({ icon: 'ep:refresh-right' })"
@click="onReset"
>
{{ t('common.reset') }}
</ElButton>
<ElButton
v-if="showExpand"
:icon="useIcon({ icon: visible ? 'ep:arrow-down' : 'ep:arrow-up' })"
text
@click="onExpand"
>
{{ t(visible ? 'common.shrink' : 'common.expand') }}
</ElButton>
</template>

View File

@@ -0,0 +1,16 @@
import { FormSchema } from '@/components/Form'
export interface SearchProps {
schema?: FormSchema[]
isCol?: boolean
labelWidth?: string | number
layout?: 'inline' | 'bottom'
buttonPosition?: 'left' | 'right' | 'center'
showSearch?: boolean
showReset?: boolean
showExpand?: boolean
expandField?: string
inline?: boolean
removeNoValueItem?: boolean
model?: Recordable
}

View File

@@ -10,10 +10,12 @@ import { trim, setCssVar } from '@/utils'
import ColorRadioPicker from './components/ColorRadioPicker.vue'
import InterfaceDisplay from './components/InterfaceDisplay.vue'
import LayoutRadioPicker from './components/LayoutRadioPicker.vue'
import { useCache } from '@/hooks/web/useCache'
import { useStorage } from '@/hooks/web/useStorage'
import { useClipboard } from '@vueuse/core'
import { useDesign } from '@/hooks/web/useDesign'
const { removeStorage } = useStorage()
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('setting')
@@ -47,7 +49,6 @@ const setHeaderTheme = (color: string) => {
setCssVar('--top-header-bg-color', color)
setCssVar('--top-header-text-color', textColor)
setCssVar('--top-header-hover-color', textHoverColor)
setCssVar('--layout-border-color', topToolBorderColor)
appStore.setTheme({
topHeaderBgColor: color,
topHeaderTextColor: textColor,
@@ -92,10 +93,6 @@ const setMenuTheme = (color: string) => {
appStore.setTheme(theme)
appStore.setCssVarTheme()
}
if (layout.value === 'top' && !appStore.getIsDark) {
headerTheme.value = '#fff'
setHeaderTheme('#fff')
}
// 监听layout变化重置一些主题色
watch(
@@ -191,10 +188,9 @@ const copyConfig = async () => {
// 清空缓存
const clear = () => {
const { wsCache } = useCache()
wsCache.delete('layout')
wsCache.delete('theme')
wsCache.delete('isDark')
removeStorage('layout')
removeStorage('theme')
removeStorage('isDark')
window.location.reload()
}
</script>
@@ -202,7 +198,7 @@ const clear = () => {
<template>
<div
:class="prefixCls"
class="fixed top-[45%] right-0 w-40px h-40px flex items-center justify-center bg-[var(--el-color-primary)] cursor-pointer"
class="fixed top-[45%] right-0 w-40px h-40px flex items-center justify-center bg-[var(--el-color-primary)] cursor-pointer z-10"
@click="drawer = true"
>
<Icon icon="ant-design:setting-outlined" color="#fff" />

View File

@@ -1,11 +1,10 @@
<script setup lang="ts">
import { computed } from 'vue'
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
import { ElDropdown, ElDropdownMenu, ElDropdownItem, ComponentSize } from 'element-plus'
import { useAppStore } from '@/store/modules/app'
import { useI18n } from '@/hooks/web/useI18n'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
import { ElementPlusSize } from '@/types/elementPlus'
const { getPrefixCls } = useDesign()
@@ -21,7 +20,7 @@ const appStore = useAppStore()
const sizeMap = computed(() => appStore.sizeMap)
const setCurrentSize = (size: ElementPlusSize) => {
const setCurrentSize = (size: ComponentSize) => {
appStore.setCurrentSize(size)
}
</script>

View File

@@ -1,3 +0,0 @@
import Sticky from './src/Sticky.vue'
export { Sticky }

View File

@@ -1,141 +0,0 @@
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes'
import { ref, onMounted, onActivated, shallowRef } from 'vue'
import { useEventListener, useWindowSize, isClient } from '@vueuse/core'
import type { CSSProperties } from 'vue'
const props = defineProps({
// 距离顶部或者底部的距离(单位px)
offset: propTypes.number.def(0),
// 设置元素的堆叠顺序
zIndex: propTypes.number.def(999),
// 设置指定的class
className: propTypes.string.def(''),
// 定位方式,默认为(top)表示距离顶部位置可以设置为top或者bottom
position: {
type: String,
validator: function (value: string) {
return ['top', 'bottom'].indexOf(value) !== -1
},
default: 'top'
}
})
const width = ref('auto' as string)
const height = ref('auto' as string)
const isSticky = ref(false)
const refSticky = shallowRef<HTMLElement>()
const scrollContainer = shallowRef<HTMLElement | Window>()
const { height: windowHeight } = useWindowSize()
onMounted(() => {
height.value = refSticky.value?.getBoundingClientRect().height + 'px'
scrollContainer.value = getScrollContainer(refSticky.value!, true)
useEventListener(scrollContainer, 'scroll', handleScroll)
useEventListener('resize', handleReize)
handleScroll()
})
onActivated(() => {
handleScroll()
})
const camelize = (str: string): string => {
return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
}
const getStyle = (element: HTMLElement, styleName: keyof CSSProperties): string => {
if (!isClient || !element || !styleName) return ''
let key = camelize(styleName)
if (key === 'float') key = 'cssFloat'
try {
const style = element.style[styleName]
if (style) return style
const computed = document.defaultView?.getComputedStyle(element, '')
return computed ? computed[styleName] : ''
} catch {
return element.style[styleName]
}
}
const isScroll = (el: HTMLElement, isVertical?: boolean): boolean => {
if (!isClient) return false
const key = (
{
undefined: 'overflow',
true: 'overflow-y',
false: 'overflow-x'
} as const
)[String(isVertical)]!
const overflow = getStyle(el, key)
return ['scroll', 'auto', 'overlay'].some((s) => overflow.includes(s))
}
const getScrollContainer = (
el: HTMLElement,
isVertical: boolean
): Window | HTMLElement | undefined => {
if (!isClient) return
let parent = el
while (parent) {
if ([window, document, document.documentElement].includes(parent)) return window
if (isScroll(parent, isVertical)) return parent
parent = parent.parentNode as HTMLElement
}
return parent
}
const handleScroll = () => {
width.value = refSticky.value!.getBoundingClientRect().width! + 'px'
if (props.position === 'top') {
const offsetTop = refSticky.value?.getBoundingClientRect().top
if (offsetTop !== undefined && offsetTop < props.offset) {
sticky()
return
}
reset()
} else {
const offsetBottom = refSticky.value?.getBoundingClientRect().bottom
if (offsetBottom !== undefined && offsetBottom > windowHeight.value - props.offset) {
sticky()
return
}
reset()
}
}
const handleReize = () => {
if (isSticky.value && refSticky.value) {
width.value = refSticky.value.getBoundingClientRect().width + 'px'
}
}
const sticky = () => {
if (isSticky.value) {
return
}
isSticky.value = true
}
const reset = () => {
if (!isSticky.value) {
return
}
width.value = 'auto'
isSticky.value = false
}
</script>
<template>
<div :style="{ height: height, zIndex: zIndex }" ref="refSticky">
<div
:class="className"
:style="{
top: position === 'top' ? offset + 'px' : '',
bottom: position !== 'top' ? offset + 'px' : '',
zIndex: zIndex,
position: isSticky ? 'fixed' : 'static',
width: width,
height: height
}"
>
<slot>
<div>sticky</div>
</slot>
</div>
</div>
</template>

View File

@@ -1,11 +1,20 @@
import Table from './src/Table.vue'
import { ElTable } from 'element-plus'
import { TableSetPropsType } from '@/types/table'
import { TableColumn, TableSetProps } from './src/types'
export type {
TableColumn,
TableSlotDefault,
Pagination,
TableSetProps,
TableProps
} from './src/types'
export interface TableExpose {
setProps: (props: Recordable) => void
setColumn: (columnProps: TableSetPropsType[]) => void
selections: Recordable[]
setColumn: (columnProps: TableSetProps[]) => void
addColumn: (column: TableColumn, index?: number) => void
delColumn: (field: string) => void
elTableRef: ComponentRef<typeof ElTable>
}

View File

@@ -1,20 +1,30 @@
<script lang="tsx">
import { ElTable, ElTableColumn, ElPagination } from 'element-plus'
import {
ElTable,
ElTableColumn,
ElPagination,
ComponentSize,
ElTooltipProps,
ElImage
} from 'element-plus'
import { defineComponent, PropType, ref, computed, unref, watch, onMounted } from 'vue'
import { propTypes } from '@/utils/propTypes'
import { setIndex } from './helper'
import type { TableProps, TableColumn, Pagination, TableSetProps } from './types'
import { set, get } from 'lodash-es'
import { CSSProperties } from 'vue'
import { getSlot } from '@/utils/tsxHelper'
import type { TableProps } from './types'
import { set } from 'lodash-es'
import { TableColumn, TableSlotDefault, Pagination, TableSetPropsType } from '../../../types/table'
import TableActions from './components/TableActions.vue'
// import Sortable from 'sortablejs'
// import { Icon } from '@/components/Icon'
export default defineComponent({
name: 'Table',
props: {
pageSize: propTypes.number.def(10),
currentPage: propTypes.number.def(1),
// 是否多选
selection: propTypes.bool.def(true),
// 是否展示表格的工具栏
showAction: propTypes.bool.def(false),
// 是否所有的超出隐藏优先级低于schema中的showOverflowTooltip,
showOverflowTooltip: propTypes.bool.def(true),
// 表头
@@ -23,7 +33,7 @@ export default defineComponent({
default: () => []
},
// 展开行
expand: propTypes.bool.def(false),
// expand: propTypes.bool.def(false),
// 是否展示分页
pagination: {
type: Object as PropType<Pagination>,
@@ -46,10 +56,140 @@ export default defineComponent({
data: {
type: Array as PropType<Recordable[]>,
default: () => []
}
},
// 是否自动预览
preview: {
type: Array as PropType<string[]>,
default: () => []
},
// sortable: propTypes.bool.def(false),
height: propTypes.oneOfType([Number, String]),
maxHeight: propTypes.oneOfType([Number, String]),
stripe: propTypes.bool.def(false),
border: propTypes.bool.def(true),
size: {
type: String as PropType<ComponentSize>,
validator: (v: ComponentSize) => ['medium', 'small', 'mini'].includes(v)
},
fit: propTypes.bool.def(true),
showHeader: propTypes.bool.def(true),
highlightCurrentRow: propTypes.bool.def(false),
currentRowKey: propTypes.oneOfType([Number, String]),
// row-class-name, 类型为 (row: Recordable, rowIndex: number) => string | string
rowClassName: {
type: [Function, String] as PropType<(row: Recordable, rowIndex: number) => string | string>,
default: ''
},
rowStyle: {
type: [Function, Object] as PropType<
(row: Recordable, rowIndex: number) => Recordable | CSSProperties
>,
default: () => undefined
},
cellClassName: {
type: [Function, String] as PropType<
(row: Recordable, column: any, rowIndex: number) => string | string
>,
default: ''
},
cellStyle: {
type: [Function, Object] as PropType<
(row: Recordable, column: any, rowIndex: number) => Recordable | CSSProperties
>,
default: () => undefined
},
headerRowClassName: {
type: [Function, String] as PropType<(row: Recordable, rowIndex: number) => string | string>,
default: ''
},
headerRowStyle: {
type: [Function, Object] as PropType<
(row: Recordable, rowIndex: number) => Recordable | CSSProperties
>,
default: () => undefined
},
headerCellClassName: {
type: [Function, String] as PropType<
(row: Recordable, column: any, rowIndex: number) => string | string
>,
default: ''
},
headerCellStyle: {
type: [Function, Object] as PropType<
(row: Recordable, column: any, rowIndex: number) => Recordable | CSSProperties
>,
default: () => undefined
},
rowKey: propTypes.string.def('id'),
emptyText: propTypes.string.def('No Data'),
defaultExpandAll: propTypes.bool.def(false),
expandRowKeys: {
type: Array as PropType<string[]>,
default: () => []
},
defaultSort: {
type: Object as PropType<{ prop: string; order: string }>,
default: () => ({})
},
tooltipEffect: {
type: String as PropType<'dark' | 'light'>,
default: 'dark'
},
tooltipOptions: {
type: Object as PropType<
Pick<
ElTooltipProps,
| 'effect'
| 'enterable'
| 'hideAfter'
| 'offset'
| 'placement'
| 'popperClass'
| 'popperOptions'
| 'showAfter'
| 'showArrow'
>
>,
default: () => ({
enterable: true,
placement: 'top',
showArrow: true,
hideAfter: 200,
popperOptions: { strategy: 'fixed' }
})
},
showSummary: propTypes.bool.def(false),
sumText: propTypes.string.def('Sum'),
summaryMethod: {
type: Function as PropType<(param: { columns: any[]; data: any[] }) => any[]>,
default: () => undefined
},
spanMethod: {
type: Function as PropType<
(param: { row: any; column: any; rowIndex: number; columnIndex: number }) => any[]
>,
default: () => undefined
},
selectOnIndeterminate: propTypes.bool.def(true),
indent: propTypes.number.def(16),
lazy: propTypes.bool.def(false),
load: {
type: Function as PropType<(row: Recordable, treeNode: any, resolve: Function) => void>,
default: () => undefined
},
treeProps: {
type: Object as PropType<{ hasChildren?: string; children?: string; label?: string }>,
default: () => ({ hasChildren: 'hasChildren', children: 'children', label: 'label' })
},
tableLayout: {
type: String as PropType<'auto' | 'fixed'>,
default: 'fixed'
},
scrollbarAlwaysOn: propTypes.bool.def(false),
flexible: propTypes.bool.def(false)
},
emits: ['update:pageSize', 'update:currentPage', 'register'],
setup(props, { attrs, slots, emit, expose }) {
emits: ['update:pageSize', 'update:currentPage', 'register', 'refresh', 'sortable-change'],
setup(props, { attrs, emit, slots, expose }) {
const elTableRef = ref<ComponentRef<typeof ElTable>>()
// 注册
@@ -73,12 +213,39 @@ export default defineComponent({
return propsObj
})
// const sortableEl = ref()
// 初始化拖拽
// const initDropTable = () => {
// const el = unref(elTableRef)?.$el.querySelector('.el-table__body tbody')
// if (!el) return
// if (unref(sortableEl)) unref(sortableEl).destroy()
// sortableEl.value = Sortable.create(el, {
// handle: '.table-move',
// animation: 180,
// onEnd(e: any) {
// emit('sortable-change', e)
// }
// })
// }
// watch(
// () => getProps.value.sortable,
// async (v) => {
// await nextTick()
// v && initDropTable()
// },
// {
// immediate: true
// }
// )
const setProps = (props: TableProps = {}) => {
mergeProps.value = Object.assign(unref(mergeProps), props)
outsideProps.value = props
outsideProps.value = { ...props } as any
}
const setColumn = (columnProps: TableSetPropsType[], columnsChildren?: TableColumn[]) => {
const setColumn = (columnProps: TableSetProps[], columnsChildren?: TableColumn[]) => {
const { columns } = unref(getProps)
for (const v of columnsChildren || columns) {
for (const item of columnProps) {
@@ -91,16 +258,36 @@ export default defineComponent({
}
}
const selections = ref<Recordable[]>([])
const addColumn = (column: TableColumn, index?: number) => {
const { columns } = unref(getProps)
if (index) {
columns.splice(index, 0, column)
} else {
columns.push(column)
}
}
const selectionChange = (selection: Recordable[]) => {
selections.value = selection
const delColumn = (field: string) => {
const { columns } = unref(getProps)
const index = columns.findIndex((item) => item.field === field)
if (index > -1) {
columns.splice(index, 1)
}
}
const refresh = () => {
emit('refresh')
}
const changSize = (size: ComponentSize) => {
setProps({ size })
}
expose({
setProps,
setColumn,
selections,
delColumn,
addColumn,
elTableRef
})
@@ -149,44 +336,44 @@ export default defineComponent({
)
const getBindValue = computed(() => {
const bindValue: Recordable = { ...attrs, ...props }
const bindValue: Recordable = { ...attrs, ...unref(getProps) }
delete bindValue.columns
delete bindValue.data
return bindValue
})
const renderTableSelection = () => {
const { selection, reserveSelection, align, headerAlign } = unref(getProps)
// 渲染多选
return selection ? (
<ElTableColumn
type="selection"
reserveSelection={reserveSelection}
align={align}
headerAlign={headerAlign}
width="50"
></ElTableColumn>
) : undefined
}
const renderTableExpand = () => {
const { align, headerAlign, expand } = unref(getProps)
// 渲染展开行
return expand ? (
<ElTableColumn type="expand" align={align} headerAlign={headerAlign}>
{{
// @ts-ignore
default: (data: TableSlotDefault) => getSlot(slots, 'expand', data)
}}
</ElTableColumn>
) : undefined
}
const rnderTreeTableColumn = (columnsChildren: TableColumn[]) => {
const { align, headerAlign, showOverflowTooltip } = unref(getProps)
const renderTreeTableColumn = (columnsChildren: TableColumn[]) => {
const { align, headerAlign, showOverflowTooltip, preview } = unref(getProps)
return columnsChildren.map((v) => {
const props = { ...v }
if (v.hidden) return null
const props = { ...v } as any
if (props.children) delete props.children
const children = v.children
const slots = {
default: (...args: any[]) => {
const data = args[0]
let isImageUrl = false
if (preview.length) {
isImageUrl = preview.some((item) => (item as string) === v.field)
}
return children && children.length
? renderTreeTableColumn(children)
: props?.slots?.default
? props.slots.default(args)
: v?.formatter
? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index)
: isImageUrl
? renderPreview(get(data.row, v.field))
: get(data.row, v.field)
}
}
if (props?.slots?.header) {
slots['header'] = (...args: any[]) => props.slots.header(args)
}
return (
<ElTableColumn
showOverflowTooltip={showOverflowTooltip}
@@ -195,23 +382,28 @@ export default defineComponent({
{...props}
prop={v.field}
>
{{
default: (data: TableSlotDefault) =>
v.children && v.children.length
? rnderTableColumn(v.children)
: // @ts-ignore
getSlot(slots, v.field, data) ||
v?.formatter?.(data.row, data.column, data.row[v.field], data.$index) ||
data.row[v.field],
// @ts-ignore
header: getSlot(slots, `${v.field}-header`)
}}
{slots}
</ElTableColumn>
)
})
}
const rnderTableColumn = (columnsChildren?: TableColumn[]) => {
const renderPreview = (url: string) => {
return (
<div class="flex items-center">
<ElImage
src={url}
fit="cover"
class="w-[100%] h-100px"
lazy
preview-src-list={[url]}
preview-teleported
/>
</div>
)
}
const renderTableColumn = (columnsChildren?: TableColumn[]) => {
const {
columns,
reserveIndex,
@@ -219,80 +411,130 @@ export default defineComponent({
currentPage,
align,
headerAlign,
showOverflowTooltip
showOverflowTooltip,
reserveSelection,
preview
} = unref(getProps)
return [...[renderTableExpand()], ...[renderTableSelection()]].concat(
(columnsChildren || columns).map((v) => {
// 自定生成序号
if (v.type === 'index') {
return (
<ElTableColumn
type="index"
index={
v.index
? v.index
: (index) => setIndex(reserveIndex, index, pageSize, currentPage)
}
align={v.align || align}
headerAlign={v.headerAlign || headerAlign}
label={v.label}
width="65px"
></ElTableColumn>
)
} else {
const props = { ...v }
if (props.children) delete props.children
return (
<ElTableColumn
showOverflowTooltip={showOverflowTooltip}
align={align}
headerAlign={headerAlign}
{...props}
prop={v.field}
>
{{
default: (data: TableSlotDefault) =>
v.children && v.children.length
? rnderTreeTableColumn(v.children)
: // @ts-ignore
getSlot(slots, v.field, data) ||
v?.formatter?.(data.row, data.column, data.row[v.field], data.$index) ||
data.row[v.field],
// @ts-ignore
header: () => getSlot(slots, `${v.field}-header`) || v.label
}}
</ElTableColumn>
)
return (columnsChildren || columns).map((v) => {
if (v.hidden) return null
if (v.type === 'index') {
return (
<ElTableColumn
type="index"
index={
v.index ? v.index : (index) => setIndex(reserveIndex, index, pageSize, currentPage)
}
align={v.align || align}
headerAlign={v.headerAlign || headerAlign}
label={v.label}
width="65px"
></ElTableColumn>
)
} else if (v.type === 'selection') {
return (
<ElTableColumn
type="selection"
reserveSelection={reserveSelection}
align={align}
headerAlign={headerAlign}
width="50"
></ElTableColumn>
)
} else {
const props = { ...v } as any
if (props.children) delete props.children
const children = v.children
const slots = {
default: (...args: any[]) => {
const data = args[0]
let isImageUrl = false
if (preview.length) {
isImageUrl = preview.some((item) => (item as string) === v.field)
}
return children && children.length
? renderTreeTableColumn(children)
: props?.slots?.default
? props.slots.default(args)
: v?.formatter
? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index)
: isImageUrl
? renderPreview(get(data.row, v.field))
: get(data.row, v.field)
}
}
})
)
if (props?.slots?.header) {
slots['header'] = (...args: any[]) => props.slots.header(args)
}
return (
<ElTableColumn
showOverflowTooltip={showOverflowTooltip}
align={align}
headerAlign={headerAlign}
{...props}
prop={v.field}
>
{slots}
</ElTableColumn>
)
}
})
}
return () => (
<div v-loading={unref(getProps).loading}>
<ElTable
// @ts-ignore
ref={elTableRef}
data={unref(getProps).data}
onSelection-change={selectionChange}
{...unref(getBindValue)}
>
{{
default: () => rnderTableColumn(),
// @ts-ignore
append: () => getSlot(slots, 'append')
}}
</ElTable>
{unref(getProps).pagination ? (
<ElPagination
v-model:pageSize={pageSizeRef.value}
v-model:currentPage={currentPageRef.value}
class="mt-10px"
{...unref(pagination)}
></ElPagination>
) : undefined}
</div>
)
return () => {
const tableSlots = {}
if (getSlot(slots, 'empty')) {
tableSlots['empty'] = (...args: any[]) => getSlot(slots, 'empty', args)
}
if (getSlot(slots, 'append')) {
tableSlots['append'] = (...args: any[]) => getSlot(slots, 'append', args)
}
// const { sortable } = unref(getProps)
// const sortableEl = sortable ? (
// <ElTableColumn
// className="table-move cursor-move"
// type="sortable"
// prop="sortable"
// width="60px"
// align="center"
// >
// <Icon icon="ant-design:drag-outlined" />
// </ElTableColumn>
// ) : null
return (
<div v-loading={unref(getProps).loading}>
{unref(getProps).showAction ? (
<TableActions
columns={unref(getProps).columns}
onChangSize={changSize}
onRefresh={refresh}
/>
) : null}
<ElTable ref={elTableRef} data={unref(getProps).data} {...unref(getBindValue)}>
{{
default: () => renderTableColumn(),
...tableSlots
}}
</ElTable>
{unref(getProps).pagination ? (
<ElPagination
v-model:pageSize={pageSizeRef.value}
v-model:currentPage={currentPageRef.value}
class="mt-10px"
{...unref(pagination)}
></ElPagination>
) : undefined}
</div>
)
}
}
})
</script>

View File

@@ -0,0 +1,151 @@
<script lang="tsx">
import { defineComponent, unref, computed, PropType, watch } from 'vue'
import {
ElTooltip,
ElDropdown,
ElDropdownMenu,
ElDropdownItem,
ComponentSize
// ElPopover,
// ElTree
} from 'element-plus'
import { Icon } from '@/components/Icon'
import { useI18n } from '@/hooks/web/useI18n'
import { useAppStore } from '@/store/modules/app'
import { TableColumn } from '../types'
import { cloneDeep } from 'lodash-es'
// import { eachTree } from '@/utils/tree'
const appStore = useAppStore()
const sizeMap = computed(() => appStore.sizeMap)
const { t } = useI18n()
export default defineComponent({
name: 'TableActions',
props: {
columns: {
type: Array as PropType<TableColumn[]>,
default: () => []
}
},
emits: ['refresh', 'changSize'],
setup(props, { emit }) {
const refresh = () => {
emit('refresh')
}
const changSize = (size: ComponentSize) => {
emit('changSize', size)
}
const columns = computed(() => {
return cloneDeep(props.columns).filter((v) => {
// 去掉type为selection的列和expand的列
if (v.type !== 'selection' && v.type !== 'expand') {
return v
}
})
})
watch(
() => columns.value,
(newColumns) => {
console.log('columns change', newColumns)
},
{
deep: true
}
)
return () => (
<>
<div class="text-right h-28px flex items-center justify-end">
<ElTooltip content={t('common.refresh')} placement="top">
<span onClick={refresh}>
<Icon
icon="ant-design:sync-outlined"
class="cursor-pointer"
hover-color="var(--el-color-primary)"
/>
</span>
</ElTooltip>
<ElTooltip content={t('common.size')} placement="top">
<ElDropdown trigger="click" onCommand={changSize}>
{{
default: () => {
return (
<span>
<Icon
icon="ant-design:column-height-outlined"
class="cursor-pointer mr-8px ml-8px"
hover-color="var(--el-color-primary)"
/>
</span>
)
},
dropdown: () => {
return (
<ElDropdownMenu>
{{
default: () => {
return unref(sizeMap).map((v) => {
return (
<ElDropdownItem key={v} command={v}>
{t(`size.${v}`)}
</ElDropdownItem>
)
})
}
}}
</ElDropdownMenu>
)
}
}}
</ElDropdown>
</ElTooltip>
{/* <ElTooltip content={t('common.columnSetting')} placement="top"> */}
{/* <ElPopover trigger="click" placement="left">
{{
default: () => {
return (
<div>
<ElTree
data={unref(columns)}
show-checkbox
default-checked-keys={unref(defaultCheckeds)}
draggable
node-key="field"
allow-drop={(_draggingNode: any, _dropNode: any, type: string) => {
if (type === 'inner') {
return false
} else {
return true
}
}}
onNode-drag-end={onNodeDragEnd}
onCheck-change={onCheckChange}
/>
</div>
)
},
reference: () => {
return (
<Icon
icon="ant-design:setting-outlined"
class="cursor-pointer"
hoverColor="var(--el-color-primary)"
/>
)
}
}}
</ElPopover> */}
{/* </ElTooltip> */}
</div>
</>
)
}
})
</script>

View File

@@ -1,26 +0,0 @@
import { Pagination, TableColumn } from '@/types/table'
export type TableProps = {
pageSize?: number
currentPage?: number
// 是否多选
selection?: boolean
// 是否所有的超出隐藏优先级低于schema中的showOverflowTooltip,
showOverflowTooltip?: boolean
// 表头
columns?: TableColumn[]
// 是否展示分页
pagination?: Pagination | undefined
// 仅对 type=selection 的列有效,类型为 Boolean为 true 则会在数据更新之后保留之前选中的数据(需指定 row-key
reserveSelection?: boolean
// 加载状态
loading?: boolean
// 是否叠加索引
reserveIndex?: boolean
// 对齐方式
align?: 'left' | 'center' | 'right'
// 表头对齐方式
headerAlign?: 'left' | 'center' | 'right'
data?: Recordable
expand?: boolean
} & Recordable

View File

@@ -0,0 +1,97 @@
import { TableProps as ElTableProps } from 'element-plus'
export interface TableColumn {
field: string
label?: string
type?: string
/**
* 是否隐藏
*/
hidden?: boolean
children?: TableColumn[]
slots?: {
default?: (...args: any[]) => JSX.Element | JSX.Element[] | null
header?: (...args: any[]) => JSX.Element | null
}
index?: number | ((index: number) => number)
columnKey?: string
width?: string | number
minWidth?: string | number
fixed?: boolean | 'left' | 'right'
renderHeader?: (...args: any[]) => JSX.Element | null
// sortable?: boolean
sortMethod?: (...args: any[]) => number
sortBy?: string | string[] | ((...args: any[]) => string | string[])
sortOrders?: (string | null)[]
resizable?: boolean
formatter?: (...args: any[]) => any
showOverflowTooltip?: boolean
align?: 'left' | 'center' | 'right'
headerAlign?: 'left' | 'center' | 'right'
className?: string
labelClassName?: string
selectable?: (...args: any[]) => boolean
reserveSelection?: boolean
filters?: Array<{ text: string; value: string }>
filterPlacement?: string
filterMultiple?: boolean
filterMethod?: (...args: any[]) => boolean
filteredValue?: string[]
[key: string]: any
}
export interface TableSlotDefault {
row: Recordable
column: TableColumn
$index: number
[key: string]: any
}
export interface Pagination {
small?: boolean
background?: boolean
pageSize?: number
defaultPageSize?: number
total?: number
pageCount?: number
pagerCount?: number
currentPage?: number
defaultCurrentPage?: number
layout?: string
pageSizes?: number[]
popperClass?: string
prevText?: string
nextText?: string
disabled?: boolean
hideOnSinglePage?: boolean
}
export interface TableSetProps {
field: string
path: string
value: any
}
export interface TableProps extends Omit<Partial<ElTableProps<any[]>>, 'data'> {
pageSize?: number
currentPage?: number
showAction?: boolean
// 是否所有的超出隐藏优先级低于schema中的showOverflowTooltip,
showOverflowTooltip?: boolean
// 表头
columns?: TableColumn[]
// 是否展示分页
pagination?: Pagination | undefined
// 仅对 type=selection 的列有效,类型为 Boolean为 true 则会在数据更新之后保留之前选中的数据(需指定 row-key
reserveSelection?: boolean
// 加载状态
loading?: boolean
// 是否叠加索引
reserveIndex?: boolean
// 对齐方式
align?: 'left' | 'center' | 'right'
// 表头对齐方式
headerAlign?: 'left' | 'center' | 'right'
preview?: string[]
sortable?: boolean
data?: Recordable
}

View File

@@ -35,6 +35,8 @@ const appStore = useAppStore()
const tagsViewIcon = computed(() => appStore.getTagsViewIcon)
const isDark = computed(() => appStore.getIsDark)
// 初始化tag
const initTags = () => {
affixTagArr.value = filterAffixTags(unref(routers))
@@ -73,7 +75,7 @@ const closeAllTags = () => {
toLastView()
}
// 关闭其
// 关闭其
const closeOthersTags = () => {
tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
}
@@ -270,7 +272,8 @@ watch(
>
<Icon
icon="ep:d-arrow-left"
:color="appStore.getIsDark ? 'var(--el-text-color-regular)' : '#333'"
color="var(--el-text-color-placeholder)"
:hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
/>
</span>
<div class="overflow-hidden flex-1">
@@ -386,7 +389,8 @@ watch(
>
<Icon
icon="ep:d-arrow-right"
:color="appStore.getIsDark ? 'var(--el-text-color-regular)' : '#333'"
color="var(--el-text-color-placeholder)"
:hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
/>
</span>
<span
@@ -396,7 +400,8 @@ watch(
>
<Icon
icon="ant-design:reload-outlined"
:color="appStore.getIsDark ? 'var(--el-text-color-regular)' : '#333'"
color="var(--el-text-color-placeholder)"
:hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
/>
</span>
<ContextMenu
@@ -459,7 +464,8 @@ watch(
>
<Icon
icon="ant-design:setting-outlined"
:color="appStore.getIsDark ? 'var(--el-text-color-regular)' : '#333'"
color="var(--el-text-color-placeholder)"
:hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
/>
</span>
</ContextMenu>
@@ -476,36 +482,29 @@ watch(
&__tool {
position: relative;
&:after {
&::before {
position: absolute;
top: 1px;
left: 0;
width: 100%;
height: calc(~'100% - 1px');
border-left: 1px solid var(--layout-border-color);
border-left: 1px solid var(--el-border-color);
content: '';
}
&--first {
&:after {
display: none;
}
&:before {
&::before {
position: absolute;
top: 1px;
left: 0;
width: 100%;
height: calc(~'100% - 1px');
border-right: 1px solid var(--layout-border-color);
border-right: 1px solid var(--el-border-color);
border-left: none;
content: '';
}
}
&:hover {
:deep(span) {
color: var(--el-color-black) !important;
}
}
}
&__item {
@@ -544,7 +543,7 @@ watch(
background-color: var(--el-color-primary);
border: 1px solid var(--el-color-primary);
.@{prefix-cls}__item--close {
:deep(span) {
:deep(svg) {
color: var(--el-color-white) !important;
}
}
@@ -554,14 +553,8 @@ watch(
.dark {
.@{prefix-cls} {
&__tool {
&:hover {
:deep(span) {
color: #fff !important;
}
}
&--first {
&:after {
&::after {
display: none;
}
}
@@ -582,7 +575,7 @@ watch(
background-color: var(--el-color-primary);
border: 1px solid var(--el-color-primary);
.@{prefix-cls}__item--close {
:deep(span) {
:deep(svg) {
color: var(--el-color-white) !important;
}
}

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ElDropdown, ElDropdownMenu, ElDropdownItem, ElMessageBox } from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n'
import { useCache } from '@/hooks/web/useCache'
import { useStorage } from '@/hooks/web/useStorage'
import { resetRouter } from '@/router'
import { useRouter } from 'vue-router'
import { loginOutApi } from '@/api/login'
@@ -24,7 +24,7 @@ const prefixCls = getPrefixCls('user-info')
const { t } = useI18n()
const { wsCache } = useCache()
const { clear } = useStorage()
const { replace } = useRouter()
@@ -37,7 +37,7 @@ const loginOut = () => {
.then(async () => {
const res = await loginOutApi().catch(() => {})
if (res) {
wsCache.clear()
clear()
tagsViewStore.delAllViews()
resetRouter() // 重置静态路由表
replace('/login')
@@ -94,7 +94,9 @@ const toDocument = () => {
<style scoped lang="less">
.fade-bottom-enter-active,
.fade-bottom-leave-active {
transition: opacity 0.25s, transform 0.3s;
transition:
opacity 0.25s,
transform 0.3s;
}
.fade-bottom-enter-from {

View File

@@ -1,12 +1,12 @@
<script setup lang="ts">
import { useI18n } from '@/hooks/web/useI18n'
import { ref, unref } from 'vue'
import { ref } from 'vue'
import { Dialog } from '@/components/Dialog'
import { Form } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
import { reactive, computed } from 'vue'
import { useValidator } from '@/hooks/web/useValidator'
import { FormSchema } from '@/types/form'
import { FormSchema } from '@/components/Form'
import { ElButton } from 'element-plus'
import { useDesign } from '@/hooks/web/useDesign'
import { useLockStore } from '@/store/modules/lock'
@@ -54,14 +54,13 @@ const schema: FormSchema[] = reactive([
}
])
const { register, formRef, methods } = useForm({
schema
})
const { formRegister, formMethods } = useForm()
const { getFormData } = methods
const { getFormData, getElFormExpose } = formMethods
const handleLock = () => {
unref(formRef)?.validate(async (valid) => {
const handleLock = async () => {
const formExpose = await getElFormExpose()
formExpose?.validate(async (valid) => {
if (valid) {
dialogVisible.value = false
const formData = await getFormData()
@@ -86,7 +85,7 @@ const handleLock = () => {
<img src="@/assets/imgs/avatar.jpg" alt="" class="w-70px h-70px rounded-[50%]" />
<span class="text-14px my-10px text-[var(--top-header-text-color)]">Archer</span>
</div>
<Form :is-col="false" :rules="rules" @register="register" />
<Form :is-col="false" :schema="schema" :rules="rules" @register="formRegister" />
<template #footer>
<ElButton type="primary" @click="handleLock">{{ t('lock.lock') }}</ElButton>
</template>

View File

@@ -3,10 +3,10 @@ import { ref } from 'vue'
import { ElInput, ElButton } from 'element-plus'
import { resetRouter } from '@/router'
import { useRouter } from 'vue-router'
import { useCache } from '@/hooks/web/useCache'
import { useStorage } from '@/hooks/web/useStorage'
import { useLockStore } from '@/store/modules/lock'
import { useI18n } from '@/hooks/web/useI18n'
import { useNow } from './useNow'
import { useNow } from '@/hooks/web/useNow'
import { useDesign } from '@/hooks/web/useDesign'
import { Icon } from '@/components/Icon'
import { loginOutApi } from '@/api/login'
@@ -14,7 +14,7 @@ import { useTagsViewStore } from '@/store/modules/tagsView'
const tagsViewStore = useTagsViewStore()
const { wsCache } = useCache()
const { clear } = useStorage()
const { replace } = useRouter()
@@ -51,7 +51,7 @@ async function unLock() {
async function goLogin() {
const res = await loginOutApi().catch(() => {})
if (res) {
wsCache.clear()
clear()
tagsViewStore.delAllViews()
resetRouter() // 重置静态路由表
lockStore.resetLockInfo()

View File

@@ -1,6 +1,8 @@
import type { App } from 'vue'
import { Icon } from './Icon'
import { Permission } from './Permission'
export const setupGlobCom = (app: App<Element>): void => {
app.component('Icon', Icon)
app.component('Permission', Permission)
}

View File

@@ -4,7 +4,7 @@ import {
AxiosRequestHeaders,
AxiosError,
InternalAxiosRequestConfig
} from './type'
} from './types'
import { ElMessage } from 'element-plus'
import qs from 'qs'

View File

@@ -4,7 +4,7 @@ import config from './config'
const { defaultHeaders } = config
const request = (option: any) => {
const request = (option: AxiosConfig) => {
const { url, method, params, data, headersType, responseType } = option
return service.request({
url: url,
@@ -19,17 +19,17 @@ const request = (option: any) => {
}
export default {
get: <T = any>(option: any) => {
return request({ method: 'get', ...option }) as unknown as T
get: <T = any>(option: AxiosConfig) => {
return request({ method: 'get', ...option }) as Promise<IResponse<T>>
},
post: <T = any>(option: any) => {
return request({ method: 'post', ...option }) as unknown as T
post: <T = any>(option: AxiosConfig) => {
return request({ method: 'post', ...option }) as Promise<IResponse<T>>
},
delete: <T = any>(option: any) => {
return request({ method: 'delete', ...option }) as unknown as T
delete: <T = any>(option: AxiosConfig) => {
return request({ method: 'delete', ...option }) as Promise<IResponse<T>>
},
put: <T = any>(option: any) => {
return request({ method: 'put', ...option }) as unknown as T
put: <T = any>(option: AxiosConfig) => {
return request({ method: 'put', ...option }) as Promise<IResponse<T>>
},
cancelRequest: (url: string | string[]) => {
return service.cancelRequest(url)

View File

@@ -1,10 +1,10 @@
import axios from 'axios'
import config, { defaultRequestInterceptors, defaultResponseInterceptors } from './config'
import { AxiosInstance, InternalAxiosRequestConfig, RequestConfig, AxiosResponse } from './type'
import { AxiosInstance, InternalAxiosRequestConfig, RequestConfig, AxiosResponse } from './types'
const { interceptors, baseUrl } = config
export const PATH_URL = baseUrl[import.meta.env.VITE_API_BASEPATH]
export const PATH_URL = baseUrl[import.meta.env.VITE_API_BASE_PATH]
const { requestInterceptors, responseInterceptors } = interceptors

View File

@@ -1,28 +1,18 @@
import type { App, Directive, DirectiveBinding } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useCache } from '@/hooks/web/useCache'
import { intersection } from 'lodash-es'
import { isArray } from '@/utils/is'
import { useAppStoreWithOut } from '@/store/modules/app'
import router from '@/router'
const { t } = useI18n()
const { wsCache } = useCache()
const appStore = useAppStoreWithOut()
// 全部权限
const all_permission = ['*.*.*']
const hasPermission = (value: string | string[]): boolean => {
const permissions = wsCache.get(appStore.getUserInfo).permissions as string[]
const hasPermission = (value: string): boolean => {
const permission = (router.currentRoute.value.meta.permission || []) as string[]
if (!value) {
throw new Error(t('permission.hasPermission'))
}
if (!isArray(value)) {
return permissions?.includes(value as string)
}
if (all_permission[0] === permissions[0]) {
if (permission.includes(value)) {
return true
}
return (intersection(value, permissions) as string[]).length > 0
return false
}
function hasPermi(el: Element, binding: DirectiveBinding) {
const value = binding.value

View File

@@ -1,17 +0,0 @@
/**
* 配置浏览器本地存储的方式,可直接存储对象数组。
*/
import WebStorageCache from 'web-storage-cache'
type CacheType = 'sessionStorage' | 'localStorage'
export const useCache = (type: CacheType = 'sessionStorage') => {
const wsCache: WebStorageCache = new WebStorageCache({
storage: type
})
return {
wsCache
}
}

View File

@@ -1,4 +1,4 @@
import { ConfigGlobalTypes } from '@/types/configGlobal'
import { ConfigGlobalTypes } from '@/components/ConfigGlobal'
import { inject } from 'vue'
export const useConfigGlobal = () => {

View File

@@ -1,12 +1,8 @@
import { reactive } from 'vue'
import { eachTree, treeMap, filter } from '@/utils/tree'
import { findIndex } from '@/utils'
import { useDictStoreWithOut } from '@/store/modules/dict'
import { useI18n } from '@/hooks/web/useI18n'
import type { AxiosPromise } from 'axios'
import { FormSchema } from '@/types/form'
import { TableColumn } from '@/types/table'
import { DescriptionsSchema } from '@/types/descriptions'
import { FormSchema } from '@/components/Form'
import { TableColumn } from '@/components/Table'
import { DescriptionsSchema } from '@/components/Descriptions'
export type CrudSchema = Omit<TableColumn, 'children'> & {
search?: CrudSearchParams
@@ -16,39 +12,25 @@ export type CrudSchema = Omit<TableColumn, 'children'> & {
children?: CrudSchema[]
}
type CrudSearchParams = {
// 是否显示在查询项
show?: boolean
// 字典名称,会去取全局的字典
dictName?: string
// 接口
api?: () => Promise<any>
// 搜索字段
field?: string
} & Omit<FormSchema, 'field'>
interface CrudSearchParams extends Omit<FormSchema, 'field'> {
// 是否隐藏在查询项
hidden?: boolean
}
type CrudTableParams = {
// 是否显示表头
show?: boolean
} & Omit<FormSchema, 'field'>
interface CrudTableParams extends Omit<TableColumn, 'field'> {
// 是否隐藏表头
hidden?: boolean
}
type CrudFormParams = {
// 字典名称,会去取全局的字典
dictName?: string
// 接口
api?: () => Promise<any>
// 是否显示表单项
show?: boolean
} & Omit<FormSchema, 'field'>
interface CrudFormParams extends Omit<FormSchema, 'field'> {
// 是否隐藏表单项
hidden?: boolean
}
type CrudDescriptionsParams = {
// 是否显示表单项
show?: boolean
} & Omit<DescriptionsSchema, 'field'>
const dictStore = useDictStoreWithOut()
const { t } = useI18n()
interface CrudDescriptionsParams extends Omit<DescriptionsSchema, 'field'> {
// 是否隐藏表单项
hidden?: boolean
}
interface AllSchemas {
searchSchema: FormSchema[]
@@ -71,13 +53,14 @@ export const useCrudSchemas = (
detailSchema: []
})
const searchSchema = filterSearchSchema(crudSchema, allSchemas)
const searchSchema = filterSearchSchema(crudSchema)
// @ts-ignore
allSchemas.searchSchema = searchSchema || []
const tableColumns = filterTableSchema(crudSchema)
allSchemas.tableColumns = tableColumns || []
const formSchema = filterFormSchema(crudSchema, allSchemas)
const formSchema = filterFormSchema(crudSchema)
allSchemas.formSchema = formSchema
const detailSchema = filterDescriptionsSchema(crudSchema)
@@ -89,55 +72,26 @@ export const useCrudSchemas = (
}
// 过滤 Search 结构
const filterSearchSchema = (crudSchema: CrudSchema[], allSchemas: AllSchemas): FormSchema[] => {
const filterSearchSchema = (crudSchema: CrudSchema[]): FormSchema[] => {
const searchSchema: FormSchema[] = []
const length = crudSchema.length
// 获取字典列表队列
const searchRequestTask: Array<() => Promise<void>> = []
eachTree(crudSchema, (schemaItem: CrudSchema) => {
// 判断是否显示
if (schemaItem?.search?.show) {
for (let i = 0; i < length; i++) {
const schemaItem = crudSchema[i]
// 判断是否隐藏
if (!schemaItem?.search?.hidden) {
const searchSchemaItem = {
// 默认为 input
component: schemaItem.search.component || 'Input',
componentProps: {},
component: schemaItem?.search?.component || 'Input',
...schemaItem.search,
field: schemaItem?.search?.field || schemaItem.field,
label: schemaItem.search?.label || schemaItem.label
}
if (searchSchemaItem.dictName) {
// 如果有 dictName 则证明是从字典中获取数据
const dictArr = dictStore.getDictObj[searchSchemaItem.dictName]
searchSchemaItem.componentProps!.options = filterOptions(dictArr)
} else if (searchSchemaItem.api) {
searchRequestTask.push(async () => {
const res = await (searchSchemaItem.api as () => AxiosPromise)()
if (res) {
const index = findIndex(allSchemas.searchSchema, (v: FormSchema) => {
return v.field === searchSchemaItem.field
})
if (index !== -1) {
allSchemas.searchSchema[index]!.componentProps!.options = filterOptions(
res,
searchSchemaItem.componentProps.optionsAlias?.labelField
)
}
}
})
field: schemaItem.field,
label: schemaItem.label
}
// 删除不必要的字段
delete searchSchemaItem.show
delete searchSchemaItem.dictName
delete searchSchemaItem.hidden
searchSchema.push(searchSchemaItem)
}
})
for (const task of searchRequestTask) {
task()
}
return searchSchema
@@ -147,7 +101,7 @@ const filterSearchSchema = (crudSchema: CrudSchema[], allSchemas: AllSchemas): F
const filterTableSchema = (crudSchema: CrudSchema[]): TableColumn[] => {
const tableColumns = treeMap<CrudSchema>(crudSchema, {
conversion: (schema: CrudSchema) => {
if (schema?.table?.show !== false) {
if (!schema?.table?.hidden) {
return {
...schema.table,
...schema
@@ -166,56 +120,28 @@ const filterTableSchema = (crudSchema: CrudSchema[]): TableColumn[] => {
}
// 过滤 form 结构
const filterFormSchema = (crudSchema: CrudSchema[], allSchemas: AllSchemas): FormSchema[] => {
const filterFormSchema = (crudSchema: CrudSchema[]): FormSchema[] => {
const formSchema: FormSchema[] = []
const length = crudSchema.length
// 获取字典列表队列
const formRequestTask: Array<() => Promise<void>> = []
eachTree(crudSchema, (schemaItem: CrudSchema) => {
// 判断是否显示
if (schemaItem?.form?.show !== false) {
for (let i = 0; i < length; i++) {
const formItem = crudSchema[i]
// 判断是否隐藏
if (!formItem?.form?.hidden) {
const formSchemaItem = {
// 默认为 input
component: schemaItem?.form?.component || 'Input',
componentProps: {},
...schemaItem.form,
field: schemaItem.field,
label: schemaItem.search?.label || schemaItem.label
}
if (formSchemaItem.dictName) {
// 如果有 dictName 则证明是从字典中获取数据
const dictArr = dictStore.getDictObj[formSchemaItem.dictName]
formSchemaItem.componentProps!.options = filterOptions(dictArr)
} else if (formSchemaItem.api) {
formRequestTask.push(async () => {
const res = await (formSchemaItem.api as () => AxiosPromise)()
if (res) {
const index = findIndex(allSchemas.formSchema, (v: FormSchema) => {
return v.field === formSchemaItem.field
})
if (index !== -1) {
allSchemas.formSchema[index]!.componentProps!.options = filterOptions(
res,
formSchemaItem.componentProps.optionsAlias?.labelField
)
}
}
})
component: formItem?.form?.component || 'Input',
...formItem.form,
field: formItem.field,
label: formItem.label
}
// 删除不必要的字段
delete formSchemaItem.show
delete formSchemaItem.dictName
delete formSchemaItem.hidden
formSchema.push(formSchemaItem)
}
})
for (const task of formRequestTask) {
task()
}
return formSchema
}
@@ -224,8 +150,8 @@ const filterDescriptionsSchema = (crudSchema: CrudSchema[]): DescriptionsSchema[
const descriptionsSchema: FormSchema[] = []
eachTree(crudSchema, (schemaItem: CrudSchema) => {
// 判断是否显示
if (schemaItem?.detail?.show !== false) {
// 判断是否隐藏
if (!schemaItem?.detail?.hidden) {
const descriptionsSchemaItem = {
...schemaItem.detail,
field: schemaItem.field,
@@ -233,7 +159,7 @@ const filterDescriptionsSchema = (crudSchema: CrudSchema[]): DescriptionsSchema[
}
// 删除不必要的字段
delete descriptionsSchemaItem.show
delete descriptionsSchemaItem.hidden
descriptionsSchema.push(descriptionsSchemaItem)
}
@@ -241,15 +167,3 @@ const filterDescriptionsSchema = (crudSchema: CrudSchema[]): DescriptionsSchema[
return descriptionsSchema
}
// 给options添加国际化
const filterOptions = (options: Recordable, labelField?: string) => {
return options?.map((v: Recordable) => {
if (labelField) {
v['labelField'] = t(v.labelField)
} else {
v['label'] = t(v.label)
}
return v
})
}

View File

@@ -1,10 +1,9 @@
import type { Form, FormExpose } from '@/components/Form'
import type { ElForm } from 'element-plus'
import type { ElForm, ElFormItem } from 'element-plus'
import { ref, unref, nextTick } from 'vue'
import type { FormProps } from '@/components/Form/src/types'
import { FormSchema, FormSetPropsType } from '@/types/form'
import { FormSchema, FormSetProps, FormProps } from '@/components/Form'
export const useForm = (props?: FormProps) => {
export const useForm = () => {
// From实例
const formRef = ref<typeof Form & FormExpose>()
@@ -31,6 +30,10 @@ export const useForm = (props?: FormProps) => {
// 一些内置的方法
const methods = {
/**
* @description 设置form组件的props
* @param props form组件的props
*/
setProps: async (props: FormProps = {}) => {
const form = await getForm()
form?.setProps(props)
@@ -39,20 +42,26 @@ export const useForm = (props?: FormProps) => {
}
},
/**
* @description 设置form的值
* @param data 需要设置的数据
*/
setValues: async (data: Recordable) => {
const form = await getForm()
form?.setValues(data)
},
/**
* @description 设置schema
* @param schemaProps 需要设置的schemaProps
*/
setSchema: async (schemaProps: FormSetPropsType[]) => {
setSchema: async (schemaProps: FormSetProps[]) => {
const form = await getForm()
form?.setSchema(schemaProps)
},
/**
* @description 新增schema
* @param formSchema 需要新增数据
* @param index 在哪里新增
*/
@@ -62,6 +71,7 @@ export const useForm = (props?: FormProps) => {
},
/**
* @description 删除schema
* @param field 删除哪个数据
*/
delSchema: async (field: string) => {
@@ -70,19 +80,51 @@ export const useForm = (props?: FormProps) => {
},
/**
* @description 获取表单数据
* @returns form data
*/
getFormData: async <T = Recordable>(): Promise<T> => {
const form = await getForm()
return form?.formModel as T
},
/**
* @description 获取表单组件的实例
* @param field 表单项唯一标识
* @returns component instance
*/
getComponentExpose: async (field: string) => {
const form = await getForm()
return form?.getComponentExpose(field)
},
/**
* @description 获取formItem组件的实例
* @param field 表单项唯一标识
* @returns formItem instance
*/
getFormItemExpose: async (field: string) => {
const form = await getForm()
return form?.getFormItemExpose(field) as ComponentRef<typeof ElFormItem>
},
/**
* @description 获取ElForm组件的实例
* @returns ElForm instance
*/
getElFormExpose: async () => {
await getForm()
return unref(elFormRef)
},
getFormExpose: async () => {
await getForm()
return unref(formRef)
}
}
props && methods.setProps(props)
return {
register,
formRef: elFormRef,
methods
formRegister: register,
formMethods: methods
}
}

View File

@@ -1,7 +1,6 @@
import { h } from 'vue'
import type { VNode } from 'vue'
import { Icon } from '@/components/Icon'
import { IconTypes } from '@/types/icon'
import { Icon, IconTypes } from '@/components/Icon'
export const useIcon = (props: IconTypes): VNode => {
return h(Icon, props)

View File

@@ -2,7 +2,7 @@ import { dateUtil } from '@/utils/dateUtil'
import { reactive, toRefs } from 'vue'
import { tryOnMounted, tryOnUnmounted } from '@vueuse/core'
export function useNow(immediate = true) {
export const useNow = (immediate = true) => {
let timer: IntervalHandle
const state = reactive({

View File

@@ -0,0 +1,91 @@
import { ref, unref, nextTick } from 'vue'
import { FormSchema, FormSetProps } from '@/components/Form'
import { SearchExpose, SearchProps } from '@/components/Search'
export const useSearch = () => {
// Search实例
const searchRef = ref<SearchExpose>()
/**
* @param ref Search实例
* @param elRef ElForm实例
*/
const register = (ref: SearchExpose) => {
searchRef.value = ref
}
const getSearch = async () => {
await nextTick()
const search = unref(searchRef)
if (!search) {
console.error('The Search is not registered. Please use the register method to register')
}
return search
}
// 一些内置的方法
const methods = {
/**
* @description 设置search组件的props
* @param field FormItem的field
*/
setProps: async (props: SearchProps = {}) => {
const search = await getSearch()
search?.setProps(props)
if (props.model) {
search?.setValues(props.model)
}
},
/**
* @description 设置form的值
* @param data 需要设置的数据
*/
setValues: async (data: Recordable) => {
const search = await getSearch()
search?.setValues(data)
},
/**
* @description 设置schema
* @param schemaProps 需要设置的schemaProps
*/
setSchema: async (schemaProps: FormSetProps[]) => {
const search = await getSearch()
search?.setSchema(schemaProps)
},
/**
* @description 新增schema
* @param formSchema 需要新增数据
* @param index 在哪里新增
*/
addSchema: async (formSchema: FormSchema, index?: number) => {
const search = await getSearch()
search?.addSchema(formSchema, index)
},
/**
* @description 删除schema
* @param field 删除哪个数据
*/
delSchema: async (field: string) => {
const search = await getSearch()
search?.delSchema(field)
},
/**
* @description 获取表单数据
* @returns form data
*/
getFormData: async <T = Recordable>(): Promise<T> => {
const search = await getSearch()
return search?.formModel as T
}
}
return {
searchRegister: register,
searchMethods: methods
}
}

View File

@@ -0,0 +1,31 @@
import { isArray, isObject } from '@/utils/is'
export const useStorage = (type: 'sessionStorage' | 'localStorage' = 'sessionStorage') => {
const setStorage = (key: string, value: any) => {
window[type].setItem(key, isArray(value) || isObject(value) ? JSON.stringify(value) : value)
}
const getStorage = (key: string) => {
const value = window[type].getItem(key)
try {
return JSON.parse(value || '')
} catch (error) {
return value
}
}
const removeStorage = (key: string) => {
window[type].removeItem(key)
}
const clear = () => {
window[type].clear()
}
return {
setStorage,
getStorage,
removeStorage,
clear
}
}

View File

@@ -1,91 +1,57 @@
import { Table, TableExpose } from '@/components/Table'
import { ElTable, ElMessageBox, ElMessage } from 'element-plus'
import { ref, reactive, watch, computed, unref, nextTick } from 'vue'
import { get } from 'lodash-es'
import type { TableProps } from '@/components/Table/src/types'
import { useI18n } from '@/hooks/web/useI18n'
import { TableSetPropsType } from '@/types/table'
import { Table, TableExpose, TableProps, TableSetProps, TableColumn } from '@/components/Table'
import { ElTable, ElMessageBox, ElMessage } from 'element-plus'
import { ref, watch, unref, nextTick, onMounted } from 'vue'
const { t } = useI18n()
interface TableResponse<T = any> {
total: number
list: T[]
pageNumber: number
pageSize: number
interface UseTableConfig {
/**
* 是否初始化的时候请求一次
*/
immediate?: boolean
fetchDataApi: () => Promise<{
list: any[]
total?: number
}>
fetchDelApi?: () => Promise<boolean>
}
interface UseTableConfig<T = any> {
getListApi: (option: any) => Promise<IResponse<TableResponse<T>>>
delListApi?: (option: any) => Promise<IResponse>
// 返回数据格式配置
response: {
list: string
total?: string
}
// 默认传递的参数
defaultParams?: Recordable
props?: TableProps
}
export const useTable = (config: UseTableConfig) => {
const { immediate = true } = config
interface TableObject<T = any> {
pageSize: number
currentPage: number
total: number
tableList: T[]
params: any
loading: boolean
currentRow: Nullable<T>
}
export const useTable = <T = any>(config?: UseTableConfig<T>) => {
const tableObject = reactive<TableObject<T>>({
// 页数
pageSize: 10,
// 当前页
currentPage: 1,
// 总条数
total: 10,
// 表格数据
tableList: [],
// AxiosConfig 配置
params: {
...(config?.defaultParams || {})
},
// 加载中
loading: true,
// 当前行的数据
currentRow: null
})
const paramsObj = computed(() => {
return {
...tableObject.params,
pageSize: tableObject.pageSize,
pageIndex: tableObject.currentPage
}
})
const loading = ref(false)
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
const dataList = ref<any[]>([])
watch(
() => tableObject.currentPage,
() => currentPage.value,
() => {
methods.getList()
}
)
watch(
() => tableObject.pageSize,
() => pageSize.value,
() => {
// 当前页不为1时修改页数后会导致多次调用getList方法
if (tableObject.currentPage === 1) {
if (unref(currentPage) === 1) {
methods.getList()
} else {
tableObject.currentPage = 1
currentPage.value = 1
methods.getList()
}
}
)
onMounted(() => {
if (immediate) {
methods.getList()
}
})
// Table实例
const tableRef = ref<typeof Table & TableExpose>()
@@ -106,91 +72,122 @@ export const useTable = <T = any>(config?: UseTableConfig<T>) => {
return table
}
const delData = async (ids: string[] | number[]) => {
const res = await (config?.delListApi && config?.delListApi(ids))
if (res) {
ElMessage.success(t('common.delSuccess'))
// 计算出临界点
const currentPage =
tableObject.total % tableObject.pageSize === ids.length || tableObject.pageSize === 1
? tableObject.currentPage > 1
? tableObject.currentPage - 1
: tableObject.currentPage
: tableObject.currentPage
tableObject.currentPage = currentPage
methods.getList()
}
}
const methods = {
/**
* 获取表单数据
*/
getList: async () => {
tableObject.loading = true
const res = await config?.getListApi(unref(paramsObj)).finally(() => {
tableObject.loading = false
})
if (res) {
tableObject.tableList = get(res.data || {}, config?.response.list as string)
tableObject.total = get(res.data || {}, config?.response?.total as string) || 0
loading.value = true
try {
const res = await config?.fetchDataApi()
console.log('fetchDataApi res', res)
if (res) {
dataList.value = res.list
total.value = res.total || 0
}
} catch (err) {
console.log('fetchDataApi error')
} finally {
loading.value = false
}
},
/**
* @description 设置table组件的props
* @param props table组件的props
*/
setProps: async (props: TableProps = {}) => {
const table = await getTable()
table?.setProps(props)
},
setColumn: async (columnProps: TableSetPropsType[]) => {
/**
* @description 设置column
* @param columnProps 需要设置的列
*/
setColumn: async (columnProps: TableSetProps[]) => {
const table = await getTable()
table?.setColumn(columnProps)
},
getSelections: async () => {
/**
* @description 新增column
* @param tableColumn 需要新增数据
* @param index 在哪里新增
*/
addColumn: async (tableColumn: TableColumn, index?: number) => {
const table = await getTable()
return (table?.selections || []) as T[]
table?.addColumn(tableColumn, index)
},
// 与Search组件结合
setSearchParams: (data: Recordable) => {
tableObject.currentPage = 1
tableObject.params = Object.assign(tableObject.params, {
pageSize: tableObject.pageSize,
pageIndex: tableObject.currentPage,
...data
})
/**
* @description 删除column
* @param field 删除哪个数据
*/
delColumn: async (field: string) => {
const table = await getTable()
table?.delColumn(field)
},
/**
* @description 获取ElTable组件的实例
* @returns ElTable instance
*/
getElTableExpose: async () => {
await getTable()
return unref(elTableRef)
},
refresh: () => {
methods.getList()
},
// sortableChange: (e: any) => {
// console.log('sortableChange', e)
// const { oldIndex, newIndex } = e
// dataList.value.splice(newIndex, 0, dataList.value.splice(oldIndex, 1)[0])
// // to do something
// }
// 删除数据
delList: async (ids: string[] | number[], multiple: boolean, message = true) => {
const tableRef = await getTable()
if (multiple) {
if (!tableRef?.selections.length) {
ElMessage.warning(t('common.delNoData'))
return
}
} else {
if (!tableObject.currentRow) {
ElMessage.warning(t('common.delNoData'))
return
}
}
if (message) {
ElMessageBox.confirm(t('common.delMessage'), t('common.delWarning'), {
confirmButtonText: t('common.delOk'),
cancelButtonText: t('common.delCancel'),
type: 'warning'
}).then(async () => {
await delData(ids)
})
} else {
await delData(ids)
delList: async (idsLength: number) => {
const { fetchDelApi } = config
if (!fetchDelApi) {
console.warn('fetchDelApi is undefined')
return
}
ElMessageBox.confirm(t('common.delMessage'), t('common.delWarning'), {
confirmButtonText: t('common.delOk'),
cancelButtonText: t('common.delCancel'),
type: 'warning'
}).then(async () => {
const res = await fetchDelApi()
if (res) {
ElMessage.success(t('common.delSuccess'))
// 计算出临界点
const current =
unref(total) % unref(pageSize) === idsLength || unref(pageSize) === 1
? unref(currentPage) > 1
? unref(currentPage) - 1
: unref(currentPage)
: unref(currentPage)
currentPage.value = current
methods.getList()
}
})
}
}
config?.props && methods.setProps(config.props)
return {
register,
elTableRef,
tableObject,
methods
tableRegister: register,
tableMethods: methods,
tableState: {
currentPage,
pageSize,
total,
dataList,
loading
}
}
}

View File

@@ -40,7 +40,11 @@ export default {
delOk: 'OK',
delCancel: 'Cancel',
delNoData: 'Please select the data to delete',
delSuccess: 'Deleted successfully'
delSuccess: 'Deleted successfully',
refresh: 'Refresh',
fullscreen: 'Fullscreen',
size: 'Size',
columnSetting: 'Column setting'
},
lock: {
lockScreen: 'Lock screen',
@@ -154,7 +158,13 @@ export default {
role: 'Role management',
document: 'Document',
inputPassword: 'InputPassword',
sticky: 'Sticky'
sticky: 'Sticky',
treeTable: 'Tree table',
PicturePreview: 'Table Image Preview',
department: 'Department management',
menuManagement: 'Menu management',
// 权限测试页面
permission: 'Permission test page'
},
permission: {
hasPermission: 'Please set the operation permission value'
@@ -241,8 +251,11 @@ export default {
transfer: 'Transfer',
render: 'Render',
radio: 'Radio',
radioGroup: 'Radio Group',
button: 'Button',
checkbox: 'Checkbox',
checkboxButton: 'Checkbox Button',
checkboxGroup: 'Checkbox Group',
slider: 'Slider',
datePicker: 'Date Picker',
shortcuts: 'Shortcuts',
@@ -277,7 +290,26 @@ export default {
set: 'Set',
subitem: 'Subitem',
formValidation: 'Form validation',
verifyReset: 'Verify reset'
verifyReset: 'Verify reset',
// 富文本编辑器
richText: 'Rich text',
form: 'Form',
// 远程加载
remoteLoading: 'Remote loading',
// 聚焦
focus: 'Focus',
treeSelect: 'Tree Select',
showCheckbox: 'Show Checkbox',
selectAnyLevel: 'Select Any Level',
multiple: 'Multiple',
filterable: 'Filterable',
// 自定义节点内容
customContent: 'Custom content',
// 懒加载
lazyLoad: 'Lazy load',
upload: 'Upload',
// 用户头像
userAvatar: 'User avatar'
},
guideDemo: {
guide: 'Guide',
@@ -361,7 +393,13 @@ export default {
left: 'left',
center: 'center',
right: 'right',
dynamicOptions: 'Dynamic options'
dynamicOptions: 'Dynamic options',
// 删除单选框
deleteRadio: 'Delete radio',
// 还原单选框
restoreRadio: 'Restore radio',
loading: 'Loading',
reset: 'Reset'
},
stickyDemo: {
sticky: 'Sticky'
@@ -392,7 +430,14 @@ export default {
hiddenExpandedRows: 'Hidden expanded rows',
changeTitle: 'Change title',
header: 'Header',
selectAllNone: 'Select all / none'
selectAllNone: 'Select all / none',
delOrAddAction: 'Delete or add action',
showOrHiddenStripe: 'Show or hidden stripe',
showOrHiddenBorder: 'Show or hidden border',
fixedHeaderOrAuto: 'Fixed header or auto',
getSelections: 'Get selections',
preview: 'Preview',
showOrHiddenSortable: 'Show or hidden sortable'
},
richText: {
richText: 'Rich text',
@@ -446,7 +491,45 @@ export default {
role: 'Role',
remark: 'Remark',
remarkMessage1: 'Back end control routing permission',
remarkMessage2: 'Front end control routing permission'
remarkMessage2: 'Front end control routing permission',
// 部门列表
departmentList: 'Department list',
// 搜索部门
searchDepartment: 'Search department',
account: 'Account',
email: 'Email',
createTime: 'Create time',
// 所属部门
department: 'Department',
departmentName: 'Department name',
status: 'Status',
enable: 'Enable',
disable: 'Disable',
superiorDepartment: 'Superior department'
},
menu: {
menuName: 'Menu name',
icon: 'Icon',
// 权限
permission: 'Permission',
component: 'Component',
path: 'Path',
status: 'Status',
hidden: 'Hidden',
alwaysShow: 'Always show',
noCache: 'No cache',
breadcrumb: 'Breadcrumb',
affix: 'Affix',
noTagsView: 'No tags view',
activeMenu: 'Active menu',
canTo: 'Can to',
name: 'Name'
},
role: {
roleName: 'Role name',
role: 'Role',
// 菜单分配
menu: 'Menu allocation'
},
inputPasswordDemo: {
title: 'InputPassword',

View File

@@ -17,7 +17,7 @@ export default {
closeTab: '关闭标签页',
closeTheLeftTab: '关闭左侧标签页',
closeTheRightTab: '关闭右侧标签页',
closeOther: '关闭其标签页',
closeOther: '关闭其标签页',
closeAll: '关闭全部标签页',
prevLabel: '上一步',
nextLabel: '下一步',
@@ -40,7 +40,11 @@ export default {
delOk: '确定',
delCancel: '取消',
delNoData: '请选择需要删除的数据',
delSuccess: '删除成功'
delSuccess: '删除成功',
refresh: '刷新',
fullscreen: '全屏',
size: '尺寸',
columnSetting: '列设置'
},
lock: {
lockScreen: '锁定屏幕',
@@ -102,7 +106,7 @@ export default {
register: '注册',
checkPassword: '确认密码',
login: '登录',
otherLogin: '其登录方式',
otherLogin: '其登录方式',
remember: '记住我',
hasUser: '已有账号?去登录',
forgetPassword: '忘记密码',
@@ -154,7 +158,12 @@ export default {
role: '角色管理',
document: '文档',
inputPassword: '密码输入框',
sticky: '黏性'
sticky: '黏性',
treeTable: '树形表格',
PicturePreview: '表格图片预览',
department: '部门管理',
menuManagement: '菜单管理',
permission: '权限测试页'
},
permission: {
hasPermission: '请设置操作权限值'
@@ -241,8 +250,11 @@ export default {
transfer: '穿梭框',
render: '渲染器',
radio: '单选框',
radioGroup: '单选框组',
button: '按钮',
checkbox: '多选框',
checkboxButton: '多选框按钮',
checkboxGroup: '多选框组',
slider: '滑块',
datePicker: '日期选择器',
shortcuts: '快捷选项',
@@ -276,7 +288,23 @@ export default {
set: '设置',
subitem: '子项',
formValidation: '表单验证',
verifyReset: '验证重置'
verifyReset: '验证重置',
// 富文本编辑器
richText: '富文本编辑器',
form: '表单',
// 远程加载
remoteLoading: '远程加载',
// 聚焦
focus: '聚焦',
treeSelect: '树形选择器',
showCheckbox: '显示复选框',
selectAnyLevel: '选择任意级别',
multiple: '多选',
filterable: '可筛选',
customContent: '自定义内容',
lazyLoad: '懒加载',
upload: '上传',
userAvatar: '用户头像'
},
guideDemo: {
guide: '引导页',
@@ -358,7 +386,13 @@ export default {
left: '左',
center: '中',
right: '右',
dynamicOptions: '动态选项'
dynamicOptions: '动态选项',
// 删除单选框
deleteRadio: '删除单选框',
// 还原单选框
restoreRadio: '还原单选框',
loading: '加载中',
reset: '重置'
},
stickyDemo: {
sticky: '黏性'
@@ -389,7 +423,14 @@ export default {
hiddenExpandedRows: '隐藏展开行',
changeTitle: '修改标题',
header: '头部',
selectAllNone: '全选/全不选'
selectAllNone: '全选/全不选',
delOrAddAction: '删除/添加操作列',
showOrHiddenStripe: '显示/隐藏斑马纹',
showOrHiddenBorder: '显示/隐藏边框',
fixedHeaderOrAuto: '固定头部/自动',
getSelections: '获取多选数据',
preview: '封面',
showOrHiddenSortable: '显示/隐藏排序'
},
richText: {
richText: '富文本',
@@ -442,7 +483,45 @@ export default {
role: '角色',
remark: '备注',
remarkMessage1: '后端控制路由权限',
remarkMessage2: '前端控制路由权限'
remarkMessage2: '前端控制路由权限',
// 部门列表
departmentList: '部门列表',
searchDepartment: '搜索部门',
account: '账号',
email: '邮箱',
createTime: '创建时间',
// 所属部门
department: '所属部门',
departmentName: '部门名称',
status: '状态',
// 启用
enable: '启用',
// 禁用
disable: '禁用',
// 上级部门
superiorDepartment: '上级部门'
},
menu: {
menuName: '菜单名称',
icon: '图标',
permission: '权限标识',
component: '组件',
path: '路径',
status: '状态',
hidden: '是否隐藏',
alwaysShow: '是否一直显示',
noCache: '是否清除缓存',
breadcrumb: '是否显示面包屑',
affix: '是否固定在标签页',
noTagsView: '是否隐藏标签页',
activeMenu: '高亮菜单',
canTo: '是否可跳转',
name: '组件名称'
},
role: {
roleName: '角色名称',
role: '角色',
menu: '菜单分配'
},
inputPasswordDemo: {
title: '密码输入框',

View File

@@ -1,6 +1,6 @@
import router from './router'
import { useAppStoreWithOut } from '@/store/modules/app'
import { useCache } from '@/hooks/web/useCache'
import { useStorage } from '@/hooks/web/useStorage'
import type { RouteRecordRaw } from 'vue-router'
import { useTitle } from '@/hooks/web/useTitle'
import { useNProgress } from '@/hooks/web/useNProgress'
@@ -15,7 +15,7 @@ const appStore = useAppStoreWithOut()
const dictStore = useDictStoreWithOut()
const { wsCache } = useCache()
const { getStorage } = useStorage()
const { start, done } = useNProgress()
@@ -26,52 +26,52 @@ const whiteList = ['/login'] // 不重定向白名单
router.beforeEach(async (to, from, next) => {
start()
loadStart()
// if (!wsCache.get(appStore.getUserInfo)) {
if (to.path === '/login') {
next({ path: '/' })
} else {
if (!dictStore.getIsSetDict) {
// 获取所有字典
const res = await getDictApi()
if (res) {
dictStore.setDictObj(res.data)
dictStore.setIsSetDict(true)
}
}
if (permissionStore.getIsAddRouters) {
next()
return
}
// 开发者可根据实际情况进行修改
const roleRouters = wsCache.get('roleRouters') || []
const userInfo = wsCache.get(appStore.getUserInfo)
// 是否使用动态路由
if (appStore.getDynamicRouter) {
userInfo.role === 'admin'
? await permissionStore.generateRoutes('admin', roleRouters as AppCustomRouteRecordRaw[])
: await permissionStore.generateRoutes('test', roleRouters as string[])
if (getStorage(appStore.getUserInfo)) {
if (to.path === '/login') {
next({ path: '/' })
} else {
await permissionStore.generateRoutes('none')
}
if (!dictStore.getIsSetDict) {
// 获取所有字典
const res = await getDictApi()
if (res) {
dictStore.setDictObj(res.data)
dictStore.setIsSetDict(true)
}
}
if (permissionStore.getIsAddRouters) {
next()
return
}
permissionStore.getAddRouters.forEach((route) => {
router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表
})
const redirectPath = from.query.redirect || to.path
const redirect = decodeURIComponent(redirectPath as string)
const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }
permissionStore.setIsAddRouters(true)
next(nextData)
// 开发者可根据实际情况进行修改
const roleRouters = getStorage('roleRouters') || []
const userInfo = getStorage(appStore.getUserInfo)
// 是否使用动态路由
if (appStore.getDynamicRouter) {
userInfo.role === 'admin'
? await permissionStore.generateRoutes('admin', roleRouters as AppCustomRouteRecordRaw[])
: await permissionStore.generateRoutes('test', roleRouters as string[])
} else {
await permissionStore.generateRoutes('none')
}
permissionStore.getAddRouters.forEach((route) => {
router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表
})
const redirectPath = from.query.redirect || to.path
const redirect = decodeURIComponent(redirectPath as string)
const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }
permissionStore.setIsAddRouters(true)
next(nextData)
}
} else {
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页
}
}
// } else {
// if (whiteList.indexOf(to.path) !== -1) {
// next()
// } else {
// next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页
// }
// }
})
router.afterEach((to) => {

View File

@@ -148,335 +148,335 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
meta: {
title: t('router.defaultForm')
}
},
{
path: 'use-form',
component: () => import('@/views/Components/Form/UseFormDemo.vue'),
name: 'UseForm',
meta: {
title: 'UseForm'
}
}
// {
// path: 'use-form',
// component: () => import('@/views/Components/Form/UseFormDemo.vue'),
// name: 'UseForm',
// meta: {
// title: 'UseForm'
// }
// }
// {
// path: 'ref-form',
// component: () => import('@/views/Components/Form/RefForm.vue'),
// name: 'RefForm',
// meta: {
// title: 'RefForm'
// }
// }
]
},
{
path: 'table',
component: getParentLayout(),
redirect: '/components/table/default-table',
name: 'TableDemo',
meta: {
title: t('router.table'),
alwaysShow: true
},
children: [
{
path: 'default-table',
component: () => import('@/views/Components/Table/DefaultTable.vue'),
name: 'DefaultTable',
meta: {
title: t('router.defaultTable')
}
},
{
path: 'use-table',
component: () => import('@/views/Components/Table/UseTableDemo.vue'),
name: 'UseTable',
meta: {
title: 'UseTable'
}
},
{
path: 'tree-table',
component: () => import('@/views/Components/Table/TreeTable.vue'),
name: 'TreeTable',
meta: {
title: t('router.treeTable')
}
},
{
path: 'table-image-preview',
component: () => import('@/views/Components/Table/TableImagePreview.vue'),
name: 'TableImagePreview',
meta: {
title: t('router.PicturePreview')
}
}
]
},
{
path: 'editor-demo',
component: getParentLayout(),
redirect: '/components/editor-demo/editor',
name: 'EditorDemo',
meta: {
title: t('router.editor'),
alwaysShow: true
},
children: [
{
path: 'editor',
component: () => import('@/views/Components/Editor/Editor.vue'),
name: 'Editor',
meta: {
title: t('router.richText')
}
}
]
},
{
path: 'search',
component: () => import('@/views/Components/Search.vue'),
name: 'Search',
meta: {
title: t('router.search')
}
},
{
path: 'descriptions',
component: () => import('@/views/Components/Descriptions.vue'),
name: 'Descriptions',
meta: {
title: t('router.descriptions')
}
},
{
path: 'image-viewer',
component: () => import('@/views/Components/ImageViewer.vue'),
name: 'ImageViewer',
meta: {
title: t('router.imageViewer')
}
},
{
path: 'dialog',
component: () => import('@/views/Components/Dialog.vue'),
name: 'Dialog',
meta: {
title: t('router.dialog')
}
},
{
path: 'icon',
component: () => import('@/views/Components/Icon.vue'),
name: 'Icon',
meta: {
title: t('router.icon')
}
},
{
path: 'echart',
component: () => import('@/views/Components/Echart.vue'),
name: 'Echart',
meta: {
title: t('router.echart')
}
},
{
path: 'count-to',
component: () => import('@/views/Components/CountTo.vue'),
name: 'CountTo',
meta: {
title: t('router.countTo')
}
},
{
path: 'qrcode',
component: () => import('@/views/Components/Qrcode.vue'),
name: 'Qrcode',
meta: {
title: t('router.qrcode')
}
},
{
path: 'highlight',
component: () => import('@/views/Components/Highlight.vue'),
name: 'Highlight',
meta: {
title: t('router.highlight')
}
},
{
path: 'infotip',
component: () => import('@/views/Components/Infotip.vue'),
name: 'Infotip',
meta: {
title: t('router.infotip')
}
},
{
path: 'input-password',
component: () => import('@/views/Components/InputPassword.vue'),
name: 'InputPassword',
meta: {
title: t('router.inputPassword')
}
}
]
},
{
path: '/hooks',
component: Layout,
redirect: '/hooks/useWatermark',
name: 'Hooks',
meta: {
title: 'hooks',
icon: 'ic:outline-webhook',
alwaysShow: true
},
children: [
{
path: 'useWatermark',
component: () => import('@/views/hooks/useWatermark.vue'),
name: 'UseWatermark',
meta: {
title: 'useWatermark'
}
}
// {
// path: 'table',
// component: getParentLayout(),
// redirect: '/components/table/default-table',
// name: 'TableDemo',
// path: 'useOpenTab',
// component: () => import('@/views/hooks/useOpenTab.vue'),
// name: 'UseOpenTab',
// meta: {
// title: t('router.table'),
// alwaysShow: true
// },
// children: [
// {
// path: 'default-table',
// component: () => import('@/views/Components/Table/DefaultTable.vue'),
// name: 'DefaultTable',
// meta: {
// title: t('router.defaultTable')
// }
// },
// {
// path: 'use-table',
// component: () => import('@/views/Components/Table/UseTableDemo.vue'),
// name: 'UseTable',
// meta: {
// title: 'UseTable'
// }
// },
// {
// path: 'ref-table',
// component: () => import('@/views/Components/Table/RefTable.vue'),
// name: 'RefTable',
// meta: {
// title: 'RefTable'
// }
// }
// ]
// },
// {
// path: 'editor-demo',
// component: getParentLayout(),
// redirect: '/components/editor-demo/editor',
// name: 'EditorDemo',
// meta: {
// title: t('router.editor'),
// alwaysShow: true
// },
// children: [
// {
// path: 'editor',
// component: () => import('@/views/Components/Editor/Editor.vue'),
// name: 'Editor',
// meta: {
// title: t('router.richText')
// }
// }
// ]
// },
// {
// path: 'search',
// component: () => import('@/views/Components/Search.vue'),
// name: 'Search',
// meta: {
// title: t('router.search')
// title: 'useOpenTab'
// }
// },
// }
// {
// path: 'descriptions',
// component: () => import('@/views/Components/Descriptions.vue'),
// name: 'Descriptions',
// path: 'useCrudSchemas',
// component: () => import('@/views/hooks/useCrudSchemas.vue'),
// name: 'UseCrudSchemas',
// meta: {
// title: t('router.descriptions')
// }
// },
// {
// path: 'image-viewer',
// component: () => import('@/views/Components/ImageViewer.vue'),
// name: 'ImageViewer',
// meta: {
// title: t('router.imageViewer')
// }
// },
// {
// path: 'dialog',
// component: () => import('@/views/Components/Dialog.vue'),
// name: 'Dialog',
// meta: {
// title: t('router.dialog')
// }
// },
// {
// path: 'icon',
// component: () => import('@/views/Components/Icon.vue'),
// name: 'Icon',
// meta: {
// title: t('router.icon')
// }
// },
// {
// path: 'echart',
// component: () => import('@/views/Components/Echart.vue'),
// name: 'Echart',
// meta: {
// title: t('router.echart')
// }
// },
// {
// path: 'count-to',
// component: () => import('@/views/Components/CountTo.vue'),
// name: 'CountTo',
// meta: {
// title: t('router.countTo')
// }
// },
// {
// path: 'qrcode',
// component: () => import('@/views/Components/Qrcode.vue'),
// name: 'Qrcode',
// meta: {
// title: t('router.qrcode')
// }
// },
// {
// path: 'highlight',
// component: () => import('@/views/Components/Highlight.vue'),
// name: 'Highlight',
// meta: {
// title: t('router.highlight')
// }
// },
// {
// path: 'infotip',
// component: () => import('@/views/Components/Infotip.vue'),
// name: 'Infotip',
// meta: {
// title: t('router.infotip')
// }
// },
// {
// path: 'input-password',
// component: () => import('@/views/Components/InputPassword.vue'),
// name: 'InputPassword',
// meta: {
// title: t('router.inputPassword')
// }
// },
// {
// path: 'sticky',
// component: () => import('@/views/Components/Sticky.vue'),
// name: 'Sticky',
// meta: {
// title: t('router.sticky')
// title: 'useCrudSchemas'
// }
// }
]
},
// {
// path: '/hooks',
// component: Layout,
// redirect: '/hooks/useWatermark',
// name: 'Hooks',
// meta: {
// title: 'hooks',
// icon: 'ic:outline-webhook',
// alwaysShow: true
// },
// children: [
// {
// path: 'useWatermark',
// component: () => import('@/views/hooks/useWatermark.vue'),
// name: 'UseWatermark',
// meta: {
// title: 'useWatermark'
// }
// },
// {
// path: 'useCrudSchemas',
// component: () => import('@/views/hooks/useCrudSchemas.vue'),
// name: 'UseCrudSchemas',
// meta: {
// title: 'useCrudSchemas'
// }
// }
// ]
// },
// {
// path: '/level',
// component: Layout,
// redirect: '/level/menu1/menu1-1/menu1-1-1',
// name: 'Level',
// meta: {
// title: t('router.level'),
// icon: 'carbon:skill-level-advanced'
// },
// children: [
// {
// path: 'menu1',
// name: 'Menu1',
// component: getParentLayout(),
// redirect: '/level/menu1/menu1-1/menu1-1-1',
// meta: {
// title: t('router.menu1')
// },
// children: [
// {
// path: 'menu1-1',
// name: 'Menu11',
// component: getParentLayout(),
// redirect: '/level/menu1/menu1-1/menu1-1-1',
// meta: {
// title: t('router.menu11'),
// alwaysShow: true
// },
// children: [
// {
// path: 'menu1-1-1',
// name: 'Menu111',
// component: () => import('@/views/Level/Menu111.vue'),
// meta: {
// title: t('router.menu111')
// }
// }
// ]
// },
// {
// path: 'menu1-2',
// name: 'Menu12',
// component: () => import('@/views/Level/Menu12.vue'),
// meta: {
// title: t('router.menu12')
// }
// }
// ]
// },
// {
// path: 'menu2',
// name: 'Menu2',
// component: () => import('@/views/Level/Menu2.vue'),
// meta: {
// title: t('router.menu2')
// }
// }
// ]
// },
// {
// path: '/example',
// component: Layout,
// redirect: '/example/example-dialog',
// name: 'Example',
// meta: {
// title: t('router.example'),
// icon: 'ep:management',
// alwaysShow: true
// },
// children: [
// {
// path: 'example-dialog',
// component: () => import('@/views/Example/Dialog/ExampleDialog.vue'),
// name: 'ExampleDialog',
// meta: {
// title: t('router.exampleDialog')
// }
// },
// {
// path: 'example-page',
// component: () => import('@/views/Example/Page/ExamplePage.vue'),
// name: 'ExamplePage',
// meta: {
// title: t('router.examplePage')
// }
// },
// {
// path: 'example-add',
// component: () => import('@/views/Example/Page/ExampleAdd.vue'),
// name: 'ExampleAdd',
// meta: {
// title: t('router.exampleAdd'),
// noTagsView: true,
// noCache: true,
// hidden: true,
// canTo: true,
// activeMenu: '/example/example-page'
// }
// },
// {
// path: 'example-edit',
// component: () => import('@/views/Example/Page/ExampleEdit.vue'),
// name: 'ExampleEdit',
// meta: {
// title: t('router.exampleEdit'),
// noTagsView: true,
// noCache: true,
// hidden: true,
// canTo: true,
// activeMenu: '/example/example-page'
// }
// },
// {
// path: 'example-detail',
// component: () => import('@/views/Example/Page/ExampleDetail.vue'),
// name: 'ExampleDetail',
// meta: {
// title: t('router.exampleDetail'),
// noTagsView: true,
// noCache: true,
// hidden: true,
// canTo: true,
// activeMenu: '/example/example-page'
// }
// }
// ]
// },
{
path: '/level',
component: Layout,
redirect: '/level/menu1/menu1-1/menu1-1-1',
name: 'Level',
meta: {
title: t('router.level'),
icon: 'carbon:skill-level-advanced'
},
children: [
{
path: 'menu1',
name: 'Menu1',
component: getParentLayout(),
redirect: '/level/menu1/menu1-1/menu1-1-1',
meta: {
title: t('router.menu1')
},
children: [
{
path: 'menu1-1',
name: 'Menu11',
component: getParentLayout(),
redirect: '/level/menu1/menu1-1/menu1-1-1',
meta: {
title: t('router.menu11'),
alwaysShow: true
},
children: [
{
path: 'menu1-1-1',
name: 'Menu111',
component: () => import('@/views/Level/Menu111.vue'),
meta: {
title: t('router.menu111')
}
}
]
},
{
path: 'menu1-2',
name: 'Menu12',
component: () => import('@/views/Level/Menu12.vue'),
meta: {
title: t('router.menu12')
}
}
]
},
{
path: 'menu2',
name: 'Menu2',
component: () => import('@/views/Level/Menu2.vue'),
meta: {
title: t('router.menu2')
}
}
]
},
{
path: '/example',
component: Layout,
redirect: '/example/example-dialog',
name: 'Example',
meta: {
title: t('router.example'),
icon: 'ep:management',
alwaysShow: true
},
children: [
{
path: 'example-dialog',
component: () => import('@/views/Example/Dialog/ExampleDialog.vue'),
name: 'ExampleDialog',
meta: {
title: t('router.exampleDialog')
}
},
{
path: 'example-page',
component: () => import('@/views/Example/Page/ExamplePage.vue'),
name: 'ExamplePage',
meta: {
title: t('router.examplePage')
}
},
{
path: 'example-add',
component: () => import('@/views/Example/Page/ExampleAdd.vue'),
name: 'ExampleAdd',
meta: {
title: t('router.exampleAdd'),
noTagsView: true,
noCache: true,
hidden: true,
canTo: true,
activeMenu: '/example/example-page'
}
},
{
path: 'example-edit',
component: () => import('@/views/Example/Page/ExampleEdit.vue'),
name: 'ExampleEdit',
meta: {
title: t('router.exampleEdit'),
noTagsView: true,
noCache: true,
hidden: true,
canTo: true,
activeMenu: '/example/example-page'
}
},
{
path: 'example-detail',
component: () => import('@/views/Example/Page/ExampleDetail.vue'),
name: 'ExampleDetail',
meta: {
title: t('router.exampleDetail'),
noTagsView: true,
noCache: true,
hidden: true,
canTo: true,
activeMenu: '/example/example-page'
}
}
]
},
{
path: '/error',
component: Layout,
@@ -513,36 +513,80 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
}
}
]
},
{
path: '/authorization',
component: Layout,
redirect: '/authorization/user',
name: 'Authorization',
meta: {
title: t('router.authorization'),
icon: 'eos-icons:role-binding',
alwaysShow: true
},
children: [
{
path: 'department',
component: () => import('@/views/Authorization/Department/Department.vue'),
name: 'Department',
meta: {
title: t('router.department')
}
},
{
path: 'user',
component: () => import('@/views/Authorization/User/User.vue'),
name: 'User',
meta: {
title: t('router.user')
}
},
{
path: 'menu',
component: () => import('@/views/Authorization/Menu/Menu.vue'),
name: 'Menu',
meta: {
title: t('router.menuManagement')
}
},
{
path: 'role',
component: () => import('@/views/Authorization/Role/Role.vue'),
name: 'Role',
meta: {
title: t('router.role')
}
},
{
path: 'test',
component: () => import('@/views/Authorization/Test/Test.vue'),
name: 'Test',
meta: {
title: t('router.permission'),
permission: ['add', 'edit', 'delete']
}
}
]
},
{
path: '/dynamic',
component: Layout,
redirect: '/404',
name: 'Dynamic',
meta: {
hidden: true
},
children: [
{
path: 'tab/:id',
component: () => import('@/components/Dynamic/src/Dynamic.vue'),
name: 'Dynamic',
meta: {
title: '详情页'
}
}
]
}
// {
// path: '/authorization',
// component: Layout,
// redirect: '/authorization/user',
// name: 'Authorization',
// meta: {
// title: t('router.authorization'),
// icon: 'eos-icons:role-binding',
// alwaysShow: true
// },
// children: [
// {
// path: 'user',
// component: () => import('@/views/Authorization/User.vue'),
// name: 'User',
// meta: {
// title: t('router.user')
// }
// },
// {
// path: 'role',
// component: () => import('@/views/Authorization/Role.vue'),
// name: 'Role',
// meta: {
// title: t('router.role')
// }
// }
// ]
// }
]
const router = createRouter({

Some files were not shown because too many files have changed in this diff Show More