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_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,防止本地出现跨域问题 # 如果接口地址匹配到,则会转发到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 # 是否删除Console.log
VITE_DROP_CONSOLE = false 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 VITE_GLOB_UPLOAD_URL = /upload

2
.env.production

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

7
.vscode/settings.json vendored

@ -74,5 +74,10 @@
"vuedraggable", "vuedraggable",
"vueuse" "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/custom-event-name-casing': 'off',
'vue/component-name-in-template-casing': 'off', 'vue/component-name-in-template-casing': 'off',
'vue/require-toggle-inside-transition': 'off', 'vue/require-toggle-inside-transition': 'off',
'ts/no-use-before-define': 'off',
}, },
}, },
unocss.configs.flat, unocss.configs.flat,

16
src/App.vue

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

6
src/api/base/login.ts

@ -1,4 +1,3 @@
import type { TentantNameVO } from './model/loginModel'
import { defHttp } from '@/utils/http/axios' import { defHttp } from '@/utils/http/axios'
import { getRefreshToken } from '@/utils/auth' import { getRefreshToken } from '@/utils/auth'
@ -18,11 +17,6 @@ export function refreshToken() {
return defHttp.post({ url: Api.RefreshToken + refreshToken }) return defHttp.post({ url: Api.RefreshToken + refreshToken })
} }
// 使用租户名,获得租户编号
export function getTenantIdByName(name: string) {
return defHttp.get<TentantNameVO>({ url: Api.GetTenantIdByName + name })
}
// 登出 // 登出
export function loginOut() { export function loginOut() {
return defHttp.delete({ url: Api.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 * @description: Login interface parameters
*/ */
export interface LoginParams { export interface LoginParams {
username: string username: string
password: string password: string
captchaVerification: string
} }
/** /**
@ -31,10 +28,6 @@ export interface LoginResultModel {
* @description: Get user information return value * @description: Get user information return value
*/ */
export interface GetUserInfoModel { export interface GetUserInfoModel {
roles: string[]
permissions: string[]
menus: RouteItem[]
// 用户id
user: userModel 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' import { defHttp } from '@/utils/http/axios'
export interface DeptVO { export function lazyGetDeptList(params?: LazyGetDeptListParams) {
id?: number return defHttp.get<Department[]>({
name: string url: '/system/dept/lazy-list',
parentId: number params,
status: number })
sort: number
leaderUserId: number
phone: string
email: string
createTime: Date
} }
export interface DeptPageReqVO { export function createDept(data: Partial<Department>) {
name?: string return defHttp.post({
status?: number url: '/system/dept/save',
data,
})
} }
// 查询部门(精简)列表 export function updateDept(data: Partial<Department>) {
export function listSimpleDept() { return defHttp.post({
return defHttp.get({ url: '/system/dept/list-all-simple' }) url: '/system/dept/update',
data,
})
} }
// 查询部门列表 export function deleteDept(id: string) {
export function getDeptPage(params: DeptPageReqVO) { return defHttp.post({
return defHttp.get({ url: '/system/dept/list', params }) url: `/system/dept/delete?id=${id}`,
})
} }
// 查询部门详情 export function getDeptTree(params?: { tenantId: string }) {
export function getDept(id: number) { return defHttp.get<{ id: string, title: string }[]>({
return defHttp.get({ url: `/system/dept/get?id=${id}` }) url: '/system/dept/tree',
} params,
})
// 新增部门
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}` })
} }

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

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

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' import { defHttp } from '@/utils/http/axios'
export interface TenantVO { export function getTenantList(params: GetTenantListParams) {
id: number return defHttp.get<PageResult<Tenant>>({
name: string url: '/system/tenant/page',
contactName: string params,
contactMobile: string })
status: number
domain: string
packageId: number
username: string
password: string
expireTime: Date
accountCount: number
createTime: Date
} }
export interface TenantPageReqVO extends PageParam { export function updateTenant(data: Tenant) {
name?: string return defHttp.post({
contactName?: string url: '/system/tenant/update',
contactMobile?: string data,
status?: number })
createTime?: Date[]
} }
export interface TenantExportReqVO { export function createTenant(data: Tenant) {
name?: string return defHttp.post({
contactName?: string url: '/system/tenant/save',
contactMobile?: string data,
status?: number })
createTime?: Date[]
} }
// 查询租户列表 export function deleteTenant(id: string) {
export function getTenantPage(params: TenantPageReqVO) { return defHttp.post({
return defHttp.get({ url: '/system/tenant/page', params }) url: `/system/tenant/remove?id=${id}`,
})
} }
// 查询租户详情 export function getAllTenants() {
export function getTenant(id: number) { return defHttp.get({
return defHttp.get({ url: `/system/tenant/get?id=${id}` }) url: '/system/tenant/select',
} })
// 新增租户
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')
} }

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' import { defHttp } from '@/utils/http/axios'
export interface UserVO { export function getUserList(params: GetUserListParams) {
id: number return defHttp.get<PageResult<SystemUser>>({
username: string url: '/system/user/page',
nickname: string params,
deptId: number })
postIds: string[]
email: string
mobile: string
sex: number
avatar: string
loginIp: string
status: number
remark: string
loginDate: Date
createTime: Date
} }
export interface UserPageReqVO extends PageParam { export function createUser(data: Partial<SystemUser>) {
deptId?: number return defHttp.post({
username?: string url: '/system/user/save',
mobile?: string data,
status?: number })
createTime?: Date[]
} }
export interface UserExportReqVO { export function updateUser(data: Partial<SystemUser>) {
code?: string return defHttp.post({
name?: string url: '/system/user/update',
status?: number data,
createTime?: Date[] })
} }
// 查询用户管理列表 export function deleteUser(id: string) {
export function getUserPage(params: UserPageReqVO) { return defHttp.post({
return defHttp.get({ url: '/system/user/page', params }) url: `/system/user/delete?id=${id}`,
} })
// 查询用户详情
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' })
} }

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; margin: 0 6px 0 2px;
} }
&-with-help {
margin-bottom: 0;
}
&:not(.ant-form-item-with-help) {
margin-bottom: 20px;
}
&.suffix-item { &.suffix-item {
.ant-form-item-children { .ant-form-item-children {
display: flex; 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>>> }, api: { type: Function as PropType<(arg?: any) => Promise<Recordable<any>>> },
params: { type: Object }, params: { type: Object },
immediate: propTypes.bool.def(true), 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(''), resultField: propTypes.string.def(''),
handleTree: propTypes.string.def(''), handleTree: propTypes.string.def(''),
parentId: propTypes.number.def(0), parentId: propTypes.number.def(0),
@ -24,7 +24,7 @@ const props = defineProps({
valueField: propTypes.string.def('id'), valueField: propTypes.string.def('id'),
childrenField: propTypes.string.def('children'), childrenField: propTypes.string.def('children'),
}) })
const emit = defineEmits(['options-change', 'change', 'load-data']) const emit = defineEmits(['options-change', 'change'])
const attrs = useAttrs() const attrs = useAttrs()
const treeData = ref<Recordable<any>[]>([]) const treeData = ref<Recordable<any>[]>([])
const isFirstLoaded = ref<boolean>(false) const isFirstLoaded = ref<boolean>(false)
@ -64,14 +64,15 @@ onMounted(() => {
props.immediate && fetch() props.immediate && fetch()
}) })
function onLoadData(treeNode) { async function onLoadData(treeNode) {
return new Promise((resolve: (value?: unknown) => void) => { if (isArray(treeNode.children) && treeNode.children.length > 0)
if (isArray(treeNode.children) && treeNode.children.length > 0) { return
resolve()
return try {
} treeNode.dataRef.children = await props.loadData!(treeNode)
emit('load-data', { treeData, treeNode, resolve }) treeData.value = [...treeData.value]
}) }
catch {}
} }
async function fetch() { async function fetch() {
@ -114,7 +115,7 @@ async function fetch() {
<TreeSelect <TreeSelect
v-bind="getAttrs" v-bind="getAttrs"
:field-names="fieldNames" :field-names="fieldNames"
:load-data="async ? onLoadData : undefined" :load-data="loadData ? onLoadData : undefined"
@change="handleChange" @change="handleChange"
> >
<template v-for="item in Object.keys($slots)" #[item]="data"> <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 || {} const { componentProps } = schema || {}
let _props = componentProps as any let _props = componentProps as any
if (typeof componentProps === 'function') if (typeof componentProps === 'function')
_props = _props({ formModel: unref(formModel) }) _props = _props({ formModel: unref(formModel), formActionType })
const constructValue = tryConstructArray(key, values) || tryConstructObject(key, values) const constructValue = tryConstructArray(key, values) || tryConstructObject(key, values)
@ -340,6 +340,10 @@ export function useFormEvents({
return handleFormValues(values) return handleFormValues(values)
} }
async function setProps(formProps: Partial<FormProps>): Promise<void> {
await unref(formElRef)?.setProps(formProps)
}
async function validate(nameList?: NamePath[] | false | undefined) { async function validate(nameList?: NamePath[] | false | undefined) {
let _nameList: any let _nameList: any
if (nameList === undefined) 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 { return {
handleSubmit, handleSubmit,
clearValidate, clearValidate,

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

@ -67,6 +67,7 @@ export function useFormValues({
continue continue
const transformDateFunc = unref(getProps).transformDateFunc const transformDateFunc = unref(getProps).transformDateFunc
if (isObject(value)) if (isObject(value))
value = transformDateFunc?.(value) value = transformDateFunc?.(value)
@ -75,11 +76,7 @@ export function useFormValues({
// Remove spaces // Remove spaces
if (isString(value)) { if (isString(value)) {
// remove params from URL
if (value === '') if (value === '')
value = undefined
else
value = value.trim() value = value.trim()
} }
if (!tryDeconstructArray(key, value, res) && !tryDeconstructObject(key, value, res)) { 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'] ApiSelect: CustomComponents['ApiSelect'] & ComponentProps['Select']
TreeSelect: ExtractPropTypes<(typeof import('ant-design-vue/es/tree-select'))['default']> TreeSelect: ExtractPropTypes<(typeof import('ant-design-vue/es/tree-select'))['default']>
ApiTree: CustomComponents['ApiTree'] & ExtractPropTypes<(typeof import('ant-design-vue/es/tree'))['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'] ApiRadioGroup: CustomComponents['ApiRadioGroup'] & ComponentProps['RadioGroup']
RadioButtonGroup: CustomComponents['RadioButtonGroup'] & ComponentProps['RadioGroup'] RadioButtonGroup: CustomComponents['RadioButtonGroup'] & ComponentProps['RadioGroup']
RadioGroup: ExtractPropTypes<(typeof import('ant-design-vue/es/radio'))['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 { .ant-table-wrapper {
padding: 6px; padding: 20px;
background-color: var(--component-background); background-color: var(--component-background);
border-radius: 6px; border-radius: 6px;

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

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

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

@ -122,7 +122,6 @@ export function useColumns(
columns.forEach((item) => { columns.forEach((item) => {
const { customRender, slots } = item const { customRender, slots } = item
handleItem(item, Reflect.has(item, 'ellipsis') ? !!item.ellipsis : !!ellipsis && !customRender && !slots) handleItem(item, Reflect.has(item, 'ellipsis') ? !!item.ellipsis : !!ellipsis && !customRender && !slots)
}) })
return columns 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` * , `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 beforeFetch?: Fn
// 自定义处理接口返回参数 // 自定义处理接口返回参数

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

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

15
src/enums/appEnum.ts

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

2
src/enums/httpEnum.ts

@ -2,7 +2,7 @@
* @description: Request result set * @description: Request result set
*/ */
export enum ResultEnum { export enum ResultEnum {
SUCCESS = 0, SUCCESS = 200,
ERROR = -1, ERROR = -1,
TIMEOUT = 400, TIMEOUT = 400,
UNAUTHORIZED = 401, 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 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' import { isEqual } from 'lodash-es'
@ -37,9 +37,7 @@ export function useRuleFormItem<T extends Recordable>(props: T, key: keyof T = '
return return
innerState.value = value as T[keyof T] 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_APP_SHORT_NAME,
VITE_GLOB_API_URL_PREFIX, VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_UPLOAD_URL, VITE_GLOB_UPLOAD_URL,
VITE_GLOB_APP_TENANT_ENABLE,
VITE_GLOB_APP_CAPTCHA_ENABLE,
} = getAppEnvConfig() } = getAppEnvConfig()
if (!/[a-zA-Z\_]*/.test(VITE_GLOB_APP_SHORT_NAME)) { 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, shortName: VITE_GLOB_APP_SHORT_NAME,
urlPrefix: VITE_GLOB_API_URL_PREFIX, urlPrefix: VITE_GLOB_API_URL_PREFIX,
uploadUrl: VITE_GLOB_UPLOAD_URL, uploadUrl: VITE_GLOB_UPLOAD_URL,
tenantEnable: VITE_GLOB_APP_TENANT_ENABLE,
captchaEnable: VITE_GLOB_APP_CAPTCHA_ENABLE,
} }
return glob return glob
} }

3
src/hooks/setting/useRootSetting.ts

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

50
src/hooks/web/usePermission.ts

@ -1,42 +1,19 @@
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from 'vue-router'
import { intersection } from 'lodash-es' import { intersection } from 'lodash-es'
import { useTabs } from './useTabs' import { useTabs } from './useTabs'
import { useAppStore } from '@/store/modules/app'
import { usePermissionStore } from '@/store/modules/permission' import { usePermissionStore } from '@/store/modules/permission'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { resetRouter, router } from '@/router' 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 type { RoleEnum } from '@/enums/roleEnum'
import { isArray } from '@/utils/is' import { isArray } from '@/utils/is'
import { useMultipleTabStore } from '@/store/modules/multipleTab' import { useMultipleTabStore } from '@/store/modules/multipleTab'
// User permissions related operations // User permissions related operations
export function usePermission() { export function usePermission() {
const userStore = useUserStore() const userStore = useUserStore()
const appStore = useAppStore()
const permissionStore = usePermissionStore() const permissionStore = usePermissionStore()
const { closeAll } = useTabs(router) 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 * Reset and regain authority resource information
* *
@ -64,23 +41,11 @@ export function usePermission() {
if (!value) if (!value)
return def return def
const permMode = appStore.getProjectConfig.permissionMode const allCodeList = permissionStore.getPermCodeList as string[]
if (!isArray(value))
if ([PermissionModeEnum.ROUTE_MAPPING, PermissionModeEnum.ROLE].includes(permMode)) { return allCodeList.includes(value)
if (!isArray(value))
return userStore.getRoleList?.includes(value as RoleEnum)
return (intersection(value, userStore.getRoleList) as RoleEnum[]).length > 0 return (intersection(value, allCodeList)).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
} }
/** /**
@ -88,9 +53,6 @@ export function usePermission() {
* @param roles * @param roles
*/ */
async function changeRole(roles: RoleEnum | RoleEnum[]): Promise<void> { 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)) if (!isArray(roles))
roles = [roles] roles = [roles]
@ -102,8 +64,8 @@ export function usePermission() {
* refresh menu data * refresh menu data
*/ */
function refreshMenu() { 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 userStore = useUserStore()
const lockStore = useLockStore() const lockStore = useLockStore()
const getRealName = computed(() => userStore.getUserInfo?.user.nickname) const getRealName = computed(() => userStore.getUserInfo?.user.realName)
const [register, { closeModal }] = useModalInner() const [register, { closeModal }] = useModalInner()
const [registerForm, { validateFields, resetFields }] = useForm({ 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 userStore = useUserStore()
const getUserInfo = computed(() => { const getUserInfo = computed(() => {
const { nickname = '', avatar } = userStore.getUserInfo.user || {} const { realName = '', avatar } = userStore.getUserInfo.user || {}
return { nickname, avatar: avatar || headerImg } return { realName, avatar: avatar || headerImg }
}) })
const [register, { openModal }] = useModal() const [register, { openModal }] = useModal()
@ -76,7 +76,7 @@ function handleMenuClick(e: MenuInfo) {
</Avatar> </Avatar>
<span :class="`${prefixCls}__info hidden md:block`"> <span :class="`${prefixCls}__info hidden md:block`">
<span :class="`${prefixCls}__name`" class="truncate"> <span :class="`${prefixCls}__name`" class="truncate">
{{ getUserInfo.nickname }} {{ getUserInfo.realName }}
</span> </span>
</span> </span>
</span> </span>

23
src/router/guard/paramMenuGuard.ts

@ -1,9 +1,6 @@
import type { Router } from 'vue-router' import type { Router } from 'vue-router'
import { configureDynamicParamsMenu } from '../helper/menuHelper' import { configureDynamicParamsMenu } from '../helper/menuHelper'
import type { Menu } from '../types' import type { Menu } from '../types'
import { PermissionModeEnum } from '@/enums/appEnum'
import { useAppStoreWithOut } from '@/store/modules/app'
import { usePermissionStoreWithOut } from '@/store/modules/permission' import { usePermissionStoreWithOut } from '@/store/modules/permission'
export function createParamMenuGuard(router: Router) { export function createParamMenuGuard(router: Router) {
@ -21,27 +18,9 @@ export function createParamMenuGuard(router: Router) {
return return
} }
let menus: Menu[] = [] const menus: Menu[] = permissionStore.getBackMenuList
if (isBackMode())
menus = permissionStore.getBackMenuList
else if (isRouteMappingMode())
menus = permissionStore.getFrontMenuList
menus.forEach(item => configureDynamicParamsMenu(item, to.params)) menus.forEach(item => configureDynamicParamsMenu(item, to.params))
next() 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 { usePermissionStoreWithOut } from '@/store/modules/permission'
import { PageEnum } from '@/enums/pageEnum' import { PageEnum } from '@/enums/pageEnum'
import { useDictStoreWithOut } from '@/store/modules/dict'
import { useUserStoreWithOut } from '@/store/modules/user' import { useUserStoreWithOut } from '@/store/modules/user'
import { PAGE_NOT_FOUND_ROUTE } from '@/router/routes/basic' import { PAGE_NOT_FOUND_ROUTE } from '@/router/routes/basic'
@ -17,7 +16,7 @@ const LOGIN_PATH = PageEnum.BASE_LOGIN
const whitePathList: PageEnum[] = [LOGIN_PATH] const whitePathList: PageEnum[] = [LOGIN_PATH]
export function createPermissionGuard(router: Router) { export function createPermissionGuard(router: Router) {
const dictStore = useDictStoreWithOut() // const dictStore = useDictStoreWithOut()
const userStore = useUserStoreWithOut() const userStore = useUserStoreWithOut()
const permissionStore = usePermissionStoreWithOut() const permissionStore = usePermissionStoreWithOut()
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
@ -79,8 +78,8 @@ export function createPermissionGuard(router: Router) {
return return
} }
if (!dictStore.getIsSetDict) // if (!dictStore.getIsSetDict)
await dictStore.setDictMap() // await dictStore.setDictMap()
// get userinfo while last fetch time is empty // get userinfo while last fetch time is empty
if (userStore.getLastUpdateTime === 0) { if (userStore.getLastUpdateTime === 0) {
@ -108,7 +107,6 @@ export function createPermissionGuard(router: Router) {
}) })
router.addRoute(PAGE_NOT_FOUND_ROUTE as unknown as RouteRecordRaw) router.addRoute(PAGE_NOT_FOUND_ROUTE as unknown as RouteRecordRaw)
permissionStore.setDynamicAddedRoute(true) permissionStore.setDynamicAddedRoute(true)
if (to.name === PAGE_NOT_FOUND_ROUTE.name) { 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 type { AppRouteModule, AppRouteRecordRaw } from '@/router/types'
import { warn } from '@/utils/log' import { warn } from '@/utils/log'
import { isHttpUrl } from '@/utils/is' import { isHttpUrl } from '@/utils/is'
import { toCamelCase } from '@/utils'
import type { MenuItem } from '@/api/system/menu/types'
export type LayoutMapKey = 'LAYOUT' export type LayoutMapKey = 'LAYOUT'
const IFRAME = () => import('@/views/base/iframe/FrameBlank.vue') const IFRAME = () => import('@/views/base/iframe/FrameBlank.vue')
@ -44,7 +46,7 @@ function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) {
meta.orderNo = item.sort meta.orderNo = item.sort
meta.ignoreKeepAlive = !item.keepAlive meta.ignoreKeepAlive = !item.keepAlive
item.meta = meta 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) children && asyncImportRoute(children)
}) })
} }
@ -76,11 +78,13 @@ function dynamicImport(dynamicViewsModules: Record<string, () => Promise<Recorda
// Turn background objects into routing objects // Turn background objects into routing objects
// 将背景对象变成路由对象 // 将背景对象变成路由对象
export function transformObjToRoute<T = AppRouteModule>(routeList: AppRouteModule[]): T[] { export function transformObjToRoute(menuList: MenuItem[]): AppRouteModule[] {
routeList.forEach((route) => { const routeList: AppRouteModule[] = []
menuList.forEach((item) => {
const route = { ...item } as unknown as AppRouteModule
if (isHttpUrl(route.path)) if (isHttpUrl(route.path))
route.component = 'IFrame' route.component = 'IFrame'
else if (route.children && route.parentId === 0) else if (route.children && route.parentId === '0')
route.component = 'LAYOUT' route.component = 'LAYOUT'
else if (!route.children) else if (!route.children)
route.component = route.component as string route.component = route.component as string
@ -112,9 +116,10 @@ export function transformObjToRoute<T = AppRouteModule>(routeList: AppRouteModul
else { else {
warn(`请正确配置路由:${route?.name}的component属性`) warn(`请正确配置路由:${route?.name}的component属性`)
} }
routeList.push(route)
route.children && asyncImportRoute(route.children) route.children && asyncImportRoute(route.children)
}) })
return routeList as unknown as T[] return routeList
} }
/** /**
@ -194,16 +199,3 @@ function isMultipleRoute(routeModule: AppRouteModule) {
} }
return flag 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 type { Menu, MenuModule } from '@/router/types'
import { useAppStoreWithOut } from '@/store/modules/app'
import { usePermissionStore } from '@/store/modules/permission' import { usePermissionStore } from '@/store/modules/permission'
import { getAllParentPath, transformMenuModule } from '@/router/helper/menuHelper' 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 }) const modules = import.meta.glob('./modules/**/*.ts', { eager: true })
@ -24,22 +16,6 @@ Object.keys(modules).forEach((key) => {
// ==========Helper=========== // ==========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[] = [] const staticMenus: Menu[] = []
;(() => { ;(() => {
menuModules.sort((a, b) => { menuModules.sort((a, b) => {
@ -62,21 +38,12 @@ function getAsyncMenus() {
return show 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[]> { export async function getMenus(): Promise<Menu[]> {
const menus = await getAsyncMenus() const menus = await getAsyncMenus()
if (isRoleMode()) {
const routes = router.getRoutes()
return filter(menus, basicFilter(routes))
}
return menus return menus
} }
@ -90,10 +57,6 @@ export async function getCurrentParentPath(currentPath: string) {
export async function getShallowMenus(): Promise<Menu[]> { export async function getShallowMenus(): Promise<Menu[]> {
const menus = await getAsyncMenus() const menus = await getAsyncMenus()
const shallowMenuList = menus.map(item => ({ ...item, children: undefined })) const shallowMenuList = menus.map(item => ({ ...item, children: undefined }))
if (isRoleMode()) {
const routes = router.getRoutes()
return shallowMenuList.filter(basicFilter(routes))
}
return shallowMenuList return shallowMenuList
} }
@ -104,36 +67,5 @@ export async function getChildrenMenus(parentPath: string) {
if (!parent || !parent.children || !!parent?.meta?.hideChildrenInMenu) if (!parent || !parent.children || !!parent?.meta?.hideChildrenInMenu)
return [] as Menu[] return [] as Menu[]
if (isRoleMode()) {
const routes = router.getRoutes()
return filter(parent.children, basicFilter(routes))
}
return parent.children 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 = { export const ProfileRoute: AppRouteRecordRaw = {
path: '/profile', path: '/profile',
component: LAYOUT, component: LAYOUT,
@ -75,7 +66,6 @@ export const ProfileRoute: AppRouteRecordRaw = {
// 未经许可的基本路由 // 未经许可的基本路由
export const basicRoutes = [ export const basicRoutes = [
LoginRoute, LoginRoute,
SSORoute,
RootRoute, RootRoute,
ProfileRoute, ProfileRoute,
REDIRECT_ROUTE, REDIRECT_ROUTE,

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

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

2
src/router/types.ts

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

6
src/settings/componentSetting.ts

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

6
src/settings/projectSetting.ts

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

3
src/store/modules/lock.ts

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

122
src/store/modules/permission.ts

@ -1,12 +1,9 @@
/* eslint-disable no-case-declarations */
import { toRaw } from 'vue' import { toRaw } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { useUserStore } from './user' import { useUserStore } from './user'
import { useAppStoreWithOut } from './app'
import { store } from '@/store' import { store } from '@/store'
import type { AppRouteRecordRaw, Menu } from '@/router/types' import type { AppRouteRecordRaw, Menu } from '@/router/types'
import { asyncRoutes } from '@/router/routes'
import dashboard from '@/router/routes/modules/dashboard' import dashboard from '@/router/routes/modules/dashboard'
import { PAGE_NOT_FOUND_ROUTE } from '@/router/routes/basic' import { PAGE_NOT_FOUND_ROUTE } from '@/router/routes/basic'
import { transformRouteToMenu } from '@/router/helper/menuHelper' import { transformRouteToMenu } from '@/router/helper/menuHelper'
@ -14,9 +11,7 @@ import { flatMultiLevelRoutes, transformObjToRoute } from '@/router/helper/route
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage' import { useMessage } from '@/hooks/web/useMessage'
import { filter } from '@/utils/helper/treeHelper' import { filter } from '@/utils/helper/treeHelper'
import projectSetting from '@/settings/projectSetting'
import { PageEnum } from '@/enums/pageEnum' import { PageEnum } from '@/enums/pageEnum'
import { PermissionModeEnum } from '@/enums/appEnum'
interface PermissionState { interface PermissionState {
// Permission code list // Permission code list
@ -31,8 +26,6 @@ interface PermissionState {
// Backstage menu list // Backstage menu list
// 后台菜单列表 // 后台菜单列表
backMenuList: Menu[] backMenuList: Menu[]
// 菜单列表
frontMenuList: Menu[]
} }
export const usePermissionStore = defineStore('app-permission', { export const usePermissionStore = defineStore('app-permission', {
@ -48,9 +41,6 @@ export const usePermissionStore = defineStore('app-permission', {
// Backstage menu list // Backstage menu list
// 后台菜单列表 // 后台菜单列表
backMenuList: [], backMenuList: [],
// menu List
// 菜单列表
frontMenuList: [],
}), }),
getters: { getters: {
getPermCodeList(state): string[] | number[] { getPermCodeList(state): string[] | number[] {
@ -59,9 +49,6 @@ export const usePermissionStore = defineStore('app-permission', {
getBackMenuList(state): Menu[] { getBackMenuList(state): Menu[] {
return state.backMenuList return state.backMenuList
}, },
getFrontMenuList(state): Menu[] {
return state.frontMenuList
},
getLastBuildMenuTime(state): number { getLastBuildMenuTime(state): number {
return state.lastBuildMenuTime return state.lastBuildMenuTime
}, },
@ -79,10 +66,6 @@ export const usePermissionStore = defineStore('app-permission', {
list?.length > 0 && this.setLastBuildMenuTime() list?.length > 0 && this.setLastBuildMenuTime()
}, },
setFrontMenuList(list: Menu[]) {
this.frontMenuList = list
},
setLastBuildMenuTime() { setLastBuildMenuTime() {
this.lastBuildMenuTime = new Date().getTime() this.lastBuildMenuTime = new Date().getTime()
}, },
@ -104,29 +87,15 @@ export const usePermissionStore = defineStore('app-permission', {
async buildRoutesAction(): Promise<AppRouteRecordRaw[]> { async buildRoutesAction(): Promise<AppRouteRecordRaw[]> {
const { t } = useI18n() const { t } = useI18n()
const userStore = useUserStore() const userStore = useUserStore()
const appStore = useAppStoreWithOut()
let routes: AppRouteRecordRaw[] = [] let routes: AppRouteRecordRaw[] = []
const roleList = toRaw(userStore.getRoleList) || []
const userInfo = toRaw(userStore.getUserInfo) || {} 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 routeRemoveIgnoreFilter = (route: AppRouteRecordRaw) => {
const { meta } = route const { meta } = route
// ignoreRoute 为true 则路由仅用于菜单生成,不会在实际的路由表中出现
const { ignoreRoute } = meta || {} const { ignoreRoute } = meta || {}
// arr.filter 返回 true 表示该元素通过测试
return !ignoreRoute return !ignoreRoute
} }
@ -165,81 +134,22 @@ export const usePermissionStore = defineStore('app-permission', {
} }
} }
switch (permissionMode) { const { createMessage } = useMessage()
// 角色权限 createMessage.loading({ content: t('sys.app.menuLoading'), duration: 1 })
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)
})
// 设置菜单列表 // Dynamically introduce components
this.setFrontMenuList(menuList) let routeList = transformObjToRoute(userInfo.menus)
// Convert multi-level routing to level 2 routing // Background routing to menu structure
// 将多级路由转换为 2 级路由 const backMenuList = transformRouteToMenu([dashboard, ...routeList])
routes = flatMultiLevelRoutes(routes) this.setBackMenuList(backMenuList)
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
}
// 从用户中获取权限 // remove meta.ignoreRoute item
if (userInfo) routeList = filter(routeList, routeRemoveIgnoreFilter)
this.setPermCodeList(userInfo.permissions) routeList = routeList.filter(routeRemoveIgnoreFilter)
routeList = flatMultiLevelRoutes(routeList)
routes = [PAGE_NOT_FOUND_ROUTE, dashboard, ...routeList]
patchHomeAffix(routes) patchHomeAffix(routes)
return 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 { usePermissionStore } from '@/store/modules/permission'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage' import { useMessage } from '@/hooks/web/useMessage'
import { getAuthCache, setAuthCache } from '@/utils/auth' import { getAuthCache, setAuthCache, setTenantId } from '@/utils/auth'
import { doLogout, getUserInfo, loginApi, smsLogin } from '@/api/base/user' import { doLogout, getUserInfo, loginApi } from '@/api/base/user'
import type { GetUserInfoModel, LoginParams, SmsLoginParams } from '@/api/base/model/userModel' import type { LoginParams, UserInfo } from '@/api/base/user/types'
import { isArray } from '@/utils/is'
interface UserState { interface UserState {
userInfo: Nullable<GetUserInfoModel> userInfo: Nullable<UserInfo>
accessToken?: string accessToken?: string
refreshToken?: string refreshToken?: string
roleList: RoleEnum[] roleList: RoleEnum[]
@ -42,8 +40,8 @@ export const useUserStore = defineStore('app-user', {
lastUpdateTime: 0, lastUpdateTime: 0,
}), }),
getters: { getters: {
getUserInfo(state): GetUserInfoModel { getUserInfo(state): UserInfo {
return state.userInfo || getAuthCache<GetUserInfoModel>(USER_INFO_KEY) || {} return state.userInfo || getAuthCache<UserInfo>(USER_INFO_KEY) || {}
}, },
getAccessToken(state): string { getAccessToken(state): string {
return state.accessToken || getAuthCache<string>(ACCESS_TOKEN_KEY) return state.accessToken || getAuthCache<string>(ACCESS_TOKEN_KEY)
@ -74,7 +72,7 @@ export const useUserStore = defineStore('app-user', {
this.roleList = roleList this.roleList = roleList
setAuthCache(ROLES_KEY, roleList) setAuthCache(ROLES_KEY, roleList)
}, },
setUserInfo(info: GetUserInfoModel | null) { setUserInfo(info: UserInfo | null) {
this.userInfo = info this.userInfo = info
this.lastUpdateTime = new Date().getTime() this.lastUpdateTime = new Date().getTime()
setAuthCache(USER_INFO_KEY, info) setAuthCache(USER_INFO_KEY, info)
@ -96,7 +94,7 @@ export const useUserStore = defineStore('app-user', {
goHome?: boolean goHome?: boolean
mode?: ErrorMessageMode mode?: ErrorMessageMode
}, },
): Promise<GetUserInfoModel | null> { ): Promise<UserInfo | null> {
try { try {
const { goHome = true, mode, ...loginParams } = params const { goHome = true, mode, ...loginParams } = params
const data = await loginApi(loginParams, mode) const data = await loginApi(loginParams, mode)
@ -111,26 +109,7 @@ export const useUserStore = defineStore('app-user', {
return Promise.reject(error) return Promise.reject(error)
} }
}, },
async smsLogin( async afterLoginAction(goHome?: boolean): Promise<UserInfo | null> {
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> {
if (!this.getAccessToken) if (!this.getAccessToken)
return null return null
// get user info // get user info
@ -157,19 +136,12 @@ export const useUserStore = defineStore('app-user', {
} }
return userInfo return userInfo
}, },
async getUserInfoAction(): Promise<GetUserInfoModel | null> { async getUserInfoAction(): Promise<UserInfo | null> {
if (!this.getAccessToken) if (!this.getAccessToken)
return null return null
const userInfo = await getUserInfo() const userInfo = await getUserInfo()
const { roles = [] } = userInfo setTenantId(userInfo.user.tenantId)
if (isArray(roles)) {
const roleList = roles.map(item => item) as RoleEnum[]
this.setRoleList(roleList)
}
else {
userInfo.roles = []
this.setRoleList([])
}
this.setUserInfo(userInfo) this.setUserInfo(userInfo)
return userInfo return userInfo
}, },

2
src/types/axios.d.ts vendored

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

11
src/types/config.d.ts vendored

@ -1,7 +1,6 @@
import type { MenuModeEnum, MenuTypeEnum, MixSidebarTriggerEnum, TriggerEnum } from '@/enums/menuEnum' import type { MenuModeEnum, MenuTypeEnum, MixSidebarTriggerEnum, TriggerEnum } from '@/enums/menuEnum'
import type { import type {
ContentEnum, ContentEnum,
PermissionModeEnum,
RouterTransitionEnum, RouterTransitionEnum,
SessionTimeoutProcessingEnum, SessionTimeoutProcessingEnum,
SettingButtonPositionEnum, SettingButtonPositionEnum,
@ -103,8 +102,6 @@ export interface ProjectConfig {
showDarkModeToggle: boolean showDarkModeToggle: boolean
// Configure where the button is displayed // Configure where the button is displayed
settingButtonPosition: SettingButtonPositionEnum settingButtonPosition: SettingButtonPositionEnum
// Permission mode
permissionMode: PermissionModeEnum
// Session timeout processing // Session timeout processing
sessionTimeoutProcessing: SessionTimeoutProcessingEnum sessionTimeoutProcessing: SessionTimeoutProcessingEnum
// Website gray mode, open for possible mourning dates // Website gray mode, open for possible mourning dates
@ -161,10 +158,6 @@ export interface GlobConfig {
urlPrefix?: string urlPrefix?: string
// Project abbreviation // Project abbreviation
shortName: string shortName: string
// 租户开关
tenantEnable: string
// 验证码开关
captchaEnable: string
} }
export interface GlobEnvConfig { export interface GlobEnvConfig {
// Site title // Site title
@ -177,8 +170,4 @@ export interface GlobEnvConfig {
VITE_GLOB_APP_SHORT_NAME: string VITE_GLOB_APP_SHORT_NAME: string
// Upload url // Upload url
VITE_GLOB_UPLOAD_URL?: string 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 { 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 { declare global {
const __APP_INFO__: { const __APP_INFO__: {
@ -12,9 +11,6 @@ declare global {
} }
lastBuildTime: string lastBuildTime: string
} }
declare interface Window {
_hmt: [string, string][]
}
interface Document { interface Document {
mozFullScreenElement?: Element mozFullScreenElement?: Element
@ -30,6 +26,7 @@ declare global {
-readonly [P in keyof T]: T[P] -readonly [P in keyof T]: T[P]
} }
declare type BooleanFlag = 0 | 1
declare type Nullable<T> = T | null declare type Nullable<T> = T | null
declare type NonNullable<T> = T extends null | undefined ? never : T declare type NonNullable<T> = T extends null | undefined ? never : T
declare type Recordable<T = any> = Record<string, 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 type RefType<T> = T | null
declare interface PageParam { declare interface PageParam {
pageSize?: number size?: number
pageNo?: number current?: number
} }
declare interface PageResult<T = any> { declare interface PageResult<T = any> {
list: T[] records: T[]
current: number
size: number
total: number total: number
} }

2
src/utils/auth/index.ts

@ -26,7 +26,7 @@ export function getTenantId(): string {
return getAuthCache(TENANT_ID_KEY) return getAuthCache(TENANT_ID_KEY)
} }
export function setTenantId(value) { export function setTenantId(value: string) {
return setAuthCache(TENANT_ID_KEY, value) 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_APP_SHORT_NAME,
VITE_GLOB_API_URL_PREFIX, VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_UPLOAD_URL, VITE_GLOB_UPLOAD_URL,
VITE_GLOB_APP_TENANT_ENABLE,
VITE_GLOB_APP_CAPTCHA_ENABLE,
} = ENV } = ENV
if (!/^[a-zA-Z\_]*$/.test(VITE_GLOB_APP_SHORT_NAME)) { if (!/^[a-zA-Z\_]*$/.test(VITE_GLOB_APP_SHORT_NAME)) {
@ -38,8 +36,6 @@ export function getAppEnvConfig() {
VITE_GLOB_APP_SHORT_NAME, VITE_GLOB_APP_SHORT_NAME,
VITE_GLOB_API_URL_PREFIX, VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_UPLOAD_URL, 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 globSetting = useGlobSetting()
const urlPrefix = globSetting.urlPrefix const urlPrefix = globSetting.urlPrefix
const tenantEnable = globSetting.tenantEnable
const { createMessage, createErrorModal, createSuccessModal } = useMessage() const { createMessage, createErrorModal, createSuccessModal } = useMessage()
// 请求白名单,无须token的接口 // 请求白名单,无须token的接口
@ -59,11 +58,11 @@ const transform: AxiosTransform = {
throw new Error(t('sys.api.apiRequestFailed')) throw new Error(t('sys.api.apiRequestFailed'))
} }
// 这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式 // 这里 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 const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS
if (hasSuccess) { if (hasSuccess) {
let successMsg = msg let successMsg = message
if (isNull(successMsg) || isUndefined(successMsg) || isEmpty(successMsg)) if (isNull(successMsg) || isUndefined(successMsg) || isEmpty(successMsg))
successMsg = t('sys.api.operationSuccess') successMsg = t('sys.api.operationSuccess')
@ -89,8 +88,8 @@ const transform: AxiosTransform = {
userStore.logout(true) userStore.logout(true)
break break
default: default:
if (msg) if (message)
timeoutMsg = msg timeoutMsg = message
} }
// errorMessageMode='modal' 的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误 // errorMessageMode='modal' 的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误
@ -201,11 +200,10 @@ const transform: AxiosTransform = {
: token : token
} }
// 设置租户 // 设置租户
if (tenantEnable && tenantEnable === 'true') { const tenantId = getTenantId()
const tenantId = getTenantId() if (tenantId)
if (tenantId) config.headers['tenant-id'] = tenantId
(config as Recordable).headers['tenant-id'] = tenantId
}
return config return config
}, },

13
src/utils/index.ts

@ -173,3 +173,16 @@ export function simpleDebounce(fn, delay = 100) {
}, delay) }, 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`"> <div :class="`${prefixCls}-entry__header enter-x`">
<img :src="userinfo.user.avatar || headerImg" :class="`${prefixCls}-entry__header-img`"> <img :src="userinfo.user.avatar || headerImg" :class="`${prefixCls}-entry__header-img`">
<p :class="`${prefixCls}-entry__header-name`"> <p :class="`${prefixCls}-entry__header-name`">
{{ userinfo.user.nickname }} {{ userinfo.user.realName }}
</p> </p>
</div> </div>
<InputPassword v-model:value="password" :placeholder="t('sys.lock.placeholder')" class="enter-x" /> <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 LoginForm from './LoginForm.vue'
import ForgetPasswordForm from './ForgetPasswordForm.vue' import ForgetPasswordForm from './ForgetPasswordForm.vue'
import RegisterForm from './RegisterForm.vue' import RegisterForm from './RegisterForm.vue'
import MobileForm from './MobileForm.vue'
import QrCodeForm from './QrCodeForm.vue'
import { AppDarkModeToggle, AppLocalePicker, AppLogo } from '@/components/Application' import { AppDarkModeToggle, AppLocalePicker, AppLogo } from '@/components/Application'
import { useGlobSetting } from '@/hooks/setting' import { useGlobSetting } from '@/hooks/setting'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
@ -59,8 +57,6 @@ const title = computed(() => globSetting?.title ?? '')
<LoginForm /> <LoginForm />
<ForgetPasswordForm /> <ForgetPasswordForm />
<RegisterForm /> <RegisterForm />
<MobileForm />
<QrCodeForm />
</div> </div>
</div> </div>
</div> </div>

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

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

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

@ -12,8 +12,6 @@ const getFormTitle = computed(() => {
[LoginStateEnum.RESET_PASSWORD]: t('sys.login.forgetFormTitle'), [LoginStateEnum.RESET_PASSWORD]: t('sys.login.forgetFormTitle'),
[LoginStateEnum.LOGIN]: t('sys.login.signInFormTitle'), [LoginStateEnum.LOGIN]: t('sys.login.signInFormTitle'),
[LoginStateEnum.REGISTER]: t('sys.login.signUpFormTitle'), [LoginStateEnum.REGISTER]: t('sys.login.signUpFormTitle'),
[LoginStateEnum.MOBILE]: t('sys.login.mobileSignInFormTitle'),
[LoginStateEnum.QR_CODE]: t('sys.login.qrSignInFormTitle'),
} }
return titleObj[unref(getLoginState)] 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 Login from './Login.vue'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { usePermissionStore } from '@/store/modules/permission' import { usePermissionStore } from '@/store/modules/permission'
import { useAppStore } from '@/store/modules/app'
import { PermissionModeEnum } from '@/enums/appEnum'
const userStore = useUserStore() const userStore = useUserStore()
const permissionStore = usePermissionStore() const permissionStore = usePermissionStore()
const appStore = useAppStore()
const userId = ref<Nullable<number | string>>(0) const userId = ref<Nullable<number | string>>(0)
function isBackMode() {
return appStore.getProjectConfig.permissionMode === PermissionModeEnum.BACK
}
onMounted(() => { onMounted(() => {
// UserId // UserId
userId.value = userStore.getUserInfo?.user.id userId.value = userStore.getUserInfo?.user.id
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (userId.value && userId.value !== userStore.getUserInfo.user.id) { if (userId.value && userId.value !== userStore.getUserInfo.user.id)
// 便
document.location.reload() document.location.reload()
}
else if (isBackMode() && permissionStore.getLastBuildMenuTime === 0) { else if (permissionStore.getLastBuildMenuTime === 0)
// F5
document.location.reload() document.location.reload()
}
}) })
</script> </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 getAccountFormRule = computed(() => createRule(t('sys.login.accountPlaceholder')))
const getPasswordFormRule = computed(() => createRule(t('sys.login.passwordPlaceholder'))) 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) => { const validatePolicy = async (_: RuleObject, value: boolean) => {
return !value ? Promise.reject(t('sys.login.policyPlaceholder')) : Promise.resolve() 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 getFormRules = computed((): { [k: string]: Rule | Rule[] } => {
const accountFormRule = unref(getAccountFormRule) const accountFormRule = unref(getAccountFormRule)
const passwordFormRule = unref(getPasswordFormRule) const passwordFormRule = unref(getPasswordFormRule)
const smsFormRule = unref(getSmsFormRule)
const mobileFormRule = unref(getMobileFormRule)
const mobileRule = {
sms: smsFormRule,
mobile: mobileFormRule,
}
switch (unref(currentState)) { switch (unref(currentState)) {
// register form rules // register form rules
case LoginStateEnum.REGISTER: case LoginStateEnum.REGISTER:
@ -90,20 +82,14 @@ export function useFormRules(formData?: Recordable) {
password: passwordFormRule, password: passwordFormRule,
confirmPassword: [{ validator: validateConfirmPassword(formData?.password), trigger: 'change' }], confirmPassword: [{ validator: validateConfirmPassword(formData?.password), trigger: 'change' }],
policy: [{ validator: validatePolicy, trigger: 'change' }], policy: [{ validator: validatePolicy, trigger: 'change' }],
...mobileRule,
} }
// reset password form rules // reset password form rules
case LoginStateEnum.RESET_PASSWORD: case LoginStateEnum.RESET_PASSWORD:
return { return {
account: accountFormRule, account: accountFormRule,
...mobileRule,
} }
// mobile form rules
case LoginStateEnum.MOBILE:
return mobileRule
// login form rules // login form rules
default: default:
return { 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" /> <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"> <div class="mt-2 flex flex-col justify-center md:ml-6 md:mt-0">
<h1 class="text-md md:text-lg"> <h1 class="text-md md:text-lg">
早安, {{ userinfo.user.nickname }}, 开始您一天的工作吧 早安, {{ userinfo.user.realName }}, 开始您一天的工作吧
</h1> </h1>
<span class="text-secondary"> 今日晴20 - 32 </span> <span class="text-secondary"> 今日晴20 - 32 </span>
</div> </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> <script lang="ts" setup>
import { nextTick, onMounted, ref } from 'vue' import { PlusOutlined } from '@ant-design/icons-vue'
import DeptModal from './DeptModal.vue' import DeptFormModal from './DeptFormModal.vue'
import { columns, searchFormSchema } from './dept.data' import { columns, searchFormSchema } from './data'
import { handleTree } from '@/utils/tree'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage' import { useMessage } from '@/hooks/web/useMessage'
import { useModal } from '@/components/Modal' import { useModal } from '@/components/Modal'
import { IconEnum } from '@/enums/appEnum' import { IconEnum } from '@/enums/appEnum'
import { BasicTable, TableAction, useTable } from '@/components/Table' import { BasicTable, TableAction, useTable } from '@/components/Table'
import { getListSimpleUsers } from '@/api/system/user' import { deleteDept, lazyGetDeptList } from '@/api/system/dept'
import { deleteDept, getDeptPage } from '@/api/system/dept' import type { Department } from '@/api/system/dept/types'
defineOptions({ name: 'SystemDept' }) defineOptions({ name: 'SystemDept' })
const { t } = useI18n() const { t } = useI18n()
const { createMessage } = useMessage()
const [registerModal, { openModal }] = useModal()
const [register, { expandAll, collapseAll, getForm, reload }] = useTable({ const [registerModal, { openModal }] = useModal<Department>()
title: '部门列表',
api: getList, 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, columns,
rowKey: 'id', formConfig: {
formConfig: { labelWidth: 120, schemas: searchFormSchema }, labelWidth: 80,
schemas: searchFormSchema,
},
bordered: true,
canResize: false,
isTreeTable: true, isTreeTable: true,
pagination: false,
useSearchForm: true, useSearchForm: true,
showTableSetting: true, pagination: false,
showIndexColumn: false,
actionColumn: { actionColumn: {
width: 140, width: 140,
title: t('common.action'), title: t('common.action'),
@ -36,83 +51,43 @@ const [register, { expandAll, collapseAll, getForm, reload }] = useTable({
}, },
}) })
async function getList() { async function handleDelete(id: string) {
const res = await getDeptPage(getForm().getFieldsValue() as any) try {
return handleTree(res, 'id') await deleteDept(id)
} useMessage().createMessage.success(t('common.delSuccessText'))
reload()
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
} }
return `未知【${row.leaderUserId}` catch {}
} }
onMounted(async () => {
await getUserList()
})
</script> </script>
<template> <template>
<div> <div>
<BasicTable @register="register" @fetch-success="onFetchSuccess"> <BasicTable :api="async () => ([] as Department[])" @register="register">
<template #toolbar> <template #tableTitle>
<a-button v-auth="['system:dept:create']" type="primary" :pre-icon="IconEnum.ADD" @click="handleCreate"> <a-button type="primary" @click="openModal(true)">
<PlusOutlined />
{{ t('action.create') }} {{ t('action.create') }}
</a-button> </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>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'"> <template v-if="column.key === 'action'">
<TableAction <TableAction
:actions="[ :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, icon: IconEnum.DELETE,
danger: true, danger: true,
label: t('action.delete'), label: t('action.delete'),
auth: 'system:dept:delete',
popConfirm: { popConfirm: {
title: t('common.delMessage'), title: t('common.delMessage'),
placement: 'left', placement: 'left',
confirm: handleDelete.bind(null, record), confirm: handleDelete.bind(null, record.id),
}, },
}, },
]" ]"
@ -120,6 +95,7 @@ onMounted(async () => {
</template> </template>
</template> </template>
</BasicTable> </BasicTable>
<DeptModal @register="registerModal" @success="reload()" />
<DeptFormModal @register="registerModal" @success="reload()" />
</div> </div>
</template> </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 type { BasicColumn, FormSchema } from '@/components/Table'
import { useRender } from '@/components/Table' import { SystemMenuTypeEnum } from '@/api/system/menu/types'
import { DICT_TYPE, getDictOptions } from '@/utils/dict'
import { SystemMenuTypeEnum } from '@/enums/systemEnum'
export const columns: BasicColumn[] = [ export const columns: BasicColumn[] = [
{ {
@ -15,8 +14,21 @@ export const columns: BasicColumn[] = [
title: '菜单类型', title: '菜单类型',
dataIndex: 'type', dataIndex: 'type',
width: 80, width: 80,
customRender: ({ text }) => { customRender: ({ record }) => {
return useRender.renderDict(text, DICT_TYPE.SYSTEM_MENU_TYPE) 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', dataIndex: 'icon',
width: 60, width: 60,
customRender: ({ record }) => { customRender: ({ record }) => {
return useRender.renderIcon(record.icon) return record.icon && <span class={record.icon}></span>
}, },
}, },
{ {
@ -34,21 +46,18 @@ export const columns: BasicColumn[] = [
}, },
{ {
title: '权限标识', title: '权限标识',
dataIndex: 'permission', dataIndex: 'code',
width: 140, width: 140,
}, },
{ {
title: '组件路径', title: '路由路径',
dataIndex: 'component', dataIndex: 'path',
width: 140, width: 140,
}, },
{ {
title: '状态', title: '组件路径',
dataIndex: 'status', dataIndex: 'component',
width: 80, width: 140,
customRender: ({ text }) => {
return useRender.renderDict(text, DICT_TYPE.COMMON_STATUS)
},
}, },
] ]
@ -60,19 +69,15 @@ export const searchFormSchema: FormSchema[] = [
colProps: { span: 8 }, colProps: { span: 8 },
}, },
{ {
label: '状态', label: '权限标识',
field: 'status', field: 'code',
component: 'Select', component: 'Input',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS),
},
colProps: { span: 8 }, colProps: { span: 8 },
}, },
] ]
export const formSchema: FormSchema[] = [ export const formSchema: FormSchema[] = [
{ {
label: '编号',
field: 'id', field: 'id',
show: false, show: false,
component: 'Input', component: 'Input',
@ -80,22 +85,34 @@ export const formSchema: FormSchema[] = [
{ {
label: '上级菜单', label: '上级菜单',
field: 'parentId', field: 'parentId',
required: true,
component: 'ApiTreeSelect', component: 'ApiTreeSelect',
componentProps: { componentProps: {
api: () => listSimpleMenus(), api: getMenuListWithoutButtons,
parentLabel: '主类目', labelField: 'title',
handleTree: 'id', valueField: 'id',
}, },
}, },
{ {
label: '菜单类型', label: '菜单类型',
field: 'type', field: 'type',
required: true, required: true,
defaultValue: '0', defaultValue: 1,
component: 'RadioButtonGroup', component: 'RadioButtonGroup',
componentProps: { 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 }, colProps: { lg: 24, md: 24 },
}, },
@ -126,59 +143,43 @@ export const formSchema: FormSchema[] = [
helpMessage: '访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头', helpMessage: '访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头',
ifShow: ({ values }) => values.type !== SystemMenuTypeEnum.BUTTON, 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: '组件路径', label: '组件路径',
field: 'component', field: 'component',
required: true,
component: 'Input', component: 'Input',
helpMessage: '例如:system/user/index', helpMessage: '例如:system/user/index,不定义时使用 path 字段',
ifShow: ({ values }) => values.type === SystemMenuTypeEnum.MENU, ifShow: ({ values }) => values.type === SystemMenuTypeEnum.MENU,
}, },
{ {
label: '组件名称', label: '组件名称',
field: 'componentName', field: 'componentName',
required: ({ values }) => values.keepAlive,
component: 'Input', component: 'Input',
helpMessage: '例如:SystemUser', helpMessage: '例如:SystemName,当开启缓存时它是必传的',
ifShow: ({ values }) => values.type === SystemMenuTypeEnum.MENU, ifShow: ({ values }) => values.type === SystemMenuTypeEnum.MENU,
}, },
{ {
label: '菜单状态', label: '权限标识',
field: 'status', field: 'code',
required: true, required: true,
component: 'RadioButtonGroup', component: 'Input',
helpMessage: '选择停用时,路由将不会出现在侧边栏,也不能被访问', helpMessage: '输入一个不重复的标识',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS),
},
}, },
{ {
label: '显示状态', label: '是否在菜单栏显示',
field: 'visible', field: 'visible',
component: 'Switch', component: 'Switch',
componentProps: { componentProps: {
checkedChildren: '显示', checkedChildren: '显示',
unCheckedChildren: '隐藏', unCheckedChildren: '隐藏',
checkedValue: 1,
unCheckedValue: 0,
}, },
defaultValue: 1,
helpMessage: '选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问', helpMessage: '选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问',
ifShow: ({ values }) => values.type !== SystemMenuTypeEnum.BUTTON, ifShow: ({ values }) => values.type !== SystemMenuTypeEnum.BUTTON,
}, },
{
label: '总是显示',
field: 'alwaysShow',
component: 'Switch',
componentProps: {
checkedChildren: '显示',
unCheckedChildren: '隐藏',
},
helpMessage: '选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单',
ifShow: ({ values }) => values.type !== SystemMenuTypeEnum.BUTTON,
},
{ {
label: '是否缓存', label: '是否缓存',
field: 'keepAlive', field: 'keepAlive',
@ -186,8 +187,10 @@ export const formSchema: FormSchema[] = [
componentProps: { componentProps: {
checkedChildren: '缓存', checkedChildren: '缓存',
unCheckedChildren: '不缓存', unCheckedChildren: '不缓存',
checkedValue: 1,
unCheckedValue: 0,
}, },
helpMessage: '选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段', helpMessage: '选择缓存时,则会被 `keep-alive` 缓存,同时必须指定组件的 Name 值',
ifShow: ({ values }) => values.type === SystemMenuTypeEnum.MENU, ifShow: ({ values }) => values.type === SystemMenuTypeEnum.MENU,
}, },
] ]

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

@ -1,59 +1,49 @@
<script lang="ts" setup> <script lang="ts" setup>
import MenuModal from './MenuModal.vue' import { Space } from 'ant-design-vue'
import { columns, searchFormSchema } from './menu.data' import { PlusOutlined } from '@ant-design/icons-vue'
import { handleTree } from '@/utils/tree' import MenuFormModal from './MenuFormModal.vue'
import { useI18n } from '@/hooks/web/useI18n' import { columns, searchFormSchema } from './data'
import { useMessage } from '@/hooks/web/useMessage' import { useMessage } from '@/hooks/web/useMessage'
import { useModal } from '@/components/Modal' import { useModal } from '@/components/Modal'
import { IconEnum } from '@/enums/appEnum'
import { BasicTable, TableAction, useTable } from '@/components/Table' import { BasicTable, TableAction, useTable } from '@/components/Table'
import { deleteMenu, getMenuList } from '@/api/system/menu' import { deleteMenu, getMenuList } from '@/api/system/menu'
import { usePermission } from '@/hooks/web/usePermission' import { usePermission } from '@/hooks/web/usePermission'
import type { MenuItem } from '@/api/system/menu/types'
defineOptions({ name: 'SystemMenu' }) defineOptions({ name: 'SystemMenu' })
const { t } = useI18n() const [registerModal, { openModal }] = useModal<MenuItem>()
const { createMessage, createConfirm } = useMessage()
const [registerModal, { openModal }] = useModal()
const [register, { expandAll, collapseAll, getForm, reload }] = useTable({ const [register, { reload }] = useTable<MenuItem>({
title: '菜单列表',
api: getList,
columns, columns,
api: getMenuList,
rowKey: 'id', rowKey: 'id',
formConfig: { labelWidth: 120, schemas: searchFormSchema }, formConfig: {
isTreeTable: true, labelWidth: 80,
pagination: false, schemas: searchFormSchema,
striped: false, },
useSearchForm: true, useSearchForm: true,
showTableSetting: true, striped: false,
bordered: true, bordered: true,
showIndexColumn: false,
canResize: false, canResize: false,
isTreeTable: true,
pagination: false,
actionColumn: { actionColumn: {
width: 140, width: 140,
title: t('common.action'), title: '操作',
dataIndex: 'action', dataIndex: 'action',
fixed: 'right', fixed: 'right',
}, },
}) })
async function getList() {
const res = await getMenuList(getForm().getFieldsValue() as any)
return handleTree(res, 'id')
}
function handleCreate() { const { createMessage, createConfirm } = useMessage()
openModal(true, { isUpdate: false }) async function handleDelete(id: string) {
} try {
await deleteMenu(id)
function handleEdit(record: Recordable) { createMessage.success('删除成功')
openModal(true, { record, isUpdate: true }) reload()
} }
catch {}
async function handleDelete(record: Recordable) {
await deleteMenu(record.id)
createMessage.success(t('common.delSuccessText'))
reload()
} }
function refreshMenu() { function refreshMenu() {
@ -65,7 +55,6 @@ function refreshMenu() {
const { refreshMenu } = usePermission() const { refreshMenu } = usePermission()
await refreshMenu() await refreshMenu()
createMessage.success('刷新成功') createMessage.success('刷新成功')
//
location.reload() location.reload()
}, },
}) })
@ -74,35 +63,39 @@ function refreshMenu() {
<template> <template>
<div> <div>
<BasicTable @register="register"> <BasicTable :api="async () => ([] as MenuItem[])" @register="register">
<template #toolbar> <template #tableTitle>
<a-button v-auth="['system:menu:create']" type="primary" :pre-icon="IconEnum.ADD" @click="handleCreate"> <Space>
{{ t('action.create') }} <a-button type="primary" @click="openModal(true)">
</a-button> <PlusOutlined />
<a-button @click="expandAll"> 新增
{{ t('component.tree.expandAll') }} </a-button>
</a-button> <a-button type="primary" danger @click="refreshMenu">
<a-button @click="collapseAll"> <template #icon>
{{ t('component.tree.unExpandAll') }} <span class="i-ant-design:sync-outlined align-text-top" />
</a-button> </template>
<a-button color="warning" pre-icon="ep:refresh" @click="refreshMenu"> 缓存刷新
刷新菜单缓存 </a-button>
</a-button> </Space>
</template> </template>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'"> <template v-if="column.key === 'action'">
<TableAction <TableAction
:actions="[ :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, danger: true,
label: t('action.delete'),
auth: 'system:menu:delete',
popConfirm: { popConfirm: {
title: t('common.delMessage'), title: '确定要删除数据吗?',
placement: 'left', placement: 'left',
confirm: handleDelete.bind(null, record), confirm: () => handleDelete(record.id),
}, },
}, },
]" ]"
@ -110,6 +103,7 @@ function refreshMenu() {
</template> </template>
</template> </template>
</BasicTable> </BasicTable>
<MenuModal @register="registerModal" @success="reload()" />
<MenuFormModal @register="registerModal" @success="reload()" />
</div> </div>
</template> </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> <script lang="ts" setup>
import { ref, unref } from 'vue' import { ref } from 'vue'
import { without } from 'lodash-es' import { useAsyncState } from '@vueuse/core'
import { menuScopeFormSchema } 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 { 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 { BasicTree } from '@/components/Tree'
import { listSimpleMenus } from '@/api/system/menu' import { assignMenuToRole, getMenuIdsByRole, getMenuTree } from '@/api/system/role'
import { handleTree } from '@/utils/tree' import { useMessage } from '@/hooks/web/useMessage'
import { assignRoleMenu, listRoleMenus } from '@/api/system/permission'
defineOptions({ name: 'SystemRoleMenuModal' })
defineOptions({ name: 'RoleMenuModal' })
const emit = defineEmits(['success', 'register']) const emit = defineEmits(['success', 'register'])
const { t } = useI18n()
const { createMessage } = useMessage()
const treeData = ref<TreeItem[]>([])
const menuKeys = ref<number[]>([])
const menuHalfKeys = ref<number[]>([])
// const checkedIds = ref<string[]>([])
const defaultExpandLevel = ref<number>(1) const { state, execute } = useAsyncState(getMenuTree, [], { immediate: false })
// list
const parentIdSets = ref<Set<number>>(new Set())
const treeRef = ref()
const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({ let roleId: string
labelWidth: 120, const [registerModal, { setModalProps, closeModal }] = useModalInner(async (id: string) => {
baseColProps: { span: 24 }, try {
schemas: menuScopeFormSchema, if (!state.value.length)
showActionButtonGroup: false, await execute()
actionColOptions: { span: 23 },
})
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => { checkedIds.value = await getMenuIdsByRole(roleId = id)
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))
} }
const role = await getRole(data.record.id) catch {}
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)
}) })
async function handleSubmit() { function handleSubmit() {
try { setModalProps({ confirmLoading: true })
const values = await validate() assignMenuToRole({
setModalProps({ confirmLoading: true }) roleId,
await assignRoleMenu({ menuIds: checkedIds.value,
roleId: values.id, }).then(() => {
menuIds: [...menuKeys.value, ...menuHalfKeys.value],
})
closeModal() closeModal()
emit('success') emit('success')
createMessage.success(t('common.saveSuccessText')) checkedIds.value = []
} useMessage().createMessage.success('操作成功')
finally { }).finally(() => {
setModalProps({ confirmLoading: false }) 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> </script>
<template> <template>
<BasicModal v-bind="$attrs" title="修改角色菜单权限" @register="registerModal" @ok="handleSubmit"> <BasicModal v-bind="$attrs" title="菜单权限配置" width="20%" @register="registerModal" @ok="handleSubmit">
<BasicForm @register="registerForm"> <BasicTree
<template #menuIds="{ model, field }"> v-if="state.length"
<BasicTree v-model:value="checkedIds"
v-if="treeData.length" checkable
ref="treeRef" default-expand-all
v-model:checkedKeys="model[field]" :tree-data="state"
:tree-data="treeData" :selectable="false"
:field-names="{ title: 'name', key: 'id' }" :field-names="{ key: 'id' }"
toolbar />
checkable
search
:show-strictly-button="false"
:selectable="false"
title="菜单分配"
@check="menuCheck"
/>
</template>
</BasicForm>
</BasicModal> </BasicModal>
</template> </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> <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 RoleMenuModal from './RoleMenuModal.vue'
import RoleScopeModal from './RoleScopeModal.vue' import { columns, searchFormSchema } from './data'
import { columns, searchFormSchema } from './role.data'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage' import { useMessage } from '@/hooks/web/useMessage'
import { useModal } from '@/components/Modal' import { useModal } from '@/components/Modal'
import { IconEnum } from '@/enums/appEnum'
import { BasicTable, TableAction, useTable } from '@/components/Table' import { BasicTable, TableAction, useTable } from '@/components/Table'
import type { RoleExportReqVO } from '@/api/system/role' import { deleteRole, lazyGetRoleList } from '@/api/system/role'
import { deleteRole, exportRole, getRolePage } from '@/api/system/role' import type { Role } from '@/api/system/role/types'
defineOptions({ name: 'SystemRole' }) defineOptions({ name: 'SystemRole' })
const { t } = useI18n() const { createMessage } = useMessage()
const { createConfirm, createMessage } = useMessage() const [registerFormModal, { openModal: openFormModal }] = useModal<Role>()
const [registerModal, { openModal }] = useModal() const [registerMenuModal, { openModal: openMenuModal }] = useModal<string>()
const [registerMenuModal, { openModal: openMenuModal }] = useModal()
const [registerScopeModal, { openModal: openScopeModal }] = useModal() const [registerTable, { reload }] = useTable({
const [registerTable, { getForm, reload }] = useTable({ async api(params) {
title: '角色列表', try {
api: getRolePage, 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, columns,
formConfig: { labelWidth: 120, schemas: searchFormSchema }, formConfig: {
labelWidth: 80,
schemas: searchFormSchema,
actionColOptions: { span: 3 },
},
bordered: true,
canResize: false,
useSearchForm: true, useSearchForm: true,
showTableSetting: true,
showIndexColumn: false,
actionColumn: { actionColumn: {
width: 140, width: 140,
title: t('common.action'), title: '操作',
dataIndex: 'action', dataIndex: 'action',
fixed: 'right', fixed: 'right',
}, },
}) })
function handleCreate() { async function handleDelete(id: string) {
openModal(true, { isUpdate: false }) try {
} await deleteRole(id)
createMessage.success('删除成功!')
function handleEdit(record: Recordable) { reload()
openModal(true, { record, isUpdate: true }) }
} catch {}
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()
} }
</script> </script>
<template> <template>
<div> <div>
<BasicTable @register="registerTable"> <BasicTable :api="async () => ([] as Role[])" @register="registerTable">
<template #toolbar> <template #tableTitle>
<a-button v-auth="['system:role:create']" type="primary" :pre-icon="IconEnum.ADD" @click="handleCreate"> <a-button type="primary" @click="openFormModal(true)">
{{ t('action.create') }} <PlusOutlined />
</a-button> 新建
<a-button v-auth="['system:role:create']" :pre-icon="IconEnum.EXPORT" @click="handleExport">
{{ t('action.export') }}
</a-button> </a-button>
</template> </template>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'"> <template v-if="column.key === 'action'">
<TableAction <TableAction
:actions="[ :actions="[
{ icon: IconEnum.EDIT, label: t('action.edit'), auth: 'system:role:update', onClick: handleEdit.bind(null, record) },
]"
:drop-down-actions="[
{ {
icon: IconEnum.EDIT, icon: 'i-ant-design:edit-outlined',
label: '菜单权限', label: '编辑',
auth: 'system:permission:assign-role-menu', onClick: () => openFormModal(true, record),
onClick: handleMenu.bind(null, record),
}, },
{ {
icon: IconEnum.EDIT, icon: 'i-ant-design:appstore-outlined',
label: '数据权限', label: '菜单权限',
auth: 'system:permission:assign-role-data-scope', onClick: () => openMenuModal(true, record.id),
onClick: handleDataScope.bind(null, record),
}, },
{ {
icon: IconEnum.DELETE, icon: 'i-ant-design:delete-outlined',
label: '删除',
danger: true, danger: true,
label: t('action.delete'),
auth: 'system:role:delete',
popConfirm: { popConfirm: {
title: t('common.delMessage'), title: '确定要删除数据吗?',
placement: 'left', placement: 'left',
confirm: handleDelete.bind(null, record), confirm: () => handleDelete(record.id),
}, },
}, },
]" ]"
@ -115,8 +103,8 @@ async function handleDelete(record: Recordable) {
</template> </template>
</template> </template>
</BasicTable> </BasicTable>
<RoleModal @register="registerModal" @success="reload()" />
<RoleFormModal @register="registerFormModal" @success="reload()" />
<RoleMenuModal @register="registerMenuModal" @success="reload()" /> <RoleMenuModal @register="registerMenuModal" @success="reload()" />
<RoleScopeModal @register="registerScopeModal" @success="reload()" />
</div> </div>
</template> </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> <script lang="ts" setup>
import TenantModal from './TenantModal.vue' import { PlusOutlined } from '@ant-design/icons-vue'
import { columns, searchFormSchema } from './tenant.data' import TenantModal from './TenantFormModal.vue'
import { columns, searchFormSchema } from './data'
import { BasicTable, TableAction, useTable } from '@/components/Table' import { BasicTable, TableAction, useTable } from '@/components/Table'
import type { TenantExportReqVO } from '@/api/system/tenant' import { deleteTenant, getTenantList } from '@/api/system/tenant'
import { deleteTenant, exportTenant, getTenantPage } from '@/api/system/tenant' import type { Tenant } from '@/api/system/tenant/types'
import { useModal } from '@/components/Modal' import { useModal } from '@/components/Modal'
import { IconEnum } from '@/enums/appEnum'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage' import { useMessage } from '@/hooks/web/useMessage'
defineOptions({ name: 'SystemTenant' }) defineOptions({ name: 'SystemTenant' })
const { t } = useI18n() const [registerModal, { openModal }] = useModal<Tenant>()
const { createConfirm, createMessage } = useMessage() const [registerTable, { reload }] = useTable({
const [registerModal, { openModal }] = useModal() api: getTenantList,
const [registerTable, { getForm, reload }] = useTable({
title: '租户列表',
api: getTenantPage,
columns, columns,
formConfig: { labelWidth: 120, schemas: searchFormSchema }, formConfig: {
labelWidth: 100,
schemas: searchFormSchema,
actionColOptions: { span: 4 },
},
bordered: true,
canResize: false,
useSearchForm: true, useSearchForm: true,
showTableSetting: true,
showIndexColumn: false,
actionColumn: { actionColumn: {
width: 140, width: 140,
title: t('common.action'), title: '操作',
dataIndex: 'action', dataIndex: 'action',
fixed: 'right', fixed: 'right',
}, },
}) })
function handleCreate() { async function handleDelete(id: string) {
openModal(true, { isUpdate: false }) try {
} await deleteTenant(id)
useMessage().createMessage.success('删除成功!')
function handleEdit(record: Recordable) { reload()
openModal(true, { record, isUpdate: true }) }
} catch {}
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()
} }
</script> </script>
<template> <template>
<div> <div>
<BasicTable @register="registerTable"> <BasicTable :api="async () => ([] as Tenant[])" @register="registerTable">
<template #toolbar> <template #tableTitle>
<a-button v-auth="['system:tenant:create']" type="primary" :pre-icon="IconEnum.ADD" @click="handleCreate"> <a-button type="primary" @click="openModal(true)">
{{ t('action.create') }} <PlusOutlined />
</a-button> 新建
<a-button v-auth="['system:tenant:export']" :pre-icon="IconEnum.EXPORT" @click="handleExport">
{{ t('action.export') }}
</a-button> </a-button>
</template> </template>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'"> <template v-if="column.key === 'action'">
<TableAction <TableAction
:actions="[ :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, danger: true,
label: t('action.delete'),
auth: 'system:tenant:delete',
popConfirm: { popConfirm: {
title: t('common.delMessage'), title: '确定要删除数据吗?',
placement: 'left', placement: 'left',
confirm: handleDelete.bind(null, record), confirm: () => handleDelete(record.id),
}, },
}, },
]" ]"
@ -89,6 +74,7 @@ async function handleDelete(record: Recordable) {
</template> </template>
</template> </template>
</BasicTable> </BasicTable>
<TenantModal @register="registerModal" @success="reload()" /> <TenantModal @register="registerModal" @success="reload()" />
</div> </div>
</template> </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 { BasicTree } from '@/components/Tree'
import { BasicModal, useModalInner } from '@/components/Modal' import { BasicModal, useModalInner } from '@/components/Modal'
import { createTenantPackage, getTenantPackage, updateTenantPackage } from '@/api/system/tenantPackage' 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' import { handleTree } from '@/utils/tree'
defineOptions({ name: 'SystemTenantPackageModal' }) defineOptions({ name: 'SystemTenantPackageModal' })
@ -24,7 +25,7 @@ const menuHalfKeys = ref<number[]>([])
// //
const defaultExpandLevel = ref<number>(1) const defaultExpandLevel = ref<number>(1)
// list // list
const parentIdSets = ref<Set<number>>(new Set()) const parentIdSets = ref<Set<string>>(new Set())
const treeRef = ref() const treeRef = ref()
const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({ const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({
@ -40,10 +41,10 @@ const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data
menuReset() menuReset()
setModalProps({ confirmLoading: false }) setModalProps({ confirmLoading: false })
if (unref(treeData).length === 0) { if (unref(treeData).length === 0) {
const res = await listSimpleMenus() const res = await getMenuListWithoutButtons()
treeData.value = handleTree(res, 'id') 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 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 // checkedKeys
menuKeys.value = res.menuIds menuKeys.value = res.menuIds
// //

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

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

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

@ -1,41 +1,41 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { ref } from 'vue'
import { userPwdFormSchema } from './user.data' import { getFormSchema } from './data'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage' import { useMessage } from '@/hooks/web/useMessage'
import { BasicForm, useForm } from '@/components/Form' import { BasicForm, useForm } from '@/components/Form'
import { BasicModal, useModalInner } from '@/components/Modal' 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 emit = defineEmits(['success', 'register'])
const { t } = useI18n() 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, labelWidth: 120,
baseColProps: { span: 24 }, baseColProps: { span: 24 },
schemas: userPwdFormSchema, schemas: getFormSchema(isUpdate),
showActionButtonGroup: false, showActionButtonGroup: false,
actionColOptions: { span: 23 }, actionColOptions: { span: 23 },
}) })
const [registerModal, { setModalProps, closeModal }] = useModalInner((data) => { const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data: SystemUser) => {
resetFields() isUpdate.value = true
userId.value = data.record.id setFieldsValue({ ...data })
setModalProps({ confirmLoading: false })
}) })
async function handleSubmit() { async function handleSubmit() {
try { try {
const values = await validate() const values = await validate<SystemUser>()
await resetUserPwd(userId.value, values.newPassword)
setModalProps({ confirmLoading: true }) setModalProps({ confirmLoading: true })
await (isUpdate.value ? updateUser(values) : createUser(values))
closeModal() closeModal()
emit('success') emit('success')
createMessage.success(t('common.saveSuccessText')) useMessage().createMessage.success(t('common.saveSuccessText'))
} }
finally { finally {
setModalProps({ confirmLoading: false }) setModalProps({ confirmLoading: false })
@ -44,7 +44,13 @@ async function handleSubmit() {
</script> </script>
<template> <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" /> <BasicForm @register="registerForm" />
</BasicModal> </BasicModal>
</template> </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> <script lang="ts" setup>
import { reactive } from 'vue' import { PlusOutlined } from '@ant-design/icons-vue'
import UserModal from './UserModal.vue' import UserFormModal from './UserFormModal.vue'
import UserRoleModal from './UserRoleModal.vue' import { columns, searchFormSchema } from './data'
import ResetPwdModal from './ResetPwdModal.vue'
import DeptTree from './DeptTree.vue'
import { columns, searchFormSchema } from './user.data'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage' import { useMessage } from '@/hooks/web/useMessage'
import { useModal } from '@/components/Modal' import { useModal } from '@/components/Modal'
import { IconEnum } from '@/enums/appEnum'
import { BasicTable, TableAction, useTable } from '@/components/Table' import { BasicTable, TableAction, useTable } from '@/components/Table'
import type { UserExportReqVO } from '@/api/system/user' import { deleteUser, getUserList } from '@/api/system/user'
import { deleteUser, exportUser, getUserPage } from '@/api/system/user' import type { SystemUser } from '@/api/system/user/types'
defineOptions({ name: 'SystemUser' }) defineOptions({ name: 'SystemUser' })
const { t } = useI18n() const { t } = useI18n()
const { createConfirm, createMessage } = useMessage() const { createMessage } = useMessage()
const [registerModal, { openModal }] = useModal() const [registerModal, { openModal }] = useModal<SystemUser>()
const [registerRoleModal, { openModal: openRoleModal }] = useModal()
const [registerPwdModal, { openModal: openPwdModal }] = useModal()
const searchInfo = reactive<Recordable>({})
const [registerTable, { getForm, reload }] = useTable({ const [registerTable, { reload }] = useTable({
title: '账号列表', api(params) {
api: getUserPage, return getUserList(params)
},
columns, columns,
formConfig: { formConfig: {
labelWidth: 120, labelWidth: 80,
schemas: searchFormSchema, schemas: searchFormSchema,
autoSubmitOnEnter: true, autoSubmitOnEnter: true,
}, },
useSearchForm: true, useSearchForm: true,
showTableSetting: true, bordered: true,
showIndexColumn: false, canResize: false,
actionColumn: { actionColumn: {
width: 140, width: 140,
title: t('common.action'), title: t('common.action'),
@ -42,92 +36,43 @@ const [registerTable, { getForm, reload }] = useTable({
}, },
}) })
/** 新增按钮操作 */ async function handleDelete(id: string) {
function handleCreate() { try {
openModal(true, { isUpdate: false }) await deleteUser(id)
} createMessage.success(t('common.delSuccessText'))
reload()
/** 导出按钮操作 */ }
async function handleExport() { catch {}
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()
} }
</script> </script>
<template> <template>
<div class="flex"> <div>
<DeptTree class="w-1/4 xl:w-1/5" @select="handleSelect" /> <BasicTable :api="async () => ([] as SystemUser[])" @register="registerTable">
<BasicTable class="w-3/4 xl:w-4/5" :search-info="searchInfo" @register="registerTable"> <template #tableTitle>
<template #toolbar> <a-button type="primary" @click="openModal(true)">
<a-button v-auth="['system:user:create']" type="primary" :pre-icon="IconEnum.ADD" @click="handleCreate"> <PlusOutlined />
{{ t('action.create') }} {{ t('action.create') }}
</a-button> </a-button>
<a-button v-auth="['system:user:export']" :pre-icon="IconEnum.EXPORT" @click="handleExport">
{{ t('action.export') }}
</a-button>
</template> </template>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'"> <template v-if="column.key === 'action'">
<TableAction <TableAction
:actions="[ :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, icon: 'i-ant-design:edit-outlined',
label: '重置密码', label: t('action.edit'),
auth: 'system:user:update-password', onClick: () => openModal(true, record),
onClick: handleResetPwd.bind(null, record),
}, },
{ {
icon: IconEnum.DELETE, icon: 'i-ant-design:delete-outlined',
danger: true, danger: true,
label: t('action.delete'), label: t('action.delete'),
auth: 'system:user:delete',
popConfirm: { popConfirm: {
title: t('common.delMessage'), title: t('common.delMessage'),
placement: 'left', placement: 'left',
confirm: handleDelete.bind(null, record), confirm: () => handleDelete(record.id),
}, },
}, },
]" ]"
@ -135,8 +80,7 @@ function handleSelect(deptId = '') {
</template> </template>
</template> </template>
</BasicTable> </BasicTable>
<UserModal @register="registerModal" @success="reload()" />
<UserRoleModal @register="registerRoleModal" @success="reload()" /> <UserFormModal @register="registerModal" @success="reload()" />
<ResetPwdModal @register="registerPwdModal" @success="reload()" />
</div> </div>
</template> </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()
},
},
]
},
},
]