Browse Source

refactor: 系统管理重构

dev
刘凯 1 year ago
parent
commit
26ec154e34
  1. 6
      .env
  2. 4
      .env.development
  3. 2
      .env.production
  4. 7
      .vscode/settings.json
  5. 1
      eslint.config.js
  6. 16
      src/App.vue
  7. 6
      src/api/base/login.ts
  8. 7
      src/api/base/model/userModel.ts
  9. 36
      src/api/base/user.ts
  10. 29
      src/api/base/user/index.ts
  11. 32
      src/api/base/user/types.ts
  12. 62
      src/api/system/dept/index.ts
  13. 17
      src/api/system/dept/types.ts
  14. 67
      src/api/system/menu/index.ts
  15. 26
      src/api/system/menu/types.ts
  16. 92
      src/api/system/role/index.ts
  17. 29
      src/api/system/role/types.ts
  18. 75
      src/api/system/tenant/index.ts
  19. 16
      src/api/system/tenant/types.ts
  20. 102
      src/api/system/user/index.ts
  21. 23
      src/api/system/user/types.ts
  22. 8
      src/components/Form/src/BasicForm.vue
  23. 23
      src/components/Form/src/components/ApiTreeSelect.vue
  24. 22
      src/components/Form/src/hooks/useFormEvents.ts
  25. 5
      src/components/Form/src/hooks/useFormValues.ts
  26. 2
      src/components/Form/src/types/index.ts
  27. 2
      src/components/Table/src/BasicTable.vue
  28. 3
      src/components/Table/src/components/TableAction.vue
  29. 1
      src/components/Table/src/hooks/useColumns.ts
  30. 2
      src/components/Table/src/types/table.ts
  31. 2
      src/components/Tree/src/types/tree.ts
  32. 15
      src/enums/appEnum.ts
  33. 2
      src/enums/httpEnum.ts
  34. 9
      src/enums/systemEnum.ts
  35. 6
      src/hooks/component/useFormItem.ts
  36. 4
      src/hooks/setting/index.ts
  37. 3
      src/hooks/setting/useRootSetting.ts
  38. 50
      src/hooks/web/usePermission.ts
  39. 2
      src/layouts/default/header/components/lock/LockModal.vue
  40. 6
      src/layouts/default/header/components/user-dropdown/index.vue
  41. 23
      src/router/guard/paramMenuGuard.ts
  42. 8
      src/router/guard/permissionGuard.ts
  43. 28
      src/router/helper/routeHelper.ts
  44. 70
      src/router/menus/index.ts
  45. 10
      src/router/routes/index.ts
  46. 2
      src/router/routes/modules/dashboard.ts
  47. 2
      src/router/types.ts
  48. 6
      src/settings/componentSetting.ts
  49. 6
      src/settings/projectSetting.ts
  50. 3
      src/store/modules/lock.ts
  51. 122
      src/store/modules/permission.ts
  52. 52
      src/store/modules/user.ts
  53. 2
      src/types/axios.d.ts
  54. 11
      src/types/config.d.ts
  55. 7
      src/types/global.d.ts
  56. 8
      src/types/index.d.ts
  57. 2
      src/utils/auth/index.ts
  58. 4
      src/utils/env.ts
  59. 18
      src/utils/http/axios/index.ts
  60. 13
      src/utils/index.ts
  61. 2
      src/views/base/lock/LockPage.vue
  62. 4
      src/views/base/login/Login.vue
  63. 107
      src/views/base/login/LoginForm.vue
  64. 2
      src/views/base/login/LoginFormTitle.vue
  65. 155
      src/views/base/login/MobileForm.vue
  66. 37
      src/views/base/login/QrCodeForm.vue
  67. 199
      src/views/base/login/SSOForm.vue
  68. 16
      src/views/base/login/SessionTimeoutLogin.vue
  69. 200
      src/views/base/login/sso.vue
  70. 14
      src/views/base/login/useLogin.ts
  71. 2
      src/views/dashboard/workbench/components/WorkbenchHeader.vue
  72. 59
      src/views/system/dept/DeptFormModal.vue
  73. 61
      src/views/system/dept/DeptModal.vue
  74. 85
      src/views/system/dept/data.ts
  75. 146
      src/views/system/dept/dept.data.ts
  76. 124
      src/views/system/dept/index.vue
  77. 55
      src/views/system/menu/MenuFormModal.vue
  78. 58
      src/views/system/menu/MenuModal.vue
  79. 119
      src/views/system/menu/data.tsx
  80. 106
      src/views/system/menu/index.vue
  81. 56
      src/views/system/role/RoleFormModal.vue
  82. 136
      src/views/system/role/RoleMenuModal.vue
  83. 58
      src/views/system/role/RoleModal.vue
  84. 73
      src/views/system/role/RoleScopeModal.vue
  85. 116
      src/views/system/role/data.ts
  86. 140
      src/views/system/role/index.vue
  87. 184
      src/views/system/role/role.data.ts
  88. 52
      src/views/system/tenant/TenantFormModal.vue
  89. 58
      src/views/system/tenant/TenantModal.vue
  90. 106
      src/views/system/tenant/data.ts
  91. 92
      src/views/system/tenant/index.vue
  92. 191
      src/views/system/tenant/tenant.data.ts
  93. 11
      src/views/system/tenantPackage/TenantPackageModal.vue
  94. 64
      src/views/system/user/DeptTree.vue
  95. 36
      src/views/system/user/UserFormModal.vue
  96. 58
      src/views/system/user/UserModal.vue
  97. 52
      src/views/system/user/UserRoleModal.vue
  98. 309
      src/views/system/user/data.ts
  99. 124
      src/views/system/user/index.vue
  100. 283
      src/views/system/user/user.data.ts

6
.env

@ -6,9 +6,3 @@ VITE_GLOB_APP_TITLE = Fast Iot Web
# 简称,用于配置文件名字 不要出现空格、数字开头等特殊字符
VITE_GLOB_APP_SHORT_NAME = FAST_IOT_Admin
# 租户开关
VITE_GLOB_APP_TENANT_ENABLE = true
# 验证码的开关
VITE_GLOB_APP_CAPTCHA_ENABLE = true

4
.env.development

@ -7,13 +7,13 @@ VITE_PUBLIC_PATH = /
# 本地开发代理,可以解决跨域及多地址代理
# 如果接口地址匹配到,则会转发到http://localhost:3000,防止本地出现跨域问题
# 可以有多个,注意多个不能换行,否则代理将会失效
VITE_PROXY = [["/dev-api","192.168.1.100:48081/admin-api"],["/upload","192.168.1.100:48081/admin-api/infra/file/upload"]]
VITE_PROXY = []
# 是否删除Console.log
VITE_DROP_CONSOLE = false
# 接口地址,如果没有跨域问题,直接在这里配置即可
VITE_GLOB_API_URL = /dev-api
VITE_GLOB_API_URL = http://192.168.1.100:48081/admin-api
# 文件上传接口 可选
VITE_GLOB_UPLOAD_URL = /upload

2
.env.production

@ -13,7 +13,7 @@ VITE_BUILD_COMPRESS = 'gzip'
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
# 接口地址 可以由nginx做转发或者直接写实际地址
VITE_GLOB_API_URL = http://localhost:48080/admin-api
VITE_GLOB_API_URL = /admin-api
# 文件上传地址 可以由nginx做转发或者直接写实际地址
VITE_GLOB_UPLOAD_URL = /upload

7
.vscode/settings.json vendored

@ -74,5 +74,10 @@
"vuedraggable",
"vueuse"
],
"terminal.integrated.scrollback": 10000
"terminal.integrated.scrollback": 10000,
"i18n-ally.localesPaths": [
"src/locales",
"src/locales/lang",
"public/resource/tinymce/langs"
]
}

1
eslint.config.js

@ -9,6 +9,7 @@ export default antfu(
'vue/custom-event-name-casing': 'off',
'vue/component-name-in-template-casing': 'off',
'vue/require-toggle-inside-transition': 'off',
'ts/no-use-before-define': 'off',
},
},
unocss.configs.flat,

16
src/App.vue

@ -1,10 +1,9 @@
<script lang="ts" setup>
import 'dayjs/locale/zh-cn'
import { App, ConfigProvider } from 'ant-design-vue'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { App, ConfigProvider } from 'ant-design-vue'
import type { TransformCellTextProps } from 'ant-design-vue/lib/table/interface'
import { AppProvider } from '@/components/Application'
import { useTitle } from '@/hooks/web/useTitle'
import { useLocale } from '@/locales/useLocale'
@ -18,10 +17,17 @@ const { themeConfig } = storeToRefs(appStore)
const componentSize = computed(() => appStore.getComponentSize)
// Listening to page changes and dynamically changing site titles
useTitle()
function transformCellText(props: TransformCellTextProps) {
if (!props.text || !props.text.length)
return '-'
return props.text
}
</script>
<template>
<ConfigProvider :locale="getAntdLocale" :theme="themeConfig" :component-size="componentSize">
<ConfigProvider :locale="getAntdLocale" :theme="themeConfig" :component-size="componentSize" :transform-cell-text="transformCellText">
<App class="h-full w-full">
<AppProvider>
<RouterView />

6
src/api/base/login.ts

