租户平台管理

This commit is contained in:
zcx
2026-01-29 16:47:05 +08:00
parent 6db7edfc2f
commit 75fc4de6bd
29 changed files with 3998 additions and 41 deletions

View File

@@ -4,11 +4,11 @@ VITE_NODE_ENV=development
VITE_DEV=true VITE_DEV=true
# 请求路径 # 请求路径
# VITE_BASE_URL='http://localhost:48080' VITE_BASE_URL='http://localhost:48080'
# VITE_BASE_URL='http://47.98.161.246:48080' # VITE_BASE_URL='http://47.98.161.246:48080'
# VITE_BASE_URL='http://114.55.169.15:48080' # VITE_BASE_URL='http://114.55.169.15:48080'
VITE_BASE_URL='http://114.215.207.150:48080' # VITE_BASE_URL='http://114.215.207.150:48080'
# 上传路径 # 上传路径
VITE_UPLOAD_URL='http://47.98.161.246:48080/admin-api/system/file/upload' VITE_UPLOAD_URL='http://47.98.161.246:48080/admin-api/system/file/upload'

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"i18n-ally.localesPaths": [
"src/locales"
]
}

View File

@@ -24,8 +24,8 @@ export const getServiceAppList = () => {
} }
// 查询菜单(精简)列表 // 查询菜单(精简)列表
export const getSimpleMenusList = () => { export const getSimpleMenusList = (params?) => {
return request.get({ url: '/admin-api/system/menu/list-all-simple' }) return request.get({ url: '/admin-api/system/menu/list-all-simple', params })
} }
// 查询菜单列表 // 查询菜单列表

View File

@@ -0,0 +1,76 @@
import request from '@/config/axios'
export interface TenantVO {
id: number
name: string
contactName: string
contactMobile: string
status: number
domain: string
packageId: number
username: string
password: string
expireTime: Date
accountCount: number
websites: string[]
createTime: Date
}
export interface TenantPageReqVO extends PageParam {
name?: string
contactName?: string
contactMobile?: string
status?: number
createTime?: Date[]
}
export interface TenantExportReqVO {
name?: string
contactName?: string
contactMobile?: string
status?: number
createTime?: Date[]
}
// 查询租户列表
export const getTenantPage = (params: TenantPageReqVO) => {
return request.get({ url: '/admin-api/system/tenant/page', params })
}
// 查询租户详情
export const getTenant = (id: number) => {
return request.get({ url: '/admin-api/system/tenant/get?id=' + id })
}
// 获取租户精简信息列表
export const getTenantList = () => {
return request.get({ url: '/admin-api/system/tenant/simple-list' })
}
// 新增租户
export const createTenant = (data: TenantVO) => {
return request.post({ url: '/admin-api/system/tenant/create', data })
}
// 修改租户
export const updateTenant = (data: TenantVO) => {
return request.put({ url: '/admin-api/system/tenant/update', data })
}
// 删除租户
export const deleteTenant = (id: number) => {
return request.delete({ url: '/admin-api/system/tenant/delete?id=' + id })
}
// 批量删除租户
export const deleteTenantList = (ids: number[]) => {
return request.delete({
url: '/admin-api/system/tenant/delete-list',
params: { ids: ids.join(',') }
})
}
// 导出租户
export const exportTenant = (params: TenantExportReqVO) => {
return request.download({ url: '/admin-api/system/tenant/export-excel', params })
}

View File

@@ -0,0 +1,77 @@
import request from '@/config/axios'
export interface TenantInstanceVO {
instanceId: number
tenantId: number
serviceId: number
instanceName: string
menuIds: string
instanceType: number
instanceIcon: string
dbUrl: string
dbName: string
dbUsername: string
dbPassword: string
status: number
creator: string
createTime: Date
updater: string
updateTime: Date
deleted: boolean
}
export interface TenantInstancePageReqVO extends PageParam {
tenantId?: number
serviceId?: number
instanceName?: string
status?: number
createTime?: Date[]
}
export interface TenantInstanceExportReqVO {
tenantId?: number
serviceId?: number
instanceName?: string
status?: number
createTime?: Date[]
}
// 查询租户实例列表
export const getTenantInstancePage = (params: TenantInstancePageReqVO) => {
return request.get({ url: '/admin-api/system/serviceInstance/all/page', params })
}
// 查询租户实例详情
export const getTenantInstance = (instanceId: number) => {
return request.get({ url: '/admin-api/system/serviceInstance/get?instanceId=' + instanceId })
}
// 新增租户实例
export const createTenantInstance = (data: TenantInstanceVO) => {
return request.post({ url: '/admin-api/system/serviceInstance/create', data })
}
// 修改租户实例
export const updateTenantInstance = (data: TenantInstanceVO) => {
return request.put({ url: '/admin-api/system/serviceInstance/update', data })
}
// 删除租户实例
export const deleteTenantInstance = (instanceId: number) => {
return request.delete({
url: '/admin-api/system/serviceInstance/delete?instanceId=' + instanceId
})
}
// 批量删除租户实例
export const deleteTenantInstanceList = (instanceIds: number[]) => {
return request.delete({
url: '/admin-api/system/serviceInstance/delete-list',
params: { instanceIds: instanceIds.join(',') }
})
}
// 导出租户实例
export const exportTenantInstance = (params: TenantInstanceExportReqVO) => {
return request.download({ url: '/admin-api/system/serviceInstance/export-excel', params })
}

View File

@@ -0,0 +1,51 @@
import request from '@/config/axios'
export interface TenantPackageVO {
id: number
name: string
status: number
remark: string
creator: string
updater: string
updateTime: string
menuIds: number[]
createTime: Date
}
// 查询租户套餐列表
export const getTenantPackagePage = (params: PageParam) => {
return request.get({ url: '/admin-api/system/tenant-package/page', params })
}
// 获得租户套餐
export const getTenantPackage = (id: number) => {
return request.get({ url: '/admin-api/system/tenant-package/get?id=' + id })
}
// 新增租户套餐
export const createTenantPackage = (data: TenantPackageVO) => {
return request.post({ url: '/admin-api/system/tenant-package/create', data })
}
// 修改租户套餐
export const updateTenantPackage = (data: TenantPackageVO) => {
return request.put({ url: '/admin-api/system/tenant-package/update', data })
}
// 删除租户套餐
export const deleteTenantPackage = (id: number) => {
return request.delete({ url: '/admin-api/system/tenant-package/delete?id=' + id })
}
// 批量删除租户套餐
export const deleteTenantPackageList = (ids: number[]) => {
return request.delete({
url: '/admin-api/system/tenant-package/delete-list',
params: { ids: ids.join(',') }
})
}
// 获取租户套餐精简信息列表
export const getTenantPackageList = () => {
return request.get({ url: '/admin-api/system/tenant-package/simple-list' })
}

View File

@@ -55,7 +55,7 @@ export default defineComponent({
// 注册 // 注册
onMounted(() => { onMounted(() => {
const tableRef = unref(elTableRef) const tableRef = unref(elTableRef)
emit('register', tableRef?.$parent, elTableRef) emit('register', tableRef?.$parent, elTableRef.value)
}) })
const pageSizeRef = ref(props.pageSize) const pageSizeRef = ref(props.pageSize)

View File

@@ -33,23 +33,23 @@ const { t } = useI18n()
} }
**/ **/
const remainingRouter: AppRouteRecordRaw[] = [ const remainingRouter: AppRouteRecordRaw[] = [
{ // {
path: '/redirect', // path: '/redirect',
component: Layout, // component: Layout,
name: 'Redirect', // name: 'Redirect',
children: [ // children: [
{ // {
path: '/redirect/:path(.*)', // path: '/redirect/:path(.*)',
name: 'Redirect', // name: 'Redirect',
component: () => import('@/views/Redirect/Redirect.vue'), // component: () => import('@/views/Redirect/Redirect.vue'),
meta: {} // meta: {}
} // }
], // ],
meta: { // meta: {
hidden: true, // hidden: true,
noTagsView: true // noTagsView: true
} // }
}, // },
{ {
path: '/', path: '/',
component: Layout, component: Layout,
@@ -107,17 +107,17 @@ const remainingRouter: AppRouteRecordRaw[] = [
] ]
}, },
// { // {
// path: '/Basic', // path: '/system',
// component: Layout, // component: Layout,
// name: 'Basic', // name: 'system',
// meta: { // meta: {
// title: '菜单管理' // title: '系统管理'
// }, // },
// redirect: '/Basic/menu', // redirect: 'system/menu',
// children: [ // children: [
// { // {
// path: 'menu', // path: 'menu',
// component: () => import('@/views/Basic/Menu/index.vue'), // component: () => import('@/views/system/Menu/index.vue'),
// name: 'Menu', // name: 'Menu',
// meta: { // meta: {
// canTo: true, // canTo: true,

View File

@@ -172,7 +172,7 @@ export const useAppStore = defineStore('app', {
}, },
getFooter(): boolean { getFooter(): boolean {
return this.footer return this.footer
}, }
}, },
actions: { actions: {
setBreadcrumb(breadcrumb: boolean) { setBreadcrumb(breadcrumb: boolean) {
@@ -264,7 +264,7 @@ export const useAppStore = defineStore('app', {
}, },
setFooter(footer: boolean) { setFooter(footer: boolean) {
this.footer = footer this.footer = footer
}, }
} }
}) })

View File

