refactor(tenant): remove frontend multi-tenant features / 删除前端多租户能力

This commit is contained in:
2026-09-01 10:56:44 +08:00
parent 29f20b7e4d
commit 2ca7a11c8a
22 changed files with 8 additions and 1782 deletions
+1 -2
View File
@@ -39,8 +39,7 @@ export function useFormRules() {
phone: [createRequiredRule($t('form.phone.required')), patternRules.phone], phone: [createRequiredRule($t('form.phone.required')), patternRules.phone],
pwd: [createRequiredRule($t('form.pwd.required')), patternRules.pwd], pwd: [createRequiredRule($t('form.pwd.required')), patternRules.pwd],
code: [createRequiredRule($t('form.code.required')), patternRules.code], code: [createRequiredRule($t('form.code.required')), patternRules.code],
email: [createRequiredRule($t('form.email.required')), patternRules.email], email: [createRequiredRule($t('form.email.required')), patternRules.email]
tenantId: [createRequiredRule('请选择/输入公司名称')]
} satisfies Record<string, App.Global.FormRule[]>; } satisfies Record<string, App.Global.FormRule[]>;
/** the default required rule */ /** the default required rule */
-101
View File
@@ -284,8 +284,6 @@ const local: App.I18n.Schema = {
system_oss: 'File Management', system_oss: 'File Management',
'system_oss-config': 'OSS Config', 'system_oss-config': 'OSS Config',
system_client: 'Client Management', system_client: 'Client Management',
system_tenant: 'Tenant Management',
'system_tenant-package': 'Tenant Package Management',
monitor: 'Monitor', monitor: 'Monitor',
monitor_logininfor: 'Login Log', monitor_logininfor: 'Login Log',
monitor_operlog: 'Operate Log', monitor_operlog: 'Operate Log',
@@ -305,7 +303,6 @@ const local: App.I18n.Schema = {
about: 'About' about: 'About'
}, },
menu: { menu: {
system_tenant: 'Tenant Management',
system_log: 'Log Management', system_log: 'Log Management',
'monitor_snail-job': 'Job Management', 'monitor_snail-job': 'Job Management',
monitor_admin: 'Admin Monitor' monitor_admin: 'Admin Monitor'
@@ -1036,104 +1033,6 @@ const local: App.I18n.Schema = {
selectDataScope: 'Select Data Scope', selectDataScope: 'Select Data Scope',
selectDeptPermission: 'Select Dept Permission' selectDeptPermission: 'Select Dept Permission'
}, },
tenant: {
title: 'Tenant List',
tenantName: 'Tenant Name',
tenantId: 'Tenant ID',
contactUserName: 'Contact Person',
contactPhone: 'Contact Phone',
companyName: 'Company Name',
licenseNumber: 'License Number',
address: 'Address',
intro: 'Introduction',
domain: 'Domain',
packageId: 'Tenant Package',
expireTime: 'Expiration Time',
accountCount: 'Account Count',
status: 'Status',
createTime: 'Create Time',
form: {
tenantName: {
required: 'Please enter Tenant Name',
invalid: 'Tenant Name cannot be empty'
},
contactUserName: {
required: 'Please enter Contact Person',
invalid: 'Contact Person cannot be empty'
},
contactPhone: {
required: 'Please enter Contact Phone',
invalid: 'Contact Phone cannot be empty'
},
companyName: {
required: 'Please enter Company Name',
invalid: 'Company Name cannot be empty'
},
licenseNumber: {
required: 'Please enter License Number',
invalid: 'License Number cannot be empty'
},
address: {
required: 'Please enter Address',
invalid: 'Address cannot be empty'
},
intro: {
required: 'Please enter Introduction',
invalid: 'Introduction cannot be empty'
},
domain: {
required: 'Please enter Domain',
invalid: 'Domain cannot be empty'
},
packageId: {
required: 'Please select Tenant Package',
invalid: 'Tenant Package cannot be empty'
},
expireTime: {
required: 'Please select Expiration Time',
invalid: 'Expiration Time cannot be empty'
},
accountCount: {
required: 'Please enter Account Count',
invalid: 'Account Count cannot be empty'
},
status: {
required: 'Please select Status',
invalid: 'Status cannot be empty'
}
},
addTenant: 'Add Tenant',
editTenant: 'Edit Tenant'
},
tenantPackage: {
title: 'Tenant Package List',
packageName: 'Package Name',
menuIds: 'Menu Permission',
remark: 'Remark',
status: 'Status',
createTime: 'Create Time',
form: {
packageName: {
required: 'Please enter Package Name',
invalid: 'Package Name cannot be empty'
},
menuIds: {
required: 'Please select Menu Permission',
invalid: 'Menu Permission cannot be empty'
},
status: {
required: 'Please select Status',
invalid: 'Status cannot be empty'
},
remark: {
required: 'Please enter Remark',
invalid: 'Remark cannot be empty'
}
},
addTenantPackage: 'Add Tenant Package',
editTenantPackage: 'Edit Tenant Package',
statusChangeSuccess: 'Status modified successfully'
},
user: { user: {
title: 'User List', title: 'User List',
userName: 'Username', userName: 'Username',
-101
View File
@@ -280,8 +280,6 @@ const local: App.I18n.Schema = {
system_oss: '文件管理', system_oss: '文件管理',
'system_oss-config': 'OSS 配置', 'system_oss-config': 'OSS 配置',
system_client: '客户端管理', system_client: '客户端管理',
system_tenant: '租户管理',
'system_tenant-package': '租户套餐',
monitor: '系统监控', monitor: '系统监控',
monitor_cache: '缓存监控', monitor_cache: '缓存监控',
monitor_logininfor: '登录日志', monitor_logininfor: '登录日志',
@@ -301,7 +299,6 @@ const local: App.I18n.Schema = {
about: '关于' about: '关于'
}, },
menu: { menu: {
system_tenant: '租户管理',
system_log: '日志管理', system_log: '日志管理',
'monitor_snail-job': '任务调度中心', 'monitor_snail-job': '任务调度中心',
monitor_admin: 'Admin 监控' monitor_admin: 'Admin 监控'
@@ -1028,104 +1025,6 @@ const local: App.I18n.Schema = {
selectDataScope: '选择数据权限', selectDataScope: '选择数据权限',
selectDeptPermission: '选择部门权限' selectDeptPermission: '选择部门权限'
}, },
tenant: {
title: '租户列表',
tenantName: '租户名称',
tenantId: '租户编号',
contactUserName: '联系人',
contactPhone: '联系电话',
companyName: '公司名称',
licenseNumber: '营业执照编号',
address: '地址',
intro: '企业简介',
domain: '域名',
packageId: '租户套餐',
expireTime: '过期时间',
accountCount: '账号数量',
status: '状态',
createTime: '创建时间',
form: {
tenantName: {
required: '请输入租户名称',
invalid: '租户名称不能为空'
},
contactUserName: {
required: '请输入联系人',
invalid: '联系人不能为空'
},
contactPhone: {
required: '请输入联系电话',
invalid: '联系电话不能为空'
},
companyName: {
required: '请输入公司名称',
invalid: '公司名称不能为空'
},
licenseNumber: {
required: '请输入营业执照编号',
invalid: '营业执照编号不能为空'
},
address: {
required: '请输入地址',
invalid: '地址不能为空'
},
intro: {
required: '请输入企业简介',
invalid: '企业简介不能为空'
},
domain: {
required: '请输入域名',
invalid: '域名不能为空'
},
packageId: {
required: '请选择租户套餐',
invalid: '租户套餐不能为空'
},
expireTime: {
required: '请选择过期时间',
invalid: '过期时间不能为空'
},
accountCount: {
required: '请输入账号数量',
invalid: '账号数量不能为空'
},
status: {
required: '请选择状态',
invalid: '状态不能为空'
}
},
addTenant: '新增租户',
editTenant: '编辑租户'
},
tenantPackage: {
title: '租户套餐列表',
packageName: '套餐名称',
menuIds: '菜单权限',
remark: '备注',
status: '状态',
createTime: '创建时间',
form: {
packageName: {
required: '请输入套餐名称',
invalid: '套餐名称不能为空'
},
menuIds: {
required: '请选择菜单权限',
invalid: '菜单权限不能为空'
},
status: {
required: '请选择状态',
invalid: '状态不能为空'
},
remark: {
required: '请输入备注',
invalid: '备注不能为空'
}
},
addTenantPackage: '新增租户套餐',
editTenantPackage: '编辑租户套餐',
statusChangeSuccess: '状态修改成功'
},
user: { user: {
title: '用户列表', title: '用户列表',
userName: '用户名称', userName: '用户名称',
-2
View File
@@ -40,8 +40,6 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
system_oss: () => import("@/views/system/oss/index.vue"), system_oss: () => import("@/views/system/oss/index.vue"),
system_post: () => import("@/views/system/post/index.vue"), system_post: () => import("@/views/system/post/index.vue"),
system_role: () => import("@/views/system/role/index.vue"), system_role: () => import("@/views/system/role/index.vue"),
"system_tenant-package": () => import("@/views/system/tenant-package/index.vue"),
system_tenant: () => import("@/views/system/tenant/index.vue"),
system_user: () => import("@/views/system/user/index.vue"), system_user: () => import("@/views/system/user/index.vue"),
tool_gen: () => import("@/views/tool/gen/index.vue"), tool_gen: () => import("@/views/tool/gen/index.vue"),
}; };
-18
View File
@@ -279,24 +279,6 @@ export const generatedRoutes: GeneratedRoute[] = [
i18nKey: 'route.system_role' i18nKey: 'route.system_role'
} }
}, },
{
name: 'system_tenant',
path: '/system/tenant',
component: 'view.system_tenant',
meta: {
title: 'system_tenant',
i18nKey: 'route.system_tenant'
}
},
{
name: 'system_tenant-package',
path: '/system/tenant-package',
component: 'view.system_tenant-package',
meta: {
title: 'system_tenant-package',
i18nKey: 'route.system_tenant-package'
}
},
{ {
name: 'system_user', name: 'system_user',
path: '/system/user', path: '/system/user',
-2
View File
@@ -194,8 +194,6 @@ const routeMap: RouteMap = {
"system_oss-config": "/system/oss-config", "system_oss-config": "/system/oss-config",
"system_post": "/system/post", "system_post": "/system/post",
"system_role": "/system/role", "system_role": "/system/role",
"system_tenant": "/system/tenant",
"system_tenant-package": "/system/tenant-package",
"system_user": "/system/user", "system_user": "/system/user",
"tool": "/tool", "tool": "/tool",
"tool_gen": "/tool/gen", "tool_gen": "/tool/gen",
-8
View File
@@ -52,14 +52,6 @@ export function fetchGetRoleMenuTreeSelect(roleId: CommonType.IdType) {
}); });
} }
/** 获取租户套餐关联菜单 */
export function fetchGetTenantPackageMenuTreeSelect(packageId: CommonType.IdType) {
return request<Api.System.TenantPackageMenuTreeSelect>({
url: `/system/menu/tenantPackageMenuTreeselect/${packageId}`,
method: 'get'
});
}
/** 级联删除菜单 */ /** 级联删除菜单 */
export function fetchCascadeDeleteMenu(menuIds: CommonType.IdType[]) { export function fetchCascadeDeleteMenu(menuIds: CommonType.IdType[]) {
return request<boolean>({ return request<boolean>({
-52
View File
@@ -1,52 +0,0 @@
import { request } from '@/service/request';
/** 获取租户套餐列表 */
export function fetchGetTenantPackageList(params?: Api.System.TenantPackageSearchParams) {
return request<Api.System.TenantPackageList>({
url: '/system/tenant/package/list',
method: 'get',
params
});
}
/** 获取租户下拉列表 */
export function fetchGetTenantPackageSelectList() {
return request<Api.System.TenantPackageSelectList[]>({
url: '/system/tenant/package/selectList',
method: 'get'
});
}
/** 新增租户套餐 */
export function fetchCreateTenantPackage(data: Api.System.TenantPackageOperateParams) {
return request<boolean>({
url: '/system/tenant/package',
method: 'post',
data
});
}
/** 修改租户套餐 */
export function fetchUpdateTenantPackage(data: Api.System.TenantPackageOperateParams) {
return request<boolean>({
url: '/system/tenant/package',
method: 'put',
data
});
}
/** 批量删除租户套餐 */
export function fetchBatchDeleteTenantPackage(packageIds: CommonType.IdType[]) {
return request<boolean>({
url: `/system/tenant/package/${packageIds.join(',')}`,
method: 'delete'
});
}
/** 修改租户套餐状态 */
export function fetchUpdateTenantPackageStatus(data: Api.System.TenantPackageOperateParams) {
return request<boolean>({
url: `/system/tenant/package/changeStatus`,
method: 'put',
data
});
}
-81
View File
@@ -1,81 +0,0 @@
import { request } from '@/service/request';
/** 获取租户列表 */
export function fetchGetTenantList(params?: Api.System.TenantSearchParams) {
return request<Api.System.TenantList>({
url: '/system/tenant/list',
method: 'get',
params
});
}
/** 新增租户 */
export function fetchCreateTenant(data: Api.System.TenantOperateParams) {
return request<boolean>({
url: '/system/tenant',
method: 'post',
headers: {
isEncrypt: true,
repeatSubmit: false
},
data
});
}
/** 修改租户 */
export function fetchUpdateTenant(data: Api.System.TenantOperateParams) {
return request<boolean>({
url: '/system/tenant',
method: 'put',
data
});
}
/** 批量删除租户 */
export function fetchBatchDeleteTenant(ids: CommonType.IdType[]) {
return request<boolean>({
url: `/system/tenant/${ids.join(',')}`,
method: 'delete'
});
}
/** 同步租户字典 */
export function fetchSyncTenantDict() {
return request<boolean>({
url: '/system/tenant/syncTenantDict',
method: 'get'
});
}
/** 同步租户套餐 */
export function fetchSyncTenantPackage(params: Api.System.TenantPackageSyncParams) {
return request<boolean>({
url: '/system/tenant/syncTenantPackage',
method: 'get',
params
});
}
/** 同步租户参数配置 */
export function fetchSyncTenantConfig() {
return request<boolean>({
url: '/system/tenant/syncTenantConfig',
method: 'get'
});
}
/** 动态切换租户 */
export function fetchChangeTenant(tenantId: CommonType.IdType) {
return request<boolean>({
url: `/system/tenant/dynamic/${tenantId}`,
method: 'get'
});
}
/** 清空租户 */
export function fetchClearTenant() {
return request<boolean>({
url: '/system/tenant/dynamic/clear',
method: 'get'
});
}
-18
View File
@@ -80,24 +80,6 @@ declare namespace Api {
permissions: string[]; permissions: string[];
} }
/** tenant */
interface Tenant {
/** 企业名称 */
companyName: string;
/** 域名 */
domain: string;
/** 租户编号 */
tenantId: string;
}
/** login tenant */
interface LoginTenant {
/** 租户开关 */
tenantEnabled: boolean;
/** 租户列表 */
voList: Tenant[];
}
interface CaptchaCode { interface CaptchaCode {
/** 是否开启验证码 */ /** 是否开启验证码 */
captchaEnabled: boolean; captchaEnabled: boolean;
-108
View File
@@ -517,114 +517,6 @@ declare namespace Api {
/** config list */ /** config list */
type ConfigList = Api.Common.PaginatingQueryRecord<Config>; type ConfigList = Api.Common.PaginatingQueryRecord<Config>;
/** tenant */
type Tenant = Common.CommonRecord<{
/** id */
id: CommonType.IdType;
/** 租户编号 */
tenantId: CommonType.IdType;
/** 联系人 */
contactUserName: string;
/** 联系电话 */
contactPhone: string;
/** 企业名称 */
companyName: string;
/** 统一社会信用代码 */
licenseNumber: string;
/** 地址 */
address: string;
/** 企业简介 */
intro: string;
/** 域名 */
domain: string;
/** 备注 */
remark: string;
/** 租户套餐编号 */
packageId: CommonType.IdType;
/** 过期时间 */
expireTime: string;
/** 用户数量(-1不限制) */
accountCount: number;
/** 租户状态(0正常 1停用) */
status: Common.EnableStatus;
/** 删除标志(0代表存在 1代表删除) */
delFlag: string;
}>;
/** tenant search params */
type TenantSearchParams = CommonType.RecordNullable<
Pick<Api.System.Tenant, 'tenantId' | 'contactUserName' | 'contactPhone' | 'companyName'> &
Api.Common.CommonSearchParams
>;
/** tenant operate params */
type TenantOperateParams = CommonType.RecordNullable<
Pick<
Api.System.Tenant,
| 'id'
| 'tenantId'
| 'contactUserName'
| 'contactPhone'
| 'companyName'
| 'licenseNumber'
| 'address'
| 'intro'
| 'domain'
| 'remark'
| 'packageId'
| 'expireTime'
| 'accountCount'
| 'status'
> & {
username: string;
password: string;
}
>;
/** tenant package sync params */
type TenantPackageSyncParams = CommonType.RecordNullable<Pick<Api.System.Tenant, 'tenantId' | 'packageId'>>;
/** tenant list */
type TenantList = Api.Common.PaginatingQueryRecord<Tenant>;
/** tenant package */
type TenantPackage = Common.CommonRecord<{
/** 租户套餐id */
packageId: CommonType.IdType;
/** 套餐名称 */
packageName: string;
/** 关联菜单id */
menuIds: CommonType.IdType[];
/** 备注 */
remark: string;
/** 菜单树选择项是否关联显示 */
menuCheckStrictly: boolean;
/** 状态(0正常 1停用) */
status: Common.EnableStatus;
/** 删除标志(0代表存在 1代表删除) */
delFlag: string;
}>;
/** tenant package search params */
type TenantPackageSearchParams = CommonType.RecordNullable<
Pick<Api.System.TenantPackage, 'packageName' | 'menuIds' | 'menuCheckStrictly' | 'status'> &
Api.Common.CommonSearchParams
>;
/** tenant package operate params */
type TenantPackageOperateParams = CommonType.RecordNullable<
Pick<
Api.System.TenantPackage,
'packageId' | 'packageName' | 'menuIds' | 'remark' | 'menuCheckStrictly' | 'status'
>
>;
/** tenant package list */
type TenantPackageList = Api.Common.PaginatingQueryRecord<TenantPackage>;
/** tenant package select list */
type TenantPackageSelectList = Common.CommonRecord<Pick<TenantPackage, 'packageId' | 'packageName'>>;
/** 通知公告类型 */ /** 通知公告类型 */
type NoticeType = '1' | '2'; type NoticeType = '1' | '2';
-50
View File
@@ -926,56 +926,6 @@ declare namespace App {
selectDataScope: string; selectDataScope: string;
selectDeptPermission: string; selectDeptPermission: string;
}; };
tenant: {
title: string;
tenantName: string;
tenantId: string;
contactUserName: string;
contactPhone: string;
companyName: string;
licenseNumber: string;
address: string;
intro: string;
domain: string;
packageId: string;
expireTime: string;
accountCount: string;
status: string;
createTime: string;
form: {
tenantName: FormMsg;
contactUserName: FormMsg;
contactPhone: FormMsg;
companyName: FormMsg;
licenseNumber: FormMsg;
address: FormMsg;
intro: FormMsg;
domain: FormMsg;
packageId: FormMsg;
expireTime: FormMsg;
accountCount: FormMsg;
status: FormMsg;
};
addTenant: string;
editTenant: string;
};
tenantPackage: {
title: string;
packageName: string;
menuIds: string;
remark: string;
status: string;
createTime: string;
form: {
packageName: FormMsg;
menuIds: FormMsg;
status: FormMsg;
remark: FormMsg;
};
addTenantPackage: string;
editTenantPackage: string;
statusChangeSuccess: string;
};
user: { user: {
title: string; title: string;
userName: string; userName: string;
-14
View File
@@ -51,17 +51,11 @@ declare module 'vue' {
IconMaterialSymbolsUploadRounded: typeof import('~icons/material-symbols/upload-rounded')['default'] IconMaterialSymbolsUploadRounded: typeof import('~icons/material-symbols/upload-rounded')['default']
IconMaterialSymbolsWarningOutlineRounded: typeof import('~icons/material-symbols/warning-outline-rounded')['default'] IconMaterialSymbolsWarningOutlineRounded: typeof import('~icons/material-symbols/warning-outline-rounded')['default']
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default'] IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
IconMdiArrowRightThick: typeof import('~icons/mdi/arrow-right-thick')['default']
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default'] IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
IconMdiCheckCircleOutline: typeof import('~icons/mdi/check-circle-outline')['default']
IconMdiDrag: typeof import('~icons/mdi/drag')['default'] IconMdiDrag: typeof import('~icons/mdi/drag')['default']
IconMdiGithub: typeof import('~icons/mdi/github')['default'] IconMdiGithub: typeof import('~icons/mdi/github')['default']
IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default'] IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default'] IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
IconMdiLoading: typeof import('~icons/mdi/loading')['default']
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
IconMdiShieldAlertOutline: typeof import('~icons/mdi/shield-alert-outline')['default']
IconMdiShieldCheckOutline: typeof import('~icons/mdi/shield-check-outline')['default']
IconOcticonPin16: typeof import('~icons/octicon/pin16')['default'] IconOcticonPin16: typeof import('~icons/octicon/pin16')['default']
IconOcticonPinSlash16: typeof import('~icons/octicon/pin-slash16')['default'] IconOcticonPinSlash16: typeof import('~icons/octicon/pin-slash16')['default']
IconQuillCollapse: typeof import('~icons/quill/collapse')['default'] IconQuillCollapse: typeof import('~icons/quill/collapse')['default']
@@ -159,7 +153,6 @@ declare module 'vue' {
TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default'] TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
TableRowCheckAlert: typeof import('./../components/advanced/table-row-check-alert.vue')['default'] TableRowCheckAlert: typeof import('./../components/advanced/table-row-check-alert.vue')['default']
TableSiderLayout: typeof import('./../components/advanced/table-sider-layout.vue')['default'] TableSiderLayout: typeof import('./../components/advanced/table-sider-layout.vue')['default']
TenantSelect: typeof import('./../components/custom/tenant-select.vue')['default']
ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default'] ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
UserSelect: typeof import('./../components/custom/user-select.vue')['default'] UserSelect: typeof import('./../components/custom/user-select.vue')['default']
WangEditor: typeof import('./../components/custom/wang-editor.vue')['default'] WangEditor: typeof import('./../components/custom/wang-editor.vue')['default']
@@ -208,17 +201,11 @@ declare global {
const IconMaterialSymbolsUploadRounded: typeof import('~icons/material-symbols/upload-rounded')['default'] const IconMaterialSymbolsUploadRounded: typeof import('~icons/material-symbols/upload-rounded')['default']
const IconMaterialSymbolsWarningOutlineRounded: typeof import('~icons/material-symbols/warning-outline-rounded')['default'] const IconMaterialSymbolsWarningOutlineRounded: typeof import('~icons/material-symbols/warning-outline-rounded')['default']
const IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default'] const IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
const IconMdiArrowRightThick: typeof import('~icons/mdi/arrow-right-thick')['default']
const IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default'] const IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
const IconMdiCheckCircleOutline: typeof import('~icons/mdi/check-circle-outline')['default']
const IconMdiDrag: typeof import('~icons/mdi/drag')['default'] const IconMdiDrag: typeof import('~icons/mdi/drag')['default']
const IconMdiGithub: typeof import('~icons/mdi/github')['default'] const IconMdiGithub: typeof import('~icons/mdi/github')['default']
const IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default'] const IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
const IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default'] const IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
const IconMdiLoading: typeof import('~icons/mdi/loading')['default']
const IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
const IconMdiShieldAlertOutline: typeof import('~icons/mdi/shield-alert-outline')['default']
const IconMdiShieldCheckOutline: typeof import('~icons/mdi/shield-check-outline')['default']
const IconOcticonPin16: typeof import('~icons/octicon/pin16')['default'] const IconOcticonPin16: typeof import('~icons/octicon/pin16')['default']
const IconOcticonPinSlash16: typeof import('~icons/octicon/pin-slash16')['default'] const IconOcticonPinSlash16: typeof import('~icons/octicon/pin-slash16')['default']
const IconQuillCollapse: typeof import('~icons/quill/collapse')['default'] const IconQuillCollapse: typeof import('~icons/quill/collapse')['default']
@@ -316,7 +303,6 @@ declare global {
const TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default'] const TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
const TableRowCheckAlert: typeof import('./../components/advanced/table-row-check-alert.vue')['default'] const TableRowCheckAlert: typeof import('./../components/advanced/table-row-check-alert.vue')['default']
const TableSiderLayout: typeof import('./../components/advanced/table-sider-layout.vue')['default'] const TableSiderLayout: typeof import('./../components/advanced/table-sider-layout.vue')['default']
const TenantSelect: typeof import('./../components/custom/tenant-select.vue')['default']
const ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default'] const ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
const UserSelect: typeof import('./../components/custom/user-select.vue')['default'] const UserSelect: typeof import('./../components/custom/user-select.vue')['default']
const WangEditor: typeof import('./../components/custom/wang-editor.vue')['default'] const WangEditor: typeof import('./../components/custom/wang-editor.vue')['default']
-4
View File
@@ -48,8 +48,6 @@ declare module "@elegant-router/types" {
"system_oss-config": "/system/oss-config"; "system_oss-config": "/system/oss-config";
"system_post": "/system/post"; "system_post": "/system/post";
"system_role": "/system/role"; "system_role": "/system/role";
"system_tenant": "/system/tenant";
"system_tenant-package": "/system/tenant-package";
"system_user": "/system/user"; "system_user": "/system/user";
"tool": "/tool"; "tool": "/tool";
"tool_gen": "/tool/gen"; "tool_gen": "/tool/gen";
@@ -144,8 +142,6 @@ declare module "@elegant-router/types" {
| "system_oss" | "system_oss"
| "system_post" | "system_post"
| "system_role" | "system_role"
| "system_tenant-package"
| "system_tenant"
| "system_user" | "system_user"
| "tool_gen" | "tool_gen"
>; >;
+5 -20
View File
@@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, reactive, ref } from 'vue'; import { computed, reactive, ref } from 'vue';
import type { SelectOption } from 'naive-ui';
import { useLoading } from '@sa/hooks'; import { useLoading } from '@sa/hooks';
import CryptoJS from 'crypto-js'; import CryptoJS from 'crypto-js';
import { fetchCaptchaCode } from '@/service/api'; import { fetchCaptchaCode } from '@/service/api';
@@ -22,34 +21,28 @@ const authStore = useAuthStore();
const { toggleLoginModule } = useRouterPush(); const { toggleLoginModule } = useRouterPush();
const { formRef, validate } = useNaiveForm(); const { formRef, validate } = useNaiveForm();
const { loading: codeLoading, startLoading: startCodeLoading, endLoading: endCodeLoading } = useLoading(); const { loading: codeLoading, startLoading: startCodeLoading, endLoading: endCodeLoading } = useLoading();
const { loading: tenantLoading } = useLoading();
const codeUrl = ref<string>(); const codeUrl = ref<string>();
const captchaEnabled = ref<boolean>(false); const captchaEnabled = ref<boolean>(false);
const registerEnabled = ref<boolean>(false); const registerEnabled = ref<boolean>(false);
const remberMe = ref<boolean>(false); const remberMe = ref<boolean>(false);
const tenantEnabled = ref<boolean>(false);
const tenantOption = ref<SelectOption[]>([]);
const model: Api.Auth.PwdLoginForm = reactive({ const model: Api.Auth.PwdLoginForm = reactive({
tenantId: '000000', tenantId: '000000',
username: 'admin', username: 'admin',
password: 'admin123' password: 'admin123'
}); });
type RuleKey = Extract<keyof Api.Auth.PwdLoginForm, 'username' | 'password' | 'code' | 'tenantId'>; type RuleKey = Extract<keyof Api.Auth.PwdLoginForm, 'username' | 'password' | 'code'>;
const rules = computed<Record<RuleKey, App.Global.FormRule[]>>(() => { const rules = computed<Record<RuleKey, App.Global.FormRule[]>>(() => {
// inside computed to make locale reactive, if not apply i18n, you can define it without computed // inside computed to make locale reactive, if not apply i18n, you can define it without computed
const { formRules, createRequiredRule } = useFormRules(); const { createRequiredRule } = useFormRules();
const loginRules: Record<RuleKey, App.Global.FormRule[]> = { const loginRules: Record<RuleKey, App.Global.FormRule[]> = {
username: [createRequiredRule($t('form.userName.required'))], username: [createRequiredRule($t('form.userName.required'))],
password: [createRequiredRule($t('form.pwd.required'))], password: [createRequiredRule($t('form.pwd.required'))],
code: captchaEnabled.value ? [createRequiredRule($t('form.code.required'))] : [], code: captchaEnabled.value ? [createRequiredRule($t('form.code.required'))] : []
tenantId: tenantEnabled.value ? formRules.tenantId : []
}; };
return loginRules; return loginRules;
@@ -59,8 +52,8 @@ async function handleSubmit() {
await validate(); await validate();
// 勾选了需要记住密码设置在 localStorage 中设置记住用户名和密码 // 勾选了需要记住密码设置在 localStorage 中设置记住用户名和密码
if (remberMe.value) { if (remberMe.value) {
const { tenantId, username, password } = model; const { username, password } = model;
localStg.set('loginRember', encryptWithAes(JSON.stringify({ tenantId, username, password }), aesKey)); localStg.set('loginRember', encryptWithAes(JSON.stringify({ username, password }), aesKey));
} else { } else {
// 否则移除 // 否则移除
localStg.remove('loginRember'); localStg.remove('loginRember');
@@ -126,14 +119,6 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
:show-label="false" :show-label="false"
@keyup.enter="() => !authStore.loginLoading && handleSubmit()" @keyup.enter="() => !authStore.loginLoading && handleSubmit()"
> >
<NFormItem v-if="tenantEnabled" path="tenantId">
<NSelect
v-model:value="model.tenantId"
placeholder="请选择租户"
:options="tenantOption"
:loading="tenantLoading"
/>
</NFormItem>
<NFormItem path="username"> <NFormItem path="username">
<NInput v-model:value="model.username" :placeholder="$t('page.login.common.userNamePlaceholder')" /> <NInput v-model:value="model.username" :placeholder="$t('page.login.common.userNamePlaceholder')" />
</NFormItem> </NFormItem>
@@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, reactive, ref } from 'vue'; import { computed, reactive, ref } from 'vue';
import type { SelectOption } from 'naive-ui';
import { useLoading } from '@sa/hooks'; import { useLoading } from '@sa/hooks';
import { fetchCaptchaCode, fetchRegister } from '@/service/api'; import { fetchCaptchaCode, fetchRegister } from '@/service/api';
import { useRouterPush } from '@/hooks/common/router'; import { useRouterPush } from '@/hooks/common/router';
@@ -18,8 +17,6 @@ const { loading: registerLoading, startLoading: startRegisterLoading, endLoading
const codeUrl = ref<string>(); const codeUrl = ref<string>();
const captchaEnabled = ref<boolean>(false); const captchaEnabled = ref<boolean>(false);
const tenantEnabled = ref<boolean>(false);
const tenantOption = ref<SelectOption[]>([]);
const model: Api.Auth.RegisterForm = reactive({ const model: Api.Auth.RegisterForm = reactive({
tenantId: '000000', tenantId: '000000',
@@ -30,13 +27,12 @@ const model: Api.Auth.RegisterForm = reactive({
userType: 'sys_user' userType: 'sys_user'
}); });
type RuleKey = Extract<keyof Api.Auth.RegisterForm, 'username' | 'password' | 'confirmPassword' | 'code' | 'tenantId'>; type RuleKey = Extract<keyof Api.Auth.RegisterForm, 'username' | 'password' | 'confirmPassword' | 'code'>;
const rules = computed<Record<RuleKey, App.Global.FormRule[]>>(() => { const rules = computed<Record<RuleKey, App.Global.FormRule[]>>(() => {
const { formRules, createConfirmPwdRule, createRequiredRule } = useFormRules(); const { createConfirmPwdRule, createRequiredRule } = useFormRules();
return { return {
tenantId: tenantEnabled.value ? formRules.tenantId : [],
username: [createRequiredRule($t('form.userName.required'))], username: [createRequiredRule($t('form.userName.required'))],
password: [createRequiredRule($t('form.pwd.required'))], password: [createRequiredRule($t('form.pwd.required'))],
confirmPassword: createConfirmPwdRule(model.password!), confirmPassword: createConfirmPwdRule(model.password!),
@@ -100,9 +96,6 @@ handleFetchCaptchaCode();
:show-label="false" :show-label="false"
@keyup.enter="() => !registerLoading && handleSubmit()" @keyup.enter="() => !registerLoading && handleSubmit()"
> >
<NFormItem v-if="tenantEnabled" path="tenantId">
<NSelect v-model:value="model.tenantId" :options="tenantOption" :enabled="tenantEnabled" />
</NFormItem>
<NFormItem path="username"> <NFormItem path="username">
<NInput v-model:value="model.username" :placeholder="$t('page.login.common.userNamePlaceholder')" /> <NInput v-model:value="model.username" :placeholder="$t('page.login.common.userNamePlaceholder')" />
</NFormItem> </NFormItem>
-237
View File
@@ -1,237 +0,0 @@
<script setup lang="tsx">
import { ref } from 'vue';
import { NDivider } from 'naive-ui';
import {
fetchBatchDeleteTenantPackage,
fetchGetTenantPackageList,
fetchUpdateTenantPackageStatus
} from '@/service/api/system/tenant-package';
import { useAppStore } from '@/store/modules/app';
import { useAuth } from '@/hooks/business/auth';
import { useDownload } from '@/hooks/business/download';
import { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table';
import { useDict } from '@/hooks/business/dict';
import { $t } from '@/locales';
import ButtonIcon from '@/components/custom/button-icon.vue';
import TableHeaderOperation from '@/components/advanced/table-header-operation.vue';
import StatusSwitch from '@/components/custom/status-switch.vue';
import TenantPackageSearch from './modules/tenant-package-search.vue';
import TenantPackageOperateDrawer from './modules/tenant-package-operate-drawer.vue';
defineOptions({
name: 'TenantPackageList'
});
const appStore = useAppStore();
const { download } = useDownload();
const { hasAuth } = useAuth();
useDict('sys_normal_disable', false);
const searchParams = ref<Api.System.TenantPackageSearchParams>({
pageNum: 1,
pageSize: 10,
packageName: null,
status: null,
params: {}
});
const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagination, scrollX } =
useNaivePaginatedTable({
api: () => fetchGetTenantPackageList(searchParams.value),
transform: response => defaultTransform(response),
onPaginationParamsChange: params => {
searchParams.value.pageNum = params.page;
searchParams.value.pageSize = params.pageSize;
},
columns: () => [
{
type: 'selection',
align: 'center',
width: 48
},
{
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
},
{
key: 'packageName',
title: $t('page.system.tenantPackage.packageName'),
align: 'center',
minWidth: 120
},
{
key: 'status',
title: $t('page.system.tenantPackage.status'),
align: 'center',
minWidth: 120,
render: row => {
return (
<StatusSwitch
v-model:value={row.status}
info={row.packageName}
onSubmitted={(value, callback) => handleStatusChange(row, value, callback)}
/>
);
}
},
{
key: 'remark',
title: $t('page.system.tenantPackage.remark'),
align: 'center',
minWidth: 120
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render: row => {
const divider = () => {
if (!hasAuth('system:tenantPackage:edit') || !hasAuth('system:tenantPackage:remove')) {
return null;
}
return <NDivider vertical />;
};
const editBtn = () => {
if (!hasAuth('system:tenantPackage:edit')) {
return null;
}
return (
<ButtonIcon
text
type="primary"
icon="material-symbols:drive-file-rename-outline-outline"
tooltipContent={$t('common.edit')}
onClick={() => edit(row.packageId!)}
/>
);
};
const deleteBtn = () => {
if (!hasAuth('system:tenantPackage:remove')) {
return null;
}
return (
<ButtonIcon
text
type="error"
icon="material-symbols:delete-outline"
tooltipContent={$t('common.delete')}
popconfirmContent={$t('common.confirmDelete')}
onPositiveClick={() => handleDelete(row.packageId!)}
/>
);
};
return (
<div class="flex-center gap-8px">
{editBtn()}
{divider()}
{deleteBtn()}
</div>
);
}
}
]
});
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
useTableOperate(data, 'packageId', getData);
async function handleBatchDelete() {
// request
const { error } = await fetchBatchDeleteTenantPackage(checkedRowKeys.value);
if (error) return;
onBatchDeleted();
}
async function handleDelete(packageId: CommonType.IdType) {
// request
const { error } = await fetchBatchDeleteTenantPackage([packageId]);
if (error) return;
onDeleted();
}
function edit(packageId: CommonType.IdType) {
handleEdit(packageId);
}
function handleExport() {
download(
'/system/tenant/package/export',
searchParams,
`${$t('page.system.tenantPackage.title')}_${new Date().getTime()}.xlsx`
);
}
/** 处理状态切换 */
async function handleStatusChange(
row: Api.System.TenantPackage,
value: Api.Common.EnableStatus,
callback: (flag: boolean) => void
) {
const { error } = await fetchUpdateTenantPackageStatus({
packageId: row.packageId,
status: value
});
callback(!error);
if (!error) {
window.$message?.success($t('page.system.tenantPackage.statusChangeSuccess'));
getData();
}
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<TenantPackageSearch v-model:model="searchParams" @search="getDataByPage" />
<NCard
:title="$t('page.system.tenantPackage.title')"
:bordered="false"
size="small"
class="card-wrapper sm:flex-1-hidden"
>
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
:show-add="hasAuth('system:tenantPackage:add')"
:show-delete="hasAuth('system:tenantPackage:remove')"
:show-export="hasAuth('system:tenantPackage:export')"
@add="handleAdd"
@delete="handleBatchDelete"
@export="handleExport"
@refresh="getData"
/>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="scrollX"
:loading="loading"
remote
:row-key="row => row.packageId"
:pagination="mobilePagination"
class="sm:h-full"
/>
<TenantPackageOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getData"
/>
</NCard>
</div>
</template>
<style scoped></style>
@@ -1,175 +0,0 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { jsonClone } from '@sa/utils';
import { useLoading } from '@sa/hooks';
import { fetchCreateTenantPackage, fetchUpdateTenantPackage } from '@/service/api/system/tenant-package';
import { fetchGetTenantPackageMenuTreeSelect } from '@/service/api/system/menu';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import MenuTree from '@/components/custom/menu-tree.vue';
import { $t } from '@/locales';
defineOptions({
name: 'TenantPackageOperateDrawer'
});
interface Props {
/** the type of operation */
operateType: NaiveUI.TableOperateType;
/** the edit row data */
rowData?: Api.System.TenantPackage | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const menuTreeRef = ref<InstanceType<typeof MenuTree> | null>(null);
const menuOptions = ref<Api.System.MenuList>([]);
const { loading: menuLoading, startLoading: startMenuLoading, endLoading: stopMenuLoading } = useLoading();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate, restoreValidation } = useNaiveForm();
const { createRequiredRule } = useFormRules();
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
add: $t('page.system.tenantPackage.addTenantPackage'),
edit: $t('page.system.tenantPackage.editTenantPackage')
};
return titles[props.operateType];
});
type Model = Api.System.TenantPackageOperateParams;
const model = ref<Model>(createDefaultModel());
function createDefaultModel(): Model {
return {
packageName: '',
menuIds: [],
remark: '',
menuCheckStrictly: true
};
}
type RuleKey = Extract<keyof Model, 'packageId' | 'packageName'>;
const rules: Record<RuleKey, App.Global.FormRule> = {
packageId: createRequiredRule($t('page.system.tenantPackage.form.packageName.invalid')),
packageName: createRequiredRule($t('page.system.tenantPackage.form.packageName.required'))
};
async function handleUpdateModelWhenEdit() {
menuOptions.value = [];
model.value = createDefaultModel();
model.value.menuIds = [];
if (props.operateType === 'add') {
const { data, error } = await fetchGetTenantPackageMenuTreeSelect(0);
if (error) return;
model.value.menuIds = data.checkedKeys || [];
menuOptions.value = data.menus;
return;
}
if (props.operateType === 'edit' && props.rowData) {
startMenuLoading();
Object.assign(model.value, jsonClone(props.rowData));
const { data, error } = await fetchGetTenantPackageMenuTreeSelect(model.value.packageId!);
if (error) return;
model.value.menuIds = data.checkedKeys || [];
menuOptions.value = data.menus;
stopMenuLoading();
}
}
function closeDrawer() {
visible.value = false;
}
async function handleSubmit() {
await validate();
const { packageId, packageName, remark, menuCheckStrictly } = model.value;
const menuIds = menuTreeRef.value?.getCheckedMenuIds();
// request
if (props.operateType === 'add') {
const { error } = await fetchCreateTenantPackage({ packageName, menuIds, remark, menuCheckStrictly });
if (error) return;
window.$message?.success($t('common.addSuccess'));
}
if (props.operateType === 'edit') {
const { error } = await fetchUpdateTenantPackage({
packageId,
packageName,
menuIds,
remark,
menuCheckStrictly
});
if (error) return;
window.$message?.success($t('common.updateSuccess'));
}
closeDrawer();
emit('submitted');
}
watch(visible, () => {
if (visible.value) {
handleUpdateModelWhenEdit();
restoreValidation();
}
});
</script>
<template>
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="800" class="max-w-90%">
<NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules">
<NFormItem :label="$t('page.system.tenantPackage.packageName')" path="packageName">
<NInput
v-model:value="model.packageName"
:placeholder="$t('page.system.tenantPackage.form.packageName.required')"
/>
</NFormItem>
<NFormItem :label="$t('page.system.tenantPackage.menuIds')" path="menuIds">
<MenuTree
v-if="visible"
ref="menuTreeRef"
v-model:checked-keys="model.menuIds"
v-model:options="menuOptions"
v-model:cascade="model.menuCheckStrictly"
v-model:loading="menuLoading"
:immediate="false"
/>
</NFormItem>
<NFormItem :label="$t('page.system.tenantPackage.remark')" path="remark">
<NInput
v-model:value="model.remark"
:placeholder="$t('page.system.tenantPackage.form.remark.required')"
type="textarea"
/>
</NFormItem>
</NForm>
<template #footer>
<NSpace :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
</NSpace>
</template>
</NDrawerContent>
</NDrawer>
</template>
<style scoped></style>
@@ -1,95 +0,0 @@
<script setup lang="ts">
import { toRaw } from 'vue';
import { jsonClone } from '@sa/utils';
import { useDict } from '@/hooks/business/dict';
import { useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
defineOptions({
name: 'TenantPackageSearch'
});
interface Emits {
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const { options: sysNormalDisableOptions } = useDict('sys_normal_disable', false);
const model = defineModel<Api.System.TenantPackageSearchParams>('model', { required: true });
const defaultModel = jsonClone(toRaw(model.value));
function resetModel() {
Object.assign(model.value, defaultModel);
}
async function reset() {
await restoreValidation();
resetModel();
emit('search');
}
async function search() {
await validate();
emit('search');
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<NCollapse>
<NCollapseItem :title="$t('common.search')" name="user-search">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive>
<NFormItemGi
span="24 s:12 m:8"
:label="$t('page.system.tenantPackage.packageName')"
path="packageName"
class="pr-24px"
>
<NInput
v-model:value="model.packageName"
:placeholder="$t('page.system.tenantPackage.form.packageName.required')"
/>
</NFormItemGi>
<NFormItemGi
span="24 s:12 m:8"
:label="$t('page.system.tenantPackage.status')"
path="status"
class="pr-24px"
>
<NSelect
v-model:value="model.status"
:placeholder="$t('page.system.tenantPackage.form.status.required')"
:options="sysNormalDisableOptions"
clearable
/>
</NFormItemGi>
<NFormItemGi span="24 s:24 m:8" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCollapseItem>
</NCollapse>
</NCard>
</template>
<style scoped></style>
-302
View File
@@ -1,302 +0,0 @@
<script setup lang="tsx">
import { computed, ref } from 'vue';
import { NButton, NDivider } from 'naive-ui';
import {
fetchBatchDeleteTenant,
fetchGetTenantList,
fetchSyncTenantConfig,
fetchSyncTenantDict,
fetchSyncTenantPackage
} from '@/service/api/system/tenant';
import { useAppStore } from '@/store/modules/app';
import { useAuthStore } from '@/store/modules/auth';
import { useAuth } from '@/hooks/business/auth';
import { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table';
import { useDownload } from '@/hooks/business/download';
import { useDict } from '@/hooks/business/dict';
import DictTag from '@/components/custom/dict-tag.vue';
import { $t } from '@/locales';
import ButtonIcon from '@/components/custom/button-icon.vue';
import TenantOperateDrawer from './modules/tenant-operate-drawer.vue';
import TenantSearch from './modules/tenant-search.vue';
defineOptions({
name: 'TenantList'
});
useDict('sys_normal_disable');
const appStore = useAppStore();
const { download } = useDownload();
const { hasAuth } = useAuth();
const { userInfo } = useAuthStore();
const isSuperAdmin = computed(() => {
return userInfo.user?.userId === 1;
});
const searchParams = ref<Api.System.TenantSearchParams>({
pageNum: 1,
pageSize: 10,
tenantId: null,
contactUserName: null,
contactPhone: null,
companyName: null,
params: {}
});
const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagination, scrollX } =
useNaivePaginatedTable({
api: () => fetchGetTenantList(searchParams.value),
transform: response => defaultTransform(response),
onPaginationParamsChange: params => {
searchParams.value.pageNum = params.page;
searchParams.value.pageSize = params.pageSize;
},
columns: () => [
{
type: 'selection',
align: 'center',
width: 48
},
{
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64,
render: (_, index) => index + 1
},
{
key: 'tenantId',
title: '租户编号',
align: 'center',
minWidth: 80
},
{
key: 'contactUserName',
title: '联系人',
align: 'center',
minWidth: 80
},
{
key: 'contactPhone',
title: '联系电话',
align: 'center',
minWidth: 120
},
{
key: 'companyName',
title: '企业名称',
align: 'center',
minWidth: 120
},
{
key: 'expireTime',
title: '过期时间',
align: 'center',
minWidth: 120
},
{
key: 'status',
title: '租户状态',
align: 'center',
minWidth: 120,
render(row) {
return <DictTag size="small" value={row.status} dictCode="sys_normal_disable" />;
}
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 180,
render: row => {
if (row.tenantId === '000000') return null;
const editBtn = () => {
return (
<ButtonIcon
type="primary"
text
icon="material-symbols:drive-file-rename-outline-outline"
tooltipContent={$t('common.edit')}
onClick={() => edit(row.id!)}
/>
);
};
const syncBtn = () => {
return (
<ButtonIcon
text
type="primary"
icon="material-symbols:sync-outline"
tooltipContent="同步套餐"
popconfirmContent={`确认同步[${row.companyName}]的套餐吗?`}
onPositiveClick={() => handleSyncTenantPackage(row)}
/>
);
};
const deleteBtn = () => {
return (
<ButtonIcon
text
type="error"
icon="material-symbols:delete-outline"
tooltipContent={$t('common.delete')}
popconfirmContent={$t('common.confirmDelete')}
onPositiveClick={() => handleDelete(row.id!)}
/>
);
};
const buttons = [];
if (hasAuth('system:tenant:edit')) buttons.push(editBtn());
if (hasAuth('system:tenant:edit')) buttons.push(syncBtn());
if (hasAuth('system:tenant:delete')) buttons.push(deleteBtn());
return (
<div class="flex-center gap-8px">
{buttons.map((btn, index) => (
<>
{index !== 0 && <NDivider vertical />}
{btn}
</>
))}
</div>
);
}
}
]
});
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
useTableOperate(data, 'id', getData);
async function handleBatchDelete() {
// request
const { error } = await fetchBatchDeleteTenant(checkedRowKeys.value);
if (error) return;
onBatchDeleted();
}
async function handleDelete(id: CommonType.IdType) {
// request
const { error } = await fetchBatchDeleteTenant([id]);
if (error) return;
onDeleted();
}
async function edit(id: CommonType.IdType) {
handleEdit(id);
}
async function handleSyncTenantDict() {
window.$dialog?.warning({
title: '同步租户字典',
content: '确认同步租户字典吗?',
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
const { error } = await fetchSyncTenantDict();
if (error) return;
window.$message?.success('同步租户字典成功');
await getData();
}
});
}
async function handleSyncTenantConfig() {
window.$dialog?.warning({
title: '同步租户参数配置',
content: '确认同步租户参数配置吗?',
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
const { error } = await fetchSyncTenantConfig();
if (error) return;
window.$message?.success('同步租户参数配置成功');
await getData();
}
});
}
async function handleSyncTenantPackage(row: Api.System.Tenant) {
const params: Api.System.TenantPackageSyncParams = {
tenantId: row.tenantId,
packageId: row.packageId
};
const { error } = await fetchSyncTenantPackage(params);
if (error) return;
window.$message?.success('同步租户套餐成功');
await getData();
}
async function handleExport() {
download('/system/tenant/export', searchParams.value, `租户列表_${new Date().getTime()}.xlsx`);
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<TenantSearch v-model:model="searchParams" @search="getDataByPage" />
<NCard title="租户列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
:show-add="hasAuth('system:tenant:add')"
:show-delete="hasAuth('system:tenant:delete')"
:show-export="hasAuth('system:tenant:export')"
@add="handleAdd"
@delete="handleBatchDelete"
@export="handleExport"
@refresh="getData"
>
<template #prefix>
<NButton v-if="isSuperAdmin" ghost size="small" @click="handleSyncTenantDict">
<template #icon>
<icon-material-symbols-sync-outline />
</template>
同步租户字典
</NButton>
<NButton v-if="isSuperAdmin" ghost size="small" @click="handleSyncTenantConfig">
<template #icon>
<icon-material-symbols-sync-outline />
</template>
同步租户参数配置
</NButton>
</template>
</TableHeaderOperation>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="scrollX"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="mobilePagination"
class="sm:h-full"
/>
<TenantOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getData"
/>
</NCard>
</div>
</template>
<style scoped>
:deep(.n-card-header__main) {
min-width: 36px !important;
}
</style>
@@ -1,303 +0,0 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useLoading } from '@sa/hooks';
import { jsonClone } from '@sa/utils';
import { fetchCreateTenant, fetchUpdateTenant } from '@/service/api/system/tenant';
import { fetchGetTenantPackageSelectList } from '@/service/api/system/tenant-package';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
defineOptions({
name: 'TenantOperateDrawer'
});
interface Props {
/** the type of operation */
operateType: NaiveUI.TableOperateType;
/** the edit row data */
rowData?: Api.System.Tenant | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate, restoreValidation } = useNaiveForm();
const { createRequiredRule, patternRules } = useFormRules();
const { loading: packageLoading, startLoading: startPackageLoading, endLoading: endPackageLoading } = useLoading();
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
add: '新增租户',
edit: '编辑租户'
};
return titles[props.operateType];
});
type Model = Api.System.TenantOperateParams;
const model = ref<Model>(createDefaultModel());
function createDefaultModel(): Model {
return {
tenantId: '',
contactUserName: '',
contactPhone: '',
companyName: '',
licenseNumber: '',
address: '',
intro: '',
domain: '',
remark: '',
packageId: null,
expireTime: null,
accountCount: null,
status: '0',
username: '',
password: ''
};
}
type RuleKey = Extract<
keyof Model,
'id' | 'contactUserName' | 'contactPhone' | 'companyName' | 'packageId' | 'accountCount' | 'username' | 'password'
>;
const rules: Record<RuleKey, App.Global.FormRule | App.Global.FormRule[]> = {
id: createRequiredRule('id不能为空'),
contactUserName: createRequiredRule('联系人不能为空'),
contactPhone: [createRequiredRule('联系电话不能为空'), { ...patternRules.phone, trigger: ['blur', 'change'] }],
companyName: createRequiredRule('企业名称不能为空'),
packageId: createRequiredRule('租户套餐不能为空'),
accountCount: createRequiredRule('用户数量不能为空'),
username: [
createRequiredRule('管理员账号不能为空'),
{
min: 2,
max: 20,
message: '账号长度必须介于2-20之间',
trigger: ['blur', 'change']
}
],
password: [
createRequiredRule('管理员密码不能为空'),
{
min: 5,
max: 20,
message: '密码长度必须介于5-20之间',
trigger: ['blur', 'change']
}
]
};
/** the enabled package options */
const packageOptions = ref<CommonType.Option<CommonType.IdType>[]>([]);
async function getPackageOptions() {
startPackageLoading();
const { error, data } = await fetchGetTenantPackageSelectList();
if (!error) {
packageOptions.value = data.map(item => ({
label: item.packageName,
value: item.packageId
}));
}
endPackageLoading();
}
function handleUpdateModelWhenEdit() {
model.value = createDefaultModel();
if (props.operateType === 'edit' && props.rowData) {
Object.assign(model.value, jsonClone(props.rowData));
}
}
function closeDrawer() {
visible.value = false;
}
async function handleSubmit() {
await validate();
const {
id,
tenantId,
contactUserName,
contactPhone,
companyName,
username,
password,
licenseNumber,
address,
intro,
domain,
remark,
packageId,
expireTime,
accountCount,
status
} = model.value;
// request
if (props.operateType === 'add') {
const { error } = await fetchCreateTenant({
contactUserName,
contactPhone,
companyName,
username,
password,
licenseNumber,
address,
intro,
domain,
remark,
packageId,
expireTime,
accountCount,
status
});
if (error) return;
window.$message?.success($t('common.addSuccess'));
}
if (props.operateType === 'edit') {
const { error } = await fetchUpdateTenant({
id,
tenantId,
contactUserName,
contactPhone,
companyName,
licenseNumber,
address,
intro,
domain,
remark,
packageId,
expireTime,
accountCount,
status
});
if (error) return;
window.$message?.success($t('common.updateSuccess'));
}
closeDrawer();
emit('submitted');
}
watch(visible, () => {
if (visible.value) {
getPackageOptions();
handleUpdateModelWhenEdit();
restoreValidation();
}
});
</script>
<template>
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="800" class="max-w-90%">
<NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules">
<NDivider>基本信息</NDivider>
<NFormItem label="企业名称" path="companyName">
<NInput v-model:value="model.companyName" placeholder="请输入企业名称" />
</NFormItem>
<NFormItem label="联系人" path="contactUserName">
<NInput v-model:value="model.contactUserName" placeholder="请输入联系人" />
</NFormItem>
<NFormItem label="联系电话" path="contactPhone">
<NInput v-model:value="model.contactPhone" placeholder="请输入联系电话" />
</NFormItem>
<div v-if="props.operateType === 'add'">
<NDivider>管理员信息</NDivider>
<NFormItem label="管理员账号" path="username">
<NInput v-model:value="model.username" placeholder="请输入管理员账号" />
</NFormItem>
<NFormItem label="管理员密码" path="password">
<NInput
v-model:value="model.password"
type="password"
show-password-on="click"
placeholder="请输入管理员密码"
/>
</NFormItem>
</div>
<NDivider>租户设置</NDivider>
<NFormItem label="租户套餐" path="packageId">
<NSelect
v-model:value="model.packageId"
clearable
:disabled="props.operateType === 'edit'"
placeholder="请选择租户套餐"
:options="packageOptions"
:loading="packageLoading"
/>
</NFormItem>
<NFormItem label="过期时间" path="expireTime">
<NDatePicker
v-model:formatted-value="model.expireTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
clearable
class="w-full"
/>
</NFormItem>
<NFormItem path="accountCount">
<template #label>
<div class="flex-center">
<FormTip content="-1不限制用户数量" />
<span>用户数量</span>
</div>
</template>
<NInputNumber v-model:value="model.accountCount" placeholder="请输入用户数量" min="-1" class="w-full" />
</NFormItem>
<NFormItem path="domain">
<template #label>
<div class="flex-center">
<FormTip
content="可填写域名/端口 填写域名如: www.test.com 或者 www.test.com:8080 填写ip:端口如: 127.0.0.1:8080"
/>
<span>绑定域名</span>
</div>
</template>
<NInputGroup>
<NInputGroupLabel>http(s)://</NInputGroupLabel>
<NInput v-model:value="model.domain" placeholder="请输入" />
</NInputGroup>
</NFormItem>
<NFormItem label="租户状态" path="status">
<DictRadio v-model:value="model.status" dict-code="sys_normal_disable" />
</NFormItem>
<NDivider>企业信息</NDivider>
<NFormItem label="企业地址" path="address">
<NInput v-model:value="model.address" placeholder="请输入企业地址" />
</NFormItem>
<NFormItem label="企业代码" path="licenseNumber">
<NInput v-model:value="model.licenseNumber" placeholder="请输入企业代码" />
</NFormItem>
<NFormItem label="企业简介" path="intro">
<NInput v-model:value="model.intro" type="textarea" placeholder="请输入企业简介" />
</NFormItem>
<NFormItem label="备注" path="remark">
<NInput v-model:value="model.remark" type="textarea" placeholder="请输入备注" />
</NFormItem>
</NForm>
<template #footer>
<NSpace :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
</NSpace>
</template>
</NDrawerContent>
</NDrawer>
</template>
<style scoped></style>
@/service/api/system/tenant-package
@@ -1,80 +0,0 @@
<script setup lang="ts">
import { toRaw } from 'vue';
import { jsonClone } from '@sa/utils';
import { useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
defineOptions({
name: 'TenantSearch'
});
interface Emits {
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.System.TenantSearchParams>('model', { required: true });
const defaultModel = jsonClone(toRaw(model.value));
function resetModel() {
Object.assign(model.value, defaultModel);
}
async function reset() {
await restoreValidation();
resetModel();
emit('search');
}
async function search() {
await validate();
emit('search');
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<NCollapse>
<NCollapseItem :title="$t('common.search')" name="user-search">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" label="租户编号" path="tenantId" class="pr-24px">
<NInput v-model:value="model.tenantId as string" placeholder="请输入租户编号" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="联系人" path="contactUserName" class="pr-24px">
<NInput v-model:value="model.contactUserName" placeholder="请输入联系人" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="联系电话" path="contactPhone" class="pr-24px">
<NInput v-model:value="model.contactPhone" placeholder="请输入联系电话" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="企业名称" path="companyName" class="pr-24px">
<NInput v-model:value="model.companyName" placeholder="请输入企业名称" />
</NFormItemGi>
<NFormItemGi span="24" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCollapseItem>
</NCollapse>
</NCard>
</template>
<style scoped></style>