数据权限

master
zzy 2026-04-21 00:08:08 +08:00
parent 80481f1dcb
commit 356298e599
6 changed files with 323 additions and 107 deletions

View File

@ -71,3 +71,7 @@ export const deleteTenantList = (ids: number[]) => {
export const exportTenant = (params: TenantExportReqVO) => {
return request.download({ url: '/system/tenant/export-excel', params })
}
export const getSimpleTenantList = () => {
return request.get({ url: '/system/tenant/simple-list' })
}

View File

@ -0,0 +1,37 @@
// src/api/system/userTenant/index.ts
import request from '@/config/axios'
export interface TenantInfoVO {
tenantId: number
tenantName: string
isDefault: boolean
}
export interface UserTenantRelVO {
id: number
userId: number
tenantId: number
tenantName: string
isDefault: boolean
createTime: Date
}
export const getUserTenants = (userId: number) => {
return request.get({ url: '/system/user-tenant/list-by-user', params: { userId } })
}
export const assignUserToTenant = (data: { userId: number; tenantId: number; isDefault?: boolean }) => {
return request.post({ url: '/system/user-tenant/assign', data })
}
export const batchAssignUserToTenants = (data: { userId: number; tenantIds: number[] }) => {
return request.post({ url: '/system/user-tenant/batch-assign', data })
}
export const removeUserFromTenant = (userId: number, tenantId: number) => {
return request.delete({ url: '/system/user-tenant/remove', params: { userId, tenantId } })
}
export const getSimpleTenantList = () => {
return request.get({ url: '/system/tenant/simple-list' })
}

View File

@ -86,64 +86,6 @@
mode="pop"
@success="handleLogin"
/>
<el-col :span="24" class="px-10px">
<el-form-item>
<el-row :gutter="5" justify="space-between" style="width: 100%">
<el-col :span="8">
<XButton
:title="t('login.btnMobile')"
class="w-full"
@click="setLoginState(LoginStateEnum.MOBILE)"
/>
</el-col>
<el-col :span="8">
<XButton
:title="t('login.btnQRCode')"
class="w-full"
@click="setLoginState(LoginStateEnum.QR_CODE)"
/>
</el-col>
<el-col :span="8">
<XButton
:title="t('login.btnRegister')"
class="w-full"
@click="setLoginState(LoginStateEnum.REGISTER)"
/>
</el-col>
</el-row>
</el-form-item>
</el-col>
<el-divider content-position="center">{{ t('login.otherLogin') }}</el-divider>
<el-col :span="24" class="px-10px">
<el-form-item>
<div class="w-full flex justify-between">
<Icon
v-for="(item, key) in socialList"
:key="key"
:icon="item.icon"
:size="30"
class="anticon cursor-pointer"
color="#999"
@click="doSocialLogin(item.type)"
/>
</div>
</el-form-item>
</el-col>
<el-divider content-position="center">萌新必读</el-divider>
<el-col :span="24" class="px-10px">
<el-form-item>
<div class="w-full flex justify-between">
<el-link href="https://doc.iocoder.cn/" target="_blank">📚开发指南</el-link>
<el-link href="https://doc.iocoder.cn/video/" target="_blank">🔥视频教程</el-link>
<el-link href="https://www.iocoder.cn/Interview/good-collection/" target="_blank">
面试手册
</el-link>
<el-link href="http://static.yudao.iocoder.cn/mp/Aix9975.jpeg" target="_blank">
🤝外包咨询
</el-link>
</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>

View File

@ -7,44 +7,51 @@
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="小区名称" prop="communityId">
<el-select
v-model="formData.communityId"
placeholder="请选择小区"
clearable
filterable
style="width: 100%"
@change="handleCommunityChange"
>
<el-option
v-for="item in communityOptions"
:key="item.id"
:label="item.communityName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="楼号" prop="buildingNo">
<el-input v-model="formData.buildingNo" placeholder="请输入楼号" />
</el-form-item>
<el-form-item label="单元号" prop="unitNo">
<el-input v-model="formData.unitNo" placeholder="请输入单元号" />
</el-form-item>
<el-form-item label="门牌号" prop="roomNo">
<el-input v-model="formData.roomNo" placeholder="请输入门牌号" />
</el-form-item>
<el-form-item label="产权面积(㎡)" prop="propertyArea">
<el-input v-model="formData.propertyArea" placeholder="请输入产权面积(㎡)" />
</el-form-item>
<el-form-item label="业主ID" prop="memberId">
<el-input v-model="formData.memberId" placeholder="请输入业主ID" />
</el-form-item>
<el-form-item label="业主姓名" prop="ownerName">
<el-input v-model="formData.ownerName" placeholder="请输入业主姓名" />
</el-form-item>
<el-form-item label="业主手机号" prop="ownerPhone">
<el-input v-model="formData.ownerPhone" placeholder="请输入业主手机号" />
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="小区名称" prop="communityId">
<el-select
v-model="formData.communityId"
placeholder="请选择"
clearable
filterable
style="width: 100%"
@change="handleCommunityChange"
>
<el-option
v-for="item in communityOptions"
:key="item.id"
:label="item.communityName"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="楼号" prop="buildingNo">
<el-input v-model="formData.buildingNo" placeholder="请输入楼号" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="单元号" prop="unitNo">
<el-input v-model="formData.unitNo" placeholder="请输入单元号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="门牌号" prop="roomNo">
<el-input v-model="formData.roomNo" placeholder="请输入门牌号" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="产权面积" prop="propertyArea">
<el-input v-model="formData.propertyArea" placeholder="请输入产权面积(㎡)" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
@ -73,16 +80,14 @@ const formData = ref({
buildingNo: undefined,
unitNo: undefined,
roomNo: undefined,
propertyArea: undefined,
memberId: undefined,
ownerName: undefined,
ownerPhone: undefined
propertyArea: undefined
})
const formRules = reactive({
communityId: [{ required: true, message: '小区不能为空', trigger: 'change' }],
buildingNo: [{ required: true, message: '楼号不能为空', trigger: 'blur' }],
unitNo: [{ required: true, message: '单元号不能为空', trigger: 'blur' }],
roomNo: [{ required: true, message: '门牌号不能为空', trigger: 'blur' }]
roomNo: [{ required: true, message: '门牌号不能为空', trigger: 'blur' }],
propertyArea: [{ required: true, message: '产权面积不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
const communityOptions = ref<CommunitySimpleVO[]>([]) //
@ -161,10 +166,7 @@ const resetForm = () => {
buildingNo: undefined,
unitNo: undefined,
roomNo: undefined,
propertyArea: undefined,
memberId: undefined,
ownerName: undefined,
ownerPhone: undefined
propertyArea: undefined
}
formRef.value?.resetFields()
}

View File

@ -125,6 +125,13 @@
prop="deptName"
:show-overflow-tooltip="true"
/>
<el-table-column
label="数据权限"
align="center"
prop="tenantNames"
:show-overflow-tooltip="true"
width="200"
/>
<el-table-column label="状态" key="status">
<template #default="scope">
<el-switch
@ -170,6 +177,14 @@
>
分配角色
</el-button>
<el-button
type="primary"
link
@click="handleAssignPermission(scope.row)"
v-hasPermi="['system:user:assign-tenant']"
>
分配权限
</el-button>
<el-button
type="danger"
link
@ -194,10 +209,9 @@
<!-- 添加或修改用户对话框 -->
<UserForm ref="formRef" @success="getList" />
<!-- 用户导入对话框 -->
<UserImportForm ref="importFormRef" @success="getList" />
<!-- 分配角色 -->
<UserAssignRoleForm ref="assignRoleFormRef" @success="getList" />
<UserTenantAssignForm ref="assignPermissionFormRef" @success="getList" />
</template>
<script lang="ts" setup>
@ -210,6 +224,7 @@ import * as UserApi from '@/api/system/user'
import UserForm from '../UserForm.vue'
import UserImportForm from '../UserImportForm.vue'
import UserAssignRoleForm from '../UserAssignRoleForm.vue'
import UserTenantAssignForm from './UserTenantAssignForm.vue'
defineOptions({ name: 'UserManagement' })
@ -325,6 +340,11 @@ const handleRole = (row: UserApi.UserVO) => {
assignRoleFormRef.value.open(row)
}
const assignPermissionFormRef = ref()
const handleAssignPermission = (row: UserApi.UserVO) => {
assignPermissionFormRef.value.open(row)
}
onMounted(() => {
getList()
})

View File

@ -0,0 +1,211 @@
<!-- src/views/system/user/components/UserTenantAssignForm.vue -->
<template>
<Dialog v-model="visible" title="分配小区权限" width="800px">
<el-form ref="formRef" :model="formData" label-width="80px">
<el-form-item label="用户名称">
<el-input v-model="currentUserInfo.username" disabled />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="currentUserInfo.mobile" disabled />
</el-form-item>
</el-form>
<div class="assign-content">
<div class="left-panel">
<div class="panel-header">可选择的小区</div>
<el-input
v-model="availableTenantSearch"
placeholder="搜索小区"
clearable
class="search-input"
/>
<el-checkbox-group v-model="checkedTenantIds" class="tenant-list">
<el-checkbox
v-for="tenant in filteredAvailableTenants"
:key="tenant.tenantId"
:label="tenant.tenantId"
:value="tenant.tenantId"
>
{{ tenant.tenantName }}
</el-checkbox>
</el-checkbox-group>
</div>
<div class="middle-panel">
<el-button type="primary" :icon="Right" @click="handleAssign" :disabled="checkedTenantIds.length === 0" />
<el-button type="info" :icon="Right" @click="handleRemove" :disabled="selectedAssignedIds.length === 0" />
</div>
<div class="right-panel">
<div class="panel-header">已分配的小区</div>
<el-checkbox-group v-model="selectedAssignedIds" class="tenant-list">
<el-checkbox
v-for="rel in assignedTenants"
:key="rel.tenantId"
:label="rel.tenantId"
:value="rel.tenantId"
>
{{ rel.tenantName }}
<el-tag v-if="rel.isDefault" size="small" type="success" class="ml-5px"></el-tag>
</el-checkbox>
</el-checkbox-group>
</div>
</div>
<template #footer>
<el-button type="primary" @click="handleSubmit"> </el-button>
<el-button @click="visible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { Right } from '@element-plus/icons-vue'
import * as UserTenantApi from '@/api/system/userTenant'
import * as TenantApi from '@/api/system/tenant'
import type { UserVO } from '@/api/system/user'
defineOptions({ name: 'UserTenantAssignForm' })
const message = useMessage()
const visible = ref(false)
const formRef = ref()
const currentUserInfo = ref<UserVO>({} as UserVO)
const allTenants = ref<any[]>([])
const assignedTenants = ref<any[]>([])
const checkedTenantIds = ref<number[]>([])
const selectedAssignedIds = ref<number[]>([])
const availableTenantSearch = ref('')
const filteredAvailableTenants = computed(() => {
if (!availableTenantSearch.value) {
return allTenants.value
}
return allTenants.value.filter(
(t) => t.tenantName?.toLowerCase().includes(availableTenantSearch.value.toLowerCase())
)
})
const open = (user: UserVO) => {
visible.value = true
currentUserInfo.value = user
checkedTenantIds.value = []
selectedAssignedIds.value = []
loadTenants()
loadAssignedTenants(user.id)
}
const loadTenants = async () => {
try {
const data = await TenantApi.getSimpleTenantList()
allTenants.value = data || []
} catch (error) {
console.error('加载小区列表失败', error)
}
}
const loadAssignedTenants = async (userId: number) => {
try {
const data = await UserTenantApi.getUserTenants(userId)
assignedTenants.value = data || []
} catch (error) {
console.error('加载已分配小区失败', error)
}
}
const handleAssign = async () => {
if (checkedTenantIds.value.length === 0) {
message.warning('请选择要分配的小区')
return
}
try {
await UserTenantApi.batchAssignUserToTenants({
userId: currentUserInfo.value.id,
tenantIds: checkedTenantIds.value
})
message.success('分配成功')
checkedTenantIds.value = []
loadAssignedTenants(currentUserInfo.value.id)
} catch (error) {
console.error('分配失败', error)
}
}
const handleRemove = async () => {
if (selectedAssignedIds.value.length === 0) {
message.warning('请选择要移除的小区')
return
}
try {
for (const tenantId of selectedAssignedIds.value) {
await UserTenantApi.removeUserFromTenant(currentUserInfo.value.id, tenantId)
}
message.success('移除成功')
selectedAssignedIds.value = []
loadAssignedTenants(currentUserInfo.value.id)
} catch (error) {
console.error('移除失败', error)
}
}
const handleSubmit = () => {
visible.value = false
emit('success')
}
defineExpose({ open })
</script>
<style scoped lang="scss">
.assign-content {
display: flex;
gap: 15px;
margin-top: 15px;
min-height: 400px;
}
.left-panel,
.right-panel {
flex: 1;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
flex-direction: column;
}
.middle-panel {
display: flex;
flex-direction: column;
justify-content: center;
gap: 10px;
}
.panel-header {
padding: 10px;
background-color: #f5f7fa;
border-bottom: 1px solid #dcdfe6;
font-weight: bold;
text-align: center;
}
.search-input {
padding: 10px;
border-bottom: 1px solid #ebeef5;
}
.tenant-list {
flex: 1;
overflow-y: auto;
padding: 10px;
display: flex;
flex-direction: column;
gap: 8px;
}
:deep(.el-checkbox) {
margin-right: 0;
}
</style>