@@ -2,7 +2,7 @@ import { store } from '../index'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { getAccessToken, removeToken } from '@/utils/auth' import { getAccessToken, removeToken } from '@/utils/auth'
import { CACHE_KEY } from '@/hooks/web/useCache' import { CACHE_KEY } from '@/hooks/web/useCache'
import { loginOut } from '@/api/login' import { getInfo, loginOut } from '@/api/login'
import { getUserProfile as getUserInfo } from '@/api/system/user/profile' import { getUserProfile as getUserInfo } from '@/api/system/user/profile'
import cache from '@/plugins/cache' import cache from '@/plugins/cache'
@@ -51,16 +51,16 @@ export const useUserStore = defineStore('admin-user', {
return null return null
} }
let userInfo = cache.local.get(CACHE_KEY.USER) let userInfo = cache.local.get(CACHE_KEY.USER)
// if (!userInfo || !userInfo?.menus || userInfo.menus.length == 0) { if (!userInfo || !userInfo?.menus || userInfo.menus.length == 0) {
// userInfo = await getInfo({}) userInfo = await getInfo({})
}
// if (!userInfo?.user) {
// userInfo = {
// user: await getUserInfo()
// } // }
if (!userInfo?.user) { // }
userInfo = { this.permissions = userInfo.permissions
user: await getUserInfo() this.roles = userInfo.roles
}
}
// this.permissions = userInfo.permissions
// this.roles = userInfo.roles
this.user = userInfo.user this.user = userInfo.user
this.isSetUser = true this.isSetUser = true
cache.local.set(CACHE_KEY.USER, userInfo) cache.local.set(CACHE_KEY.USER, userInfo)

View File

@@ -114,5 +114,7 @@ export enum DICT_TYPE {
PRODUCT_UNIT = 'product_unit', // 商品单位 PRODUCT_UNIT = 'product_unit', // 商品单位
PRODUCT_SPU_STATUS = 'product_spu_status', //商品状态 PRODUCT_SPU_STATUS = 'product_spu_status', //商品状态
// ========== 驾校管理 ========== // ========== 驾校管理 ==========
LINCENSE_TYPE = 'license_type' //驾照类型 LINCENSE_TYPE = 'license_type', //驾照类型
// ========== 租户实例 ==========
INSTANCE_TYPE = 'instance_type' // 实例类型
} }

View File