@ -1,4 +1,3 @@
import type { TentantNameVO } from './model/loginModel'
import { defHttp } from '@/utils/http/axios'
import { getRefreshToken } from '@/utils/auth'
@ -18,11 +17,6 @@ export function refreshToken() {
return defHttp.post({ url: Api.RefreshToken + refreshToken })
}
// 使用租户名,获得租户编号
export function getTenantIdByName(name: string) {
return defHttp.get<TentantNameVO>({ url: Api.GetTenantIdByName + name })
}
// 登出
export function loginOut() {
return defHttp.delete({ url: Api.LoginOut })

7
src/api/base/model/userModel.ts

@ -1,12 +1,9 @@
import type { RouteItem } from './menuModel'
/**
* @description: Login interface parameters
*/
export interface LoginParams {
username: string
password: string
captchaVerification: string
}
/**
@ -31,10 +28,6 @@ export interface LoginResultModel {
* @description: Get user information return value
*/
export interface GetUserInfoModel {
roles: string[]
permissions: string[]
menus: RouteItem[]
// 用户id
user: userModel
}

36
src/api/base/user.ts

@ -1,36 +0,0 @@
import type { GetUserInfoModel, LoginParams, LoginResultModel, SmsLoginParams } from './model/userModel'
import { defHttp } from '@/utils/http/axios'
import type { ErrorMessageMode } from '@/types/axios'
enum Api {
Login = '/system/auth/login',
Logout = '/system/auth/logout',
SmsLogin = '/system/auth/sms-login',
GetUserInfo = '/system/auth/get-permission-info',
}
/**
* @description: user login api
*/
export function loginApi(params: LoginParams, mode: ErrorMessageMode = 'modal') {
return defHttp.post<LoginResultModel>({ url: Api.Login, params }, { errorMessageMode: mode })
}
/**
* @description: user smslogin api
*/
export function smsLogin(params: SmsLoginParams, mode: ErrorMessageMode = 'modal') {
return defHttp.post<LoginResultModel>({ url: Api.SmsLogin, params }, { errorMessageMode: mode })
}
/**
* @description: getUserInfo
*/
export function getUserInfo() {
return defHttp.get<GetUserInfoModel>({ url: Api.GetUserInfo }, { errorMessageMode: 'none' })
}
export function doLogout() {
return defHttp.post({ url: Api.Logout })
}

29
src/api/base/user/index.ts

@ -0,0 +1,29 @@
import type { LoginParams, LoginResult, UserInfo } from './types'
import { defHttp } from '@/utils/http/axios'
import type { ErrorMessageMode } from '@/types/axios'
export function loginApi(params: LoginParams, mode: ErrorMessageMode = 'modal') {
return defHttp.post<LoginResult>({
url: '/auth/login',
params,
headers: {
'tenant-id': '000000',
},
}, {
errorMessageMode: mode,
})
}
export function getUserInfo() {
return defHttp.get<UserInfo>({
url: '/system/permission/get-permission-info',
}, {
errorMessageMode: 'none',
})
}
export function doLogout() {
return defHttp.post({
url: '/auth/logout',
})
}

32
src/api/base/user/types.ts

@ -0,0 +1,32 @@
import type { MenuItem } from '@/api/system/menu/types'
export interface LoginParams {
username: string
password: string
}
export interface LoginResult {
userId: string
accessToken: string
refreshToken: string
expiresTime: number
}
export interface UserInfo {
user: User
menus: MenuItem[]
buttons: string[]
}
export interface User {
id: string
tenantId: string
account: string
realName: string
avatar?: string
email?: string
mobile?: string
remark?: string
deptId: string
roleId: string
}

62
src/api/system/dept/index.ts

@ -1,48 +1,36 @@
import type { Department, LazyGetDeptListParams } from './types'
import { defHttp } from '@/utils/http/axios'
export interface DeptVO {
id?: number
name: string
parentId: number
status: number
sort: number
leaderUserId: number
phone: string
email: string
createTime: Date
export function lazyGetDeptList(params?: LazyGetDeptListParams) {
return defHttp.get<Department[]>({
url: '/system/dept/lazy-list',
params,
})
}
export interface DeptPageReqVO {
name?: string
status?: number
export function createDept(data: Partial<Department>) {
return defHttp.post({
url: '/system/dept/save',
data,
})
}
// 查询部门(精简)列表
export function listSimpleDept() {
return defHttp.get({ url: '/system/dept/list-all-simple' })
export function updateDept(data: Partial<Department>) {
return defHttp.post({
url: '/system/dept/update',
data,
})
}
// 查询部门列表
export function getDeptPage(params: DeptPageReqVO) {
return defHttp.get({ url: '/system/dept/list', params })
export function deleteDept(id: string) {
return defHttp.post({
url: `/system/dept/delete?id=${id}`,
})
}
// 查询部门详情
export function getDept(id: number) {
return defHttp.get({ url: `/system/dept/get?id=${id}` })
}
// 新增部门
export function createDept(data: DeptVO) {
return defHttp.post({ url: '/system/dept/create', data })
}
// 修改部门
export function updateDept(params: DeptVO) {
return defHttp.put({ url: '/system/dept/update', data: params })
}
// 删除部门
export function deleteDept(id: number) {
return defHttp.delete({ url: `/system/dept/delete?id=${id}` })
export function getDeptTree(params?: { tenantId: string }) {
return defHttp.get<{ id: string, title: string }[]>({
url: '/system/dept/tree',
params,
})
}

17
src/api/system/dept/types.ts

@ -0,0 +1,17 @@
export interface Department {
id: string
tenantId: string
parentId: string
deptName: string
parentName: string
deptTypeName: string
children: Department[]
hasChildren: boolean
}
export interface LazyGetDeptListParams {
id?: string
tenantId?: string
parentId?: string
deptName?: string
}

67
src/api/system/menu/index.ts

@ -1,52 +1,41 @@
import type { GetMenuListParams, MenuItem } from './types'
import { defHttp } from '@/utils/http/axios'
export interface MenuVO {
id: number
name: string
permission: string
type: number
sort: number
parentId: number
path: string
icon: string
component: string
status: number
visible: boolean
keepAlive: boolean
createTime: Date
export function getMenuListWithoutButtons() {
return defHttp.get<MenuItem[]>({
url: '/system/menu/tree',
})
}
export interface MenuPageReqVO {
name?: string
status?: number
export function getMenuList(params: GetMenuListParams) {
return defHttp.get<PageResult<MenuItem>>({
url: '/system/menu/list',
params,
})
}
// 查询菜单(精简)列表
export function listSimpleMenus() {
return defHttp.get({ url: '/system/menu/list-all-simple' })
export function getMenu(id: string) {
return defHttp.get({
url: `/system/menu/get?id=${id}`,
})
}
// 查询菜单列表
export function getMenuList(params: MenuPageReqVO) {
return defHttp.get({ url: '/system/menu/list', params })
export function createMenu(data: Omit<MenuItem, 'id' | 'children'>) {
return defHttp.post({
url: '/system/menu/save',
data,
})
}
// 获取菜单详情
export function getMenu(id: number) {
return defHttp.get({ url: `/system/menu/get?id=${id}` })
export function updateMenu(data: Omit<MenuItem, 'children'>) {
return defHttp.post({
url: '/system/menu/update',
data,
})
}
// 新增菜单
export function createMenu(data: MenuVO) {
return defHttp.post({ url: '/system/menu/create', data })
}
// 修改菜单
export function updateMenu(data: MenuVO) {
return defHttp.put({ url: '/system/menu/update', data })
}
// 删除菜单
export function deleteMenu(id: number) {
return defHttp.delete({ url: `/system/menu/delete?id=${id}` })
export function deleteMenu(id: string) {
return defHttp.post({
url: `/system/menu/delete?id=${id}`,
})
}

26
src/api/system/menu/types.ts

@ -0,0 +1,26 @@
export interface GetMenuListParams {
name?: string
code?: string
}
export enum SystemMenuTypeEnum {
DIR = 0, // 目录
MENU = 1, // 菜单
BUTTON = 2, // 按钮
}
export interface MenuItem {
id: string
parentId: string
name: string
component: string
componentName?: string
path: string
icon: string
sort: number
children: MenuItem[]
code: string
type: SystemMenuTypeEnum
visible: BooleanFlag
keepAlive: BooleanFlag
}

92
src/api/system/role/index.ts

@ -1,70 +1,56 @@
import type { GetRoleListParams, MenuTreeNode, Role } from './types'
import { defHttp } from '@/utils/http/axios'
export interface RoleVO {
id: number
name: string
code: string
sort: number
status: number
type: number
createTime: Date
export function lazyGetRoleList(params: GetRoleListParams) {
return defHttp.get<Role[]>({
url: '/system/role/lazy-list',
params,
})
}
export interface RolePageReqVO extends PageParam {
name?: string
code?: string
status?: number
createTime?: Date[]
export function createRole(data: Partial<Role>) {
return defHttp.post({
url: '/system/role/save',
data,
})
}
export interface UpdateStatusReqVO {
id: number
status: number
export function updateRole(data: Partial<Role>) {
return defHttp.post({
url: '/system/role/update',
data,
})
}
export interface RoleExportReqVO {
name?: string
code?: string
status?: number
createTime?: Date[]
export function deleteRole(id: string) {
return defHttp.post({
url: `/system/role/delete?id=${id}`,
})
}
// 查询角色列表
export function getRolePage(params: RolePageReqVO) {
return defHttp.get({ url: '/system/role/page', params })
export function getRoleTree(params?: { tenantId: string }) {
return defHttp.get<{ id: string, title: string }[]>({
url: '/system/role/tree',
params,
})
}
// 查询角色(精简)列表
export function listSimpleRoles() {
return defHttp.get({ url: '/system/role/list-all-simple' })
export function getMenuTree() {
return defHttp.get<MenuTreeNode[]>({
url: '/system/menu/grant-tree',
})
}
// 查询角色详情
export function getRole(id: number) {
return defHttp.get({ url: `/system/role/get?id=${id}` })
export function getMenuIdsByRole(roleId: string) {
return defHttp.get<string[]>({
url: '/system/permission/list-role-menus',
params: { roleId },
})
}
// 新增角色
export function createRole(data: RoleVO) {
return defHttp.post({ url: '/system/role/create', data })
}
// 修改角色
export function updateRole(data: RoleVO) {
return defHttp.put({ url: '/system/role/update', data })
}
// 修改角色状态
export function updateRoleStatus(data: UpdateStatusReqVO) {
return defHttp.put({ url: '/system/role/update-status', data })
}
// 删除角色
export function deleteRole(id: number) {
return defHttp.delete({ url: `/system/role/delete?id=${id}` })
}
// 导出角色
export function exportRole(params: RoleExportReqVO) {
return defHttp.download({ url: '/system/post/export', params }, '导出角色.xls')
export function assignMenuToRole(data: { roleId: string, menuIds: string[] }) {
return defHttp.post({
url: '/system/permission/assign-role-menu',
data,
})
}

29
src/api/system/role/types.ts

@ -0,0 +1,29 @@
export interface GetRoleListParams extends PageParam {
tenantId?: string
parentId?: string
roleName?: string
roleAlias?: string
}
export interface Role {
id: string
tenantId: string
tenantName: string
parentId: string
roleName: string
roleAlias: string
createTime?: string
createUser?: string
createDept?: string
updateTime?: string
updateUser?: string
hasChildren?: boolean
children?: Role[]
}
export interface MenuTreeNode {
id: string
parentId: string
title: string
children?: MenuTreeNode[]
}

75
src/api/system/tenant/index.ts

@ -1,62 +1,35 @@
import type { GetTenantListParams, Tenant } from './types'
import { defHttp } from '@/utils/http/axios'
export interface TenantVO {
id: number
name: string
contactName: string
contactMobile: string
status: number
domain: string
packageId: number
username: string
password: string
expireTime: Date
accountCount: number
createTime: Date
export function getTenantList(params: GetTenantListParams) {
return defHttp.get<PageResult<Tenant>>({
url: '/system/tenant/page',
params,
})
}
export interface TenantPageReqVO extends PageParam {
name?: string
contactName?: string
contactMobile?: string
status?: number
createTime?: Date[]
export function updateTenant(data: Tenant) {
return defHttp.post({
url: '/system/tenant/update',
data,
})
}
export interface TenantExportReqVO {
name?: string
contactName?: string
contactMobile?: string
status?: number
createTime?: Date[]
export function createTenant(data: Tenant) {
return defHttp.post({
url: '/system/tenant/save',
data,
})
}
// 查询租户列表
export function getTenantPage(params: TenantPageReqVO) {
return defHttp.get({ url: '/system/tenant/page', params })
export function deleteTenant(id: string) {
return defHttp.post({
url: `/system/tenant/remove?id=${id}`,
})
}
// 查询租户详情
export function getTenant(id: number) {
return defHttp.get({ url: `/system/tenant/get?id=${id}` })
}
// 新增租户
export function createTenant(data: TenantVO) {
return defHttp.post({ url: '/system/tenant/create', data })
}
// 修改租户
export function updateTenant(data: TenantVO) {
return defHttp.put({ url: '/system/tenant/update', data })
}
// 删除租户
export function deleteTenant(id: number) {
return defHttp.delete({ url: `/system/tenant/delete?id=${id}` })
}
// 导出租户
export function exportTenant(params: TenantExportReqVO) {
return defHttp.download({ url: '/system/tenant/export-excel', params }, '租户.xls')
export function getAllTenants() {
return defHttp.get({
url: '/system/tenant/select',
})
}

16
src/api/system/tenant/types.ts

@ -0,0 +1,16 @@
export interface Tenant {
id: string
tenantId?: string
tenantName: string
adminAccount: string
contactName: string
contactMobile: string
expireTime?: string
}
export interface GetTenantListParams extends PageParam {
tenantId?: string
tenantName?: string
contactName?: string
contactMobile?: string
}

102
src/api/system/user/index.ts

@ -1,91 +1,29 @@
import type { GetUserListParams, SystemUser } from './types'
import { defHttp } from '@/utils/http/axios'
export interface UserVO {
id: number
username: string
nickname: string
deptId: number
postIds: string[]
email: string
mobile: string
sex: number
avatar: string
loginIp: string
status: number
remark: string
loginDate: Date
createTime: Date
export function getUserList(params: GetUserListParams) {
return defHttp.get<PageResult<SystemUser>>({
url: '/system/user/page',
params,
})
}
export interface UserPageReqVO extends PageParam {
deptId?: number
username?: string
mobile?: string
status?: number
createTime?: Date[]
export function createUser(data: Partial<SystemUser>) {
return defHttp.post({
url: '/system/user/save',
data,
})
}
export interface UserExportReqVO {
code?: string
name?: string
status?: number
createTime?: Date[]
export function updateUser(data: Partial<SystemUser>) {
return defHttp.post({
url: '/system/user/update',
data,
})
}
// 查询用户管理列表
export function getUserPage(params: UserPageReqVO) {
return defHttp.get({ url: '/system/user/page', params })
}
// 查询用户详情
export function getUser(id: number) {
return defHttp.get({ url: `/system/user/get?id=${id}` })
}
// 新增用户
export function createUser(data: UserVO) {
return defHttp.post({ url: '/system/user/create', data })
}
// 修改用户
export function updateUser(data: UserVO) {
return defHttp.put({ url: '/system/user/update', data })
}
// 删除用户
export function deleteUser(id: number) {
return defHttp.delete({ url: `/system/user/delete?id=${id}` })
}
// 导出用户
export function exportUser(params: UserExportReqVO) {
return defHttp.download({ url: '/system/user/export', params }, '用户.xls')
}
// 下载用户导入模板
export function importUserTemplate() {
return defHttp.download({ url: '/system/user/get-import-template' }, '用户导入模板.xls')
}
// 用户密码重置
export function resetUserPwd(id: number, password: string) {
const data = {
id,
password,
}
return defHttp.put({ url: '/system/user/update-password', data })
}
// 用户状态修改
export function updateUserStatus(id: number, status: number) {
const data = {
id,
status,
}
return defHttp.put({ url: '/system/user/update-status', data })
}
// 获取用户精简信息列表
export function getListSimpleUsers() {
return defHttp.get({ url: '/system/user/list-all-simple' })
export function deleteUser(id: string) {
return defHttp.post({
url: `/system/user/delete?id=${id}`,
})
}

23
src/api/system/user/types.ts

@ -0,0 +1,23 @@
export interface GetUserListParams extends PageParam {
tenantId?: string
deptId?: string
account?: string
mobile?: string
}
export interface SystemUser {
id: string
account: string
avatar: string
deptId: string
deptName: string
email: string
mobile: string
realName: string
remark: string
roleId: string
roleName: string
sex: 1 | 2 | 3
tenantId: string
tenantName: string
}

8
src/components/Form/src/BasicForm.vue

@ -309,14 +309,6 @@ const getFormActionBindProps = computed(() => ({ ...getProps.value, ...advanceSt
margin: 0 6px 0 2px;
}
&-with-help {
margin-bottom: 0;
}
&:not(.ant-form-item-with-help) {
margin-bottom: 20px;
}
&.suffix-item {
.ant-form-item-children {
display: flex;

23
src/components/Form/src/components/ApiTreeSelect.vue

@ -14,7 +14,7 @@ const props = defineProps({
api: { type: Function as PropType<(arg?: any) => Promise<Recordable<any>>> },
params: { type: Object },
immediate: propTypes.bool.def(true),
async: propTypes.bool.def(false),
loadData: { type: Function as PropType<(treeNode: Recordable<any>) => Promise<Recordable<any>>> },
resultField: propTypes.string.def(''),
handleTree: propTypes.string.def(''),
parentId: propTypes.number.def(0),
@ -24,7 +24,7 @@ const props = defineProps({
valueField: propTypes.string.def('id'),
childrenField: propTypes.string.def('children'),
})
const emit = defineEmits(['options-change', 'change', 'load-data'])
const emit = defineEmits(['options-change', 'change'])
const attrs = useAttrs()
const treeData = ref<Recordable<any>[]>([])
const isFirstLoaded = ref<boolean>(false)
@ -64,14 +64,15 @@ onMounted(() => {
props.immediate && fetch()
})
function onLoadData(treeNode) {
return new Promise((resolve: (value?: unknown) => void) => {
if (isArray(treeNode.children) && treeNode.children.length > 0) {
resolve()
return
}
emit('load-data', { treeData, treeNode, resolve })
})
async function onLoadData(treeNode) {
if (isArray(treeNode.children) && treeNode.children.length > 0)
return
try {
treeNode.dataRef.children = await props.loadData!(treeNode)
treeData.value = [...treeData.value]
}
catch {}
}
async function fetch() {
@ -114,7 +115,7 @@ async function fetch() {
<TreeSelect
v-bind="getAttrs"
:field-names="fieldNames"
:load-data="async ? onLoadData : undefined"
:load-data="loadData ? onLoadData : undefined"
@change="handleChange"
>
<template v-for="item in Object.keys($slots)" #[item]="data">

22
src/components/Form/src/hooks/useFormEvents.ts

@ -124,7 +124,7 @@ export function useFormEvents({
const { componentProps } = schema || {}
let _props = componentProps as any
if (typeof componentProps === 'function')
_props = _props({ formModel: unref(formModel) })
_props = _props({ formModel: unref(formModel), formActionType })
const constructValue = tryConstructArray(key, values) || tryConstructObject(key, values)
@ -340,6 +340,10 @@ export function useFormEvents({
return handleFormValues(values)
}
async function setProps(formProps: Partial<FormProps>): Promise<void> {
await unref(formElRef)?.setProps(formProps)
}
async function validate(nameList?: NamePath[] | false | undefined) {
let _nameList: any
if (nameList === undefined)
@ -385,6 +389,22 @@ export function useFormEvents({
}
}
const formActionType: FormActionType = {
getFieldsValue,
setFieldsValue,
resetFields,
updateSchema,
resetSchema,
setProps,
removeSchemaByField,
appendSchemaByField,
clearValidate,
validateFields,
validate,
submit: handleSubmit,
scrollToField,
}
return {
handleSubmit,
clearValidate,

5
src/components/Form/src/hooks/useFormValues.ts

@ -67,6 +67,7 @@ export function useFormValues({
continue
const transformDateFunc = unref(getProps).transformDateFunc
if (isObject(value))
value = transformDateFunc?.(value)
@ -75,11 +76,7 @@ export function useFormValues({
// Remove spaces
if (isString(value)) {
// remove params from URL
if (value === '')
value = undefined
else
value = value.trim()
}
if (!tryDeconstructArray(key, value, res) && !tryDeconstructObject(key, value, res)) {

2
src/components/Form/src/types/index.ts

@ -134,7 +134,7 @@ export interface ComponentProps {
ApiSelect: CustomComponents['ApiSelect'] & ComponentProps['Select']
TreeSelect: ExtractPropTypes<(typeof import('ant-design-vue/es/tree-select'))['default']>
ApiTree: CustomComponents['ApiTree'] & ExtractPropTypes<(typeof import('ant-design-vue/es/tree'))['default']>
ApiTreeSelect: CustomComponents['ApiTreeSelect'] & ComponentProps['TreeSelect']
ApiTreeSelect: CustomComponents['ApiTreeSelect'] & Omit<ComponentProps['TreeSelect'], 'loadData'>
ApiRadioGroup: CustomComponents['ApiRadioGroup'] & ComponentProps['RadioGroup']
RadioButtonGroup: CustomComponents['RadioButtonGroup'] & ComponentProps['RadioGroup']
RadioGroup: ExtractPropTypes<(typeof import('ant-design-vue/es/radio'))['RadioGroup']>

2
src/components/Table/src/BasicTable.vue

@ -366,7 +366,7 @@ emit('register', tableAction, formActions)
}
.ant-table-wrapper {
padding: 6px;
padding: 20px;
background-color: var(--component-background);
border-radius: 6px;

3
src/components/Table/src/components/TableAction.vue

@ -1,7 +1,6 @@
<script lang="ts" setup>
import type { PropType } from 'vue'
import { computed, toRaw, unref } from 'vue'
import { DownOutlined } from '@ant-design/icons-vue'
import type { TooltipProps } from 'ant-design-vue'
import { Divider, Tooltip } from 'ant-design-vue'
import { useTableContext } from '../hooks/useTableContext'
@ -144,7 +143,7 @@ function onCellClick(e: MouseEvent) {
>
<slot name="more" />
<a-button v-if="!$slots.more" type="link">
{{ t('action.more') }} <DownOutlined class="icon-more" />
{{ t('action.more') }} <span class="i-tabler:chevron-down" />
</a-button>
</Dropdown>
</div>

1
src/components/Table/src/hooks/useColumns.ts

@ -122,7 +122,6 @@ export function useColumns(
columns.forEach((item) => {
const { customRender, slots } = item
handleItem(item, Reflect.has(item, 'ellipsis') ? !!item.ellipsis : !!ellipsis && !customRender && !slots)
})
return columns

2
src/components/Table/src/types/table.ts

@ -172,7 +172,7 @@ export interface BasicTableProps<T = Recordable<any>> {
/**
* , `src/settings/componentSetting.ts > table > fetchSetting`
*/
api?: (...arg: any) => Promise<T[] | { list: T[], pageNo?: number, pageSize?: number, total?: number }>
api?: (...arg: any) => Promise<T[] | { records: T[], current?: number, size?: number, total?: number }>
// 请求之前处理参数
beforeFetch?: Fn
// 自定义处理接口返回参数

2
src/components/Tree/src/types/tree.ts

@ -77,7 +77,7 @@ export const treeProps = buildProps({
},
treeData: {
type: Array as PropType<TreeDataItem[]>,
type: Array as PropType<(Omit<TreeDataItem, 'key'> & { key?: string | number })[]>,
},
actionList: {

15
src/enums/appEnum.ts

@ -25,21 +25,6 @@ export enum SessionTimeoutProcessingEnum {
PAGE_COVERAGE,
}
/**
*
*/
export enum PermissionModeEnum {
// role
// 角色权限
ROLE = 'ROLE',
// black
// 后端
BACK = 'BACK',
// route mapping
// 路由映射
ROUTE_MAPPING = 'ROUTE_MAPPING',
}
// Route switching animation
// 路由切换动画
export enum RouterTransitionEnum {

2
src/enums/httpEnum.ts

@ -2,7 +2,7 @@
* @description: Request result set
*/
export enum ResultEnum {
SUCCESS = 0,
SUCCESS = 200,
ERROR = -1,
TIMEOUT = 400,
UNAUTHORIZED = 401,

9
src/enums/systemEnum.ts

@ -1,12 +1,3 @@
/**
*
*/
export const SystemMenuTypeEnum = {
DIR: 1, // 目录
MENU: 2, // 菜单
BUTTON: 3, // 按钮
}
/**
*
*/

6
src/hooks/component/useFormItem.ts

@ -1,5 +1,5 @@
import type { DeepReadonly, Ref, UnwrapRef, WritableComputedRef } from 'vue'
import { computed, getCurrentInstance, nextTick, reactive, readonly, toRaw, unref, watchEffect } from 'vue'
import { computed, getCurrentInstance, reactive, readonly, toRaw, unref, watchEffect } from 'vue'
import { isEqual } from 'lodash-es'
@ -37,9 +37,7 @@ export function useRuleFormItem<T extends Recordable>(props: T, key: keyof T = '
return
innerState.value = value as T[keyof T]
nextTick(() => {
emit?.(changeEvent, value, ...(toRaw(unref(emitData)) || []))
})
emit?.(changeEvent, value, ...(toRaw(unref(emitData)) || []))
},
})

4
src/hooks/setting/index.ts

@ -10,8 +10,6 @@ export function useGlobSetting(): Readonly<GlobConfig> {
VITE_GLOB_APP_SHORT_NAME,
VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_UPLOAD_URL,
VITE_GLOB_APP_TENANT_ENABLE,
VITE_GLOB_APP_CAPTCHA_ENABLE,
} = getAppEnvConfig()
if (!/[a-zA-Z\_]*/.test(VITE_GLOB_APP_SHORT_NAME)) {
@ -27,8 +25,6 @@ export function useGlobSetting(): Readonly<GlobConfig> {
shortName: VITE_GLOB_APP_SHORT_NAME,
urlPrefix: VITE_GLOB_API_URL_PREFIX,
uploadUrl: VITE_GLOB_UPLOAD_URL,
tenantEnable: VITE_GLOB_APP_TENANT_ENABLE,
captchaEnable: VITE_GLOB_APP_CAPTCHA_ENABLE,
}
return glob
}

3
src/hooks/setting/useRootSetting.ts

@ -18,8 +18,6 @@ export function useRootSetting() {
const getCanEmbedIFramePage = computed(() => appStore.getProjectConfig.canEmbedIFramePage)
const getPermissionMode = computed(() => appStore.getProjectConfig.permissionMode)
const getShowLogo = computed(() => appStore.getProjectConfig.showLogo)
const getContentMode = computed(() => appStore.getProjectConfig.contentMode)
@ -70,7 +68,6 @@ export function useRootSetting() {
getPageLoading,
getOpenKeepAlive,
getCanEmbedIFramePage,
getPermissionMode,
getShowLogo,
getUseErrorHandle,
getShowBreadCrumb,

50
src/hooks/web/usePermission.ts

@ -1,42 +1,19 @@
import type { RouteRecordRaw } from 'vue-router'
import { intersection } from 'lodash-es'
import { useTabs } from './useTabs'
import { useAppStore } from '@/store/modules/app'
import { usePermissionStore } from '@/store/modules/permission'
import { useUserStore } from '@/store/modules/user'
import { resetRouter, router } from '@/router'
// import { RootRoute } from '@/router/routes';
import projectSetting from '@/settings/projectSetting'
import { PermissionModeEnum } from '@/enums/appEnum'
import type { RoleEnum } from '@/enums/roleEnum'
import { isArray } from '@/utils/is'
import { useMultipleTabStore } from '@/store/modules/multipleTab'
// User permissions related operations
export function usePermission() {
const userStore = useUserStore()
const appStore = useAppStore()
const permissionStore = usePermissionStore()
const { closeAll } = useTabs(router)
/**
* Change permission mode
*/
function togglePermissionMode() {
appStore.setProjectConfig({
permissionMode: projectSetting.permissionMode
=== PermissionModeEnum.BACK
? PermissionModeEnum.ROUTE_MAPPING
: PermissionModeEnum.BACK,
})
location.reload()
}
/**
* Reset and regain authority resource information
*
@ -64,23 +41,11 @@ export function usePermission() {
if (!value)
return def
const permMode = appStore.getProjectConfig.permissionMode
if ([PermissionModeEnum.ROUTE_MAPPING, PermissionModeEnum.ROLE].includes(permMode)) {
if (!isArray(value))
return userStore.getRoleList?.includes(value as RoleEnum)
const allCodeList = permissionStore.getPermCodeList as string[]
if (!isArray(value))
return allCodeList.includes(value)
return (intersection(value, userStore.getRoleList) as RoleEnum[]).length > 0
}
if (PermissionModeEnum.BACK === permMode) {
const allCodeList = permissionStore.getPermCodeList as string[]
if (!isArray(value))
return allCodeList.includes(value)
return (intersection(value, allCodeList)).length > 0
}
return true
return (intersection(value, allCodeList)).length > 0
}
/**
@ -88,9 +53,6 @@ export function usePermission() {
* @param roles
*/
async function changeRole(roles: RoleEnum | RoleEnum[]): Promise<void> {
if (projectSetting.permissionMode !== PermissionModeEnum.ROUTE_MAPPING)
throw new Error('Please switch PermissionModeEnum to ROUTE_MAPPING mode in the configuration to operate!')
if (!isArray(roles))
roles = [roles]
@ -102,8 +64,8 @@ export function usePermission() {
* refresh menu data
*/
function refreshMenu() {
resume()
return resume()
}
return { changeRole, hasPermission, togglePermissionMode, refreshMenu }
return { changeRole, hasPermission, refreshMenu }
}

2
src/layouts/default/header/components/lock/LockModal.vue

@ -13,7 +13,7 @@ const { t } = useI18n()
const userStore = useUserStore()
const lockStore = useLockStore()
const getRealName = computed(() => userStore.getUserInfo?.user.nickname)
const getRealName = computed(() => userStore.getUserInfo?.user.realName)
const [register, { closeModal }] = useModalInner()
const [registerForm, { validateFields, resetFields }] = useForm({

6
src/layouts/default/header/components/user-dropdown/index.vue

@ -32,8 +32,8 @@ const { getShowDoc, getUseLockPage } = useHeaderSetting()
const userStore = useUserStore()
const getUserInfo = computed(() => {
const { nickname = '', avatar } = userStore.getUserInfo.user || {}
return { nickname, avatar: avatar || headerImg }
const { realName = '', avatar } = userStore.getUserInfo.user || {}
return { realName, avatar: avatar || headerImg }
})
const [register, { openModal }] = useModal()
@ -76,7 +76,7 @@ function handleMenuClick(e: MenuInfo) {
</Avatar>
<span :class="`${prefixCls}__info hidden md:block`">
<span :class="`${prefixCls}__name`" class="truncate">
{{ getUserInfo.nickname }}
{{ getUserInfo.realName }}
</span>
</span>
</span>

23
src/router/guard/paramMenuGuard.ts

@ -1,9 +1,6 @@
import type { Router } from 'vue-router'
import { configureDynamicParamsMenu } from '../helper/menuHelper'
import type { Menu } from '../types'
import { PermissionModeEnum } from '@/enums/appEnum'
import { useAppStoreWithOut } from '@/store/modules/app'
import { usePermissionStoreWithOut } from '@/store/modules/permission'
export function createParamMenuGuard(router: Router) {
@ -21,27 +18,9 @@ export function createParamMenuGuard(router: Router) {
return
}
let menus: Menu[] = []
if (isBackMode())
menus = permissionStore.getBackMenuList
else if (isRouteMappingMode())
menus = permissionStore.getFrontMenuList
const menus: Menu[] = permissionStore.getBackMenuList
menus.forEach(item => configureDynamicParamsMenu(item, to.params))
next()
})
}
function getPermissionMode() {
const appStore = useAppStoreWithOut()
return appStore.getProjectConfig.permissionMode
}
function isBackMode() {
return getPermissionMode() === PermissionModeEnum.BACK
}
function isRouteMappingMode() {
return getPermissionMode() === PermissionModeEnum.ROUTE_MAPPING
}

8
src/router/guard/permissionGuard.ts

@ -3,7 +3,6 @@ import type { RouteRecordRaw, Router } from 'vue-router'
import { usePermissionStoreWithOut } from '@/store/modules/permission'
import { PageEnum } from '@/enums/pageEnum'
import { useDictStoreWithOut } from '@/store/modules/dict'
import { useUserStoreWithOut } from '@/store/modules/user'
import { PAGE_NOT_FOUND_ROUTE } from '@/router/routes/basic'
@ -17,7 +16,7 @@ const LOGIN_PATH = PageEnum.BASE_LOGIN
const whitePathList: PageEnum[] = [LOGIN_PATH]
export function createPermissionGuard(router: Router) {
const dictStore = useDictStoreWithOut()
// const dictStore = useDictStoreWithOut()
const userStore = useUserStoreWithOut()
const permissionStore = usePermissionStoreWithOut()
router.beforeEach(async (to, from, next) => {
@ -79,8 +78,8 @@ export function createPermissionGuard(router: Router) {
return
}
if (!dictStore.getIsSetDict)
await dictStore.setDictMap()
// if (!dictStore.getIsSetDict)
// await dictStore.setDictMap()
// get userinfo while last fetch time is empty
if (userStore.getLastUpdateTime === 0) {
@ -108,7 +107,6 @@ export function createPermissionGuard(router: Router) {
})
router.addRoute(PAGE_NOT_FOUND_ROUTE as unknown as RouteRecordRaw)
permissionStore.setDynamicAddedRoute(true)
if (to.name === PAGE_NOT_FOUND_ROUTE.name) {

28
src/router/helper/routeHelper.ts

@ -6,6 +6,8 @@ import { EXCEPTION_COMPONENT, LAYOUT, getParentLayout } from '@/router/constant'
import type { AppRouteModule, AppRouteRecordRaw } from '@/router/types'
import { warn } from '@/utils/log'
import { isHttpUrl } from '@/utils/is'
import { toCamelCase } from '@/utils'
import type { MenuItem } from '@/api/system/menu/types'
export type LayoutMapKey = 'LAYOUT'
const IFRAME = () => import('@/views/base/iframe/FrameBlank.vue')
@ -44,7 +46,7 @@ function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) {
meta.orderNo = item.sort
meta.ignoreKeepAlive = !item.keepAlive
item.meta = meta
item.name = item.name = item.componentName && item.componentName.length > 0 ? item.componentName : toCamelCase(item.path, true)
item.name = item.componentName && item.componentName.length > 0 ? item.componentName : toCamelCase(item.path, true)
children && asyncImportRoute(children)
})
}
@ -76,11 +78,13 @@ function dynamicImport(dynamicViewsModules: Record<string, () => Promise<Recorda
// Turn background objects into routing objects
// 将背景对象变成路由对象
export function transformObjToRoute<T = AppRouteModule>(routeList: AppRouteModule[]): T[] {
routeList.forEach((route) => {
export function transformObjToRoute(menuList: MenuItem[]): AppRouteModule[] {
const routeList: AppRouteModule[] = []
menuList.forEach((item) => {
const route = { ...item } as unknown as AppRouteModule
if (isHttpUrl(route.path))
route.component = 'IFrame'
else if (route.children && route.parentId === 0)
else if (route.children && route.parentId === '0')
route.component = 'LAYOUT'
else if (!route.children)
route.component = route.component as string
@ -112,9 +116,10 @@ export function transformObjToRoute<T = AppRouteModule>(routeList: AppRouteModul
else {
warn(`请正确配置路由:${route?.name}的component属性`)
}
routeList.push(route)
route.children && asyncImportRoute(route.children)
})
return routeList as unknown as T[]
return routeList
}
/**
@ -194,16 +199,3 @@ function isMultipleRoute(routeModule: AppRouteModule) {
}
return flag
}
function toCamelCase(str: string, upperCaseFirst: boolean) {
str = (str || '')
.replace(/-(.)/g, (group1: string) => {
return group1.toUpperCase()
})
.replaceAll('-', '')
if (upperCaseFirst && str)
str = str.charAt(0).toUpperCase() + str.slice(1)
return str
}

70
src/router/menus/index.ts

@ -1,14 +1,6 @@
import type { RouteRecordNormalized } from 'vue-router'
import { pathToRegexp } from 'path-to-regexp'
import type { Menu, MenuModule } from '@/router/types'
import { useAppStoreWithOut } from '@/store/modules/app'
import { usePermissionStore } from '@/store/modules/permission'
import { getAllParentPath, transformMenuModule } from '@/router/helper/menuHelper'
import { filter } from '@/utils/helper/treeHelper'
import { isHttpUrl } from '@/utils/is'
import { router } from '@/router'
import { PermissionModeEnum } from '@/enums/appEnum'
const modules = import.meta.glob('./modules/**/*.ts', { eager: true })
@ -24,22 +16,6 @@ Object.keys(modules).forEach((key) => {
// ==========Helper===========
// ===========================
function getPermissionMode() {
const appStore = useAppStoreWithOut()
return appStore.getProjectConfig.permissionMode
}
function isBackMode() {
return getPermissionMode() === PermissionModeEnum.BACK
}
function isRouteMappingMode() {
return getPermissionMode() === PermissionModeEnum.ROUTE_MAPPING
}
function isRoleMode() {
return getPermissionMode() === PermissionModeEnum.ROLE
}
const staticMenus: Menu[] = []
;(() => {
menuModules.sort((a, b) => {
@ -62,21 +38,12 @@ function getAsyncMenus() {
return show
})
}
if (isBackMode())
return menuFilter(permissionStore.getBackMenuList)
if (isRouteMappingMode())
return menuFilter(permissionStore.getFrontMenuList)
return staticMenus
return menuFilter(permissionStore.getBackMenuList)
}
export async function getMenus(): Promise<Menu[]> {
const menus = await getAsyncMenus()
if (isRoleMode()) {
const routes = router.getRoutes()
return filter(menus, basicFilter(routes))
}
return menus
}
@ -90,10 +57,6 @@ export async function getCurrentParentPath(currentPath: string) {
export async function getShallowMenus(): Promise<Menu[]> {
const menus = await getAsyncMenus()
const shallowMenuList = menus.map(item => ({ ...item, children: undefined }))
if (isRoleMode()) {
const routes = router.getRoutes()
return shallowMenuList.filter(basicFilter(routes))
}
return shallowMenuList
}
@ -104,36 +67,5 @@ export async function getChildrenMenus(parentPath: string) {
if (!parent || !parent.children || !!parent?.meta?.hideChildrenInMenu)
return [] as Menu[]
if (isRoleMode()) {
const routes = router.getRoutes()
return filter(parent.children, basicFilter(routes))
}
return parent.children
}
function basicFilter(routes: RouteRecordNormalized[]) {
return (menu: Menu) => {
const matchRoute = routes.find((route) => {
if (isHttpUrl(menu.path))
return true
if (route.meta?.carryParam)
return pathToRegexp(route.path).test(menu.path)
const isSame = route.path === menu.path
if (!isSame)
return false
if (route.meta?.ignoreAuth)
return true
return isSame || pathToRegexp(route.path).test(menu.path)
})
if (!matchRoute)
return false
menu.icon = (menu.icon || matchRoute.meta.icon) as string
menu.meta = matchRoute.meta
return true
}
}

10
src/router/routes/index.ts

@ -38,15 +38,6 @@ export const LoginRoute: AppRouteRecordRaw = {
},
}
export const SSORoute: AppRouteRecordRaw = {
path: '/sso',
name: 'SSO',
component: () => import('@/views/base/login/sso.vue'),
meta: {
title: t('routes.basic.sso'),
},
}
export const ProfileRoute: AppRouteRecordRaw = {
path: '/profile',
component: LAYOUT,
@ -75,7 +66,6 @@ export const ProfileRoute: AppRouteRecordRaw = {
// 未经许可的基本路由
export const basicRoutes = [
LoginRoute,
SSORoute,
RootRoute,
ProfileRoute,
REDIRECT_ROUTE,

2
src/router/routes/modules/dashboard.ts

@ -7,7 +7,7 @@ const dashboard: AppRouteModule = {
path: '/dashboard',
name: 'Dashboard',
component: LAYOUT,
parentId: 0,
parentId: '0',
redirect: '/dashboard/analysis',
meta: {
orderNo: 10,

2
src/router/types.ts

@ -10,7 +10,7 @@ export interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta' | 'childr
icon?: string
name: string
sort?: number
parentId?: number
parentId?: string
meta: RouteMeta
component?: Component | string
components?: Component

6
src/settings/componentSetting.ts

@ -9,11 +9,11 @@ export default {
// 支持 xxx.xxx.xxx格式
fetchSetting: {
// 传给后台的当前页字段
pageField: 'pageNo',
pageField: 'current',
// 传给后台的每页显示多少条的字段
sizeField: 'pageSize',
sizeField: 'size',
// 接口返回表格数据的字段
listField: 'list',
listField: 'records',
// 接口返回表格总数的字段
totalField: 'total',
},

6
src/settings/projectSetting.ts

@ -5,7 +5,6 @@ import { MenuModeEnum, MenuTypeEnum, MixSidebarTriggerEnum, TriggerEnum } from '
import { CacheTypeEnum } from '@/enums/cacheEnum'
import {
ContentEnum,
PermissionModeEnum,
RouterTransitionEnum,
SessionTimeoutProcessingEnum,
SettingButtonPositionEnum,
@ -26,11 +25,6 @@ const setting: ProjectConfig = {
// SettingButtonPositionEnum.FIXED: 固定在右侧
settingButtonPosition: SettingButtonPositionEnum.AUTO,
// 权限模式,默认前端角色权限模式
// ROUTE_MAPPING: 前端模式(菜单由路由生成,默认)
// ROLE:前端模式(菜单路由分开)
// BACK: 后端模式
permissionMode: PermissionModeEnum.BACK,
// 权限缓存存放位置。默认存放于localStorage
permissionCacheType: CacheTypeEnum.LOCAL,
// 会话超时处理方案

3
src/store/modules/lock.ts

@ -37,13 +37,12 @@ export const useLockStore = defineStore('app-lock', {
const tryLogin = async () => {
// TODO 滑块验证码
try {
const username = userStore.getUserInfo?.user.nickname
const username = userStore.getUserInfo?.user.realName
const res = await userStore.login({
username,
password: password!,
goHome: false,
mode: 'none',
captchaVerification: '',
})
if (res)
this.resetLockInfo()

122
src/store/modules/permission.ts

@ -1,12 +1,9 @@
/* eslint-disable no-case-declarations */
import { toRaw } from 'vue'
import { defineStore } from 'pinia'
import { useUserStore } from './user'
import { useAppStoreWithOut } from './app'
import { store } from '@/store'
import type { AppRouteRecordRaw, Menu } from '@/router/types'
import { asyncRoutes } from '@/router/routes'
import dashboard from '@/router/routes/modules/dashboard'
import { PAGE_NOT_FOUND_ROUTE } from '@/router/routes/basic'
import { transformRouteToMenu } from '@/router/helper/menuHelper'
@ -14,9 +11,7 @@ import { flatMultiLevelRoutes, transformObjToRoute } from '@/router/helper/route
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { filter } from '@/utils/helper/treeHelper'
import projectSetting from '@/settings/projectSetting'
import { PageEnum } from '@/enums/pageEnum'
import { PermissionModeEnum } from '@/enums/appEnum'
interface PermissionState {
// Permission code list
@ -31,8 +26,6 @@ interface PermissionState {
// Backstage menu list
// 后台菜单列表
backMenuList: Menu[]
// 菜单列表
frontMenuList: Menu[]
}
export const usePermissionStore = defineStore('app-permission', {
@ -48,9 +41,6 @@ export const usePermissionStore = defineStore('app-permission', {
// Backstage menu list
// 后台菜单列表
backMenuList: [],
// menu List
// 菜单列表
frontMenuList: [],
}),
getters: {
getPermCodeList(state): string[] | number[] {
@ -59,9 +49,6 @@ export const usePermissionStore = defineStore('app-permission', {
getBackMenuList(state): Menu[] {
return state.backMenuList
},
getFrontMenuList(state): Menu[] {
return state.frontMenuList
},
getLastBuildMenuTime(state): number {
return state.lastBuildMenuTime
},
@ -79,10 +66,6 @@ export const usePermissionStore = defineStore('app-permission', {
list?.length > 0 && this.setLastBuildMenuTime()
},
setFrontMenuList(list: Menu[]) {
this.frontMenuList = list
},
setLastBuildMenuTime() {
this.lastBuildMenuTime = new Date().getTime()
},
@ -104,29 +87,15 @@ export const usePermissionStore = defineStore('app-permission', {
async buildRoutesAction(): Promise<AppRouteRecordRaw[]> {
const { t } = useI18n()
const userStore = useUserStore()
const appStore = useAppStoreWithOut()
let routes: AppRouteRecordRaw[] = []
const roleList = toRaw(userStore.getRoleList) || []
const userInfo = toRaw(userStore.getUserInfo) || {}
const { permissionMode = projectSetting.permissionMode } = appStore.getProjectConfig
// 路由过滤器 在 函数filter 作为回调传入遍历使用
const routeFilter = (route: AppRouteRecordRaw) => {
const { meta } = route
// 抽出角色
const { roles } = meta || {}
if (!roles)
return true
// 进行角色权限判断
return roleList.some(role => roles.includes(role))
}
/**
* @description ignoreRoute
*/
const routeRemoveIgnoreFilter = (route: AppRouteRecordRaw) => {
const { meta } = route
// ignoreRoute 为true 则路由仅用于菜单生成,不会在实际的路由表中出现
const { ignoreRoute } = meta || {}
// arr.filter 返回 true 表示该元素通过测试
return !ignoreRoute
}
@ -165,81 +134,22 @@ export const usePermissionStore = defineStore('app-permission', {
}
}
switch (permissionMode) {
// 角色权限
case PermissionModeEnum.ROLE:
// 对非一级路由进行过滤
routes = filter(asyncRoutes, routeFilter)
// 对一级路由根据角色权限过滤
routes = routes.filter(routeFilter)
// Convert multi-level routing to level 2 routing
// 将多级路由转换为 2 级路由
routes = flatMultiLevelRoutes(routes)
break
// 路由映射, 默认进入该case
case PermissionModeEnum.ROUTE_MAPPING:
// 对非一级路由进行过滤
routes = filter(asyncRoutes, routeFilter)
// 对一级路由再次根据角色权限过滤
routes = routes.filter(routeFilter)
// 将路由转换成菜单
const menuList = transformRouteToMenu(routes, true)
// 移除掉 ignoreRoute: true 的路由 非一级路由
routes = filter(routes, routeRemoveIgnoreFilter)
// 移除掉 ignoreRoute: true 的路由 一级路由;
routes = routes.filter(routeRemoveIgnoreFilter)
// 对菜单进行排序
menuList.sort((a, b) => {
return (a.meta?.orderNo || 0) - (b.meta?.orderNo || 0)
})
const { createMessage } = useMessage()
createMessage.loading({ content: t('sys.app.menuLoading'), duration: 1 })
// 设置菜单列表
this.setFrontMenuList(menuList)
// Convert multi-level routing to level 2 routing
// 将多级路由转换为 2 级路由
routes = flatMultiLevelRoutes(routes)
break
// If you are sure that you do not need to do background dynamic permissions, please comment the entire judgment below
// 如果确定不需要做后台动态权限,请在下方注释整个判断
case PermissionModeEnum.BACK:
const { createMessage } = useMessage()
createMessage.loading({ content: t('sys.app.menuLoading'), duration: 1 })
// !Simulate to obtain permission codes from the background,
// 模拟从后台获取权限码,
// this function may only need to be executed once, and the actual project can be put at the right time by itself
// 这个功能可能只需要执行一次,实际项目可以自己放在合适的时间
let routeList: AppRouteRecordRaw[] = []
try {
routeList = userInfo.menus as AppRouteRecordRaw[]
}
catch (error) {
console.error(error)
}
// Dynamically introduce components
// 动态引入组件
routeList = transformObjToRoute(routeList)
// Background routing to menu structure
// 后台路由到菜单结构
const backMenuList = transformRouteToMenu([dashboard, ...routeList])
this.setBackMenuList(backMenuList)
// remove meta.ignoreRoute item
// 删除 meta.ignoreRoute 项
routeList = filter(routeList, routeRemoveIgnoreFilter)
routeList = routeList.filter(routeRemoveIgnoreFilter)
routeList = flatMultiLevelRoutes(routeList)
routes = [PAGE_NOT_FOUND_ROUTE, dashboard, ...routeList]
break
}
// Dynamically introduce components
let routeList = transformObjToRoute(userInfo.menus)
// Background routing to menu structure
const backMenuList = transformRouteToMenu([dashboard, ...routeList])
this.setBackMenuList(backMenuList)
// 从用户中获取权限
if (userInfo)
this.setPermCodeList(userInfo.permissions)
// remove meta.ignoreRoute item
routeList = filter(routeList, routeRemoveIgnoreFilter)
routeList = routeList.filter(routeRemoveIgnoreFilter)
routeList = flatMultiLevelRoutes(routeList)
routes = [PAGE_NOT_FOUND_ROUTE, dashboard, ...routeList]
patchHomeAffix(routes)
return routes
},

52
src/store/modules/user.ts

@ -12,14 +12,12 @@ import { PAGE_NOT_FOUND_ROUTE } from '@/router/routes/basic'
import { usePermissionStore } from '@/store/modules/permission'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { getAuthCache, setAuthCache } from '@/utils/auth'
import { doLogout, getUserInfo, loginApi, smsLogin } from '@/api/base/user'
import type { GetUserInfoModel, LoginParams, SmsLoginParams } from '@/api/base/model/userModel'
import { isArray } from '@/utils/is'
import { getAuthCache, setAuthCache, setTenantId } from '@/utils/auth'
import { doLogout, getUserInfo, loginApi } from '@/api/base/user'
import type { LoginParams, UserInfo } from '@/api/base/user/types'
interface UserState {
userInfo: Nullable<GetUserInfoModel>
userInfo: Nullable<UserInfo>
accessToken?: string
refreshToken?: string
roleList: RoleEnum[]
@ -42,8 +40,8 @@ export const useUserStore = defineStore('app-user', {
lastUpdateTime: 0,
}),
getters: {
getUserInfo(state): GetUserInfoModel {
return state.userInfo || getAuthCache<GetUserInfoModel>(USER_INFO_KEY) || {}
getUserInfo(state): UserInfo {
return state.userInfo || getAuthCache<UserInfo>(USER_INFO_KEY) || {}
},
getAccessToken(state): string {
return state.accessToken || getAuthCache<string>(ACCESS_TOKEN_KEY)
@ -74,7 +72,7 @@ export const useUserStore = defineStore('app-user', {
this.roleList = roleList
setAuthCache(ROLES_KEY, roleList)
},
setUserInfo(info: GetUserInfoModel | null) {
setUserInfo(info: UserInfo | null) {
this.userInfo = info
this.lastUpdateTime = new Date().getTime()
setAuthCache(USER_INFO_KEY, info)
@ -96,7 +94,7 @@ export const useUserStore = defineStore('app-user', {
goHome?: boolean
mode?: ErrorMessageMode
},
): Promise<GetUserInfoModel | null> {
): Promise<UserInfo | null> {
try {
const { goHome = true, mode, ...loginParams } = params
const data = await loginApi(loginParams, mode)
@ -111,26 +109,7 @@ export const useUserStore = defineStore('app-user', {
return Promise.reject(error)
}
},
async smsLogin(
params: SmsLoginParams & {
goHome?: boolean
mode?: ErrorMessageMode
},
): Promise<GetUserInfoModel | null> {
try {
const { goHome = true, mode, ...smsLoginParams } = params
const data = await smsLogin(smsLoginParams, mode)
const { accessToken, refreshToken } = data
// save token
this.setAccessToken(accessToken)
this.setRefreshToken(refreshToken)
return this.afterLoginAction(goHome)
}
catch (error) {
return Promise.reject(error)
}
},
async afterLoginAction(goHome?: boolean): Promise<GetUserInfoModel | null> {
async afterLoginAction(goHome?: boolean): Promise<UserInfo | null> {
if (!this.getAccessToken)
return null
// get user info
@ -157,19 +136,12 @@ export const useUserStore = defineStore('app-user', {
}
return userInfo
},
async getUserInfoAction(): Promise<GetUserInfoModel | null> {
async getUserInfoAction(): Promise<UserInfo | null> {
if (!this.getAccessToken)
return null
const userInfo = await getUserInfo()
const { roles = [] } = userInfo
if (isArray(roles)) {
const roleList = roles.map(item => item) as RoleEnum[]
this.setRoleList(roleList)
}
else {
userInfo.roles = []
this.setRoleList([])
}
setTenantId(userInfo.user.tenantId)
this.setUserInfo(userInfo)
return userInfo
},

2
src/types/axios.d.ts vendored

@ -38,7 +38,7 @@ export interface RetryRequest {
export interface Result<T = any> {
code: number
msg: string
message: string
data: T
}

11
src/types/config.d.ts vendored

@ -1,7 +1,6 @@
import type { MenuModeEnum, MenuTypeEnum, MixSidebarTriggerEnum, TriggerEnum } from '@/enums/menuEnum'
import type {
ContentEnum,
PermissionModeEnum,
RouterTransitionEnum,
SessionTimeoutProcessingEnum,
SettingButtonPositionEnum,
@ -103,8 +102,6 @@ export interface ProjectConfig {
showDarkModeToggle: boolean
// Configure where the button is displayed
settingButtonPosition: SettingButtonPositionEnum
// Permission mode
permissionMode: PermissionModeEnum
// Session timeout processing
sessionTimeoutProcessing: SessionTimeoutProcessingEnum
// Website gray mode, open for possible mourning dates
@ -161,10 +158,6 @@ export interface GlobConfig {
urlPrefix?: string
// Project abbreviation
shortName: string
// 租户开关
tenantEnable: string
// 验证码开关
captchaEnable: string
}
export interface GlobEnvConfig {
// Site title
@ -177,8 +170,4 @@ export interface GlobEnvConfig {
VITE_GLOB_APP_SHORT_NAME: string
// Upload url
VITE_GLOB_UPLOAD_URL?: string
// 租户开关
VITE_GLOB_APP_TENANT_ENABLE: string
// 验证码开关
VITE_GLOB_APP_CAPTCHA_ENABLE: string
}

7
src/types/global.d.ts vendored

@ -1,6 +1,5 @@
import type { ComponentPublicInstance, ComponentRenderProxy, FunctionalComponent, VNode, VNodeChild, PropType as VuePropType } from 'vue'
import type { AttributifyAttributes } from '@unocss/preset-attributify'
import type { AttributifyAttributes } from 'unocss/preset-attributify'
declare global {
const __APP_INFO__: {
@ -12,9 +11,6 @@ declare global {
}
lastBuildTime: string
}
declare interface Window {
_hmt: [string, string][]
}
interface Document {
mozFullScreenElement?: Element
@ -30,6 +26,7 @@ declare global {
-readonly [P in keyof T]: T[P]
}
declare type BooleanFlag = 0 | 1
declare type Nullable<T> = T | null
declare type NonNullable<T> = T extends null | undefined ? never : T
declare type Recordable<T = any> = Record<string, T>

8
src/types/index.d.ts vendored

@ -9,12 +9,14 @@ declare interface PromiseFn<T = any, R = T> {
declare type RefType<T> = T | null
declare interface PageParam {
pageSize?: number
pageNo?: number
size?: number
current?: number
}
declare interface PageResult<T = any> {
list: T[]
records: T[]
current: number
size: number
total: number
}

2
src/utils/auth/index.ts

@ -26,7 +26,7 @@ export function getTenantId(): string {
return getAuthCache(TENANT_ID_KEY)
}
export function setTenantId(value) {
export function setTenantId(value: string) {
return setAuthCache(TENANT_ID_KEY, value)
}

4
src/utils/env.ts

@ -22,8 +22,6 @@ export function getAppEnvConfig() {
VITE_GLOB_APP_SHORT_NAME,
VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_UPLOAD_URL,
VITE_GLOB_APP_TENANT_ENABLE,
VITE_GLOB_APP_CAPTCHA_ENABLE,
} = ENV
if (!/^[a-zA-Z\_]*$/.test(VITE_GLOB_APP_SHORT_NAME)) {
@ -38,8 +36,6 @@ export function getAppEnvConfig() {
VITE_GLOB_APP_SHORT_NAME,
VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_UPLOAD_URL,
VITE_GLOB_APP_TENANT_ENABLE,
VITE_GLOB_APP_CAPTCHA_ENABLE,
}
}

18
src/utils/http/axios/index.ts

@ -22,7 +22,6 @@ import { AxiosRetry } from '@/utils/http/axios/axiosRetry'
const globSetting = useGlobSetting()
const urlPrefix = globSetting.urlPrefix
const tenantEnable = globSetting.tenantEnable
const { createMessage, createErrorModal, createSuccessModal } = useMessage()
// 请求白名单,无须token的接口
@ -59,11 +58,11 @@ const transform: AxiosTransform = {
throw new Error(t('sys.api.apiRequestFailed'))
}
// 这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
const { code, data: result, msg } = data
const { code, data: result, message } = data
// 这里逻辑可以根据项目进行修改
const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS
if (hasSuccess) {
let successMsg = msg
let successMsg = message
if (isNull(successMsg) || isUndefined(successMsg) || isEmpty(successMsg))
successMsg = t('sys.api.operationSuccess')
@ -89,8 +88,8 @@ const transform: AxiosTransform = {
userStore.logout(true)
break
default:
if (msg)
timeoutMsg = msg
if (message)
timeoutMsg = message
}
// errorMessageMode='modal' 的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误
@ -201,11 +200,10 @@ const transform: AxiosTransform = {
: token
}
// 设置租户
if (tenantEnable && tenantEnable === 'true') {
const tenantId = getTenantId()
if (tenantId)
(config as Recordable).headers['tenant-id'] = tenantId
}
const tenantId = getTenantId()
if (tenantId)
config.headers['tenant-id'] = tenantId
return config
},

13
src/utils/index.ts

@ -173,3 +173,16 @@ export function simpleDebounce(fn, delay = 100) {
}, delay)
}
}
export function toCamelCase(str: string, upperCaseFirst: boolean) {
str = (str || '')
.replace(/[-|\/](.)/g, (group1) => {
return group1.toUpperCase()
})
.replaceAll(/[-|\/]/g, '')
if (upperCaseFirst && str)
str = str.charAt(0).toUpperCase() + str.slice(1)
return str
}

2
src/views/base/lock/LockPage.vue

@ -85,7 +85,7 @@ function handleShowForm(show = false) {
<div :class="`${prefixCls}-entry__header enter-x`">
<img :src="userinfo.user.avatar || headerImg" :class="`${prefixCls}-entry__header-img`">
<p :class="`${prefixCls}-entry__header-name`">
{{ userinfo.user.nickname }}
{{ userinfo.user.realName }}
</p>
</div>
<InputPassword v-model:value="password" :placeholder="t('sys.lock.placeholder')" class="enter-x" />

4
src/views/base/login/Login.vue

@ -3,8 +3,6 @@ import { computed } from 'vue'
import LoginForm from './LoginForm.vue'
import ForgetPasswordForm from './ForgetPasswordForm.vue'
import RegisterForm from './RegisterForm.vue'
import MobileForm from './MobileForm.vue'
import QrCodeForm from './QrCodeForm.vue'
import { AppDarkModeToggle, AppLocalePicker, AppLogo } from '@/components/Application'
import { useGlobSetting } from '@/hooks/setting'
import { useI18n } from '@/hooks/web/useI18n'
@ -59,8 +57,6 @@ const title = computed(() => globSetting?.title ?? '')
<LoginForm />
<ForgetPasswordForm />
<RegisterForm />
<MobileForm />
<QrCodeForm />
</div>
</div>
</div>

107
src/views/base/login/LoginForm.vue

@ -1,25 +1,14 @@
<script lang="ts" setup>
import { computed, reactive, ref, unref } from 'vue'
import { Checkbox, Col, Divider, Form, Input, Row } from 'ant-design-vue'
import { AlipayCircleFilled, GithubFilled, WechatFilled } from '@ant-design/icons-vue'
import { Col, Form, Input, Row } from 'ant-design-vue'
import LoginFormTitle from './LoginFormTitle.vue'
import { LoginStateEnum, useFormRules, useFormValid, useLoginState } from './useLogin'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useUserStore } from '@/store/modules/user'
import { usePermissionStore } from '@/store/modules/permission'
import { useGlobSetting } from '@/hooks/setting'
import { useDesign } from '@/hooks/web/useDesign'
import * as authUtil from '@/utils/auth'
import { Verify } from '@/components/Verifition'
import { getTenantIdByName } from '@/api/base/login'
const FormItem = Form.Item
const InputPassword = Input.Password
@ -29,54 +18,21 @@ const { prefixCls } = useDesign('login')
const userStore = useUserStore()
const permissionStore = usePermissionStore()
const { tenantEnable, captchaEnable } = useGlobSetting()
const { setLoginState, getLoginState } = useLoginState()
const { getFormRules } = useFormRules()
const formRef = ref()
const loading = ref(false)
const rememberMe = ref(false)
const verify = ref()
const captchaType = ref('blockPuzzle') // blockPuzzle clickWord
const formData = reactive({
tenantName: '芋道源码',
username: 'admin',
password: 'admin123',
captchaVerification: '',
password: '123456',
})
const { validForm } = useFormValid(formRef)
// onKeyStroke('Enter', handleLogin);
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
//
async function getCode() {
//
if (captchaEnable === 'false') {
await handleLogin({})
}
else {
//
//
verify.value.show()
}
}
// && ID
async function getTenantId() {
if (tenantEnable === 'true') {
const res = await getTenantIdByName(formData.tenantName)
authUtil.setTenantId(res)
}
}
async function handleLogin(params) {
await getTenantId()
async function handleLogin() {
const data = await validForm()
if (!data)
return
@ -85,14 +41,13 @@ async function handleLogin(params) {
const userInfo = await userStore.login({
password: data.password,
username: data.username,
captchaVerification: params.captchaVerification,
mode: 'none', //
})
if (userInfo) {
await permissionStore.changePermissionCode(userInfo.permissions)
permissionStore.changePermissionCode(userInfo.buttons)
notification.success({
message: t('sys.login.loginSuccessTitle'),
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.user.nickname}`,
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.user.realName}`,
duration: 3,
})
}
@ -116,15 +71,6 @@ async function handleLogin(params) {
v-show="getShow" ref="formRef" class="enter-x p-4" :model="formData" :rules="getFormRules"
@keypress.enter="handleLogin"
>
<FormItem name="tenantName" class="enter-x">
<Input
v-if="tenantEnable === 'true'"
v-model:value="formData.tenantName"
size="large"
:placeholder="t('sys.login.tenantName')"
class="fix-auto-fill"
/>
</FormItem>
<FormItem name="username" class="enter-x">
<Input
v-model:value="formData.username" size="large" :placeholder="t('sys.login.userName')"
@ -144,15 +90,13 @@ async function handleLogin(params) {
<Row class="enter-x">
<Col :span="12">
<FormItem>
<!-- No logic, you need to deal with it yourself -->
<Checkbox v-model:checked="rememberMe" size="small">
{{ t('sys.login.rememberMe') }}
</Checkbox>
<a-button type="link" size="small" @click="setLoginState(LoginStateEnum.REGISTER)">
{{ t('sys.login.registerButton') }}
</a-button>
</FormItem>
</Col>
<Col :span="12">
<FormItem :style="{ 'text-align': 'right' }">
<!-- No logic, you need to deal with it yourself -->
<a-button type="link" size="small" @click="setLoginState(LoginStateEnum.RESET_PASSWORD)">
{{ t('sys.login.forgetPassword') }}
</a-button>
@ -161,42 +105,9 @@ async function handleLogin(params) {
</Row>
<FormItem class="enter-x">
<a-button type="primary" size="large" block :loading="loading" @click="getCode">
<a-button type="primary" size="large" block :loading="loading" @click="handleLogin">
{{ t('sys.login.loginButton') }}
</a-button>
<!-- <a-button size="large" class="mt-4 enter-x" block @click="handleRegister">
{{ t('sys.login.registerButton') }}
</a-button> -->
</FormItem>
<Row class="enter-x" :gutter="[16, 16]">
<Col :md="8" :xs="24">
<a-button block @click="setLoginState(LoginStateEnum.MOBILE)">
{{ t('sys.login.mobileSignInFormTitle') }}
</a-button>
</Col>
<Col :md="8" :xs="24">
<a-button block @click="setLoginState(LoginStateEnum.QR_CODE)">
{{ t('sys.login.qrSignInFormTitle') }}
</a-button>
</Col>
<Col :md="8" :xs="24">
<a-button block @click="setLoginState(LoginStateEnum.REGISTER)">
{{ t('sys.login.registerButton') }}
</a-button>
</Col>
</Row>
<Divider class="enter-x">
{{ t('sys.login.otherSignIn') }}
</Divider>
<div class="enter-x flex justify-evenly" :class="`${prefixCls}-sign-in-way`">
<GithubFilled />
<WechatFilled />
<AlipayCircleFilled />
<!-- <GoogleCircleFilled /> -->
<!-- <TwitterCircleFilled /> -->
</div>
</Form>
<Verify ref="verify" mode="pop" :captcha-type="captchaType" :img-size="{ width: '360px', height: '180px' }" @success="handleLogin" />
</template>

2
src/views/base/login/LoginFormTitle.vue

@ -12,8 +12,6 @@ const getFormTitle = computed(() => {
[LoginStateEnum.RESET_PASSWORD]: t('sys.login.forgetFormTitle'),
[LoginStateEnum.LOGIN]: t('sys.login.signInFormTitle'),
[LoginStateEnum.REGISTER]: t('sys.login.signUpFormTitle'),
[LoginStateEnum.MOBILE]: t('sys.login.mobileSignInFormTitle'),
[LoginStateEnum.QR_CODE]: t('sys.login.qrSignInFormTitle'),
}
return titleObj[unref(getLoginState)]
})

155
src/views/base/login/MobileForm.vue

@ -1,155 +0,0 @@
<script lang="ts" setup>
import { computed, reactive, ref, unref } from 'vue'
import { Form, Input } from 'ant-design-vue'
import LoginFormTitle from './LoginFormTitle.vue'
import { LoginStateEnum, useFormRules, useFormValid, useLoginState } from './useLogin'
import { CountdownInput } from '@/components/CountDown'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useUserStore } from '@/store/modules/user'
import { usePermissionStore } from '@/store/modules/permission'
import { useGlobSetting } from '@/hooks/setting'
import { useDesign } from '@/hooks/web/useDesign'
import * as authUtil from '@/utils/auth'
import { Verify } from '@/components/Verifition'
import { getTenantIdByName, sendSmsCode } from '@/api/base/login'
const FormItem = Form.Item
const { t } = useI18n()
const { prefixCls } = useDesign('login')
const { createMessage, notification, createErrorModal } = useMessage()
const { handleBackLogin, getLoginState } = useLoginState()
const { tenantEnable, captchaEnable } = useGlobSetting()
const { getFormRules } = useFormRules()
const userStore = useUserStore()
const permissionStore = usePermissionStore()
const formRef = ref()
const loading = ref(false)
const mobileCodeTimer = ref(0)
const scene = ref(21)
const verify = ref()
const captchaType = ref('blockPuzzle') // blockPuzzle clickWord
const formData = reactive({
tenantName: '芋道源码',
mobile: '',
mobileCode: '',
captchaVerification: '',
})
const { validForm } = useFormValid(formRef)
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.MOBILE)
//
async function getCode() {
//
if (captchaEnable === 'false') {
await handleLogin()
}
else {
//
//
verify.value.show()
}
}
// ID
async function getTenantId() {
if (tenantEnable === 'true') {
const res = await getTenantIdByName(formData.tenantName)
authUtil.setTenantId(res)
}
}
async function handleLogin() {
const data = await validForm()
if (!data)
return
try {
loading.value = true
const userInfo = await userStore.smsLogin({
mobile: data.mobile,
code: data.mobileCode,
mode: 'none', //
})
if (userInfo) {
await permissionStore.changePermissionCode(userInfo.permissions)
notification.success({
message: t('sys.login.loginSuccessTitle'),
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.user.nickname}`,
duration: 3,
})
}
}
catch (error) {
createErrorModal({
title: t('sys.api.errorTip'),
content: (error as unknown as Error).message || t('sys.api.networkExceptionMsg'),
getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body,
})
}
finally {
loading.value = false
}
}
async function getSmsCode() {
await getTenantId()
if (mobileCodeTimer.value > 0)
return
const data = await validForm()
if (!data)
return
const res = await sendSmsCode(formData.mobile, scene.value)
if (res) {
createMessage.success(t('common.successText'))
mobileCodeTimer.value = 60
}
}
</script>
<template>
<div v-if="getShow">
<LoginFormTitle class="enter-x" />
<Form ref="formRef" class="enter-x p-4" :model="formData" :rules="getFormRules">
<FormItem name="tenantName" class="enter-x">
<Input
v-if="tenantEnable === 'true'"
v-model:value="formData.tenantName"
size="large"
:placeholder="t('sys.login.tenantName')"
class="fix-auto-fill"
/>
</FormItem>
<FormItem name="mobile" class="enter-x">
<Input v-model:value="formData.mobile" size="large" :placeholder="t('sys.login.mobile')" class="fix-auto-fill" />
</FormItem>
<FormItem name="mobileCode" class="enter-x">
<CountdownInput
v-model:value="formData.mobileCode"
size="large"
class="fix-auto-fill"
:count="mobileCodeTimer"
:send-code-api="getSmsCode"
:placeholder="t('sys.login.smsCode')"
/>
</FormItem>
<FormItem class="enter-x">
<a-button type="primary" size="large" block :loading="loading" @click="getCode">
{{ t('sys.login.loginButton') }}
</a-button>
<a-button size="large" block class="mt-4" @click="handleBackLogin">
{{ t('sys.login.backSignIn') }}
</a-button>
</FormItem>
</Form>
<Verify ref="verify" mode="pop" :captcha-type="captchaType" :img-size="{ width: '400px', height: '200px' }" @success="handleLogin" />
</div>
</template>

37
src/views/base/login/QrCodeForm.vue

@ -1,37 +0,0 @@
<script lang="ts" setup>
import { computed, unref } from 'vue'
import { Divider, Popover, QRCode } from 'ant-design-vue'
import LoginFormTitle from './LoginFormTitle.vue'
import { LoginStateEnum, useLoginState } from './useLogin'
import { useI18n } from '@/hooks/web/useI18n'
import loginImg from '@/assets/images/logo.png'
// Login QR code
const qrCodeUrl = ''
const { t } = useI18n()
const { handleBackLogin, getLoginState } = useLoginState()
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.QR_CODE)
</script>
<template>
<div v-if="getShow">
<LoginFormTitle class="enter-x" />
<div class="enter-x min-h-64 min-w-64">
<Popover :overlay-inner-style="{ padding: 0 }">
<template #content>
<QRCode :value="qrCodeUrl" class="enter-x flex justify-center xl:justify-start" :width="280" :bordered="false" />
</template>
<img width="100" height="100" :src="loginImg">
</Popover>
<Divider class="enter-x">
{{ t('sys.login.scanSign') }}
</Divider>
<a-button size="large" block class="enter-x mt-4" @click="handleBackLogin">
{{ t('sys.login.backSignIn') }}
</a-button>
</div>
</div>
</template>

199
src/views/base/login/SSOForm.vue

@ -1,199 +0,0 @@
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue'
import { useRoute } from 'vue-router'
import { Checkbox, Col, Form, Row } from 'ant-design-vue'
import { useFormValid, useLoginState } from './useLogin'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useDesign } from '@/hooks/web/useDesign'
import { authorize, getAuthorize } from '@/api/base/login'
const FormItem = Form.Item
const { t } = useI18n()
const { query } = useRoute()
const { notification, createErrorModal } = useMessage()
const { prefixCls } = useDesign('login')
const { handleBackLogin } = useLoginState()
const formRef = ref()
const loading = ref(false)
const loginForm = reactive({
scopes: [] as any[], // scope
})
// URL client_idscope
const params = reactive({
responseType: undefined as any,
clientId: undefined as any,
redirectUri: undefined as any,
state: undefined as any,
scopes: [] as any[], // query
})
//
let client = reactive({
name: '',
logo: '',
})
const { validForm } = useFormValid(formRef)
async function init() {
//
// client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read%20user.write
// client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read
params.responseType = query.response_type as any
params.clientId = query.client_id as any
params.redirectUri = query.redirect_uri as any
params.state = query.state as any
if (query.scope)
params.scopes = (query.scope as any).split(' ')
// scope
if (params.scopes.length > 0) {
const res = await doAuthorize(true, params.scopes, [])
const href = res
if (!href) {
console.log('自动授权未通过!')
return
}
location.href = href
}
//
const res = await getAuthorize(params.clientId)
client = res.client
// scope
let scopes
// 1.1 params.scope scopes
if (params.scopes.length > 0) {
scopes = []
for (const scope of res.scopes) {
if (params.scopes.includes(scope.key))
scopes.push(scope)
}
// 1.2 params.scope 使 scopes
}
else {
scopes = res.scopes
for (const scope of scopes)
params.scopes.push(scope.key)
}
// checkedScopes
for (const scope of scopes) {
if (scope.value)
loginForm.scopes.push(scope.key)
}
}
async function handleAuthorize(approved) {
const data = await validForm()
if (!data)
return
try {
loading.value = true
// checkedScopes + uncheckedScopes
let checkedScopes
let uncheckedScopes
if (approved) {
//
checkedScopes = loginForm.scopes
uncheckedScopes = params.scopes.filter(item => !checkedScopes.includes(item))
}
else {
//
checkedScopes = []
uncheckedScopes = params.scopes
}
//
const res = await doAuthorize(false, checkedScopes, uncheckedScopes)
if (res) {
const href = res
if (!href)
return
location.href = href
notification.success({
message: t('sys.login.loginSuccessTitle'),
description: `${t('sys.login.loginSuccessDesc')}`,
duration: 3,
})
}
}
catch (error) {
createErrorModal({
title: t('sys.api.errorTip'),
content: (error as unknown as Error).message || t('sys.api.networkExceptionMsg'),
getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body,
})
}
finally {
loading.value = false
}
}
async function doAuthorize(autoApprove, checkedScopes, uncheckedScopes) {
return await authorize(
params.responseType,
params.clientId,
params.redirectUri,
params.state,
autoApprove,
checkedScopes,
uncheckedScopes,
)
}
function formatScope(scope) {
// scope 便
// demo "system_oauth2_scope" scope
switch (scope) {
case 'user.read':
return t('sys.login.ssoInfoDesc')
case 'user.write':
return t('sys.login.ssoEditDesc')
default:
return scope
}
}
onMounted(() => {
init()
})
</script>
<template>
<h2 class="enter-x mb-3 text-center text-2xl font-bold xl:text-left xl:text-3xl">
{{ client.name + t('sys.login.ssoSignInFormTitle') }}
</h2>
<Form ref="formRef" class="enter-x p-4" :model="loginForm" @keypress.enter="handleAuthorize(true)">
此第三方应用请求获取以下权限
<Row class="enter-x">
<Col :span="12">
<template v-for="scope in params.scopes" :key="scope">
<FormItem>
<!-- No logic, you need to deal with it yourself -->
<Checkbox :checked="scope" size="small">
<a-button type="link" size="small">
{{ formatScope(scope) }}
</a-button>
</Checkbox>
</FormItem>
</template>
</Col>
</Row>
<FormItem class="enter-x">
<a-button type="primary" size="large" block :loading="loading" @click="handleAuthorize(true)">
{{ t('sys.login.loginButton') }}
</a-button>
<a-button size="large" class="enter-x mt-4" block @click="handleBackLogin">
{{ t('common.cancelText') }}
</a-button>
</FormItem>
</Form>
</template>

16
src/views/base/login/SessionTimeoutLogin.vue

@ -3,32 +3,22 @@ import { onBeforeUnmount, onMounted, ref } from 'vue'
import Login from './Login.vue'
import { useUserStore } from '@/store/modules/user'
import { usePermissionStore } from '@/store/modules/permission'
import { useAppStore } from '@/store/modules/app'
import { PermissionModeEnum } from '@/enums/appEnum'
const userStore = useUserStore()
const permissionStore = usePermissionStore()
const appStore = useAppStore()
const userId = ref<Nullable<number | string>>(0)
function isBackMode() {
return appStore.getProjectConfig.permissionMode === PermissionModeEnum.BACK
}
onMounted(() => {
// UserId
userId.value = userStore.getUserInfo?.user.id
})
onBeforeUnmount(() => {
if (userId.value && userId.value !== userStore.getUserInfo.user.id) {
// 便
if (userId.value && userId.value !== userStore.getUserInfo.user.id)
document.location.reload()
}
else if (isBackMode() && permissionStore.getLastBuildMenuTime === 0) {
// F5
else if (permissionStore.getLastBuildMenuTime === 0)
document.location.reload()
}
})
</script>

200
src/views/base/login/sso.vue

@ -1,200 +0,0 @@
<script lang="ts" setup>
import { computed } from 'vue'
import SSOForm from './SSOForm.vue'
import { AppDarkModeToggle, AppLocalePicker, AppLogo } from '@/components/Application'
import { useGlobSetting } from '@/hooks/setting'
import { useI18n } from '@/hooks/web/useI18n'
import { useDesign } from '@/hooks/web/useDesign'
import { useLocaleStore } from '@/store/modules/locale'
defineProps({
sessionTimeout: {
type: Boolean,
},
})
const globSetting = useGlobSetting()
const { prefixCls } = useDesign('login')
const { t } = useI18n()
const localeStore = useLocaleStore()
const showLocale = localeStore.getShowPicker
const title = computed(() => globSetting?.title ?? '')
</script>
<template>
<div :class="prefixCls" class="relative h-full w-full px-4">
<div class="absolute right-4 top-4 flex items-center">
<AppDarkModeToggle v-if="!sessionTimeout" class="enter-x mr-2" />
<AppLocalePicker v-if="!sessionTimeout && showLocale" class="enter-x text-white xl:text-gray-600" :show-text="false" />
</div>
<span class="-enter-x xl:hidden">
<AppLogo :always-show-title="true" />
</span>
<div class="relative mx-auto h-full py-2 container sm:px-10">
<div class="h-full flex">
<div class="mr-4 hidden min-h-full pl-4 xl:w-6/12 xl:flex xl:flex-col">
<AppLogo class="-enter-x" />
<div class="my-auto">
<img :alt="title" src="@/assets/svg/login-box-bg.svg" class="-enter-x w-1/2 -mt-16">
<div class="-enter-x mt-10 text-white font-medium">
<span class="mt-4 inline-block text-3xl"> {{ t('sys.login.signInTitle') }}</span>
</div>
<div class="-enter-x mt-5 text-white font-normal dark:text-gray-500">
{{ t('sys.login.signInDesc') }}
</div>
</div>
</div>
<div class="h-full w-full flex py-5 xl:my-0 xl:h-auto xl:w-6/12 xl:py-0">
<!-- eslint-disable max-len -->
<div
:class="`${prefixCls}-form`"
class="enter-x relative mx-auto my-auto w-full rounded-md px-5 py-8 shadow-md xl:ml-16 lg:w-2/4 sm:w-3/4 xl:w-auto xl:bg-transparent xl:p-4 sm:px-8 xl:shadow-none"
>
<SSOForm />
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="less">
@prefix-cls: ~'@{namespace}-login';
@logo-prefix-cls: ~'@{namespace}-app-logo';
@countdown-prefix-cls: ~'@{namespace}-countdown-input';
@dark-bg: #293146;
html[data-theme='dark'] {
.@{prefix-cls} {
background-color: @dark-bg;
&::before {
background-image: url('@/assets/svg/login-bg-dark.svg');
}
.ant-input,
.ant-input-password {
background-color: #232a3b;
}
.ant-btn:not(.ant-btn-link, .ant-btn-primary) {
border: 1px solid #4a5569;
}
&-form {
background: transparent !important;
}
.app-iconify {
color: #fff;
}
}
input.fix-auto-fill,
.fix-auto-fill input {
-webkit-text-fill-color: #c9d1d9 !important;
box-shadow: inherit !important;
}
}
.@{prefix-cls} {
min-height: 100%;
overflow: hidden;
@media (max-width: @screen-xl) {
background-color: #293146;
.@{prefix-cls}-form {
background-color: #fff;
}
}
&::before {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
margin-left: -48%;
content: '';
background-image: url('@/assets/svg/login-bg.svg');
background-repeat: no-repeat;
background-position: 100%;
background-size: auto 100%;
@media (max-width: @screen-xl) {
display: none;
}
}
.@{logo-prefix-cls} {
position: absolute;
top: 12px;
height: 30px;
&__title {
font-size: 16px;
color: #fff;
}
img {
width: 32px;
}
}
.container {
.@{logo-prefix-cls} {
display: flex;
width: 60%;
height: 80px;
&__title {
font-size: 24px;
color: #fff;
}
img {
width: 48px;
}
}
}
&-sign-in-way {
.anticon {
font-size: 22px;
color: #888;
cursor: pointer;
}
}
input:not([type='checkbox']) {
min-width: 360px;
@media (max-width: @screen-xl) {
min-width: 320px;
}
@media (max-width: @screen-lg) {
min-width: 260px;
}
@media (max-width: @screen-md) {
min-width: 240px;
}
@media (max-width: @screen-sm) {
min-width: 160px;
}
}
.@{countdown-prefix-cls} input {
min-width: unset;
}
.ant-divider-inner-text {
font-size: 12px;
}
}
</style>

14
src/views/base/login/useLogin.ts

@ -53,8 +53,6 @@ export function useFormRules(formData?: Recordable) {
const getAccountFormRule = computed(() => createRule(t('sys.login.accountPlaceholder')))
const getPasswordFormRule = computed(() => createRule(t('sys.login.passwordPlaceholder')))
const getSmsFormRule = computed(() => createRule(t('sys.login.smsPlaceholder')))
const getMobileFormRule = computed(() => createRule(t('sys.login.mobilePlaceholder')))
const validatePolicy = async (_: RuleObject, value: boolean) => {
return !value ? Promise.reject(t('sys.login.policyPlaceholder')) : Promise.resolve()
@ -75,13 +73,7 @@ export function useFormRules(formData?: Recordable) {
const getFormRules = computed((): { [k: string]: Rule | Rule[] } => {
const accountFormRule = unref(getAccountFormRule)
const passwordFormRule = unref(getPasswordFormRule)
const smsFormRule = unref(getSmsFormRule)
const mobileFormRule = unref(getMobileFormRule)
const mobileRule = {
sms: smsFormRule,
mobile: mobileFormRule,
}
switch (unref(currentState)) {
// register form rules
case LoginStateEnum.REGISTER:
@ -90,20 +82,14 @@ export function useFormRules(formData?: Recordable) {
password: passwordFormRule,
confirmPassword: [{ validator: validateConfirmPassword(formData?.password), trigger: 'change' }],
policy: [{ validator: validatePolicy, trigger: 'change' }],
...mobileRule,
}
// reset password form rules
case LoginStateEnum.RESET_PASSWORD:
return {
account: accountFormRule,
...mobileRule,
}
// mobile form rules
case LoginStateEnum.MOBILE:
return mobileRule
// login form rules
default:
return {

2
src/views/dashboard/workbench/components/WorkbenchHeader.vue

@ -13,7 +13,7 @@ const userinfo = computed(() => userStore.getUserInfo)
<Avatar :src="userinfo.user.avatar || headerImg" :size="72" class="!mx-auto !block" />
<div class="mt-2 flex flex-col justify-center md:ml-6 md:mt-0">
<h1 class="text-md md:text-lg">
早安, {{ userinfo.user.nickname }}, 开始您一天的工作吧
早安, {{ userinfo.user.realName }}, 开始您一天的工作吧
</h1>
<span class="text-secondary"> 今日晴20 - 32 </span>
</div>

59
src/views/system/dept/DeptFormModal.vue

@ -0,0 +1,59 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { formSchema } from './data'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { BasicForm, useForm } from '@/components/Form'
import { BasicModal, useModalInner } from '@/components/Modal'
import { createDept, updateDept } from '@/api/system/dept'
import type { Department } from '@/api/system/dept/types'
defineOptions({ name: 'DeptFormModal' })
const emit = defineEmits(['success', 'register'])
const { t } = useI18n()
const [registerForm, { setFieldsValue, validate }] = useForm({
labelWidth: 120,
baseColProps: { span: 24 },
schemas: formSchema,
showActionButtonGroup: false,
actionColOptions: { span: 23 },
})
const isUpdate = ref(false)
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data: Department) => {
isUpdate.value = true
setFieldsValue({
...data,
parentId: data.parentId === '0' ? undefined : data.parentId,
})
})
async function handleSubmit() {
try {
const values = await validate<Department>()
setModalProps({ confirmLoading: true })
values.parentId = values.parentId ? values.parentId : '0'
await (isUpdate.value ? updateDept(values) : createDept(values))
closeModal()
emit('success')
useMessage().createMessage.success(t('common.saveSuccessText'))
}
finally {
setModalProps({ confirmLoading: false })
}
}
</script>
<template>
<BasicModal
v-bind="$attrs"
:title="isUpdate ? t('action.edit') : t('action.create')"
@register="registerModal"
@ok="handleSubmit"
@cancel="isUpdate = false"
>
<BasicForm @register="registerForm" />
</BasicModal>
</template>

61
src/views/system/dept/DeptModal.vue

@ -1,61 +0,0 @@
<script lang="ts" setup>
import { ref, unref } from 'vue'
import { formSchema } from './dept.data'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { BasicForm, useForm } from '@/components/Form'
import { BasicModal, useModalInner } from '@/components/Modal'
import { createDept, getDept, updateDept } from '@/api/system/dept'
defineOptions({ name: 'DeptModal' })
const emit = defineEmits(['success', 'register'])
const { t } = useI18n()
const { createMessage } = useMessage()
const isUpdate = ref(true)
const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({
labelWidth: 120,
baseColProps: { span: 24 },
schemas: formSchema,
showActionButtonGroup: false,
actionColOptions: { span: 23 },
})
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
resetFields()
setModalProps({ confirmLoading: false })
isUpdate.value = !!data?.isUpdate
if (unref(isUpdate)) {
const res = await getDept(data.record.id)
setFieldsValue({ ...res })
}
})
async function handleSubmit() {
try {
const values = await validate() as any
setModalProps({ confirmLoading: true })
if (unref(isUpdate))
await updateDept(values)
else
await createDept(values)
closeModal()
emit('success')
createMessage.success(t('common.saveSuccessText'))
}
finally {
setModalProps({ confirmLoading: false })
}
}
</script>
<template>
<BasicModal
v-bind="$attrs" :title="isUpdate ? t('action.edit') : t('action.create')" @register="registerModal"
@ok="handleSubmit"
>
<BasicForm @register="registerForm" />
</BasicModal>
</template>

85
src/views/system/dept/data.ts

@ -0,0 +1,85 @@
import { lazyGetDeptList } from '@/api/system/dept'
import { getAllTenants } from '@/api/system/tenant'
import type { BasicColumn, FormSchema } from '@/components/Table'
export const columns: BasicColumn[] = [
{
title: '部门名称',
dataIndex: 'deptName',
width: 260,
align: 'left',
},
{
title: '所属租户',
dataIndex: 'tenantName',
width: 120,
},
]
export const searchFormSchema: FormSchema[] = [
{
label: '部门名称',
field: 'deptName',
component: 'Input',
colProps: { span: 6 },
},
{
label: '所属租户',
field: 'tenantId',
component: 'ApiSelect',
componentProps: {
api: getAllTenants,
valueField: 'tenantId',
labelField: 'tenantName',
},
colProps: { span: 6 },
},
]
export const formSchema: FormSchema[] = [
{
field: 'id',
show: false,
component: 'Input',
},
{
label: '上级部门',
field: 'parentId',
component: 'ApiTreeSelect',
componentProps: {
async api() {
try {
const res = await lazyGetDeptList()
return res.map(item => ({ ...item, isLeaf: !item.hasChildren }))
}
catch {
return []
}
},
async loadData(treeNode) {
try {
const res = await lazyGetDeptList({ parentId: treeNode.id })
return res.map(item => ({ ...item, isLeaf: !item.hasChildren }))
}
catch {
return []
}
},
valueField: 'id',
labelField: 'deptName',
},
},
{
label: '部门名称',
field: 'deptName',
required: true,
component: 'Input',
},
{
label: '显示顺序',
field: 'sort',
required: true,
defaultValue: 0,
component: 'InputNumber',
},
]

146
src/views/system/dept/dept.data.ts

@ -1,146 +0,0 @@
import { listSimpleDept } from '@/api/system/dept'
import { getListSimpleUsers } from '@/api/system/user'
import type { BasicColumn, FormSchema } from '@/components/Table'
import { useRender } from '@/components/Table'
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
let userOptions: any[] = []
async function getUserList() {
const res = await getListSimpleUsers()
userOptions = res
}
await getUserList()
export const columns: BasicColumn[] = [
{
title: '部门名称',
dataIndex: 'name',
width: 260,
align: 'left',
},
{
title: '负责人',
dataIndex: 'leaderUserId',
width: 120,
customRender: ({ text }) => {
if (!text)
return '未设置'
for (const user of userOptions) {
if (text === user.id)
return user.nickname
}
return `未知【${text}`
},
},
{
title: '排序',
dataIndex: 'sort',
width: 60,
},
{
title: '状态',
dataIndex: 'status',
width: 180,
customRender: ({ text }) => {
return useRender.renderDict(text, DICT_TYPE.COMMON_STATUS)
},
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 180,
customRender: ({ text }) => {
return useRender.renderDate(text)
},
},
]
export const searchFormSchema: FormSchema[] = [
{
label: '部门名称',
field: 'name',
component: 'Input',
colProps: { span: 8 },
},
{
label: '状态',
field: 'status',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS),
},
colProps: { span: 8 },
},
]
export const formSchema: FormSchema[] = [
{
label: '编号',
field: 'id',
show: false,
component: 'Input',
},
{
label: '上级部门',
field: 'parentId',
required: true,
component: 'ApiTreeSelect',
componentProps: {
api: () => listSimpleDept(),
parentLabel: '主类目',
handleTree: 'id',
},
},
{
label: '部门名称',
field: 'name',
required: true,
component: 'Input',
},
{
label: '显示顺序',
field: 'sort',
required: true,
defaultValue: 0,
component: 'InputNumber',
},
{
label: '负责人',
field: 'leaderUserId',
component: 'ApiSelect',
componentProps: {
api: () => getListSimpleUsers(),
labelField: 'nickname',
valueField: 'id',
},
},
{
label: '联系电话',
field: 'phone',
required: true,
rules: [
{
pattern: /^(?:(?:\+|00)86)?1(?:3[\d]|4[5-79]|5[0-35-9]|6[5-7]|7[0-8]|8[\d]|9[189])\d{8}$/,
message: '请输入正确的手机号码',
trigger: 'blur',
},
],
component: 'Input',
},
{
label: '邮箱',
field: 'email',
required: true,
component: 'Input',
},
{
label: '部门状态',
field: 'status',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS),
},
},
]

124
src/views/system/dept/index.vue

@ -1,33 +1,48 @@
<script lang="ts" setup>
import { nextTick, onMounted, ref } from 'vue'
import DeptModal from './DeptModal.vue'
import { columns, searchFormSchema } from './dept.data'
import { handleTree } from '@/utils/tree'
import { PlusOutlined } from '@ant-design/icons-vue'
import DeptFormModal from './DeptFormModal.vue'
import { columns, searchFormSchema } from './data'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useModal } from '@/components/Modal'
import { IconEnum } from '@/enums/appEnum'
import { BasicTable, TableAction, useTable } from '@/components/Table'
import { getListSimpleUsers } from '@/api/system/user'
import { deleteDept, getDeptPage } from '@/api/system/dept'
import { deleteDept, lazyGetDeptList } from '@/api/system/dept'
import type { Department } from '@/api/system/dept/types'
defineOptions({ name: 'SystemDept' })
const { t } = useI18n()
const { createMessage } = useMessage()
const [registerModal, { openModal }] = useModal()
const [register, { expandAll, collapseAll, getForm, reload }] = useTable({
title: '部门列表',
api: getList,
const [registerModal, { openModal }] = useModal<Department>()
const [register, { reload }] = useTable({
async api(params) {
const list = await lazyGetDeptList(params)
return list.map(item => ({
...item,
children: item.hasChildren ? [] : undefined,
}))
},
async onExpand(isExpand, data: Department) {
if (!isExpand || data.children.length)
return
try {
data.children = await lazyGetDeptList({ parentId: data.id })
}
catch {}
},
columns,
rowKey: 'id',
formConfig: { labelWidth: 120, schemas: searchFormSchema },
formConfig: {
labelWidth: 80,
schemas: searchFormSchema,
},
bordered: true,
canResize: false,
isTreeTable: true,
pagination: false,
useSearchForm: true,
showTableSetting: true,
showIndexColumn: false,
pagination: false,
actionColumn: {
width: 140,
title: t('common.action'),
@ -36,83 +51,43 @@ const [register, { expandAll, collapseAll, getForm, reload }] = useTable({
},
})
async function getList() {
const res = await getDeptPage(getForm().getFieldsValue() as any)
return handleTree(res, 'id')
}
const users = ref<any[]>([])
async function getUserList() {
const res = await getListSimpleUsers()
users.value = res
}
function handleCreate() {
openModal(true, { isUpdate: false })
}
function handleEdit(record: Recordable) {
openModal(true, { record, isUpdate: true })
}
async function handleDelete(record: Recordable) {
await deleteDept(record.id)
createMessage.success(t('common.delSuccessText'))
reload()
}
function onFetchSuccess() {
nextTick(expandAll)
}
function userNicknameFormat(row) {
if (!row.leaderUserId)
return '未设置'
for (const user of users.value) {
if (row.leaderUserId === user.id)
return user.nickname
async function handleDelete(id: string) {
try {
await deleteDept(id)
useMessage().createMessage.success(t('common.delSuccessText'))
reload()
}
return `未知【${row.leaderUserId}`
catch {}
}
onMounted(async () => {
await getUserList()
})
</script>
<template>
<div>
<BasicTable @register="register" @fetch-success="onFetchSuccess">
<template #toolbar>
<a-button v-auth="['system:dept:create']" type="primary" :pre-icon="IconEnum.ADD" @click="handleCreate">
<BasicTable :api="async () => ([] as Department[])" @register="register">
<template #tableTitle>
<a-button type="primary" @click="openModal(true)">
<PlusOutlined />
{{ t('action.create') }}
</a-button>
<a-button @click="expandAll">
{{ t('component.tree.expandAll') }}
</a-button>
<a-button @click="collapseAll">
{{ t('component.tree.unExpandAll') }}
</a-button>
</template>
<template #leader="{ text }">
<span> {{ userNicknameFormat(text) }} </span>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<TableAction
:actions="[
{ icon: IconEnum.EDIT, label: t('action.edit'), auth: 'system:dept:update', onClick: handleEdit.bind(null, record) },
{
icon: IconEnum.EDIT,
label: t('action.edit'),
onClick: () => openModal(true, record),
},
{
icon: IconEnum.DELETE,
danger: true,
label: t('action.delete'),
auth: 'system:dept:delete',
popConfirm: {
title: t('common.delMessage'),
placement: 'left',
confirm: handleDelete.bind(null, record),
confirm: handleDelete.bind(null, record.id),
},
},
]"
@ -120,6 +95,7 @@ onMounted(async () => {
</template>
</template>
</BasicTable>
<DeptModal @register="registerModal" @success="reload()" />
<DeptFormModal @register="registerModal" @success="reload()" />
</div>
</template>

55
src/views/system/menu/MenuFormModal.vue

@ -0,0 +1,55 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { formSchema } from './data'
import { useMessage } from '@/hooks/web/useMessage'
import { BasicForm, useForm } from '@/components/Form'
import { BasicModal, useModalInner } from '@/components/Modal'
import { createMenu, updateMenu } from '@/api/system/menu'
import type { MenuItem } from '@/api/system/menu/types'
defineOptions({ name: 'SystemMenuFormModal' })
const emit = defineEmits(['success', 'register'])
const [registerForm, { setFieldsValue, validate }] = useForm({
labelWidth: 140,
baseColProps: { span: 24 },
schemas: formSchema,
showActionButtonGroup: false,
})
const isUpdate = ref(true)
const [registerModal, { setModalProps, closeModal }] = useModalInner((data: MenuItem) => {
isUpdate.value = true
setFieldsValue({
...data,
parentId: data.parentId === '0' ? undefined : data.parentId,
})
})
async function handleSubmit() {
try {
const values = await validate<MenuItem>()
setModalProps({ confirmLoading: true })
await (isUpdate.value ? updateMenu(values) : createMenu(values))
emit('success')
closeModal()
useMessage().createMessage.success('保存成功')
}
finally {
setModalProps({ confirmLoading: false })
}
}
</script>
<template>
<BasicModal
v-bind="$attrs"
:title="isUpdate ? '编辑' : '新建'"
@register="registerModal"
@ok="handleSubmit"
@cancel="isUpdate = false"
>
<BasicForm @register="registerForm" @submit="handleSubmit" />
</BasicModal>
</template>

58
src/views/system/menu/MenuModal.vue

@ -1,58 +0,0 @@
<script lang="ts" setup>
import { ref, unref } from 'vue'
import { formSchema } from './menu.data'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { BasicForm, useForm } from '@/components/Form'
import { BasicModal, useModalInner } from '@/components/Modal'
import { createMenu, getMenu, updateMenu } from '@/api/system/menu'
defineOptions({ name: 'SystemMenuModal' })
const emit = defineEmits(['success', 'register'])
const { t } = useI18n()
const { createMessage } = useMessage()
const isUpdate = ref(true)
const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({
labelWidth: 120,
baseColProps: { span: 24 },
schemas: formSchema,
showActionButtonGroup: false,
actionColOptions: { span: 23 },
})
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
resetFields()
setModalProps({ confirmLoading: false })
isUpdate.value = !!data?.isUpdate
if (unref(isUpdate)) {
const res = await getMenu(data.record.id)
setFieldsValue({ ...res })
}
})
async function handleSubmit() {
try {
const values = await validate() as any
setModalProps({ confirmLoading: true })
if (unref(isUpdate))
await updateMenu(values)
else
await createMenu(values)
closeModal()
emit('success')
createMessage.success(t('common.saveSuccessText'))
}
finally {
setModalProps({ confirmLoading: false })
}
}
</script>
<template>
<BasicModal v-bind="$attrs" :title="isUpdate ? t('action.edit') : t('action.create')" @register="registerModal" @ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>

119
src/views/system/menu/menu.data.ts → src/views/system/menu/data.tsx

@ -1,8 +1,7 @@
import { listSimpleMenus } from '@/api/system/menu'
import { Tag } from 'ant-design-vue'
import { getMenuListWithoutButtons } from '@/api/system/menu'
import type { BasicColumn, FormSchema } from '@/components/Table'
import { useRender } from '@/components/Table'
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
import { SystemMenuTypeEnum } from '@/enums/systemEnum'
import { SystemMenuTypeEnum } from '@/api/system/menu/types'
export const columns: BasicColumn[] = [
{
@ -15,8 +14,21 @@ export const columns: BasicColumn[] = [
title: '菜单类型',
dataIndex: 'type',
width: 80,
customRender: ({ text }) => {
return useRender.renderDict(text, DICT_TYPE.SYSTEM_MENU_TYPE)
customRender: ({ record }) => {
const { type } = record
let name = ''
switch (type) {
case SystemMenuTypeEnum.DIR:
name = '目录'
break
case SystemMenuTypeEnum.MENU:
name = '菜单'
break
default:
name = '按钮'
}
return <Tag>{ name }</Tag>
},
},
{
@ -24,7 +36,7 @@ export const columns: BasicColumn[] = [
dataIndex: 'icon',
width: 60,
customRender: ({ record }) => {
return useRender.renderIcon(record.icon)
return record.icon && <span class={record.icon}></span>
},
},
{
@ -34,21 +46,18 @@ export const columns: BasicColumn[] = [
},
{
title: '权限标识',
dataIndex: 'permission',
dataIndex: 'code',
width: 140,
},
{
title: '组件路径',
dataIndex: 'component',
title: '路由路径',
dataIndex: 'path',
width: 140,
},
{
title: '状态',
dataIndex: 'status',
width: 80,
customRender: ({ text }) => {
return useRender.renderDict(text, DICT_TYPE.COMMON_STATUS)
},
title: '组件路径',
dataIndex: 'component',
width: 140,
},
]
@ -60,19 +69,15 @@ export const searchFormSchema: FormSchema[] = [
colProps: { span: 8 },
},
{
label: '状态',
field: 'status',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS),
},
label: '权限标识',
field: 'code',
component: 'Input',
colProps: { span: 8 },
},
]
export const formSchema: FormSchema[] = [
{
label: '编号',
field: 'id',
show: false,
component: 'Input',
@ -80,22 +85,34 @@ export const formSchema: FormSchema[] = [
{
label: '上级菜单',
field: 'parentId',
required: true,
component: 'ApiTreeSelect',
componentProps: {
api: () => listSimpleMenus(),
parentLabel: '主类目',
handleTree: 'id',
api: getMenuListWithoutButtons,
labelField: 'title',
valueField: 'id',
},
},
{
label: '菜单类型',
field: 'type',
required: true,
defaultValue: '0',
defaultValue: 1,
component: 'RadioButtonGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.SYSTEM_MENU_TYPE),
options: [
{
label: '目录',
value: SystemMenuTypeEnum.DIR,
},
{
label: '菜单',
value: SystemMenuTypeEnum.MENU,
},
{
label: '按钮',
value: SystemMenuTypeEnum.BUTTON,
},
],
},
colProps: { lg: 24, md: 24 },
},
@ -126,59 +143,43 @@ export const formSchema: FormSchema[] = [
helpMessage: '访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头',
ifShow: ({ values }) => values.type !== SystemMenuTypeEnum.BUTTON,
},
{
label: '权限标识',
field: 'permission',
component: 'Input',
helpMessage: 'Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission("system:user:list")`)',
ifShow: ({ values }) => values.type !== SystemMenuTypeEnum.DIR,
},
{
label: '组件路径',
field: 'component',
required: true,
component: 'Input',
helpMessage: '例如:system/user/index',
helpMessage: '例如:system/user/index,不定义时使用 path 字段',
ifShow: ({ values }) => values.type === SystemMenuTypeEnum.MENU,
},
{
label: '组件名称',
field: 'componentName',
required: ({ values }) => values.keepAlive,
component: 'Input',
helpMessage: '例如:SystemUser',
helpMessage: '例如:SystemName,当开启缓存时它是必传的',
ifShow: ({ values }) => values.type === SystemMenuTypeEnum.MENU,
},
{
label: '菜单状态',
field: 'status',
label: '权限标识',
field: 'code',
required: true,
component: 'RadioButtonGroup',
helpMessage: '选择停用时,路由将不会出现在侧边栏,也不能被访问',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS),
},
component: 'Input',
helpMessage: '输入一个不重复的标识',
},
{
label: '显示状态',
label: '是否在菜单栏显示',
field: 'visible',
component: 'Switch',
componentProps: {
checkedChildren: '显示',
unCheckedChildren: '隐藏',
checkedValue: 1,
unCheckedValue: 0,
},
defaultValue: 1,
helpMessage: '选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问',
ifShow: ({ values }) => values.type !== SystemMenuTypeEnum.BUTTON,
},
{
label: '总是显示',
field: 'alwaysShow',
component: 'Switch',
componentProps: {
checkedChildren: '显示',
unCheckedChildren: '隐藏',
},
helpMessage: '选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单',
ifShow: ({ values }) => values.type !== SystemMenuTypeEnum.BUTTON,
},
{
label: '是否缓存',
field: 'keepAlive',
@ -186,8 +187,10 @@ export const formSchema: FormSchema[] = [
componentProps: {
checkedChildren: '缓存',
unCheckedChildren: '不缓存',
checkedValue: 1,
unCheckedValue: 0,
},
helpMessage: '选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段',
helpMessage: '选择缓存时,则会被 `keep-alive` 缓存,同时必须指定组件的 Name 值',
ifShow: ({ values }) => values.type === SystemMenuTypeEnum.MENU,
},
]

106
src/views/system/menu/index.vue

@ -1,59 +1,49 @@
<script lang="ts" setup>
import MenuModal from './MenuModal.vue'
import { columns, searchFormSchema } from './menu.data'
import { handleTree } from '@/utils/tree'
import { useI18n } from '@/hooks/web/useI18n'
import { Space } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
import MenuFormModal from './MenuFormModal.vue'
import { columns, searchFormSchema } from './data'
import { useMessage } from '@/hooks/web/useMessage'
import { useModal } from '@/components/Modal'
import { IconEnum } from '@/enums/appEnum'
import { BasicTable, TableAction, useTable } from '@/components/Table'
import { deleteMenu, getMenuList } from '@/api/system/menu'
import { usePermission } from '@/hooks/web/usePermission'
import type { MenuItem } from '@/api/system/menu/types'
defineOptions({ name: 'SystemMenu' })
const { t } = useI18n()
const { createMessage, createConfirm } = useMessage()
const [registerModal, { openModal }] = useModal()
const [registerModal, { openModal }] = useModal<MenuItem>()
const [register, { expandAll, collapseAll, getForm, reload }] = useTable({
title: '菜单列表',
api: getList,
const [register, { reload }] = useTable<MenuItem>({
columns,
api: getMenuList,
rowKey: 'id',
formConfig: { labelWidth: 120, schemas: searchFormSchema },
isTreeTable: true,
pagination: false,
striped: false,
formConfig: {
labelWidth: 80,
schemas: searchFormSchema,
},
useSearchForm: true,
showTableSetting: true,
striped: false,
bordered: true,
showIndexColumn: false,
canResize: false,
isTreeTable: true,
pagination: false,
actionColumn: {
width: 140,
title: t('common.action'),
title: '操作',
dataIndex: 'action',
fixed: 'right',
},
})
async function getList() {
const res = await getMenuList(getForm().getFieldsValue() as any)
return handleTree(res, 'id')
}
function handleCreate() {
openModal(true, { isUpdate: false })
}
function handleEdit(record: Recordable) {
openModal(true, { record, isUpdate: true })
}
async function handleDelete(record: Recordable) {
await deleteMenu(record.id)
createMessage.success(t('common.delSuccessText'))
reload()
const { createMessage, createConfirm } = useMessage()
async function handleDelete(id: string) {
try {
await deleteMenu(id)
createMessage.success('删除成功')
reload()
}
catch {}
}
function refreshMenu() {
@ -65,7 +55,6 @@ function refreshMenu() {
const { refreshMenu } = usePermission()
await refreshMenu()
createMessage.success('刷新成功')
//
location.reload()
},
})
@ -74,35 +63,39 @@ function refreshMenu() {
<template>
<div>
<BasicTable @register="register">
<template #toolbar>
<a-button v-auth="['system:menu:create']" type="primary" :pre-icon="IconEnum.ADD" @click="handleCreate">
{{ t('action.create') }}
</a-button>
<a-button @click="expandAll">
{{ t('component.tree.expandAll') }}
</a-button>
<a-button @click="collapseAll">
{{ t('component.tree.unExpandAll') }}
</a-button>
<a-button color="warning" pre-icon="ep:refresh" @click="refreshMenu">
刷新菜单缓存
</a-button>
<BasicTable :api="async () => ([] as MenuItem[])" @register="register">
<template #tableTitle>
<Space>
<a-button type="primary" @click="openModal(true)">
<PlusOutlined />
新增
</a-button>
<a-button type="primary" danger @click="refreshMenu">
<template #icon>
<span class="i-ant-design:sync-outlined align-text-top" />
</template>
缓存刷新
</a-button>
</Space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<TableAction
:actions="[
{ icon: IconEnum.EDIT, label: t('action.edit'), auth: 'system:menu:update', onClick: handleEdit.bind(null, record) },
{
icon: IconEnum.DELETE,
icon: 'i-ant-design:edit-outlined',
label: '修改',
onClick: () => openModal(true, record),
},
{
icon: 'i-ant-design:delete-outlined',
label: '删除',
danger: true,
label: t('action.delete'),
auth: 'system:menu:delete',
popConfirm: {
title: t('common.delMessage'),
title: '确定要删除数据吗?',
placement: 'left',
confirm: handleDelete.bind(null, record),
confirm: () => handleDelete(record.id),
},
},
]"
@ -110,6 +103,7 @@ function refreshMenu() {
</template>
</template>
</BasicTable>
<MenuModal @register="registerModal" @success="reload()" />
<MenuFormModal @register="registerModal" @success="reload()" />
</div>
</template>

56
src/views/system/role/RoleFormModal.vue

@ -0,0 +1,56 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { formSchema } from './data'
import { useMessage } from '@/hooks/web/useMessage'
import { BasicForm, useForm } from '@/components/Form'
import { BasicModal, useModalInner } from '@/components/Modal'
import { createRole, updateRole } from '@/api/system/role'
import type { Role } from '@/api/system/role/types'
defineOptions({ name: 'SystemRoleFormModal' })
const emit = defineEmits(['success', 'register'])
const [registerForm, { setFieldsValue, validate }] = useForm({
labelWidth: 120,
baseColProps: { span: 24 },
schemas: formSchema,
showActionButtonGroup: false,
})
const isUpdate = ref(false)
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data: Role) => {
isUpdate.value = true
setFieldsValue({
...data,
parentId: data.parentId === '0' ? undefined : data.parentId,
})
})
async function handleSubmit() {
try {
const values = await validate<Role>()
values.parentId = values.parentId ? values.parentId : '0' // set default value
setModalProps({ confirmLoading: true })
await (isUpdate.value ? updateRole(values) : createRole(values))
closeModal()
emit('success')
useMessage().createMessage.success('保存成功')
}
finally {
setModalProps({ confirmLoading: false })
}
}
</script>
<template>
<BasicModal
v-bind="$attrs"
:title="isUpdate ? '编辑' : '新增'"
@register="registerModal"
@ok="handleSubmit"
@cancel="isUpdate = false"
>
<BasicForm @register="registerForm" />
</BasicModal>
</template>

136
src/views/system/role/RoleMenuModal.vue

@ -1,122 +1,54 @@
<script lang="ts" setup>
import { ref, unref } from 'vue'
import { without } from 'lodash-es'
import { menuScopeFormSchema } from './role.data'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { BasicForm, useForm } from '@/components/Form'
import { ref } from 'vue'
import { useAsyncState } from '@vueuse/core'
import { BasicModal, useModalInner } from '@/components/Modal'
import { getRole } from '@/api/system/role'
import type { CheckKeys, CheckedEvent, TreeItem } from '@/components/Tree'
import { BasicTree } from '@/components/Tree'
import { listSimpleMenus } from '@/api/system/menu'
import { handleTree } from '@/utils/tree'
import { assignRoleMenu, listRoleMenus } from '@/api/system/permission'
defineOptions({ name: 'SystemRoleMenuModal' })
import { assignMenuToRole, getMenuIdsByRole, getMenuTree } from '@/api/system/role'
import { useMessage } from '@/hooks/web/useMessage'
defineOptions({ name: 'RoleMenuModal' })
const emit = defineEmits(['success', 'register'])
const { t } = useI18n()
const { createMessage } = useMessage()
const treeData = ref<TreeItem[]>([])
const menuKeys = ref<number[]>([])
const menuHalfKeys = ref<number[]>([])
//
const defaultExpandLevel = ref<number>(1)
// list
const parentIdSets = ref<Set<number>>(new Set())
const treeRef = ref()
const checkedIds = ref<string[]>([])
const { state, execute } = useAsyncState(getMenuTree, [], { immediate: false })
const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({
labelWidth: 120,
baseColProps: { span: 24 },
schemas: menuScopeFormSchema,
showActionButtonGroup: false,
actionColOptions: { span: 23 },
})
let roleId: string
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (id: string) => {
try {
if (!state.value.length)
await execute()
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
await resetFields()
menuReset()
setModalProps({ confirmLoading: false })
if (unref(treeData).length === 0) {
const res = await listSimpleMenus()
treeData.value = handleTree(res, 'id')
//
parentIdSets.value = new Set(res.map(item => item.parentId))
checkedIds.value = await getMenuIdsByRole(roleId = id)
}
const role = await getRole(data.record.id)
const menuIds = await listRoleMenus(data.record.id)
// https://www.lodashjs.com/docs/lodash.without
//
//
// ""
const excludeParentIds = without(menuIds, ...Array.from(parentIdSets.value))
// / 使
menuKeys.value = menuIds
// view
role.menuIds = excludeParentIds
// 使
await setFieldsValue({ ...role })
//
if (unref(treeRef))
unref(treeRef).filterByLevel(defaultExpandLevel.value)
catch {}
})
async function handleSubmit() {
try {
const values = await validate()
setModalProps({ confirmLoading: true })
await assignRoleMenu({
roleId: values.id,
menuIds: [...menuKeys.value, ...menuHalfKeys.value],
})
function handleSubmit() {
setModalProps({ confirmLoading: true })
assignMenuToRole({
roleId,
menuIds: checkedIds.value,
}).then(() => {
closeModal()
emit('success')
createMessage.success(t('common.saveSuccessText'))
}
finally {
checkedIds.value = []
useMessage().createMessage.success('操作成功')
}).finally(() => {
setModalProps({ confirmLoading: false })
}
}
function menuReset() {
menuKeys.value = []
menuHalfKeys.value = []
}
function menuCheck(checkedKeys: CheckKeys, event: CheckedEvent) {
if (Array.isArray(checkedKeys)) {
// ID
menuKeys.value = checkedKeys as number[]
// ID
menuHalfKeys.value = (event.halfCheckedKeys as number[]) || []
}
})
}
</script>
<template>
<BasicModal v-bind="$attrs" title="修改角色菜单权限" @register="registerModal" @ok="handleSubmit">
<BasicForm @register="registerForm">
<template #menuIds="{ model, field }">
<BasicTree
v-if="treeData.length"
ref="treeRef"
v-model:checkedKeys="model[field]"
:tree-data="treeData"
:field-names="{ title: 'name', key: 'id' }"
toolbar
checkable
search
:show-strictly-button="false"
:selectable="false"
title="菜单分配"
@check="menuCheck"
/>
</template>
</BasicForm>
<BasicModal v-bind="$attrs" title="菜单权限配置" width="20%" @register="registerModal" @ok="handleSubmit">
<BasicTree
v-if="state.length"
v-model:value="checkedIds"
checkable
default-expand-all
:tree-data="state"
:selectable="false"
:field-names="{ key: 'id' }"
/>
</BasicModal>
</template>

58
src/views/system/role/RoleModal.vue

@ -1,58 +0,0 @@
<script lang="ts" setup>
import { ref, unref } from 'vue'
import { formSchema } from './role.data'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { BasicForm, useForm } from '@/components/Form'
import { BasicModal, useModalInner } from '@/components/Modal'
import { createRole, getRole, updateRole } from '@/api/system/role'
defineOptions({ name: 'SystemRoleModal' })
const emit = defineEmits(['success', 'register'])
const { t } = useI18n()
const { createMessage } = useMessage()
const isUpdate = ref(true)
const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({
labelWidth: 120,
baseColProps: { span: 24 },
schemas: formSchema,
showActionButtonGroup: false,
actionColOptions: { span: 23 },
})
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
resetFields()
setModalProps({ confirmLoading: false })
isUpdate.value = !!data?.isUpdate
if (unref(isUpdate)) {
const res = await getRole(data.record.id)
setFieldsValue({ ...res })
}
})
async function handleSubmit() {
try {
const values = await validate() as any
setModalProps({ confirmLoading: true })
if (unref(isUpdate))
await updateRole(values)
else
await createRole(values)
closeModal()
emit('success')
createMessage.success(t('common.saveSuccessText'))
}
finally {
setModalProps({ confirmLoading: false })
}
}
</script>
<template>
<BasicModal v-bind="$attrs" :title="isUpdate ? t('action.edit') : t('action.create')" @register="registerModal" @ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>

73
src/views/system/role/RoleScopeModal.vue

@ -1,73 +0,0 @@
<script lang="ts" setup>
import { ref, unref } from 'vue'
import { dataScopeFormSchema } from './role.data'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { BasicForm, useForm } from '@/components/Form'
import { BasicModal, useModalInner } from '@/components/Modal'
import type { TreeItem } from '@/components/Tree'
import { BasicTree } from '@/components/Tree'
import { getRole } from '@/api/system/role'
import { listSimpleDept } from '@/api/system/dept'
import { handleTree } from '@/utils/tree'
import { assignRoleDataScope } from '@/api/system/permission'
defineOptions({ name: 'SystemRoleScopeModal' })
const emit = defineEmits(['success', 'register'])
const { t } = useI18n()
const { createMessage } = useMessage()
const treeData = ref<TreeItem[]>([])
const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({
labelWidth: 120,
baseColProps: { span: 24 },
schemas: dataScopeFormSchema,
showActionButtonGroup: false,
actionColOptions: { span: 23 },
})
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
resetFields()
setModalProps({ confirmLoading: false })
if (unref(treeData).length === 0) {
const res = await listSimpleDept()
treeData.value = handleTree(res, 'id')
}
const res = await getRole(data.record.id)
res.roleId = data.record.id
setFieldsValue({ ...res })
})
async function handleSubmit() {
try {
const values = await validate() as any
setModalProps({ confirmLoading: true })
await assignRoleDataScope(values)
closeModal()
emit('success')
createMessage.success(t('common.saveSuccessText'))
}
finally {
setModalProps({ confirmLoading: false })
}
}
</script>
<template>
<BasicModal v-bind="$attrs" title="编辑角色数据权限" @register="registerModal" @ok="handleSubmit">
<BasicForm @register="registerForm">
<template #dataScopeDeptIds="{ model, field }">
<BasicTree
v-model:value="model[field]"
:tree-data="treeData"
:field-names="{ title: 'name', key: 'id' }"
:check-strictly="false"
checkable
toolbar
title="部门分配"
/>
</template>
</BasicForm>
</BasicModal>
</template>

116
src/views/system/role/data.ts

@ -0,0 +1,116 @@
import type { BasicColumn, FormSchema } from '@/components/Table'
import { getAllTenants } from '@/api/system/tenant'
import { getRoleTree } from '@/api/system/role'
import { getTenantId } from '@/utils/auth'
export const columns: BasicColumn[] = [
{
title: '角色名称',
dataIndex: 'roleName',
width: 150,
},
{
title: '所属租户',
dataIndex: 'tenantName',
width: 150,
},
{
title: '角色别名',
dataIndex: 'roleAlias',
width: 150,
},
{
title: '角色排序',
dataIndex: 'sort',
width: 120,
},
]
export const searchFormSchema: FormSchema[] = [
{
label: '角色名称',
field: 'roleName',
component: 'Input',
colProps: { span: 7 },
},
{
label: '所属租户',
field: 'tenantId',
component: 'ApiSelect',
componentProps: {
api: getAllTenants,
valueField: 'tenantId',
labelField: 'tenantName',
},
colProps: { span: 7 },
},
{
label: '角色别名',
field: 'roleAlias',
component: 'Input',
colProps: { span: 7 },
},
]
export const formSchema: FormSchema[] = [
{
field: 'id',
show: false,
component: 'Input',
},
{
label: '角色名称',
field: 'roleName',
required: true,
component: 'Input',
},
{
label: '角色别名',
field: 'roleAlias',
required: true,
component: 'Input',
},
{
label: '角色排序',
field: 'sort',
required: true,
defaultValue: 0,
component: 'InputNumber',
},
{
label: '上级角色',
field: 'parentId',
component: 'ApiTreeSelect',
componentProps: {
api: () => getRoleTree({ tenantId: getTenantId() }),
valueField: 'id',
labelField: 'title',
},
},
]
export const menuScopeFormSchema: FormSchema[] = [
{
label: '角色编号',
field: 'id',
show: false,
component: 'Input',
},
{
label: '角色名称',
field: 'name',
dynamicDisabled: true,
component: 'Input',
},
{
label: '角色标识',
field: 'code',
dynamicDisabled: true,
component: 'Input',
},
{
label: '菜单权限',
field: 'menuIds',
slot: 'menuIds',
},
]

140
src/views/system/role/index.vue

@ -1,113 +1,101 @@
<script lang="ts" setup>
import RoleModal from './RoleModal.vue'
import { PlusOutlined } from '@ant-design/icons-vue'
import RoleFormModal from './RoleFormModal.vue'
import RoleMenuModal from './RoleMenuModal.vue'
import RoleScopeModal from './RoleScopeModal.vue'
import { columns, searchFormSchema } from './role.data'
import { useI18n } from '@/hooks/web/useI18n'
import { columns, searchFormSchema } from './data'
import { useMessage } from '@/hooks/web/useMessage'
import { useModal } from '@/components/Modal'
import { IconEnum } from '@/enums/appEnum'
import { BasicTable, TableAction, useTable } from '@/components/Table'
import type { RoleExportReqVO } from '@/api/system/role'
import { deleteRole, exportRole, getRolePage } from '@/api/system/role'
import { deleteRole, lazyGetRoleList } from '@/api/system/role'
import type { Role } from '@/api/system/role/types'
defineOptions({ name: 'SystemRole' })
const { t } = useI18n()
const { createConfirm, createMessage } = useMessage()
const [registerModal, { openModal }] = useModal()
const [registerMenuModal, { openModal: openMenuModal }] = useModal()
const [registerScopeModal, { openModal: openScopeModal }] = useModal()
const [registerTable, { getForm, reload }] = useTable({
title: '角色列表',
api: getRolePage,
const { createMessage } = useMessage()
const [registerFormModal, { openModal: openFormModal }] = useModal<Role>()
const [registerMenuModal, { openModal: openMenuModal }] = useModal<string>()
const [registerTable, { reload }] = useTable({
async api(params) {
try {
const list = await lazyGetRoleList(params)
return list.map(item => ({
...item,
children: item.hasChildren ? [] : undefined,
}))
}
catch {
return []
}
},
async onExpand(isExpand, data: Role) {
if (!isExpand || data.children?.length)
return
try {
data.children = await lazyGetRoleList({ parentId: data.id })
}
catch {}
},
columns,
formConfig: { labelWidth: 120, schemas: searchFormSchema },
formConfig: {
labelWidth: 80,
schemas: searchFormSchema,
actionColOptions: { span: 3 },
},
bordered: true,
canResize: false,
useSearchForm: true,
showTableSetting: true,
showIndexColumn: false,
actionColumn: {
width: 140,
title: t('common.action'),
title: '操作',
dataIndex: 'action',
fixed: 'right',
},
})
function handleCreate() {
openModal(true, { isUpdate: false })
}
function handleEdit(record: Recordable) {
openModal(true, { record, isUpdate: true })
}
function handleMenu(record: Recordable) {
openMenuModal(true, { record })
}
function handleDataScope(record: Recordable) {
openScopeModal(true, { record })
}
async function handleExport() {
createConfirm({
title: t('common.exportTitle'),
iconType: 'warning',
content: t('common.exportMessage'),
async onOk() {
await exportRole(getForm().getFieldsValue() as RoleExportReqVO)
createMessage.success(t('common.exportSuccessText'))
},
})
}
async function handleDelete(record: Recordable) {
await deleteRole(record.id)
createMessage.success(t('common.delSuccessText'))
reload()
async function handleDelete(id: string) {
try {
await deleteRole(id)
createMessage.success('删除成功!')
reload()
}
catch {}
}
</script>
<template>
<div>
<BasicTable @register="registerTable">
<template #toolbar>
<a-button v-auth="['system:role:create']" type="primary" :pre-icon="IconEnum.ADD" @click="handleCreate">
{{ t('action.create') }}
</a-button>
<a-button v-auth="['system:role:create']" :pre-icon="IconEnum.EXPORT" @click="handleExport">
{{ t('action.export') }}
<BasicTable :api="async () => ([] as Role[])" @register="registerTable">
<template #tableTitle>
<a-button type="primary" @click="openFormModal(true)">
<PlusOutlined />
新建
</a-button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<TableAction
:actions="[
{ icon: IconEnum.EDIT, label: t('action.edit'), auth: 'system:role:update', onClick: handleEdit.bind(null, record) },
]"
:drop-down-actions="[
{
icon: IconEnum.EDIT,
label: '菜单权限',
auth: 'system:permission:assign-role-menu',
onClick: handleMenu.bind(null, record),
icon: 'i-ant-design:edit-outlined',
label: '编辑',
onClick: () => openFormModal(true, record),
},
{
icon: IconEnum.EDIT,
label: '数据权限',
auth: 'system:permission:assign-role-data-scope',
onClick: handleDataScope.bind(null, record),
icon: 'i-ant-design:appstore-outlined',
label: '菜单权限',
onClick: () => openMenuModal(true, record.id),
},
{
icon: IconEnum.DELETE,
icon: 'i-ant-design:delete-outlined',
label: '删除',
danger: true,
label: t('action.delete'),
auth: 'system:role:delete',
popConfirm: {
title: t('common.delMessage'),
title: '确定要删除数据吗?',
placement: 'left',
confirm: handleDelete.bind(null, record),
confirm: () => handleDelete(record.id),
},
},
]"
@ -115,8 +103,8 @@ async function handleDelete(record: Recordable) {
</template>
</template>
</BasicTable>
<RoleModal @register="registerModal" @success="reload()" />
<RoleFormModal @register="registerFormModal" @success="reload()" />
<RoleMenuModal @register="registerMenuModal" @success="reload()" />
<RoleScopeModal @register="registerScopeModal" @success="reload()" />
</div>
</template>

184
src/views/system/role/role.data.ts

@ -1,184 +0,0 @@
import type { BasicColumn, FormSchema } from '@/components/Table'
import { useRender } from '@/components/Table'
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
import { SystemDataScopeEnum } from '@/enums/systemEnum'
export const columns: BasicColumn[] = [
{
title: '角色编号',
dataIndex: 'id',
width: 120,
},
{
title: '角色名称',
dataIndex: 'name',
width: 150,
},
{
title: '角色标识',
dataIndex: 'code',
width: 150,
},
{
title: '角色类型',
dataIndex: 'type',
width: 150,
customRender: ({ text }) => {
return useRender.renderDict(text, DICT_TYPE.SYSTEM_ROLE_TYPE)
},
},
{
title: '显示顺序',
dataIndex: 'sort',
width: 120,
},
{
title: '状态',
dataIndex: 'status',
width: 180,
customRender: ({ text }) => {
return useRender.renderDict(text, DICT_TYPE.COMMON_STATUS)
},
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 180,
customRender: ({ text }) => {
return useRender.renderDate(text)
},
},
]
export const searchFormSchema: FormSchema[] = [
{
label: '角色名称',
field: 'name',
component: 'Input',
colProps: { span: 8 },
},
{
label: '角色标识',
field: 'code',
component: 'Input',
colProps: { span: 8 },
},
{
label: '状态',
field: 'status',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS),
},
colProps: { span: 8 },
},
{
label: '创建时间',
field: 'createTime',
component: 'RangePicker',
colProps: { span: 8 },
},
]
export const formSchema: FormSchema[] = [
{
label: '编号',
field: 'id',
show: false,
component: 'Input',
},
{
label: '角色名称',
field: 'name',
required: true,
component: 'Input',
},
{
label: '角色标识',
field: 'code',
required: true,
component: 'Input',
},
{
label: '角色顺序',
field: 'sort',
required: true,
defaultValue: 0,
component: 'InputNumber',
},
{
label: '状态',
field: 'status',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS),
},
},
{
label: '备注',
field: 'remark',
component: 'InputTextArea',
},
]
export const menuScopeFormSchema: FormSchema[] = [
{
label: '角色编号',
field: 'id',
show: false,
component: 'Input',
},
{
label: '角色名称',
field: 'name',
dynamicDisabled: true,
component: 'Input',
},
{
label: '角色标识',
field: 'code',
dynamicDisabled: true,
component: 'Input',
},
{
label: '菜单权限',
field: 'menuIds',
slot: 'menuIds',
},
]
export const dataScopeFormSchema: FormSchema[] = [
{
label: '角色编号',
field: 'roleId',
show: false,
component: 'Input',
},
{
label: '角色名称',
field: 'name',
dynamicDisabled: true,
component: 'Input',
},
{
label: '角色标识',
field: 'code',
dynamicDisabled: true,
component: 'Input',
},
{
label: '权限范围',
field: 'dataScope',
required: true,
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE),
},
},
{
label: '数据权限',
field: 'dataScopeDeptIds',
ifShow: ({ values }) => values.dataScope === SystemDataScopeEnum.DEPT_CUSTOM,
slot: 'dataScopeDeptIds',
},
]

52
src/views/system/tenant/TenantFormModal.vue

@ -0,0 +1,52 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { formSchema } from './data'
import { useMessage } from '@/hooks/web/useMessage'
import { BasicForm, useForm } from '@/components/Form'
import { BasicModal, useModalInner } from '@/components/Modal'
import { createTenant, updateTenant } from '@/api/system/tenant'
import type { Tenant } from '@/api/system/tenant/types'
defineOptions({ name: 'TenantFormModal' })
const emit = defineEmits(['success', 'register'])
const [registerForm, { setFieldsValue, validate }] = useForm({
labelWidth: 120,
baseColProps: { span: 24 },
schemas: formSchema,
showActionButtonGroup: false,
})
const isUpdate = ref(false)
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
isUpdate.value = true
setFieldsValue(data)
})
async function handleSubmit() {
try {
const values = await validate<Tenant>()
setModalProps({ confirmLoading: true })
await (isUpdate.value ? updateTenant(values) : createTenant(values))
closeModal()
emit('success')
useMessage().createMessage.success('保存成功')
}
finally {
setModalProps({ confirmLoading: false })
}
}
</script>
<template>
<BasicModal
v-bind="$attrs"
:title="isUpdate ? '编辑' : '新增'"
@register="registerModal"
@ok="handleSubmit"
@cancel="isUpdate = false"
>
<BasicForm @register="registerForm" />
</BasicModal>
</template>

58
src/views/system/tenant/TenantModal.vue

@ -1,58 +0,0 @@
<script lang="ts" setup>
import { ref, unref } from 'vue'
import { formSchema } from './tenant.data'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { BasicForm, useForm } from '@/components/Form'
import { BasicModal, useModalInner } from '@/components/Modal'
import { createTenant, getTenant, updateTenant } from '@/api/system/tenant'
defineOptions({ name: 'SystemTenantModal' })
const emit = defineEmits(['success', 'register'])
const { t } = useI18n()
const { createMessage } = useMessage()
const isUpdate = ref(true)
const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({
labelWidth: 120,
baseColProps: { span: 24 },
schemas: formSchema,
showActionButtonGroup: false,
actionColOptions: { span: 23 },
})
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
resetFields()
setModalProps({ confirmLoading: false })
isUpdate.value = !!data?.isUpdate
if (unref(isUpdate)) {
const res = await getTenant(data.record.id)
setFieldsValue({ ...res })
}
})
async function handleSubmit() {
try {
const values = await validate() as any
setModalProps({ confirmLoading: true })
if (unref(isUpdate))
await updateTenant(values)
else
await createTenant(values)
closeModal()
emit('success')
createMessage.success(t('common.saveSuccessText'))
}
finally {
setModalProps({ confirmLoading: false })
}
}
</script>
<template>
<BasicModal v-bind="$attrs" :title="isUpdate ? t('action.edit') : t('action.create')" @register="registerModal" @ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>

106
src/views/system/tenant/data.ts

@ -0,0 +1,106 @@
import { h } from 'vue'
import { Tag } from 'ant-design-vue'
import type { BasicColumn, FormSchema } from '@/components/Table'
export const columns: BasicColumn[] = [
{
title: '租户 ID',
dataIndex: 'tenantId',
width: 150,
},
{
title: '租户名称',
dataIndex: 'tenantName',
width: 150,
},
{
title: '登录账号',
dataIndex: 'adminAccount',
width: 150,
},
{
title: '联系人名称',
dataIndex: 'contactName',
width: 150,
},
{
title: '联系人手机',
dataIndex: 'contactMobile',
width: 150,
},
{
title: '过期时间',
dataIndex: 'expireTime',
width: 150,
customRender({ value }) {
return h(Tag, { color: value ? 'default' : 'blue' }, () => value || '无限制')
},
},
]
export const searchFormSchema: FormSchema[] = [
{
label: '租户 ID',
field: 'tenantId',
component: 'Input',
colProps: { span: 5 },
},
{
label: '租户名称',
field: 'tenantName',
component: 'Input',
colProps: { span: 5 },
},
{
label: '联系人名称',
field: 'contactName',
component: 'Input',
colProps: { span: 5 },
},
{
label: '联系人手机',
field: 'contactMobile',
component: 'Input',
colProps: { span: 5 },
},
]
export const formSchema: FormSchema[] = [
{
field: 'id',
show: false,
component: 'Input',
},
{
label: '租户名',
field: 'tenantName',
required: true,
component: 'Input',
},
{
label: '登录账号',
field: 'adminAccount',
required: true,
component: 'Input',
rules: [
{ min: 6, max: 30, message: '登陆账号长度为6-30' },
],
// cannot edit
show: ({ values }) => !values.id,
},
{
label: '联系人名称',
field: 'contactName',
required: true,
component: 'Input',
},
{
label: '联系人手机',
field: 'contactMobile',
component: 'InputNumber',
componentProps: {
controls: false,
precision: 0,
},
},
]

92
src/views/system/tenant/index.vue

@ -1,87 +1,72 @@
<script lang="ts" setup>
import TenantModal from './TenantModal.vue'
import { columns, searchFormSchema } from './tenant.data'
import { PlusOutlined } from '@ant-design/icons-vue'
import TenantModal from './TenantFormModal.vue'
import { columns, searchFormSchema } from './data'
import { BasicTable, TableAction, useTable } from '@/components/Table'
import type { TenantExportReqVO } from '@/api/system/tenant'
import { deleteTenant, exportTenant, getTenantPage } from '@/api/system/tenant'
import { deleteTenant, getTenantList } from '@/api/system/tenant'
import type { Tenant } from '@/api/system/tenant/types'
import { useModal } from '@/components/Modal'
import { IconEnum } from '@/enums/appEnum'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
defineOptions({ name: 'SystemTenant' })
const { t } = useI18n()
const { createConfirm, createMessage } = useMessage()
const [registerModal, { openModal }] = useModal()
const [registerTable, { getForm, reload }] = useTable({
title: '租户列表',
api: getTenantPage,
const [registerModal, { openModal }] = useModal<Tenant>()
const [registerTable, { reload }] = useTable({
api: getTenantList,
columns,
formConfig: { labelWidth: 120, schemas: searchFormSchema },
formConfig: {
labelWidth: 100,
schemas: searchFormSchema,
actionColOptions: { span: 4 },
},
bordered: true,
canResize: false,
useSearchForm: true,
showTableSetting: true,
showIndexColumn: false,
actionColumn: {
width: 140,
title: t('common.action'),
title: '操作',
dataIndex: 'action',
fixed: 'right',
},
})
function handleCreate() {
openModal(true, { isUpdate: false })
}
function handleEdit(record: Recordable) {
openModal(true, { record, isUpdate: true })
}
async function handleExport() {
createConfirm({
title: t('common.exportTitle'),
iconType: 'warning',
content: t('common.exportMessage'),
async onOk() {
await exportTenant(getForm().getFieldsValue() as TenantExportReqVO)
createMessage.success(t('common.exportSuccessText'))
},
})
}
async function handleDelete(record: Recordable) {
await deleteTenant(record.id)
createMessage.success(t('common.delSuccessText'))
reload()
async function handleDelete(id: string) {
try {
await deleteTenant(id)
useMessage().createMessage.success('删除成功!')
reload()
}
catch {}
}
</script>
<template>
<div>
<BasicTable @register="registerTable">
<template #toolbar>
<a-button v-auth="['system:tenant:create']" type="primary" :pre-icon="IconEnum.ADD" @click="handleCreate">
{{ t('action.create') }}
</a-button>
<a-button v-auth="['system:tenant:export']" :pre-icon="IconEnum.EXPORT" @click="handleExport">
{{ t('action.export') }}
<BasicTable :api="async () => ([] as Tenant[])" @register="registerTable">
<template #tableTitle>
<a-button type="primary" @click="openModal(true)">
<PlusOutlined />
新建
</a-button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<TableAction
:actions="[
{ icon: IconEnum.EDIT, label: t('action.edit'), auth: 'system:tenant:update', onClick: handleEdit.bind(null, record) },
{
icon: IconEnum.DELETE,
icon: 'i-ant-design:edit-outlined',
label: '编辑',
onClick: () => openModal(true, record),
},
{
icon: 'i-ant-design:delete-outlined',
label: '删除',
danger: true,
label: t('action.delete'),
auth: 'system:tenant:delete',
popConfirm: {
title: t('common.delMessage'),
title: '确定要删除数据吗?',
placement: 'left',
confirm: handleDelete.bind(null, record),
confirm: () => handleDelete(record.id),
},
},
]"
@ -89,6 +74,7 @@ async function handleDelete(record: Recordable) {
</template>
</template>
</BasicTable>
<TenantModal @register="registerModal" @success="reload()" />
</div>
</template>

191
src/views/system/tenant/tenant.data.ts

@ -1,191 +0,0 @@
import { getTenantPackageList } from '@/api/system/tenantPackage'
import type { BasicColumn, FormSchema } from '@/components/Table'
import { useRender } from '@/components/Table'
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
export const columns: BasicColumn[] = [
{
title: '租户编号',
dataIndex: 'id',
width: 100,
},
{
title: '租户名',
dataIndex: 'name',
width: 180,
},
{
title: '租户套餐',
dataIndex: 'packageId',
width: 100,
},
{
title: '联系人',
dataIndex: 'contactName',
width: 120,
},
{
title: '联系手机',
dataIndex: 'contactMobile',
width: 120,
},
{
title: '账号额度',
dataIndex: 'accountCount',
width: 120,
customRender: ({ text }) => {
return useRender.renderTag(text)
},
},
{
title: '过期时间',
dataIndex: 'expireTime',
width: 180,
customRender: ({ text }) => {
return useRender.renderDate(text)
},
},
{
title: '绑定域名',
dataIndex: 'website',
width: 200,
},
{
title: '租户状态',
dataIndex: 'status',
width: 180,
customRender: ({ text }) => {
return useRender.renderDict(text, DICT_TYPE.COMMON_STATUS)
},
},
{
title: '备注',
dataIndex: 'remark',
width: 180,
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 180,
customRender: ({ text }) => {
return useRender.renderDate(text)
},
},
]
export const searchFormSchema: FormSchema[] = [
{
label: '租户名',
field: 'name',
component: 'Input',
colProps: { span: 8 },
},
{
label: '联系人',
field: 'contactName',
component: 'Input',
colProps: { span: 8 },
},
{
label: '联系手机',
field: 'contactMobile',
component: 'Input',
colProps: { span: 8 },
},
{
label: '状态',
field: 'status',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS),
},
colProps: { span: 8 },
},
{
label: '创建时间',
field: 'createTime',
component: 'RangePicker',
colProps: { span: 8 },
},
]
export const formSchema: FormSchema[] = [
{
label: '编号',
field: 'id',
show: false,
component: 'Input',
},
{
label: '租户名',
field: 'name',
required: true,
component: 'Input',
},
{
label: '租户套餐',
field: 'packageId',
required: true,
component: 'ApiSelect',
componentProps: {
api: () => getTenantPackageList(),
labelField: 'name',
valueField: 'id',
},
},
{
label: '联系人',
field: 'contactName',
required: true,
component: 'Input',
},
{
label: '联系手机',
field: 'contactMobile',
component: 'Input',
},
{
label: '用户名称',
field: 'username',
component: 'Input',
ifShow: ({ values }) => !values.id,
},
{
label: '用户密码',
field: 'password',
component: 'InputPassword',
ifShow: ({ values }) => !values.id,
},
{
label: '账号额度',
field: 'accountCount',
required: true,
defaultValue: 0,
component: 'InputNumber',
},
{
label: '过期时间',
field: 'expireTime',
required: true,
component: 'DatePicker',
componentProps: {
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
},
},
{
label: '绑定域名',
field: 'website',
required: true,
component: 'Input',
},
{
label: '租户状态',
field: 'status',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS),
},
},
]

11
src/views/system/tenantPackage/TenantPackageModal.vue

@ -9,7 +9,8 @@ import type { CheckedEvent, CheckedKeys, TreeItem } from '@/components/Tree'
import { BasicTree } from '@/components/Tree'
import { BasicModal, useModalInner } from '@/components/Modal'
import { createTenantPackage, getTenantPackage, updateTenantPackage } from '@/api/system/tenantPackage'
import { listSimpleMenus } from '@/api/system/menu'
import { getMenuListWithoutButtons } from '@/api/system/menu'
import { handleTree } from '@/utils/tree'
defineOptions({ name: 'SystemTenantPackageModal' })
@ -24,7 +25,7 @@ const menuHalfKeys = ref<number[]>([])
//
const defaultExpandLevel = ref<number>(1)
// list
const parentIdSets = ref<Set<number>>(new Set())
const parentIdSets = ref<Set<string>>(new Set())
const treeRef = ref()
const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({
@ -40,10 +41,10 @@ const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data
menuReset()
setModalProps({ confirmLoading: false })
if (unref(treeData).length === 0) {
const res = await listSimpleMenus()
const res = await getMenuListWithoutButtons()
treeData.value = handleTree(res, 'id')
//
parentIdSets.value = new Set<number>(res.map(item => item.parentId))
parentIdSets.value = new Set<string>(res.map(item => item.parentId))
}
isUpdate.value = !!data?.isUpdate
@ -52,7 +53,7 @@ const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data
//
//
// ""
const excludeParentIds: number[] = without(res.menuIds, ...Array.from(parentIdSets.value))
const excludeParentIds: string[] = without(res.menuIds, ...Array.from(parentIdSets.value))
// checkedKeys
menuKeys.value = res.menuIds
//

64
src/views/system/user/DeptTree.vue

@ -1,43 +1,63 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import type { TreeItem } from '@/components/Tree'
import type { EventDataNode } from 'ant-design-vue/es/tree'
import { BasicTree } from '@/components/Tree'
import { listSimpleDept } from '@/api/system/dept'
import { handleTree } from '@/utils/tree'
import { lazyGetDeptList } from '@/api/system/dept'
import type { Department, LazyGetDeptListParams } from '@/api/system/dept/types'
defineOptions({ name: 'SystemDeptTree' })
defineProps<{ dept?: string }>()
const emit = defineEmits(['update:dept'])
const emit = defineEmits(['select'])
const treeRef = ref()
const treeData = ref<TreeItem[]>([])
const departmentList = ref<Department[]>([])
async function fetch() {
const res = await listSimpleDept()
treeData.value = handleTree(res, 'id')
async function requestDeptList(params?: LazyGetDeptListParams) {
return lazyGetDeptList(params)
.then((res) => {
return res.map((item) => {
return {
...item,
isLeaf: !item.hasChildren,
}
})
})
}
function handleSelect(keys) {
emit('select', keys[0])
const basicTreeRef = ref<InstanceType<typeof BasicTree>>()
async function onLoadDeptList(treeNode: EventDataNode) {
try {
return await requestDeptList({ parentId: treeNode.id })
}
catch {
}
}
onMounted(() => {
fetch()
requestDeptList()
.then((res) => {
departmentList.value = res
})
})
function onSelect(value: string[]) {
emit('update:dept', value[0] || '')
}
</script>
<template>
<div class="m-4 mr-0 overflow-hidden" v-bind="$attrs">
<div class="box-border h-full py-12px pl-12px" v-bind="$attrs">
<BasicTree
ref="treeRef"
ref="basicTreeRef"
title="部门列表"
toolbar
search
tree-wrapper-class-name="h-[calc(100%-35px)] overflow-auto"
:click-row-to-expand="false"
:tree-data="treeData"
:field-names="{ key: 'id', title: 'name' }"
@select="handleSelect"
/>
:tree-data="departmentList"
:field-names="{ key: 'id', title: 'deptName' }"
:load-data="onLoadDeptList"
@select="onSelect"
>
<template #icon>
<span class="i-ant-design:deployment-unit-outlined" />
</template>
</BasicTree>
</div>
</template>

36
src/views/system/user/ResetPwdModal.vue → src/views/system/user/UserFormModal.vue

@ -1,41 +1,41 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { userPwdFormSchema } from './user.data'
import { getFormSchema } from './data'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { BasicForm, useForm } from '@/components/Form'
import { BasicModal, useModalInner } from '@/components/Modal'
import { resetUserPwd } from '@/api/system/user'
import { createUser, updateUser } from '@/api/system/user'
import type { SystemUser } from '@/api/system/user/types'
defineOptions({ name: 'SystemResetPwdModal' })
defineOptions({ name: 'UserFormModal' })
const emit = defineEmits(['success', 'register'])
const { t } = useI18n()
const { createMessage } = useMessage()
const userId = ref(0)
const [registerForm, { resetFields, validate }] = useForm({
const isUpdate = ref(false)
const [registerForm, { setFieldsValue, validate }] = useForm({
name: 'user-form',
labelWidth: 120,
baseColProps: { span: 24 },
schemas: userPwdFormSchema,
schemas: getFormSchema(isUpdate),
showActionButtonGroup: false,
actionColOptions: { span: 23 },
})
const [registerModal, { setModalProps, closeModal }] = useModalInner((data) => {
resetFields()
userId.value = data.record.id
setModalProps({ confirmLoading: false })
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data: SystemUser) => {
isUpdate.value = true
setFieldsValue({ ...data })
})
async function handleSubmit() {
try {
const values = await validate()
await resetUserPwd(userId.value, values.newPassword)
const values = await validate<SystemUser>()
setModalProps({ confirmLoading: true })
await (isUpdate.value ? updateUser(values) : createUser(values))
closeModal()
emit('success')
createMessage.success(t('common.saveSuccessText'))
useMessage().createMessage.success(t('common.saveSuccessText'))
}
finally {
setModalProps({ confirmLoading: false })
@ -44,7 +44,13 @@ async function handleSubmit() {
</script>
<template>
<BasicModal v-bind="$attrs" title="重置密码" @register="registerModal" @ok="handleSubmit">
<BasicModal
v-bind="$attrs"
:title="isUpdate ? t('action.edit') : t('action.create')"
@register="registerModal"
@ok="handleSubmit"
@cancel="isUpdate = false"
>
<BasicForm @register="registerForm" />
</BasicModal>
</template>

58
src/views/system/user/UserModal.vue

@ -1,58 +0,0 @@
<script lang="ts" setup>
import { ref, unref } from 'vue'
import { formSchema } from './user.data'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { BasicForm, useForm } from '@/components/Form'
import { BasicModal, useModalInner } from '@/components/Modal'
import { createUser, getUser, updateUser } from '@/api/system/user'
defineOptions({ name: 'SystemUserModal' })
const emit = defineEmits(['success', 'register'])
const { t } = useI18n()
const { createMessage } = useMessage()
const isUpdate = ref(true)
const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({
labelWidth: 120,
baseColProps: { span: 24 },
schemas: formSchema,
showActionButtonGroup: false,
actionColOptions: { span: 23 },
})
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
resetFields()
setModalProps({ confirmLoading: false })
isUpdate.value = !!data?.isUpdate
if (unref(isUpdate)) {
const res = await getUser(data.record.id)
setFieldsValue({ ...res })
}
})
async function handleSubmit() {
try {
const values = await validate() as any
setModalProps({ confirmLoading: true })
if (unref(isUpdate))
await updateUser(values)
else
await createUser(values)
closeModal()
emit('success')
createMessage.success(t('common.saveSuccessText'))
}
finally {
setModalProps({ confirmLoading: false })
}
}
</script>
<template>
<BasicModal v-bind="$attrs" :title="isUpdate ? t('action.edit') : t('action.create')" @register="registerModal" @ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>

52
src/views/system/user/UserRoleModal.vue

@ -1,52 +0,0 @@
<script lang="ts" setup>
import { userRoleFormSchema } from './user.data'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { BasicForm, useForm } from '@/components/Form'
import { BasicModal, useModalInner } from '@/components/Modal'
import { getUser } from '@/api/system/user'
import { assignUserRole, listUserRoles } from '@/api/system/permission'
defineOptions({ name: 'SystemUserRoleModal' })
const emit = defineEmits(['success', 'register'])
const { t } = useI18n()
const { createMessage } = useMessage()
const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({
labelWidth: 120,
baseColProps: { span: 24 },
schemas: userRoleFormSchema,
showActionButtonGroup: false,
actionColOptions: { span: 23 },
})
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
resetFields()
setModalProps({ confirmLoading: false })
const res = await getUser(data.record.id)
const roleIds = await listUserRoles(data.record.id)
res.roleIds = roleIds
setFieldsValue({ ...res })
})
async function handleSubmit() {
try {
const values = await validate()
setModalProps({ confirmLoading: true })
await assignUserRole({ userId: values.id, roleIds: values.roleIds })
closeModal()
emit('success')
createMessage.success(t('common.saveSuccessText'))
}
finally {
setModalProps({ confirmLoading: false })
}
}
</script>
<template>
<BasicModal v-bind="$attrs" title="用户角色权限" @register="registerModal" @ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>

309
src/views/system/user/data.ts

@ -0,0 +1,309 @@
import type { Ref } from 'vue'
import { h } from 'vue'
import { Avatar } from 'ant-design-vue'
import { getRoleTree } from '@/api/system/role'
import type { BasicColumn, FormSchema } from '@/components/Table'
import { getAllTenants } from '@/api/system/tenant'
import { getDeptTree, lazyGetDeptList } from '@/api/system/dept'
export const columns: BasicColumn[] = [
{
title: '登陆账号',
dataIndex: 'account',
width: 180,
},
{
title: '所属租户',
dataIndex: 'tenantName',
width: 100,
},
{
title: '用户姓名',
dataIndex: 'realName',
width: 120,
customRender({ record, value }) {
return h('div', [
h(
Avatar,
{
src: /^https?/.test(record.avatar) ? record.avatar : undefined,
size: 'small',
style: { marginRight: '10px' },
},
() => value?.slice(0, 1),
),
value,
])
},
},
{
title: '手机号码',
dataIndex: 'mobile',
width: 120,
},
{
title: '用户邮箱',
dataIndex: 'email',
width: 120,
},
{
title: '所属角色',
dataIndex: 'roleName',
width: 120,
},
{
title: '所属部门',
dataIndex: 'deptName',
width: 120,
},
]
export const searchFormSchema: FormSchema[] = [
{
label: '账号',
field: 'account',
component: 'Input',
colProps: { span: 6 },
},
{
label: '手机号码',
field: 'mobile',
component: 'InputNumber',
componentProps: {
controls: false,
precision: 0,
},
colProps: { span: 6 },
},
{
label: '所属部门',
field: 'tenantId',
component: 'ApiTreeSelect',
componentProps: {
async api() {
try {
const res = await lazyGetDeptList()
return res.map(item => ({ ...item, isLeaf: !item.hasChildren }))
}
catch {
return []
}
},
async loadData(treeNode) {
try {
const res = await lazyGetDeptList({ parentId: treeNode.id })
return res.map(item => ({ ...item, isLeaf: !item.hasChildren }))
}
catch {
return []
}
},
valueField: 'id',
labelField: 'deptName',
},
colProps: { span: 6 },
},
]
export function getFormSchema(isUpdate: Ref<boolean>): FormSchema[] {
return [
{
field: 'id',
show: false,
component: 'Input',
},
{
field: 'BaseInfo',
label: '基本信息',
component: 'Divider',
componentProps: {
plain: false,
},
},
{
label: '账号',
field: 'account',
required: true,
component: 'Input',
ifShow: () => !isUpdate.value,
},
{
label: '密码',
field: 'password',
required: true,
component: 'InputPassword',
ifShow: () => !isUpdate.value,
},
{
label: '确认密码',
field: 'confirmPassword',
required: true,
component: 'InputPassword',
ifShow: () => !isUpdate.value,
dynamicRules: ({ values }) => {
return [
{
required: true,
validator: (_, value) => {
if (value !== values.password)
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject('两次输入的密码不一致!')
return Promise.resolve()
},
},
]
},
},
{
label: '所属租户',
field: 'tenantId',
required: true,
component: 'ApiSelect',
defaultValue: undefined,
dynamicDisabled: () => isUpdate.value,
componentProps({ formActionType, formModel }) {
return {
api: getAllTenants,
valueField: 'tenantId',
labelField: 'tenantName',
onChange(value?: string) {
function updateRole(treeData: { id: string, title: string }[]) {
if (!isUpdate)
formModel.roleId && formActionType.setFieldsValue({ roleId: undefined })
formActionType.updateSchema({
field: 'roleId',
componentProps: {
treeData,
},
})
}
function updateDept(treeData: { id: string, title: string }[]) {
if (!isUpdate)
formModel.deptId && formActionType.setFieldsValue({ deptId: undefined })
formActionType.updateSchema({
field: 'deptId',
componentProps: {
treeData,
},
})
}
if (!value) {
updateRole([])
updateDept([])
return
}
getRoleTree({ tenantId: value })
.then((res) => {
updateRole(res)
})
getDeptTree({ tenantId: value })
.then((res) => {
updateDept(res)
})
},
}
},
},
{
label: '所属部门',
field: 'deptId',
required: true,
component: 'TreeSelect',
componentProps: {
treeData: [],
fieldNames: {
label: 'title',
value: 'id',
},
},
},
{
label: '所属角色',
field: 'roleId',
required: true,
component: 'TreeSelect',
componentProps: {
treeData: [],
fieldNames: {
label: 'title',
value: 'id',
},
},
},
{
field: 'UserInfo',
label: '用户信息',
component: 'Divider',
componentProps: {
plain: false,
},
},
{
label: '用户姓名',
field: 'realName',
component: 'Input',
required: true,
},
{
label: '用户头像',
field: 'avatar',
component: 'FileUpload',
componentProps: {
maxCount: 1,
fileType: 'image',
},
},
{
label: '用户邮箱',
field: 'email',
component: 'Input',
rules: [
{ type: 'email', message: '邮箱格式不正确' },
],
},
{
label: '手机号码',
field: 'mobile',
component: 'InputNumber',
componentProps: {
controls: false,
precision: 0,
},
rules: [
{ pattern: /^1[3-9]\d{9}$/, message: '手机号码格式不正确' },
],
},
{
label: '用户性别',
field: 'sex',
component: 'Select',
componentProps: {
options: [
{
label: '男',
value: 0,
},
{
label: '女',
value: 1,
},
{
label: '未知',
value: 2,
},
],
},
},
{
label: '备注',
field: 'remark',
component: 'InputTextArea',
},
]
}

124
src/views/system/user/index.vue

@ -1,39 +1,33 @@
<script lang="ts" setup>
import { reactive } from 'vue'
import UserModal from './UserModal.vue'
import UserRoleModal from './UserRoleModal.vue'
import ResetPwdModal from './ResetPwdModal.vue'
import DeptTree from './DeptTree.vue'
import { columns, searchFormSchema } from './user.data'
import { PlusOutlined } from '@ant-design/icons-vue'
import UserFormModal from './UserFormModal.vue'
import { columns, searchFormSchema } from './data'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useModal } from '@/components/Modal'
import { IconEnum } from '@/enums/appEnum'
import { BasicTable, TableAction, useTable } from '@/components/Table'
import type { UserExportReqVO } from '@/api/system/user'
import { deleteUser, exportUser, getUserPage } from '@/api/system/user'
import { deleteUser, getUserList } from '@/api/system/user'
import type { SystemUser } from '@/api/system/user/types'
defineOptions({ name: 'SystemUser' })
const { t } = useI18n()
const { createConfirm, createMessage } = useMessage()
const [registerModal, { openModal }] = useModal()
const [registerRoleModal, { openModal: openRoleModal }] = useModal()
const [registerPwdModal, { openModal: openPwdModal }] = useModal()
const searchInfo = reactive<Recordable>({})
const { createMessage } = useMessage()
const [registerModal, { openModal }] = useModal<SystemUser>()
const [registerTable, { getForm, reload }] = useTable({
title: '账号列表',
api: getUserPage,
const [registerTable, { reload }] = useTable({
api(params) {
return getUserList(params)
},
columns,
formConfig: {
labelWidth: 120,
labelWidth: 80,
schemas: searchFormSchema,
autoSubmitOnEnter: true,
},
useSearchForm: true,
showTableSetting: true,
showIndexColumn: false,
bordered: true,
canResize: false,
actionColumn: {
width: 140,
title: t('common.action'),
@ -42,92 +36,43 @@ const [registerTable, { getForm, reload }] = useTable({
},
})
/** 新增按钮操作 */
function handleCreate() {
openModal(true, { isUpdate: false })
}
/** 导出按钮操作 */
async function handleExport() {
createConfirm({
title: t('common.exportTitle'),
iconType: 'warning',
content: t('common.exportMessage'),
async onOk() {
await exportUser(getForm().getFieldsValue() as UserExportReqVO)
createMessage.success(t('common.exportSuccessText'))
},
})
}
/** 修改按钮操作 */
function handleEdit(record: Recordable) {
openModal(true, { record, isUpdate: true })
}
/** 分配用户角色操作 */
function handleRole(record: Recordable) {
openRoleModal(true, { record })
}
/** 重置密码按钮操作 */
function handleResetPwd(record: Recordable) {
openPwdModal(true, { record })
}
/** 删除按钮操作 */
async function handleDelete(record: Recordable) {
await deleteUser(record.id)
createMessage.success(t('common.delSuccessText'))
reload()
}
/** 点击部门操作 */
function handleSelect(deptId = '') {
searchInfo.deptId = deptId
reload()
async function handleDelete(id: string) {
try {
await deleteUser(id)
createMessage.success(t('common.delSuccessText'))
reload()
}
catch {}
}
</script>
<template>
<div class="flex">
<DeptTree class="w-1/4 xl:w-1/5" @select="handleSelect" />
<BasicTable class="w-3/4 xl:w-4/5" :search-info="searchInfo" @register="registerTable">
<template #toolbar>
<a-button v-auth="['system:user:create']" type="primary" :pre-icon="IconEnum.ADD" @click="handleCreate">
<div>
<BasicTable :api="async () => ([] as SystemUser[])" @register="registerTable">
<template #tableTitle>
<a-button type="primary" @click="openModal(true)">
<PlusOutlined />
{{ t('action.create') }}
</a-button>
<a-button v-auth="['system:user:export']" :pre-icon="IconEnum.EXPORT" @click="handleExport">
{{ t('action.export') }}
</a-button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<TableAction
:actions="[
{ icon: IconEnum.EDIT, label: t('action.edit'), auth: 'system:user:update', onClick: handleEdit.bind(null, record) },
]"
:drop-down-actions="[
{
icon: IconEnum.EDIT,
label: '分配角色',
auth: 'system:permission:assign-user-role',
onClick: handleRole.bind(null, record),
},
{
icon: IconEnum.EDIT,
label: '重置密码',
auth: 'system:user:update-password',
onClick: handleResetPwd.bind(null, record),
icon: 'i-ant-design:edit-outlined',
label: t('action.edit'),
onClick: () => openModal(true, record),
},
{
icon: IconEnum.DELETE,
icon: 'i-ant-design:delete-outlined',
danger: true,
label: t('action.delete'),
auth: 'system:user:delete',
popConfirm: {
title: t('common.delMessage'),
placement: 'left',
confirm: handleDelete.bind(null, record),
confirm: () => handleDelete(record.id),
},
},
]"
@ -135,8 +80,7 @@ function handleSelect(deptId = '') {
</template>
</template>
</BasicTable>
<UserModal @register="registerModal" @success="reload()" />
<UserRoleModal @register="registerRoleModal" @success="reload()" />
<ResetPwdModal @register="registerPwdModal" @success="reload()" />
<UserFormModal @register="registerModal" @success="reload()" />
</div>
</template>

283
src/views/system/user/user.data.ts

@ -1,283 +0,0 @@
/* eslint-disable prefer-promise-reject-errors */
import { h } from 'vue'
import { Switch } from 'ant-design-vue'
import dayjs from 'dayjs'
import { useMessage } from '@/hooks/web/useMessage'
import { listSimpleDept } from '@/api/system/dept'
import { listSimplePosts } from '@/api/system/post'
import type { BasicColumn, FormSchema } from '@/components/Table'
import { useRender } from '@/components/Table'
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
import { updateUserStatus } from '@/api/system/user'
import { listSimpleRoles } from '@/api/system/role'
export const columns: BasicColumn[] = [
{
title: '用户编号',
dataIndex: 'id',
width: 100,
},
{
title: '用户名称',
dataIndex: 'username',
width: 180,
},
{
title: '用户昵称',
dataIndex: 'nickname',
width: 100,
},
{
title: '部门',
dataIndex: 'deptId',
width: 120,
customRender: ({ record }) => {
return useRender.renderTag(record.dept && record.dept.name)
},
},
{
title: '手机号码',
dataIndex: 'mobile',
width: 120,
},
{
title: '状态',
dataIndex: 'status',
width: 180,
// customRender: ({ text }) => {
// return useRender.renderDict(text, DICT_TYPE.COMMON_STATUS)
// }
customRender: ({ record }) => {
if (!Reflect.has(record, 'pendingStatus'))
record.pendingStatus = false
return h(Switch, {
checked: record.status === 0,
checkedChildren: '已启用',
unCheckedChildren: '已禁用',
loading: record.pendingStatus,
onChange(checked: boolean) {
record.pendingStatus = true
const newStatus = checked ? 0 : 1
const { createMessage } = useMessage()
updateUserStatus(record.id, newStatus)
.then(() => {
record.status = newStatus
createMessage.success('已成功修改用户状态')
})
.catch(() => {
createMessage.error('修改用户状态失败')
})
.finally(() => {
record.pendingStatus = false
})
},
})
},
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 180,
customRender: ({ text }) => {
return useRender.renderDate(text)
},
},
]
export const searchFormSchema: FormSchema[] = [
{
label: '用户名称',
field: 'username',
component: 'Input',
colProps: { span: 8 },
},
{
label: '手机号码',
field: 'mobile',
component: 'Input',
colProps: { span: 8 },
},
{
label: '状态',
field: 'status',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS),
},
colProps: { span: 8 },
},
{
label: '创建时间',
field: 'createTime',
component: 'RangePicker',
componentProps: {
showTime: {
defaultValue: [dayjs('00:00:00', 'HH:mm:ss'), dayjs('23:59:59', 'HH:mm:ss')],
},
},
colProps: { span: 8 },
},
]
export const formSchema: FormSchema[] = [
{
label: '编号',
field: 'id',
show: false,
component: 'Input',
},
{
label: '用户昵称',
field: 'nickname',
required: true,
component: 'Input',
},
{
label: '用户头像',
field: 'avatar',
component: 'FileUpload',
componentProps: {
maxCount: 1,
fileType: 'image',
},
},
{
label: '归属部门',
field: 'deptId',
required: true,
component: 'ApiTreeSelect',
componentProps: {
api: () => listSimpleDept(),
handleTree: 'id',
},
},
{
label: '手机号码',
field: 'mobile',
required: true,
defaultValue: 0,
component: 'InputNumber',
},
{
label: '邮箱',
field: 'email',
required: true,
component: 'Input',
},
{
label: '用户名称',
field: 'username',
component: 'Input',
dynamicDisabled: ({ values }) => !!values.id,
},
{
label: '用户密码',
field: 'password',
component: 'InputPassword',
ifShow: ({ values }) => !values.id,
},
{
label: '用户性别',
field: 'sex',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.SYSTEM_USER_SEX),
},
},
{
label: '岗位',
field: 'postIds',
component: 'ApiSelect',
defaultValue: [],
componentProps: {
api: () => listSimplePosts(),
labelField: 'name',
valueField: 'id',
mode: 'tags',
},
},
{
label: '状态',
field: 'status',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS),
},
},
{
label: '备注',
field: 'remark',
component: 'InputTextArea',
},
]
export const userRoleFormSchema: FormSchema[] = [
{
label: '编号',
field: 'id',
show: false,
component: 'Input',
},
{
label: '用户名称',
field: 'username',
component: 'Input',
dynamicDisabled: () => true,
},
{
label: '用户昵称',
field: 'nickname',
component: 'Input',
dynamicDisabled: () => true,
},
{
label: '角色',
field: 'roleIds',
component: 'ApiSelect',
componentProps: {
api: () => listSimpleRoles(),
labelField: 'name',
valueField: 'id',
mode: 'tags',
},
},
]
export const userPwdFormSchema: FormSchema[] = [
{
field: 'newPassword',
label: '新密码',
component: 'StrengthMeter',
componentProps: {
placeholder: '新密码',
},
rules: [
{
required: true,
message: '请输入新密码',
},
],
},
{
field: 'confirmPassword',
label: '确认密码',
component: 'InputPassword',
dynamicRules: ({ values }) => {
return [
{
required: true,
validator: (_, value) => {
if (!value)
return Promise.reject('密码不能为空')
if (value !== values.newPassword)
return Promise.reject('两次输入的密码不一致!')
return Promise.resolve()
},
},
]
},
},
]