@@ -0,0 +1,279 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-form-item label="服务类型" prop="serviceId">
<el-select
v-model="formData.serviceId"
placeholder="选择服务类型"
filterable
@change="handleServiceIdChange"
>
<el-option
v-for="item in props.appList"
:key="item.serviceId"
:label="item.serviceName"
:value="item.serviceId"
/>
</el-select>
</el-form-item>
<el-form-item label="上级菜单">
<el-tree-select
v-model="formData.parentId"
:data="menuTree"
:default-expanded-keys="[0]"
:props="defaultProps"
check-strictly
node-key="id"
/>
</el-form-item>
<el-form-item label="菜单名称" prop="name">
<el-input v-model="formData.name" clearable placeholder="请输入菜单名称" />
</el-form-item>
<el-form-item label="菜单类型" prop="type">
<el-radio-group v-model="formData.type">
<el-radio-button :label="1"> 目录 </el-radio-button>
<el-radio-button :label="2"> 菜单 </el-radio-button>
<el-radio-button :label="3"> 按钮 </el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="formData.type !== 3" label="菜单图标">
<IconSelect v-model="formData.icon" clearable />
</el-form-item>
<el-form-item v-if="formData.type !== 3" label="路由地址" prop="path">
<template #label>
<Tooltip
message="访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头"
titel="路由地址"
/>
</template>
<el-input v-model="formData.path" clearable placeholder="请输入路由地址" />
</el-form-item>
<el-form-item v-if="formData.type === 2" label="组件地址" prop="component">
<el-input v-model="formData.component" clearable placeholder="例如说system/user/index" />
</el-form-item>
<el-form-item v-if="formData.type === 2" label="组件名字" prop="componentName">
<el-input v-model="formData.componentName" clearable placeholder="例如说SystemUser" />
</el-form-item>
<el-form-item v-if="formData.type !== 1" label="权限标识" prop="permission">
<template #label>
<Tooltip
message="Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission('system:user:list')`)"
titel="权限标识"
/>
</template>
<el-input v-model="formData.permission" clearable placeholder="请输入权限标识" />
</el-form-item>
<el-form-item label="显示排序" prop="sort">
<el-input-number v-model="formData.sort" :min="0" clearable controls-position="right" />
</el-form-item>
<el-form-item label="菜单状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="0"> 启用 </el-radio>
<el-radio :label="1"> 禁用 </el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="formData.type !== 3" label="显示状态" prop="visible">
<template #label>
<Tooltip message="选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问" titel="显示状态" />
</template>
<el-radio-group v-model="formData.visible">
<el-radio key="true" :label="true" border>显示</el-radio>
<el-radio key="false" :label="false" border>隐藏</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="formData.type !== 3" label="总是显示" prop="alwaysShow">
<template #label>
<Tooltip
message="选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单"
titel="总是显示"
/>
</template>
<el-radio-group v-model="formData.alwaysShow">
<el-radio key="true" :label="true" border>总是</el-radio>
<el-radio key="false" :label="false" border>不是</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="formData.type === 2" label="缓存状态" prop="keepAlive">
<template #label>
<Tooltip
message="选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段"
titel="缓存状态"
/>
</template>
<el-radio-group v-model="formData.keepAlive">
<el-radio key="true" :label="true" border>缓存</el-radio>
<el-radio key="false" :label="false" border>不缓存</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" name="SystemMenuForm" setup>
// import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as MenuApi from '@/api/system/menu'
import { CACHE_KEY } from '@/hooks/web/useCache'
import { CommonStatusEnum, SystemMenuTypeEnum } from '@/utils/constants'
import { defaultProps, handleTree } from '@/utils/tree'
import cache from '@/plugins/cache'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const props = defineProps({
appList: Object as any
})
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: 0,
name: '',
permission: '',
type: SystemMenuTypeEnum.DIR,
sort: Number(undefined),
parentId: 0,
path: '',
icon: '',
component: '',
componentName: '',
status: CommonStatusEnum.ENABLE,
visible: true,
keepAlive: true,
alwaysShow: true,
serviceId: undefined
})
const formRules = reactive({
name: [{ required: true, message: '菜单名称不能为空', trigger: 'blur' }],
sort: [{ required: true, message: '菜单顺序不能为空', trigger: 'blur' }],
path: [{ required: true, message: '路由地址不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number, parentId?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
if (parentId) {
formData.value.parentId = parentId
}
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await MenuApi.getMenu(id)
// 重新加载对应服务的菜单
await getTree(formData.value.serviceId)
} finally {
formLoading.value = false
}
} else {
// 获得菜单列表
await getTree()
}
}
/** 服务类型变化时重新加载菜单 */
const handleServiceIdChange = async () => {
await getTree(formData.value.serviceId)
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
if (
formData.value.type === SystemMenuTypeEnum.DIR ||
formData.value.type === SystemMenuTypeEnum.MENU
) {
if (!isExternal(formData.value.path)) {
if (formData.value.parentId === 0 && formData.value.path.charAt(0) !== '/') {
message.error('路径必须以 / 开头')
return
} else if (formData.value.parentId !== 0 && formData.value.path.charAt(0) === '/') {
message.error('路径不能以 / 开头')
return
}
}
}
const data = formData.value as unknown as MenuApi.MenuVO
if (formType.value === 'create') {
await MenuApi.createMenu(data)
message.success(t('common.createSuccess'))
} else {
await MenuApi.updateMenu(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
// 清空,从而触发刷新
cache.local.delete(CACHE_KEY.ROLE_ROUTERS)
}
}
/** 获取下拉框[上级菜单]的数据 */
const menuTree = ref<Tree[]>([]) // 树形结构
const getTree = async (serviceId?: any) => {
menuTree.value = []
const params = serviceId
? { serviceId }
: formData.value.serviceId
? { serviceId: formData.value.serviceId }
: {}
const res = await MenuApi.getSimpleMenusList(params)
let menu: Tree = { id: 0, name: '主类目', children: [] }
menu.children = handleTree(res)
menuTree.value.push(menu)
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: 0,
name: '',
permission: '',
type: SystemMenuTypeEnum.DIR,
sort: Number(undefined),
parentId: 0,
path: '',
icon: '',
component: '',
componentName: '',
status: CommonStatusEnum.ENABLE,
visible: true,
keepAlive: true,
alwaysShow: true,
serviceId: undefined
}
formRef.value?.resetFields()
}
/** 判断 path 是不是外部的 HTTP 等链接 */
const isExternal = (path: string) => {
return /^(https?:|mailto:|tel:)/.test(path)
}
</script>

View File

@@ -0,0 +1,201 @@
<template>
<!-- 搜索工作栏 -->
<ContentWrap>
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="菜单名称" prop="name">
<el-input
v-model="queryParams.name"
class="!w-240px"
clearable
placeholder="请输入菜单名称"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="服务" prop="serviceId">
<el-select
v-model="queryParams.serviceId"
class="!w-240px"
clearable
placeholder="请选择服务"
@keyup.enter="handleQuery"
>
<el-option
v-for="item in appList"
:key="item.serviceId"
:label="item.serviceName"
:value="item.serviceId"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button plain type="primary" @click="openForm('create')">
<Icon class="mr-5px" icon="ep:plus" />
新增
</el-button>
<el-button plain type="danger" @click="toggleExpandAll">
<Icon class="mr-5px" icon="ep:sort" />
展开/折叠
</el-button>
<el-button plain @click="refreshMenu">
<Icon class="mr-5px" icon="ep:refresh" />
刷新菜单缓存
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-if="refreshTable"
v-loading="loading"
:data="list"
:default-expand-all="isExpandAll"
row-key="id"
>
<el-table-column :show-overflow-tooltip="true" label="菜单名称" prop="name" width="250" />
<el-table-column align="center" label="图标" prop="icon" width="100">
<template #default="scope">
<Icon :icon="scope.row.icon" />
</template>
</el-table-column>
<el-table-column label="排序" prop="sort" width="60" />
<el-table-column :show-overflow-tooltip="true" label="权限标识" prop="permission" />
<el-table-column :show-overflow-tooltip="true" label="组件路径" prop="component" />
<el-table-column :show-overflow-tooltip="true" label="组件名称" prop="componentName" />
<el-table-column label="状态" prop="status" width="80" />
<el-table-column label="服务类型" prop="" width="100">
<template #default="{ row }">
{{ getServiceName(row.serviceId) }}
</template>
</el-table-column>
<el-table-column align="center" label="操作">
<template #default="scope">
<el-button link type="primary" @click="openForm('update', scope.row.id)">
修改
</el-button>
<el-button link type="primary" @click="openForm('create', undefined, scope.row.id)">
新增
</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)"> 删除 </el-button>
</template>
</el-table-column>
</el-table>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<MenuForm ref="formRef" :appList="appList" @success="getList" />
</template>
<script lang="ts" name="Menu" setup>
import { handleTree } from '@/utils/tree'
import * as MenuApi from '@/api/system/menu'
import MenuForm from './MenuForm.vue'
import { CACHE_KEY } from '@/hooks/web/useCache'
import cache from '@/plugins/cache'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const loading = ref(true) // 列表的加载中
const list = ref<any>([]) // 列表的数据
const queryParams = reactive({
name: undefined,
status: undefined,
serviceId: undefined
})
const queryFormRef = ref() // 搜索的表单
const isExpandAll = ref(false) // 是否展开,默认全部折叠
const refreshTable = ref(true) // 重新渲染表格状态
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await MenuApi.getMenuList(queryParams)
list.value = handleTree(data)
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number, parentId?: number) => {
formRef.value.open(type, id, parentId)
}
/** 展开/折叠操作 */
const toggleExpandAll = () => {
refreshTable.value = false
isExpandAll.value = !isExpandAll.value
nextTick(() => {
refreshTable.value = true
})
}
/** 刷新菜单缓存按钮操作 */
const refreshMenu = async () => {
try {
await message.confirm('即将更新缓存刷新浏览器!', '刷新菜单缓存')
// 清空,从而触发刷新
cache.local.delete(CACHE_KEY.ROLE_ROUTERS)
// 刷新浏览器
location.reload()
} catch {}
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
console.log(id)
// 删除的二次确认
await message.delConfirm()
// 发起删除
await MenuApi.deleteMenu(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
const appList = ref([] as any)
const getOptions = () => {
MenuApi.getServiceAppList().then((data) => {
appList.value = data
})
}
function getServiceName(serviceId: any) {
return appList.value.find((it: any) => it.serviceId == serviceId)?.serviceName
}
/** 初始化 **/
onMounted(() => {
getOptions()
getList()
})
</script>

View File

@@ -0,0 +1,153 @@
<template>
<Dialog v-model="dialogVisible" title="菜单权限">
<el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
<el-form-item label="角色名称">
<el-tag>{{ formData.name }}</el-tag>
</el-form-item>
<el-form-item label="角色标识">
<el-tag>{{ formData.code }}</el-tag>
</el-form-item>
<el-form-item label="菜单权限">
<el-card class="w-full h-400px !overflow-y-scroll" shadow="never">
<template #header>
全选/全不选:
<el-switch
v-model="treeNodeAll"
active-text=""
inactive-text=""
inline-prompt
@change="handleCheckedTreeNodeAll"
/>
全部展开/折叠:
<el-switch
v-model="menuExpand"
active-text="展开"
inactive-text="折叠"
inline-prompt
@change="handleCheckedTreeExpand"
/>
</template>
<el-tree
ref="treeRef"
:data="menuOptions"
:props="defaultProps"
empty-text="加载中请稍候"
node-key="id"
show-checkbox
/>
</el-card>
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { defaultProps, handleTree } from '@/utils/tree'
import * as RoleApi from '@/api/system/role'
import * as MenuApi from '@/api/system/menu'
import * as PermissionApi from '@/api/system/permission'
defineOptions({ name: 'SystemRoleAssignMenuForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formData = reactive({
id: undefined,
name: '',
code: '',
menuIds: []
})
const formRef = ref() // 表单 Ref
const menuOptions = ref<any[]>([]) // 菜单树形结构
const menuExpand = ref(false) // 展开/折叠
const treeRef = ref() // 菜单树组件 Ref
const treeNodeAll = ref(false) // 全选/全不选
/** 打开弹窗 */
const open = async (row: RoleApi.RoleVO) => {
dialogVisible.value = true
resetForm()
// 加载 Menu 列表。注意,必须放在前面,不然下面 setChecked 没数据节点
menuOptions.value = handleTree(await MenuApi.getSimpleMenusList())
// 设置数据
formData.id = row.id
formData.name = row.name
formData.code = row.code
formLoading.value = true
try {
formData.value.menuIds = await PermissionApi.getRoleMenuList(row.id)
// 设置选中
formData.value.menuIds.forEach((menuId: number) => {
treeRef.value.setChecked(menuId, true, false)
})
} finally {
formLoading.value = false
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = {
roleId: formData.id,
menuIds: [
...(treeRef.value.getCheckedKeys(false) as unknown as Array<number>), // 获得当前选中节点
...(treeRef.value.getHalfCheckedKeys() as unknown as Array<number>) // 获得半选中的父节点
]
}
await PermissionApi.assignRoleMenu(data)
message.success(t('common.updateSuccess'))
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
// 重置选项
treeNodeAll.value = false
menuExpand.value = false
// 重置表单
formData.value = {
id: undefined,
name: '',
code: '',
menuIds: []
}
treeRef.value?.setCheckedNodes([])
formRef.value?.resetFields()
}
/** 全选/全不选 */
const handleCheckedTreeNodeAll = () => {
treeRef.value.setCheckedNodes(treeNodeAll.value ? menuOptions.value : [])
}
/** 展开/折叠全部 */
const handleCheckedTreeExpand = () => {
const nodes = treeRef.value?.store.nodesMap
for (let node in nodes) {
if (nodes[node].expanded === menuExpand.value) {
continue
}
nodes[node].expanded = menuExpand.value
}
}
</script>

View File

@@ -0,0 +1,169 @@
<template>
<Dialog v-model="dialogVisible" title="数据权限" width="800">
<el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
<el-form-item label="角色名称">
<el-tag>{{ formData.name }}</el-tag>
</el-form-item>
<el-form-item label="角色标识">
<el-tag>{{ formData.code }}</el-tag>
</el-form-item>
<el-form-item label="权限范围">
<el-select v-model="formData.dataScope">
<el-option
v-for="item in getIntDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
<el-form-item
v-if="formData.dataScope === SystemDataScopeEnum.DEPT_CUSTOM"
label="部门范围"
label-width="80px"
>
<el-card class="w-full h-400px !overflow-y-scroll" shadow="never">
<template #header>
全选/全不选:
<el-switch
v-model="treeNodeAll"
active-text=""
inactive-text=""
inline-prompt
@change="handleCheckedTreeNodeAll()"
/>
全部展开/折叠:
<el-switch
v-model="deptExpand"
active-text="展开"
inactive-text="折叠"
inline-prompt
@change="handleCheckedTreeExpand"
/>
父子联动(选中父节点自动选择子节点):
<el-switch v-model="checkStrictly" active-text="是" inactive-text="否" inline-prompt />
</template>
<el-tree
ref="treeRef"
:check-strictly="!checkStrictly"
:data="deptOptions"
:props="defaultProps"
default-expand-all
empty-text="加载中请稍后"
node-key="id"
show-checkbox
/>
</el-card>
</el-form-item>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { defaultProps, handleTree } from '@/utils/tree'
import { SystemDataScopeEnum } from '@/utils/constants'
import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept'
import * as PermissionApi from '@/api/system/permission'
defineOptions({ name: 'SystemRoleDataPermissionForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formData = reactive({
id: undefined,
name: '',
code: '',
dataScope: undefined,
dataScopeDeptIds: []
})
const formRef = ref() // 表单 Ref
const deptOptions = ref<any[]>([]) // 部门树形结构
const deptExpand = ref(true) // 展开/折叠
const treeRef = ref() // 菜单树组件 Ref
const treeNodeAll = ref(false) // 全选/全不选
const checkStrictly = ref(true) // 是否严格模式,即父子不关联
/** 打开弹窗 */
const open = async (row: RoleApi.RoleVO) => {
dialogVisible.value = true
resetForm()
// 加载 Dept 列表。注意,必须放在前面,不然下面 setChecked 没数据节点
deptOptions.value = handleTree(await DeptApi.getSimpleDeptList())
// 设置数据
formData.id = row.id
formData.name = row.name
formData.code = row.code
formData.dataScope = row.dataScope
await nextTick()
// 需要在 DOM 渲染完成后,再设置选中状态
row.dataScopeDeptIds?.forEach((deptId: number): void => {
treeRef.value.setChecked(deptId, true, false)
})
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
formLoading.value = true
try {
const data = {
roleId: formData.id,
dataScope: formData.dataScope,
dataScopeDeptIds:
formData.dataScope !== SystemDataScopeEnum.DEPT_CUSTOM
? []
: treeRef.value.getCheckedKeys(false)
}
await PermissionApi.assignRoleDataScope(data)
message.success(t('common.updateSuccess'))
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
// 重置选项
treeNodeAll.value = false
deptExpand.value = true
checkStrictly.value = true
// 重置表单
formData.value = {
id: undefined,
name: '',
code: '',
dataScope: undefined,
dataScopeDeptIds: []
}
treeRef.value?.setCheckedNodes([])
formRef.value?.resetFields()
}
/** 全选/全不选 */
const handleCheckedTreeNodeAll = () => {
treeRef.value.setCheckedNodes(treeNodeAll.value ? deptOptions.value : [])
}
/** 展开/折叠全部 */
const handleCheckedTreeExpand = () => {
const nodes = treeRef.value?.store.nodesMap
for (let node in nodes) {
if (nodes[node].expanded === deptExpand.value) {
continue
}
nodes[node].expanded = deptExpand.value
}
}
</script>

View File

@@ -0,0 +1,126 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="80px"
>
<el-form-item label="角色名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入角色名称" />
</el-form-item>
<el-form-item label="角色标识" prop="code">
<el-input v-model="formData.code" placeholder="请输入角色标识" />
</el-form-item>
<el-form-item label="显示顺序" prop="sort">
<el-input v-model="formData.sort" placeholder="请输入显示顺序" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="formData.status" clearable placeholder="请选择状态">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输备注" type="textarea" />
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants'
import * as RoleApi from '@/api/system/role'
defineOptions({ name: 'SystemRoleForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: undefined,
name: '',
code: '',
sort: undefined,
status: CommonStatusEnum.ENABLE,
remark: ''
})
const formRules = reactive({
name: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }],
code: [{ required: true, message: '角色标识不能为空', trigger: 'change' }],
sort: [{ required: true, message: '显示顺序不能为空', trigger: 'change' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }],
remark: [{ required: false, message: '备注不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await RoleApi.getRole(id)
} finally {
formLoading.value = false
}
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: '',
code: '',
sort: undefined,
status: CommonStatusEnum.ENABLE,
remark: ''
}
formRef.value?.resetFields()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as RoleApi.RoleVO
if (formType.value === 'create') {
await RoleApi.createRole(data)
message.success(t('common.createSuccess'))
} else {
await RoleApi.updateRole(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
</script>

View File

@@ -0,0 +1,299 @@
<template>
<doc-alert title="功能权限" url="https://doc.iocoder.cn/resource-permission" />
<doc-alert title="数据权限" url="https://doc.iocoder.cn/data-permission" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="角色名称" prop="name">
<el-input
v-model="queryParams.name"
class="!w-240px"
clearable
placeholder="请输入角色名称"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="角色标识" prop="code">
<el-input
v-model="queryParams.code"
class="!w-240px"
clearable
placeholder="请输入角色标识"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" class="!w-240px" clearable placeholder="请选择状态">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
end-placeholder="结束日期"
start-placeholder="开始日期"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
<el-button
v-hasPermi="['system:role:create']"
plain
type="primary"
@click="openForm('create')"
>
<Icon class="mr-5px" icon="ep:plus" />
新增
</el-button>
<el-button
v-hasPermi="['system:role:export']"
:loading="exportLoading"
plain
type="success"
@click="handleExport"
>
<Icon class="mr-5px" icon="ep:download" />
导出
</el-button>
<el-button
v-hasPermi="['system:role:delete']"
:disabled="checkedIds.length === 0"
plain
type="danger"
@click="handleDeleteBatch"
>
<Icon class="mr-5px" icon="ep:delete" />
批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
<el-table-column type="selection" width="55" />
<el-table-column align="center" label="角色编号" prop="id" />
<el-table-column align="center" label="角色名称" prop="name" />
<el-table-column label="角色类型" align="center" prop="type">
<template #default="scope">
<dict-tag :type="DICT_TYPE.SYSTEM_ROLE_TYPE" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column align="center" label="角色标识" prop="code" />
<el-table-column align="center" label="显示顺序" prop="sort" />
<el-table-column align="center" label="备注" prop="remark" />
<el-table-column align="center" label="状态" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
width="180"
/>
<el-table-column :width="300" align="center" label="操作">
<template #default="scope">
<el-button
v-hasPermi="['system:role:update']"
link
type="primary"
@click="openForm('update', scope.row.id)"
>
编辑
</el-button>
<el-button
v-hasPermi="['system:permission:assign-role-menu']"
link
preIcon="ep:basketball"
title="菜单权限"
type="primary"
@click="openAssignMenuForm(scope.row)"
>
菜单权限
</el-button>
<el-button
v-hasPermi="['system:permission:assign-role-data-scope']"
link
preIcon="ep:coin"
title="数据权限"
type="primary"
@click="openDataPermissionForm(scope.row)"
>
数据权限
</el-button>
<el-button
v-hasPermi="['system:role:delete']"
link
type="danger"
@click="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<RoleForm ref="formRef" @success="getList" />
<!-- 表单弹窗菜单权限 -->
<RoleAssignMenuForm ref="assignMenuFormRef" @success="getList" />
<!-- 表单弹窗数据权限 -->
<RoleDataPermissionForm ref="dataPermissionFormRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as RoleApi from '@/api/system/role'
import RoleForm from './RoleForm.vue'
import RoleAssignMenuForm from './RoleAssignMenuForm.vue'
import RoleDataPermissionForm from './RoleDataPermissionForm.vue'
defineOptions({ name: 'SystemRole' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
code: '',
name: '',
status: undefined,
createTime: []
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询角色列表 */
const getList = async () => {
loading.value = true
try {
const data = await RoleApi.getRolePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 数据权限操作 */
const dataPermissionFormRef = ref()
const openDataPermissionForm = async (row: RoleApi.RoleVO) => {
dataPermissionFormRef.value.open(row)
}
/** 菜单权限操作 */
const assignMenuFormRef = ref()
const openAssignMenuForm = async (row: RoleApi.RoleVO) => {
assignMenuFormRef.value.open(row)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await RoleApi.deleteRole(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 批量删除按钮操作 */
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (rows: RoleApi.RoleVO[]) => {
checkedIds.value = rows.map((row) => row.id)
}
const handleDeleteBatch = async () => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起批量删除
await RoleApi.deleteRoleList(checkedIds.value)
checkedIds.value = []
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await RoleApi.exportRole(queryParams)
download.excel(data, '角色数据.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getList()
})
</script>

View File

@@ -0,0 +1,225 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="50%">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="80px"
>
<el-form-item label="租户名" prop="name">
<el-input v-model="formData.name" placeholder="请输入租户名" />
</el-form-item>
<el-form-item label="租户套餐" prop="packageId">
<el-select v-model="formData.packageId" clearable placeholder="请选择租户套餐">
<el-option
v-for="item in packageList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="联系人" prop="contactName">
<el-input v-model="formData.contactName" placeholder="请输入联系人" />
</el-form-item>
<el-form-item label="联系手机" prop="contactMobile">
<el-input v-model="formData.contactMobile" placeholder="请输入联系手机" />
</el-form-item>
<el-form-item v-if="formData.id === undefined" label="用户名称" prop="username">
<el-input v-model="formData.username" placeholder="请输入用户名称" />
</el-form-item>
<el-form-item v-if="formData.id === undefined" label="用户密码" prop="password">
<el-input
v-model="formData.password"
placeholder="请输入用户密码"
show-password
type="password"
/>
</el-form-item>
<el-form-item label="账号额度" prop="accountCount">
<el-input-number
v-model="formData.accountCount"
:min="0"
controls-position="right"
placeholder="请输入账号额度"
/>
</el-form-item>
<el-form-item label="过期时间" prop="expireTime">
<el-date-picker
v-model="formData.expireTime"
clearable
placeholder="请选择过期时间"
type="date"
value-format="x"
/>
</el-form-item>
<el-form-item label="绑定域名" prop="websites">
<el-input-tag
v-model="formData.websites"
placeholder="请输入绑定域名,按回车添加"
class="w-full"
/>
</el-form-item>
<el-form-item label="租户状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="会员权益" prop="memberBenefits">
<el-select
v-model="formData.memberBenefits"
multiple
placeholder="请选择会员权益"
class="!w-320px"
>
<el-option label="考前密卷" value="SECRET" />
<el-option label="真实考场" value="REAL_EXAM" />
<el-option label="精选题库" value="JX" />
<el-option label="AI助手" value="AI" />
</el-select>
</el-form-item>
<el-form-item label="是否开放分销功能" prop="distributionEnabled">
<el-switch v-model="formData.distributionEnabled" />
</el-form-item>
<el-form-item label="是否开放赠送会员功能" prop="giftMemberEnabled">
<el-switch v-model="formData.giftMemberEnabled" />
</el-form-item>
<el-form-item label="每月赠送会员数" prop="monthlyGiftMemberNum">
<el-input-number
v-model="formData.monthlyGiftMemberNum"
:min="0"
controls-position="right"
placeholder="请输入每月赠送会员数"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as TenantApi from '@/api/system/tenant'
import { CommonStatusEnum } from '@/utils/constants'
import * as TenantPackageApi from '@/api/system/tenantPackage'
defineOptions({ name: 'SystemTenantForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: undefined,
name: undefined,
packageId: undefined,
contactName: undefined,
contactMobile: undefined,
accountCount: undefined,
expireTime: undefined,
websites: [],
status: CommonStatusEnum.ENABLE,
// 新增专属
username: undefined,
password: undefined,
// 新增字段
memberBenefits: [],
distributionEnabled: false,
giftMemberEnabled: false,
monthlyGiftMemberNum: 0,
address: undefined
})
const formRules = reactive({
name: [{ required: true, message: '租户名不能为空', trigger: 'blur' }],
packageId: [{ required: true, message: '租户套餐不能为空', trigger: 'blur' }],
contactName: [{ required: true, message: '联系人不能为空', trigger: 'blur' }],
status: [{ required: true, message: '租户状态不能为空', trigger: 'blur' }],
accountCount: [{ required: true, message: '账号额度不能为空', trigger: 'blur' }],
expireTime: [{ required: true, message: '过期时间不能为空', trigger: 'blur' }],
username: [{ required: true, message: '用户名称不能为空', trigger: 'blur' }],
password: [{ required: true, message: '用户密码不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const packageList = ref([] as TenantPackageApi.TenantPackageVO[]) // 租户套餐
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await TenantApi.getTenant(id)
} finally {
formLoading.value = false
}
}
// 加载套餐列表
packageList.value = await TenantPackageApi.getTenantPackageList()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as TenantApi.TenantVO
if (formType.value === 'create') {
await TenantApi.createTenant(data)
message.success(t('common.createSuccess'))
} else {
await TenantApi.updateTenant(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: undefined,
packageId: undefined,
contactName: undefined,
contactMobile: undefined,
accountCount: undefined,
expireTime: undefined,
websites: [],
status: CommonStatusEnum.ENABLE,
username: undefined,
password: undefined,
// 新增字段
memberBenefits: [],
distributionEnabled: false,
giftMemberEnabled: false,
monthlyGiftMemberNum: 0,
address: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,332 @@
<template>
<doc-alert title="SaaS 多租户" url="https://doc.iocoder.cn/saas-tenant/" />
<!-- 搜索 -->
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="租户名" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入租户名"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="联系人" prop="contactName">
<el-input
v-model="queryParams.contactName"
placeholder="请输入联系人"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="联系手机" prop="contactMobile">
<el-input
v-model="queryParams.contactMobile"
placeholder="请输入联系手机"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="租户状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择租户状态"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" />
重置
</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['system:tenant:create']"
>
<Icon icon="ep:plus" class="mr-5px" />
新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['system:tenant:export']"
>
<Icon icon="ep:download" class="mr-5px" />
导出
</el-button>
<el-button
type="danger"
plain
:disabled="checkedIds.length === 0"
@click="handleDeleteBatch"
v-hasPermi="['system:tenant:delete']"
>
<Icon icon="ep:delete" class="mr-5px" />
批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
<el-table-column type="selection" width="55" />
<el-table-column label="租户编号" align="center" prop="id" />
<el-table-column label="租户名" align="center" prop="name" />
<el-table-column label="租户套餐" align="center" prop="packageId">
<template #default="scope">
<el-tag v-if="scope.row.packageId === 0" type="danger">系统租户</el-tag>
<template v-else v-for="item in packageList">
<el-tag type="success" :key="item.id" v-if="item.id === scope.row.packageId">
{{ item.name }}
</el-tag>
</template>
</template>
</el-table-column>
<el-table-column label="联系人" align="center" prop="contactName" />
<el-table-column label="联系手机" align="center" prop="contactMobile" />
<el-table-column label="账号额度" align="center" prop="accountCount">
<template #default="scope">
<el-tag>{{ scope.row.accountCount }}</el-tag>
</template>
</el-table-column>
<el-table-column
label="过期时间"
align="center"
prop="expireTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="绑定域名" align="center" prop="websites" width="180">
<template #default="scope">
<el-tag v-for="website in scope.row.websites || []" :key="website" class="mr-1 mb-1">
{{ website }}
</el-tag>
<span v-if="!scope.row.websites || scope.row.websites.length === 0">-</span>
</template>
</el-table-column>
<el-table-column label="会员权益" align="center" prop="memberBenefits" width="180">
<template #default="scope">
<el-tag
v-for="benefit in scope.row.memberBenefits || []"
:key="benefit"
class="mr-1 mb-1"
>
{{ benefit }}
</el-tag>
<span v-if="!scope.row.memberBenefits || scope.row.memberBenefits.length === 0">-</span>
</template>
</el-table-column>
<el-table-column label="分销功能" align="center" prop="distributionEnabled" width="100">
<template #default="scope">
<el-tag :type="scope.row.distributionEnabled ? 'success' : 'danger'">
{{ scope.row.distributionEnabled ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="赠送会员" align="center" prop="giftMemberEnabled" width="100">
<template #default="scope">
<el-tag :type="scope.row.giftMemberEnabled ? 'success' : 'danger'">
{{ scope.row.giftMemberEnabled ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="每月赠送数" align="center" prop="monthlyGiftMemberNum" width="100" />
<el-table-column label="地址" align="center" prop="address" width="200" />
<el-table-column label="租户状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="操作" align="center" min-width="110" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['system:tenant:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['system:tenant:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<TenantForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as TenantApi from '@/api/system/tenant'
import * as TenantPackageApi from '@/api/system/tenantPackage'
import TenantForm from './TenantForm.vue'
defineOptions({ name: 'SystemTenant' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
contactName: undefined,
contactMobile: undefined,
status: undefined,
createTime: []
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
const packageList = ref([] as TenantPackageApi.TenantPackageVO[]) //租户套餐列表
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await TenantApi.getTenantPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await TenantApi.deleteTenant(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 批量删除按钮操作 */
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (rows: TenantApi.TenantVO[]) => {
checkedIds.value = rows.map((row) => row.id)
}
const handleDeleteBatch = async () => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起批量删除
await TenantApi.deleteTenantList(checkedIds.value)
checkedIds.value = []
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await TenantApi.exportTenant(queryParams)
download.excel(data, '租户列表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await getList()
// 获取租户套餐列表
packageList.value = await TenantPackageApi.getTenantPackageList()
})
</script>

View File

@@ -0,0 +1,286 @@
<template>
<el-dialog
v-model="visible"
:title="dialogTitle"
width="600px"
destroy-on-close
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
<el-form-item label="租户" prop="tenantId">
<el-select v-model="formData.tenantId" placeholder="请选择租户" clearable>
<el-option
v-for="tenant in tenantList"
:key="tenant.id"
:label="tenant.name"
:value="tenant.id"
/>
</el-select>
</el-form-item>
<el-form-item label="服务" prop="serviceId">
<el-select
v-model="formData.serviceId"
placeholder="请选择服务"
clearable
@change="handleServiceIdChange"
>
<el-option
v-for="service in serviceList"
:key="service.serviceId"
:label="service.serviceName"
:value="service.serviceId"
/>
</el-select>
</el-form-item>
<el-form-item label="租户套餐包" prop="packageId">
<el-select v-model="formData.packageId" placeholder="请选择租户套餐包" clearable>
<el-option
v-for="pkg in filteredTenantPackageList"
:key="pkg.id"
:label="pkg.name"
:value="pkg.id"
/>
</el-select>
</el-form-item>
<el-form-item label="实例名称" prop="instanceName">
<el-input v-model="formData.instanceName" placeholder="请输入实例名称" />
</el-form-item>
<el-form-item label="实例类型" prop="instanceType">
<el-select v-model="formData.instanceType" placeholder="请选择实例类型" clearable>
<el-option label="其他" value="0" />
<el-option label="驾校" value="1" />
<el-option label="攀尼" value="2" />
</el-select>
</el-form-item>
<el-form-item label="实例图标" prop="instanceIcon">
<el-input v-model="formData.instanceIcon" placeholder="请输入实例图标地址" />
</el-form-item>
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="formData.contactPhone" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="数据库URL" prop="dbUrl">
<el-input v-model="formData.dbUrl" placeholder="请输入数据库URL" />
</el-form-item>
<el-form-item label="数据库名称" prop="dbName">
<el-input v-model="formData.dbName" placeholder="请输入数据库名称" />
</el-form-item>
<el-form-item label="数据库用户名" prop="dbUsername">
<el-input v-model="formData.dbUsername" placeholder="请输入数据库用户名" />
</el-form-item>
<el-form-item label="数据库密码" prop="dbPassword">
<el-input v-model="formData.dbPassword" placeholder="请输入数据库密码" type="password" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择状态" clearable>
<el-option label="启用" :value="0" />
<el-option label="禁用" :value="1" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as TenantInstanceApi from '@/api/system/tenantInstance'
import * as TenantApi from '@/api/system/tenant'
import * as TenantPackageApi from '@/api/system/tenantPackage'
import { useMessage } from '@/hooks/web/useMessage'
import { useI18n } from 'vue-i18n'
defineProps({
tenantList: {
type: Array as () => TenantApi.TenantVO[],
default: () => []
},
serviceList: {
type: Array as () => any[],
default: () => []
}
})
const emit = defineEmits(['success'])
// 弹窗标题
const dialogTitle = computed(() => {
return formState.value === 'create' ? '新增租户实例' : '修改租户实例'
})
// 弹窗的可见性
const visible = ref(false)
// 表单状态create 或 update
const formState = ref('')
// 表单的实例ID
const instanceId = ref<number>()
// 表单数据
const formData = ref<any>({
instanceId: 0,
tenantId: 0,
packageId: 0,
serviceId: 0,
instanceName: '',
instanceType: 0,
instanceIcon: '',
contactPhone: '',
dbUrl: '',
dbName: '',
dbUsername: '',
dbPassword: '',
status: 1,
creator: '',
createTime: new Date(),
updater: '',
updateTime: new Date(),
deleted: false
})
// 表单的校验规则
const formRules = reactive({
tenantId: [{ required: true, message: '租户不能为空', trigger: 'blur' }],
packageId: [{ required: true, message: '租户套餐包不能为空', trigger: 'blur' }],
serviceId: [{ required: true, message: '服务不能为空', trigger: 'blur' }],
instanceName: [{ required: true, message: '实例名称不能为空', trigger: 'blur' }],
instanceType: [{ required: true, message: '实例类型不能为空', trigger: 'blur' }],
contactPhone: [{ required: true, message: '联系电话不能为空', trigger: 'blur' }],
dbUrl: [{ required: true, message: '数据库URL不能为空', trigger: 'blur' }],
dbName: [{ required: true, message: '数据库名称不能为空', trigger: 'blur' }],
dbUsername: [{ required: true, message: '数据库用户名不能为空', trigger: 'blur' }],
dbPassword: [{ required: true, message: '数据库密码不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
})
// 租户套餐包列表
const tenantPackageList = ref<any[]>([])
// 表单的引用
const formRef = ref<any>()
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
// 根据服务筛选套餐包
const filteredTenantPackageList = computed(() => {
if (!formData.value.serviceId) {
return tenantPackageList.value
}
return tenantPackageList.value.filter(pkg => pkg.serviceId === formData.value.serviceId)
})
// 服务ID变更时的处理
const handleServiceIdChange = () => {
// 服务变更时,重置套餐包选择
formData.value.packageId = 0
}
// 加载租户套餐包列表
const loadTenantPackages = async () => {
try {
const data = await TenantPackageApi.getTenantPackageList()
tenantPackageList.value = data
} catch (error) {
console.error('加载租户套餐包列表失败:', error)
}
}
// 打开弹窗
const open = (type: string, id?: number) => {
formState.value = type
if (type === 'create') {
// 新增时,重置表单
resetForm()
} else if (type === 'update' && id) {
// 修改时设置实例ID并获取详情
instanceId.value = id
loadDetail(id)
}
// 加载租户套餐包列表
loadTenantPackages()
// 显示弹窗
visible.value = true
}
// 重置表单
const resetForm = () => {
formData.value = {
instanceId: 0,
tenantId: 0,
packageId: 0,
serviceId: 0,
instanceName: '',
instanceType: 0,
instanceIcon: '',
contactPhone: '',
dbUrl: '',
dbName: '',
dbUsername: '',
dbPassword: '',
status: 0,
creator: '',
createTime: new Date(),
updater: '',
updateTime: new Date(),
deleted: false
}
// 重置表单的校验状态
formRef.value?.resetFields()
}
// 加载详情
const loadDetail = async (id: number) => {
try {
// 重置表单的校验状态
formRef.value?.resetFields()
// 获取租户实例详情
const data = await TenantInstanceApi.getTenantInstance(id)
formData.value = data
} catch (error) {
console.error('加载租户实例详情失败:', error)
}
}
// 处理取消
const handleCancel = () => {
visible.value = false
}
// 处理提交
const handleSubmit = async () => {
// 校验表单
await formRef.value.validate((valid) => {
if (!valid) {
return
}
// 提交表单
submitForm()
})
}
// 提交表单
const submitForm = async () => {
try {
if (formState.value === 'create') {
// 新增租户实例
await TenantInstanceApi.createTenantInstance(formData.value)
message.success(t('common.saveSuccess'))
} else if (formState.value === 'update') {
// 修改租户实例
await TenantInstanceApi.updateTenantInstance(formData.value)
message.success(t('common.updateSuccess'))
}
// 关闭弹窗
visible.value = false
// 触发成功事件
emit('success')
} catch (error) {
console.error('提交表单失败:', error)
}
}
// 暴露方法
defineExpose({
open
})
</script>

View File

@@ -0,0 +1,369 @@
<template>
<!-- 搜索 -->
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="实例名称" prop="instanceName">
<el-input
v-model="queryParams.instanceName"
placeholder="请输入实例名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="租户" prop="tenantId">
<el-select
v-model="queryParams.tenantId"
placeholder="请选择租户"
clearable
class="!w-240px"
>
<el-option
v-for="tenant in tenantList"
:key="tenant.id"
:label="tenant.name"
:value="tenant.id"
/>
</el-select>
</el-form-item>
<el-form-item label="服务" prop="serviceId">
<el-select
v-model="queryParams.serviceId"
placeholder="请选择服务"
clearable
class="!w-240px"
>
<el-option
v-for="service in serviceList"
:key="service.serviceId"
:label="service.serviceName"
:value="service.serviceId"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" />
重置
</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['system:tenantInstance:create']"
>
<Icon icon="ep:plus" class="mr-5px" />
新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['system:tenantInstance:export']"
>
<Icon icon="ep:download" class="mr-5px" />
导出
</el-button>
<el-button
type="danger"
plain
:disabled="checkedIds.length === 0"
@click="handleDeleteBatch"
v-hasPermi="['system:tenantInstance:delete']"
>
<Icon icon="ep:delete" class="mr-5px" />
批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
<el-table-column type="selection" width="55" />
<el-table-column label="实例编号" align="center" prop="instanceId" />
<el-table-column label="实例名称" align="center" prop="instanceName" />
<el-table-column label="租户" align="center" prop="tenantName"/>
<el-table-column label="服务" align="center" prop="serviceId">
<template #default="scope">
<template v-for="service in serviceList">
<el-tag
type="info"
:key="service.serviceId"
v-if="service.serviceId === scope.row.serviceId"
>
{{ service.serviceName }}
</el-tag>
</template>
</template>
</el-table-column>
<el-table-column label="实例类型" align="center" prop="instanceType">
<template #default="scope">
<el-tag :type="getInstanceTypeTagType(scope.row.instanceType)">
{{ getInstanceTypeName(scope.row.instanceType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="实例图标" align="center" prop="instanceIcon" width="100">
<template #default="scope">
<img v-if="scope.row.instanceIcon" :src="scope.row.instanceIcon" class="w-8 h-8 object-contain" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="数据库" align="center" prop="dbName" />
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status)">
{{ getStatusName(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180" />
<el-table-column label="操作" align="center" min-width="110" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.instanceId)"
v-hasPermi="['system:tenantInstance:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.instanceId)"
v-hasPermi="['system:tenantInstance:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<TenantInstanceForm
ref="formRef"
@success="getList"
:tenantList="tenantList"
:serviceList="serviceList"
/>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import * as TenantInstanceApi from '@/api/system/tenantInstance'
import * as TenantApi from '@/api/system/tenant'
import * as MenuApi from '@/api/system/menu'
import TenantInstanceForm from './TenantInstanceForm.vue'
import { ContentWrap } from '@/components/ContentWrap'
import Pagination from '@/components/Pagination/index.vue'
import { useMessage } from '@/hooks/web/useMessage'
import { useI18n } from 'vue-i18n'
import { Icon } from '@/components/Icon'
import DictTag from '@/components/DictTag/src/DictTag.vue'
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
// 获取实例类型名称
const getInstanceTypeName = (instanceType: number): string => {
const typeMap = {
0: '其他',
1: '驾校',
2: '攀尼'
}
return typeMap[instanceType] || '未知'
}
// 获取实例类型标签类型
const getInstanceTypeTagType = (instanceType: number): string => {
const typeMap = {
0: 'info',
1: 'success',
2: 'warning'
}
return typeMap[instanceType] || 'info'
}
// 获取状态名称
const getStatusName = (status: number): string => {
const statusMap = {
0: '启用',
1: '禁用'
}
return statusMap[status] || '未知'
}
// 获取状态标签类型
const getStatusTagType = (status: number): string => {
const statusMap = {
0: 'success',
1: 'danger'
}
return statusMap[status] || 'info'
}
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref<TenantInstanceApi.TenantInstanceVO[]>([]) // 列表的数据
const queryParams = reactive<TenantInstanceApi.TenantInstancePageReqVO>({
pageNo: 1,
pageSize: 10,
instanceName: undefined,
tenantId: undefined,
serviceId: undefined,
status: undefined,
createTime: []
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
const tenantList = ref<TenantApi.TenantVO[]>([]) // 租户列表
const serviceList = ref<any[]>([]) // 服务列表
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await TenantInstanceApi.getTenantInstancePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, instanceId?: number) => {
formRef.value.open(type, instanceId)
}
/** 删除按钮操作 */
const handleDelete = async (instanceId: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await TenantInstanceApi.deleteTenantInstance(instanceId)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 批量删除按钮操作 */
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (rows: TenantInstanceApi.TenantInstanceVO[]) => {
checkedIds.value = rows.map((row) => row.instanceId)
}
const handleDeleteBatch = async () => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起批量删除
await TenantInstanceApi.deleteTenantInstanceList(checkedIds.value)
checkedIds.value = []
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await TenantInstanceApi.exportTenantInstance(queryParams)
download.excel(data, '租户实例列表.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 获取租户列表 */
const getTenantList = async () => {
try {
tenantList.value = await TenantApi.getTenantList()
} catch (error) {
console.error('获取租户列表失败:', error)
}
}
/** 获取服务列表 */
const getServiceList = async () => {
try {
serviceList.value = await MenuApi.getServiceAppList()
} catch (error) {
console.error('获取服务列表失败:', error)
}
}
/** 初始化 **/
onMounted(async () => {
await getTenantList()
await getServiceList()
await getList()
})
</script>

View File

@@ -0,0 +1,227 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="80px"
>
<el-form-item label="套餐名" prop="name">
<el-input v-model="formData.name" placeholder="请输入套餐名" />
</el-form-item>
<el-form-item label="服务id" prop="serviceId">
<el-select
v-model="formData.serviceId"
placeholder="请选择服务id"
class="!w-240px"
@change="handleServiceIdChange"
>
<el-option
v-for="item in appList"
:key="item.serviceId"
:label="item.serviceName"
:value="item.serviceId"
/>
</el-select>
</el-form-item>
<el-form-item label="菜单权限">
<el-card class="w-full h-400px !overflow-y-scroll" shadow="never">
<template #header>
全选/全不选:
<el-switch
v-model="treeNodeAll"
active-text=""
inactive-text=""
inline-prompt
@change="handleCheckedTreeNodeAll"
/>
全部展开/折叠:
<el-switch
v-model="menuExpand"
active-text="展开"
inactive-text="折叠"
inline-prompt
@change="handleCheckedTreeExpand"
/>
</template>
<el-tree
ref="treeRef"
:data="menuOptions"
:props="defaultProps"
empty-text="加载中请稍候"
node-key="id"
show-checkbox
/>
</el-card>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants'
import { defaultProps, handleTree } from '@/utils/tree'
import * as TenantPackageApi from '@/api/system/tenantPackage'
import * as MenuApi from '@/api/system/menu'
import { ElTree } from 'element-plus'
defineOptions({ name: 'SystemTenantPackageForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: null,
name: null,
remark: null,
menuIds: [],
status: CommonStatusEnum.ENABLE,
serviceId: null
})
const formRules = reactive({
name: [{ required: true, message: '套餐名不能为空', trigger: 'blur' }],
serviceId: [{ required: true, message: '服务不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
menuIds: [{ required: true, message: '关联的菜单编号不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const menuOptions = ref<any[]>([]) // 树形结构数据
const menuExpand = ref(false) // 展开/折叠
const treeRef = ref<InstanceType<typeof ElTree>>() // 树组件 Ref
const treeNodeAll = ref(false) // 全选/全不选
const appList = ref([] as any) // 服务列表
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 加载服务列表
appList.value = await MenuApi.getServiceAppList()
// 加载 Menu 列表。注意,必须放在前面,不然下面 setChecked 没数据节点
await loadMenus()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
const res = await TenantPackageApi.getTenantPackage(id)
// 设置选中
formData.value = res
// 重新加载对应服务的菜单
await loadMenus(res.serviceId)
// 设置选中
res.menuIds.forEach((menuId: number) => {
treeRef.value!.setChecked(menuId, true, false)
})
} finally {
formLoading.value = false
}
}
}
/** 加载菜单列表 */
const loadMenus = async (serviceId?: any) => {
const params = serviceId
? { serviceId }
: formData.value.serviceId
? { serviceId: formData.value.serviceId }
: {}
menuOptions.value = handleTree(await MenuApi.getSimpleMenusList(params))
// 重置选中状态
treeRef.value?.setCheckedNodes([])
}
/** 服务ID变化时重新加载菜单 */
const handleServiceIdChange = async () => {
await loadMenus()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as TenantPackageApi.TenantPackageVO
data.menuIds = [
...(treeRef.value!.getCheckedKeys(false) as unknown as Array<number>), // 获得当前选中节点
...(treeRef.value!.getHalfCheckedKeys() as unknown as Array<number>) // 获得半选中的父节点
]
if (formType.value === 'create') {
await TenantPackageApi.createTenantPackage(data)
message.success(t('common.createSuccess'))
} else {
await TenantPackageApi.updateTenantPackage(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
// 重置选项
treeNodeAll.value = false
menuExpand.value = false
// 重置表单
formData.value = {
id: null,
name: null,
remark: null,
menuIds: [],
status: CommonStatusEnum.ENABLE,
serviceId: null
}
treeRef.value?.setCheckedNodes([])
formRef.value?.resetFields()
}
/** 全选/全不选 */
const handleCheckedTreeNodeAll = () => {
treeRef.value!.setCheckedNodes(treeNodeAll.value ? menuOptions.value : [])
}
/** 展开/折叠全部 */
const handleCheckedTreeExpand = () => {
const nodes = treeRef.value?.store.nodesMap
for (let node in nodes) {
if (nodes[node].expanded === menuExpand.value) {
continue
}
nodes[node].expanded = menuExpand.value
}
}
</script>

View File

@@ -0,0 +1,248 @@
<template>
<doc-alert title="SaaS 多租户" url="https://doc.iocoder.cn/saas-tenant/" />
<!-- 搜索 -->
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="套餐名" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入套餐名"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="服务id" prop="serviceId">
<el-select
v-model="queryParams.serviceId"
placeholder="请选择服务id"
clearable
class="!w-240px"
>
<el-option
v-for="item in appList"
:key="item.serviceId"
:label="item.serviceName"
:value="item.serviceId"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
start-placeholder="开始日期"
end-placeholder="结束日期"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['system:tenant-package:create']"
>
<Icon icon="ep:plus" class="mr-5px" />
新增
</el-button>
<el-button
type="danger"
plain
:disabled="checkedIds.length === 0"
@click="handleDeleteBatch"
v-hasPermi="['system:tenant-package:delete']"
>
<Icon icon="ep:delete" class="mr-5px" />
批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
<el-table-column type="selection" width="55" />
<el-table-column label="套餐编号" align="center" prop="id" width="120" />
<el-table-column label="套餐名" align="center" prop="name" />
<el-table-column label="服务" align="center" width="150">
<template #default="scope">
{{ getServiceName(scope.row.serviceId) }}
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column
label="创建时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['system:tenant-package:update']"
>
修改
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['system:tenant-package:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<TenantPackageForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import * as TenantPackageApi from '@/api/system/tenantPackage'
import * as MenuApi from '@/api/system/menu'
import TenantPackageForm from './TenantPackageForm.vue'
defineOptions({ name: 'SystemTenantPackage' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
status: undefined,
remark: undefined,
createTime: [],
serviceId: undefined
})
const appList = ref([] as any) // 服务列表
const queryFormRef = ref() // 搜索的表单
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await TenantPackageApi.getTenantPackagePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields()
getList()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await TenantPackageApi.deleteTenantPackage(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 批量删除按钮操作 */
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (rows: TenantPackageApi.TenantPackageVO[]) => {
checkedIds.value = rows.map((row) => row.id)
}
const handleDeleteBatch = async () => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起批量删除
await TenantPackageApi.deleteTenantPackageList(checkedIds.value)
checkedIds.value = []
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 获取服务列表 */
const getOptions = async () => {
try {
appList.value = await MenuApi.getServiceAppList()
} catch (error) {
console.error('获取服务列表失败:', error)
}
}
/** 根据服务id获取服务名称 */
const getServiceName = (serviceId: any) => {
return appList.value.find((it: any) => it.serviceId == serviceId)?.serviceName
}
/** 初始化 **/
onMounted(async () => {
await getOptions()
getList()
})
</script>

View File

@@ -0,0 +1,48 @@
<template>
<div class="head-container">
<el-tree
ref="treeRef"
:data="deptList"
:expand-on-click-node="false"
:filter-node-method="filterNode"
:props="defaultProps"
default-expand-all
highlight-current
node-key="id"
@node-click="handleNodeClick"
/>
</div>
</template>
<script lang="ts" name="SystemUserDeptTree" setup>
import { ElTree } from 'element-plus'
import * as DeptApi from '@/api/system/dept'
import { defaultProps, handleTree } from '@/utils/tree'
const deptList = ref<Tree[]>([]) // 树形结构
const treeRef = ref<InstanceType<typeof ElTree>>()
/** 获得部门树 */
const getTree = async () => {
const res = await DeptApi.getSimpleDeptList()
deptList.value = []
deptList.value.push(...handleTree(res))
}
/** 基于名字过滤 */
const filterNode = (name: string, data: Tree) => {
if (!name) return true
return data.name.includes(name)
}
/** 处理部门被点击 */
const handleNodeClick = async (row: { [key: string]: any }) => {
emits('node-click', row)
}
const emits = defineEmits(['node-click'])
/** 初始化 */
onMounted(async () => {
await getTree()
})
</script>

View File

@@ -0,0 +1,96 @@
<template>
<Dialog v-model="dialogVisible" title="分配角色">
<el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
<el-form-item label="用户名称">
<el-input v-model="formData.username" :disabled="true" />
</el-form-item>
<el-form-item label="用户昵称">
<el-input v-model="formData.nickname" :disabled="true" />
</el-form-item>
<el-form-item label="角色">
<el-select v-model="formData.roleIds" multiple placeholder="请选择角色">
<el-option v-for="item in roleList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as PermissionApi from '@/api/system/permission'
import * as UserApi from '@/api/system/user'
import * as RoleApi from '@/api/system/role'
defineOptions({ name: 'SystemUserAssignRoleForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formData = ref({
id: -1,
nickname: '',
username: '',
roleIds: []
})
const formRef = ref() // 表单 Ref
const roleList = ref([] as RoleApi.RoleVO[]) // 角色的列表
/** 打开弹窗 */
const open = async (row: UserApi.UserVO) => {
dialogVisible.value = true
resetForm()
// 设置数据
formData.value.id = row.id
formData.value.username = row.username
formData.value.nickname = row.nickname
// 获得角色拥有的菜单集合
formLoading.value = true
try {
formData.value.roleIds = await PermissionApi.getUserRoleList(row.id)
} finally {
formLoading.value = false
}
// 获得角色列表
roleList.value = await RoleApi.getSimpleRoleList()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
await PermissionApi.assignUserRole({
userId: formData.value.id,
roleIds: formData.value.roleIds
})
message.success(t('common.updateSuccess'))
dialogVisible.value = false
// 发送操作成功的事件
emit('success', true)
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: -1,
nickname: '',
username: '',
roleIds: []
}
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,271 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle" style="width: 800px">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="用户姓名" prop="nickname">
<el-input v-model="formData.nickname" placeholder="请输入用户姓名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="用户性别">
<el-radio-group v-model="formData.sex">
<el-radio :label="1"> </el-radio>
<el-radio :label="2"> </el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="归属部门" prop="deptId">
<el-tree-select
v-model="formData.deptId"
:data="deptList"
:props="defaultProps"
check-strictly
node-key="id"
placeholder="请选择归属部门"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="角色" prop="role">
<el-select
v-model="formData.roleIds"
multiple
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="2"
placeholder="请选择角色"
>
<el-option
v-for="item in roleOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手机号码" prop="mobile">
<el-input v-model="formData.mobile" maxlength="11" placeholder="请输入手机号码" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" maxlength="50" placeholder="请输入邮箱" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="登录账号" prop="username">
<el-input v-model="formData.username" placeholder="请输入登录账号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="formData.id === undefined" label="用户密码" prop="password">
<el-input
v-model="formData.password"
placeholder="请输入用户密码"
show-password
type="password"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12" :offset="0">
<el-form-item label="微信号" prop="wxAlias">
<el-input v-model="formData.wxAlias" placeholder="请输入微信号" />
</el-form-item>
</el-col>
<el-col :span="12" :offset="0">
<el-form-item label="入职日期" prop="hireDate">
<el-date-picker
v-model="formData.hireDate"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="选择日期时间"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12" :offset="0">
<el-form-item label="钉钉手机号">
<el-input
v-model="formData.ddPhone"
placeholder="请输入钉钉手机号"
clearable
@blur="getDDId"
/>
</el-form-item>
</el-col>
<el-col :span="12" :offset="0">
<el-form-item label="钉钉Id" prop="dingUserId">
<el-input v-model="formData.dingUserId" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" name="SystemUserForm" setup>
import { CommonStatusEnum } from '@/utils/constants'
import { defaultProps, handleTree } from '@/utils/tree'
import { formatDate } from '@/utils/formatTime'
import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept'
import * as UserApi from '@/api/system/user'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref<any>({
nickname: '',
deptId: '',
mobile: '',
email: '',
id: undefined,
username: '',
password: '',
sex: 1,
remark: '',
status: CommonStatusEnum.ENABLE,
roleIds: [],
hireDate: '',
isNewUserConfirm: false,
wxAlias: ''
})
const formRules = ref<any>({
username: [{ required: true, message: '登录账号不能为空', trigger: 'blur' }],
nickname: [{ required: true, message: '用户姓名不能为空', trigger: 'blur' }],
password: [{ required: true, message: '用户密码不能为空', trigger: 'blur' }],
email: [
{
type: 'email',
message: '请输入正确的邮箱地址',
trigger: ['blur', 'change']
}
],
mobile: [{ required: true, message: '手机号不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const deptList = ref<Tree[]>([]) // 树形结构
const roleOptions = ref<any>([])
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await UserApi.getUser(id)
} finally {
formLoading.value = false
}
}
// 加载部门树
deptList.value = handleTree(await DeptApi.getSimpleDeptList())
// 加载岗位列表
roleOptions.value = await RoleApi.getSimpleRoleList()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as UserApi.UserVO
if (formType.value === 'create') {
const resp = await UserApi.createUser(data)
if (resp.code == 18888) {
await message.confirm(resp.msg)
formData.value.isNewUserConfirm = true
submitForm()
} else {
message.success(t('common.createSuccess'))
}
} else {
await UserApi.updateUser(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
nickname: '',
deptId: '',
mobile: '',
email: '',
id: undefined,
username: '',
password: '',
sex: 1,
remark: '',
status: CommonStatusEnum.ENABLE,
roleIds: [],
hireDate: formatDate(new Date(), 'YYYY-MM-DD'),
isNewUserConfirm: false,
wxAlias: ''
}
formRef.value?.resetFields()
}
async function getDDId() {
try {
if (!formData.value.ddPhone) {
formData.value.dingUserId = undefined
return
}
const data = await UserApi.getDDIdByPhone({ mobile: formData.value.ddPhone })
formData.value.dingUserId = data
} finally {
}
}
</script>

View File

@@ -0,0 +1,138 @@
<template>
<Dialog v-model="dialogVisible" title="用户导入" width="400">
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:action="importUrl + '?updateSupport=' + updateSupport"
:auto-upload="false"
:disabled="formLoading"
:headers="uploadHeaders"
:limit="1"
:on-error="submitFormError"
:on-exceed="handleExceed"
:on-success="submitFormSuccess"
accept=".xlsx, .xls"
drag
>
<Icon icon="ep:upload" />
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<div class="el-upload__tip">
<el-checkbox v-model="updateSupport" />
是否更新已经存在的用户数据
</div>
<span>仅允许导入 xlsxlsx 格式文件</span>
<el-link
:underline="false"
style="font-size: 12px; vertical-align: baseline"
type="primary"
@click="importTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as UserApi from '@/api/system/user'
import { getAccessToken, getTenantId } from '@/utils/auth'
import download from '@/utils/download'
defineOptions({ name: 'SystemUserImportForm' })
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const uploadRef = ref()
const importUrl =
import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/system/user/import'
const uploadHeaders = ref() // 上传 Header 头
const fileList = ref([]) // 文件列表
const updateSupport = ref(0) // 是否更新已经存在的用户数据
/** 打开弹窗 */
const open = () => {
dialogVisible.value = true
updateSupport.value = 0
fileList.value = []
resetForm()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const submitForm = async () => {
if (fileList.value.length == 0) {
message.error('请上传文件')
return
}
// 提交请求
uploadHeaders.value = {
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
}
formLoading.value = true
uploadRef.value!.submit()
}
/** 文件上传成功 */
const emits = defineEmits(['success'])
const submitFormSuccess = (response: any) => {
if (response.code !== 0) {
message.error(response.msg)
resetForm()
return
}
// 拼接提示语
const data = response.data
let text = '上传成功数量:' + data.createUsernames.length + ';'
for (let username of data.createUsernames) {
text += '< ' + username + ' >'
}
text += '更新成功数量:' + data.updateUsernames.length + ';'
for (const username of data.updateUsernames) {
text += '< ' + username + ' >'
}
text += '更新失败数量:' + Object.keys(data.failureUsernames).length + ';'
for (const username in data.failureUsernames) {
text += '< ' + username + ': ' + data.failureUsernames[username] + ' >'
}
message.alert(text)
formLoading.value = false
dialogVisible.value = false
// 发送操作成功的事件
emits('success')
}
/** 上传错误提示 */
const submitFormError = (): void => {
message.error('上传失败,请您重新上传!')
formLoading.value = false
}
/** 重置表单 */
const resetForm = async (): Promise<void> => {
// 重置上传状态和文件
formLoading.value = false
await nextTick()
uploadRef.value?.clearFiles()
}
/** 文件数超出提示 */
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
/** 下载模板操作 */
const importTemplate = async () => {
const res = await UserApi.importUserTemplate()
download.excel(res, '用户导入模版.xls')
}
</script>

View File

@@ -0,0 +1,279 @@
<template>
<el-row :gutter="20">
<!-- 左侧部门树 -->
<el-col :span="4" :xs="24">
<DeptTree @node-click="handleDeptNodeClick" />
</el-col>
<el-col :span="20" :xs="24">
<!-- 搜索 -->
<el-form :model="queryParams" ref="queryFormRef" inline label-width="0">
<el-form-item>
<el-input
v-model="queryParams.nickname"
placeholder="请输入姓名"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-input
v-model="queryParams.mobile"
placeholder="请输入手机号码"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-radio-group v-model="queryParams.status" @change="handleQuery">
<el-radio :label="0"> 启用 </el-radio>
<el-radio :label="1"> 禁用 </el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery" v-hasPermi="['system:user:query']">搜索</el-button>
<el-button @click="resetQuery" v-hasPermi="['system:user:query']">重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['system:user:create']"
>
新增
</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="list">
<el-table-column label="用户编号" key="id" prop="id" width="100" />
<el-table-column label="登录账号" prop="username" />
<el-table-column label="用户姓名" prop="nickname" />
<el-table-column label="部门" key="deptName" prop="deptName" width="100" />
<el-table-column label="手机号码" prop="mobile" width="120" />
<el-table-column label="微信号" prop="wxAlias" min-width="80" />
<el-table-column label="钉钉号" prop="dingUserId" min-width="80" />
<el-table-column label="在职状态" key="hireStatus" width="120">
<template #default="scope">
<el-switch
v-model="scope.row.hireStatus"
:active-value="0"
:inactive-value="1"
active-text="在职"
inactive-text="离职"
size="small"
v-hasPermi="['system:user:update']"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column label="启用状态" key="status" width="120">
<template #default="scope">
<el-switch
v-model="scope.row.status"
:active-value="0"
:inactive-value="1"
active-text="启用"
inactive-text="禁用"
size="small"
:disabled="scope.row.hireStatus == 1"
v-hasPermi="['system:user:update']"
@change="handleUseChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column label="入职日期" prop="hireDate" width="120" :formatter="dateFormatter" />
<el-table-column label="离职日期" prop="" width="120" :formatter="dateFormatter" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="scope">
<el-button
type="primary"
link
@click="openForm('update', scope.row.id)"
v-hasPermi="['system:user:update']"
>
修改
</el-button>
<el-button
type="primary"
link
@click="handleDelete(scope.row.id)"
v-hasPermi="['system:user:delete']"
>
删除
</el-button>
<el-button
type="primary"
link
@click="handleResetPwd(scope.row)"
v-hasPermi="['system:user:password']"
>
重置密码
</el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-col>
</el-row>
<!-- 添加或修改用户对话框 -->
<UserForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts" name="SystemUser">
import { CommonStatusEnum } from '@/utils/constants'
import { dateFormatter } from '@/utils/formatTime'
import * as UserApi from '@/api/system/user'
import UserForm from './UserForm.vue'
import DeptTree from './DeptTree.vue'
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数
const queryParams = ref({
pageNo: 1,
pageSize: 20,
username: undefined,
nickname: undefined,
mobile: undefined,
deptId: undefined,
status: undefined
})
const queryFormRef = ref() // 搜索的表单
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await UserApi.getUserPage(queryParams.value)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields()
queryParams.value = {
pageNo: 1,
pageSize: 20,
username: undefined,
nickname: undefined,
mobile: undefined,
deptId: undefined,
status: undefined
}
handleQuery()
}
/** 处理部门被点击 */
const handleDeptNodeClick = async (row) => {
queryParams.value.deptId = row.id
await getList()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 修改在职状态 */
const handleStatusChange = async (row: UserApi.UserVO) => {
try {
// 修改状态的二次确认
const text = row.hireStatus === CommonStatusEnum.ENABLE ? '入职' : '离职'
let result = { value: '' }
if (text == '入职') {
await message.confirm('确认要"' + text + '""' + row.nickname + '"吗?')
} else {
result = await message.prompt(
'请输入年月日如2024-01-01',
'确认要"' + text + '""' + row.nickname + '"用户吗?'
)
const regex = /^\d{4}-\d{2}-\d{2}$/
if (!regex.test(result.value)) {
message.error('请输入离职日期')
throw new Error()
}
}
// 发起修改状态
await UserApi.updateUserStatus(row.id, row.status, result.value, row.hireStatus)
message.success('修改成功')
// 刷新列表
await getList()
} catch {
// 取消后,进行恢复按钮
row.hireStatus =
row.hireStatus === CommonStatusEnum.ENABLE
? CommonStatusEnum.DISABLE
: CommonStatusEnum.ENABLE
}
}
// 修改启用状态
const handleUseChange = async (row: UserApi.UserVO) => {
try {
// 修改状态的二次确认
const text = row.status === CommonStatusEnum.ENABLE ? '启用' : '禁用'
await message.confirm('确认要"' + text + '""' + row.nickname + '"吗?')
// 发起修改状态
await UserApi.updateUserStatus(row.id, row.status, row.retireDate, row.hireStatus)
message.success('修改成功')
// 刷新列表
await getList()
} catch {
// 取消后,进行恢复按钮
row.status =
row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
}
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await UserApi.deleteUser(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 重置密码 */
const handleResetPwd = async (row: UserApi.UserVO) => {
try {
// 重置的二次确认
const result = await message.prompt(
'请输入"' + row.username + '"的新密码',
t('common.reminder')
)
const password = result.value
// 发起重置
await UserApi.resetUserPwd(row.id, password)
message.success('修改成功,新密码是:' + password)
} catch {}
}
/** 初始化 */
onMounted(() => {
getList()
})
</script>