登录调整

master
zzy 2026-04-25 16:27:26 +08:00
parent 19825c7bc3
commit bfff16cd6b
8 changed files with 434 additions and 198 deletions

View File

@ -115,3 +115,22 @@ export const getUserTenantList = (data: UserTenantListVO) => {
data data
}) })
} }
// 切换租户并重新登录
export interface SwitchTenantVO {
tenantId: number
}
export const switchTenant = (data: SwitchTenantVO) => {
return request.post({
url: '/system/auth/switch-tenant',
data
})
}
// 获取登录用户的租户列表(已登录状态)
export const getLoggedInUserTenantList = () => {
return request.get({
url: '/system/auth/get-user-tenant-list'
})
}

View File

@ -0,0 +1,102 @@
<template>
<div class="tenant-switch-btn" @click="handleOpenDialog">
<Icon icon="ep:location" class="tenant-icon" />
<span class="tenant-name">{{ currentTenantName || '选择小区' }}</span>
<Icon icon="ep:arrow-down" class="arrow-icon" />
</div>
<TenantSwitchDialog v-model="dialogVisible" />
</template>
<script lang="ts" setup>
import { useUserStore } from '@/store/modules/user'
import TenantSwitchDialog from './TenantSwitchDialog.vue'
import * as LoginApi from '@/api/login'
defineOptions({ name: 'TenantSwitchButton' })
const userStore = useUserStore()
const dialogVisible = ref(false)
const loading = ref(false)
//
const currentTenantName = computed(() => {
return userStore.user.tenantName || ''
})
//
const handleOpenDialog = () => {
dialogVisible.value = true
}
//
const getCurrentTenant = async () => {
if (currentTenantName.value) {
return
}
loading.value = true
try {
const res = await LoginApi.getLoggedInUserTenantList()
const tenantList = Array.isArray(res) ? res : (res.data || [])
if (tenantList.length > 0) {
const defaultTenant = tenantList.find((t: any) => t.isDefault)
if (defaultTenant) {
userStore.user.tenantId = defaultTenant.tenantId
userStore.user.tenantName = defaultTenant.tenantName
} else if (tenantList.length === 1) {
userStore.user.tenantId = tenantList[0].tenantId
userStore.user.tenantName = tenantList[0].tenantName
}
}
} catch (error) {
console.error('获取当前小区信息失败:', error)
} finally {
loading.value = false
}
}
//
onMounted(() => {
getCurrentTenant()
})
</script>
<style lang="scss" scoped>
.tenant-switch-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 20px;
min-width: 160px;
margin-right: 12px;
background: rgba(255, 255, 255, 0.15);
border-radius: 20px;
cursor: pointer;
transition: all 0.3s;
color: #fff;
&:hover {
background: rgba(255, 255, 255, 0.25);
}
.tenant-icon {
font-size: 16px;
}
.tenant-name {
font-size: 14px;
font-weight: 500;
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.arrow-icon {
font-size: 12px;
transition: transform 0.3s;
}
}
</style>

View File

@ -0,0 +1,200 @@
<template>
<el-dialog
v-model="dialogVisible"
title="切换小区"
width="500px"
:close-on-click-modal="true"
>
<div v-loading="loading" class="tenant-switch-content">
<el-empty v-if="!loading && tenantList.length === 0" description="暂无可切换的小区" />
<div v-else class="tenant-list">
<div
v-for="tenant in tenantList"
:key="tenant.tenantId"
class="tenant-item"
:class="{
'tenant-item-active': tenant.isDefault,
'tenant-item-disabled': tenant.isDefault
}"
@click="handleSwitchTenant(tenant)"
>
<div class="tenant-info">
<div class="tenant-name">{{ tenant.tenantName }}</div>
<div v-if="tenant.isDefault" class="tenant-tag">
当前小区
</div>
</div>
<Icon v-if="!tenant.isDefault" icon="ep:arrow-right" class="tenant-arrow" />
<Icon v-else icon="ep:check" class="tenant-check" />
</div>
</div>
</div>
</el-dialog>
</template>
<script lang="ts" setup>
import * as LoginApi from '@/api/login'
import { useUserStore } from '@/store/modules/user'
import * as authUtil from '@/utils/auth'
import { deleteUserCache } from '@/hooks/web/useCache'
defineOptions({ name: 'TenantSwitchDialog' })
const { t } = useI18n()
const message = useMessage()
const userStore = useUserStore()
const { push } = useRouter()
const props = defineProps<{
modelValue: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
}>()
const dialogVisible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const loading = ref(false)
const tenantList = ref<any[]>([])
//
const getTenantList = async () => {
loading.value = true
try {
const res = await LoginApi.getLoggedInUserTenantList()
tenantList.value = Array.isArray(res) ? res : (res.data || [])
} catch (error) {
console.error('获取小区列表失败:', error)
message.error('获取小区列表失败')
} finally {
loading.value = false
}
}
//
const handleSwitchTenant = async (tenant: any) => {
if (tenant.isDefault) {
message.info('当前已是该小区')
return
}
try {
await message.confirm(`确定要切换到【${tenant.tenantName}】吗?`, '切换小区')
loading.value = true
//
const res = await LoginApi.switchTenant({ tenantId: tenant.tenantId })
// token
authUtil.setToken(res)
//
deleteUserCache()
//
userStore.resetState()
//
await userStore.setUserInfoAction()
message.success('切换成功')
dialogVisible.value = false
//
setTimeout(() => {
location.reload()
}, 500)
} catch (error) {
if (error !== 'cancel') {
console.error('切换小区失败:', error)
message.error('切换小区失败')
}
} finally {
loading.value = false
}
}
//
watch(() => props.modelValue, (visible) => {
if (visible) {
getTenantList()
} else {
tenantList.value = []
}
})
</script>
<style lang="scss" scoped>
.tenant-switch-content {
min-height: 200px;
}
.tenant-list {
max-height: 400px;
overflow-y: auto;
}
.tenant-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
margin-bottom: 8px;
border: 1px solid #e5e7eb;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
&:hover:not(.tenant-item-disabled) {
border-color: #1677ff;
background-color: #f0f7ff;
}
&-active {
border-color: #1677ff;
background-color: #e6f7ff;
cursor: default;
}
&-disabled {
cursor: default;
opacity: 0.85;
}
}
.tenant-info {
flex: 1;
display: flex;
align-items: center;
gap: 12px;
}
.tenant-name {
font-size: 15px;
font-weight: 500;
color: #333;
}
.tenant-tag {
font-size: 12px;
color: #1677ff;
background-color: rgba(22, 119, 255, 0.1);
padding: 2px 8px;
border-radius: 4px;
}
.tenant-arrow {
font-size: 16px;
color: #999;
}
.tenant-check {
font-size: 16px;
color: #1677ff;
}
</style>

View File

@ -1,5 +1,5 @@
<script lang="tsx"> <script lang="tsx">
import { defineComponent, computed } from 'vue' import { defineComponent, computed, ref } from 'vue'
import { Message } from '@/layout/components//Message' import { Message } from '@/layout/components//Message'
import { Collapse } from '@/layout/components/Collapse' import { Collapse } from '@/layout/components/Collapse'
import { UserInfo } from '@/layout/components/UserInfo' import { UserInfo } from '@/layout/components/UserInfo'
@ -9,6 +9,7 @@ import { SizeDropdown } from '@/layout/components/SizeDropdown'
import { LocaleDropdown } from '@/layout/components/LocaleDropdown' import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
import RouterSearch from '@/components/RouterSearch/index.vue' import RouterSearch from '@/components/RouterSearch/index.vue'
import TenantVisit from '@/layout/components/TenantVisit/index.vue' import TenantVisit from '@/layout/components/TenantVisit/index.vue'
import TenantSwitchButton from './TenantSwitchButton.vue'
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store/modules/app'
import { useDesign } from '@/hooks/web/useDesign' import { useDesign } from '@/hooks/web/useDesign'
import { checkPermi } from '@/utils/permission' import { checkPermi } from '@/utils/permission'
@ -48,6 +49,14 @@ const hasTenantVisitPermission = computed(
() => import.meta.env.VITE_APP_TENANT_ENABLE === 'true' && checkPermi(['system:tenant:visit']) () => import.meta.env.VITE_APP_TENANT_ENABLE === 'true' && checkPermi(['system:tenant:visit'])
) )
// /
const showTools = ref(false)
//
const toggleTools = () => {
showTools.value = !showTools.value
}
export default defineComponent({ export default defineComponent({
name: 'ToolHeader', name: 'ToolHeader',
setup() { setup() {
@ -69,6 +78,17 @@ export default defineComponent({
</div> </div>
) : undefined} ) : undefined}
<div class="h-full flex items-center"> <div class="h-full flex items-center">
{/* 折叠/展开工具栏按钮 */}
<div
class="custom-hover text-white hover:text-white/85"
onClick={toggleTools}
>
<Icon icon={showTools.value ? 'ep:close' : 'ep:menu'} size={18} />
</div>
{/* 工具栏图标组,根据状态显示/隐藏 */}
{showTools.value && (
<div class="flex items-center animate-fade-in">
{hasTenantVisitPermission.value ? <TenantVisit /> : undefined} {hasTenantVisitPermission.value ? <TenantVisit /> : undefined}
{screenfull.value ? ( {screenfull.value ? (
<Screenfull class="custom-hover text-white hover:text-white/85"></Screenfull> <Screenfull class="custom-hover text-white hover:text-white/85"></Screenfull>
@ -85,6 +105,10 @@ export default defineComponent({
{message.value ? ( {message.value ? (
<Message class="custom-hover text-white hover:text-white/85"></Message> <Message class="custom-hover text-white hover:text-white/85"></Message>
) : undefined} ) : undefined}
</div>
)}
<TenantSwitchButton></TenantSwitchButton>
<UserInfo></UserInfo> <UserInfo></UserInfo>
</div> </div>
</div> </div>
@ -110,4 +134,19 @@ $prefix-cls: #{$namespace}-tool-header;
} }
} }
} }
@keyframes fade-in {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.animate-fade-in {
animation: fade-in 0.3s ease-out;
}
</style> </style>

View File

@ -92,22 +92,3 @@ const toDocument = () => {
</transition> </transition>
</teleport> </teleport>
</template> </template>
<style scoped lang="scss">
.fade-bottom-enter-active,
.fade-bottom-leave-active {
transition:
opacity 0.25s,
transform 0.3s;
}
.fade-bottom-enter-from {
opacity: 0;
transform: translateY(-10%);
}
.fade-bottom-leave-to {
opacity: 0;
transform: translateY(10%);
}
</style>

View File

@ -33,9 +33,8 @@
<el-form-item prop="username"> <el-form-item prop="username">
<el-input <el-input
v-model="loginData.loginForm.username" v-model="loginData.loginForm.username"
placeholder="请输入用户名" placeholder="请输入手机号/用户名"
:prefix-icon="iconAvatar" :prefix-icon="iconAvatar"
@blur="handleUsernameBlur"
/> />
</el-form-item> </el-form-item>
@ -47,29 +46,9 @@
show-password show-password
type="password" type="password"
@keyup.enter="getCode()" @keyup.enter="getCode()"
@blur="handlePasswordBlur"
/> />
</el-form-item> </el-form-item>
<!-- 租户选择 -->
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantId">
<el-select
ref="tenantSelectRef"
v-model="loginData.loginForm.tenantId"
placeholder="请选择租户"
:prefix-icon="iconHouse"
style="width: 100%"
:disabled="tenantList.length === 0"
>
<el-option
v-for="tenant in tenantList"
:key="tenant.tenantId"
:label="tenant.tenantName"
:value="tenant.tenantId"
/>
</el-select>
</el-form-item>
<!-- 记住密码 --> <!-- 记住密码 -->
<div class="login-options"> <div class="login-options">
<el-checkbox v-model="loginData.loginForm.rememberMe"> <el-checkbox v-model="loginData.loginForm.rememberMe">
@ -138,21 +117,13 @@ const isFetchingTenants = ref(false)
const LoginRules = { const LoginRules = {
username: [required], username: [required],
password: [required], password: [required]
tenantId: [
{
required: true,
message: '请选择租户',
trigger: 'change'
}
]
} }
const loginData = reactive({ const loginData = reactive({
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE, captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE, tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
loginForm: { loginForm: {
tenantId: undefined as number | undefined,
username: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || '', username: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || '',
password: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || '', password: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || '',
captchaVerification: '', captchaVerification: '',
@ -165,104 +136,8 @@ const loading = ref()
const { push } = useRouter() const { push } = useRouter()
const permissionStore = usePermissionStore() const permissionStore = usePermissionStore()
//
const handleUsernameBlur = async () => {
if (loginData.tenantEnable !== 'true') {
return
}
await fetchTenantList()
}
//
const handlePasswordBlur = async () => {
if (loginData.tenantEnable !== 'true') {
return
}
await fetchTenantList()
}
//
const fetchTenantList = async () => {
const username = loginData.loginForm.username?.trim()
const password = loginData.loginForm.password?.trim()
if (!username || !password) {
return
}
if (isFetchingTenants.value) {
return
}
isFetchingTenants.value = true
try {
const res = await LoginApi.getUserTenantList({
username,
password
})
if (!res || !res.tenants || res.tenants.length === 0) {
message.error('无权限访问该系统')
tenantList.value = []
loginData.loginForm.tenantId = undefined
return
}
tenantList.value = res.tenants
if (res.tenants.length === 1) {
loginData.loginForm.tenantId = res.tenants[0].tenantId
} else {
loginData.loginForm.tenantId = undefined
}
} catch (error) {
console.error('获取租户列表失败:', error)
tenantList.value = []
loginData.loginForm.tenantId = undefined
} finally {
isFetchingTenants.value = false
}
}
// //
const getCode = async () => { const getCode = async () => {
if (loginData.tenantEnable === 'true' && tenantList.value.length === 0) {
const username = loginData.loginForm.username?.trim()
const password = loginData.loginForm.password?.trim()
if (!username || !password) {
message.error('请输入用户名和密码')
return
}
await fetchTenantList()
if (tenantList.value.length === 0) {
return
}
if (tenantList.value.length === 1) {
loginData.loginForm.tenantId = tenantList.value[0].tenantId
continueLogin()
} else {
if (tenantSelectRef.value) {
tenantSelectRef.value.visible = true
}
message.info('请选择要登录的租户')
}
} else if (loginData.tenantEnable === 'true' && tenantList.value.length > 0) {
if (!loginData.loginForm.tenantId) {
message.error('请选择租户')
return
}
continueLogin()
} else {
continueLogin()
}
}
const continueLogin = () => {
if (loginData.captchaEnable === 'false') { if (loginData.captchaEnable === 'false') {
handleLogin({}) handleLogin({})
} else { } else {
@ -270,38 +145,9 @@ const continueLogin = () => {
} }
} }
const setTenantId = () => {
if (loginData.loginForm.tenantId) {
authUtil.setTenantId(loginData.loginForm.tenantId)
}
}
const getLoginFormCache = () => {
const loginForm = authUtil.getLoginForm()
if (loginForm) {
loginData.loginForm = {
...loginData.loginForm,
username: loginForm.username ? loginForm.username : loginData.loginForm.username,
password: loginForm.password ? loginForm.password : loginData.loginForm.password,
rememberMe: loginForm.rememberMe
}
}
}
const getTenantByWebsite = async () => {
if (loginData.tenantEnable === 'true') {
const website = location.host
const res = await LoginApi.getTenantByWebsite(website)
if (res) {
authUtil.setTenantId(res.id)
}
}
}
const handleLogin = async (params: any) => { const handleLogin = async (params: any) => {
loginLoading.value = true loginLoading.value = true
try { try {
setTenantId()
await formLogin.value.validate() await formLogin.value.validate()
const loginDataLoginForm = { ...loginData.loginForm } const loginDataLoginForm = { ...loginData.loginForm }
loginDataLoginForm.captchaVerification = params.captchaVerification loginDataLoginForm.captchaVerification = params.captchaVerification
@ -327,9 +173,20 @@ const handleLogin = async (params: any) => {
} }
} }
const getLoginFormCache = () => {
const loginForm = authUtil.getLoginForm()
if (loginForm) {
loginData.loginForm = {
...loginData.loginForm,
username: loginForm.username ? loginForm.username : loginData.loginForm.username,
password: loginForm.password ? loginForm.password : loginData.loginForm.password,
rememberMe: loginForm.rememberMe
}
}
}
onMounted(() => { onMounted(() => {
getLoginFormCache() getLoginFormCache()
getTenantByWebsite()
}) })
</script> </script>

View File

@ -29,14 +29,28 @@
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="楼号" prop="buildingNo"> <el-form-item label="楼号" prop="buildingNo">
<el-input v-model="formData.buildingNo" placeholder="请输入楼号" /> <el-input
v-model.number="formData.buildingNo"
placeholder="请输入楼号"
type="number"
@input="handleNumberInput('buildingNo')"
>
<template #append>号楼</template>
</el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="单元号" prop="unitNo"> <el-form-item label="单元号" prop="unitNo">
<el-input v-model="formData.unitNo" placeholder="请输入单元号" /> <el-input
v-model.number="formData.unitNo"
placeholder="请输入单元号"
type="number"
@input="handleNumberInput('unitNo')"
>
<template #append>单元</template>
</el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
@ -125,7 +139,15 @@ const open = async (type: string, id?: number) => {
if (id) { if (id) {
formLoading.value = true formLoading.value = true
try { try {
formData.value = await HouseApi.getHouse(id) const data = await HouseApi.getHouse(id)
//
if (data.buildingNo) {
data.buildingNo = String(data.buildingNo).replace(/号楼$/g, '')
}
if (data.unitNo) {
data.unitNo = String(data.unitNo).replace(/单元$/g, '')
}
formData.value = data
} finally { } finally {
formLoading.value = false formLoading.value = false
} }
@ -170,4 +192,12 @@ const resetForm = () => {
} }
formRef.value?.resetFields() formRef.value?.resetFields()
} }
/** 处理数字输入 */
const handleNumberInput = (field: string) => {
if (formData.value[field] === '') {
formData.value[field] = undefined
}
}
</script> </script>

View File

@ -102,8 +102,16 @@
<el-table-column type="selection" width="55" align="center" /> <el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" type="index" width="80" /> <el-table-column label="序号" align="center" type="index" width="80" />
<el-table-column label="小区名称" align="center" prop="communityName" min-width="120" /> <el-table-column label="小区名称" align="center" prop="communityName" min-width="120" />
<el-table-column label="楼号" align="center" prop="buildingNo" width="100" /> <el-table-column label="楼号" align="center" prop="buildingNo" width="100">
<el-table-column label="单元号" align="center" prop="unitNo" width="100" /> <template #default="scope">
{{ scope.row.buildingNo }}号楼
</template>
</el-table-column>
<el-table-column label="单元号" align="center" prop="unitNo" width="100">
<template #default="scope">
{{ scope.row.unitNo }}单元
</template>
</el-table-column>
<el-table-column label="门牌号" align="center" prop="roomNo" width="100" /> <el-table-column label="门牌号" align="center" prop="roomNo" width="100" />
<el-table-column label="业主姓名" align="center" prop="ownerName" width="100" /> <el-table-column label="业主姓名" align="center" prop="ownerName" width="100" />
<el-table-column label="业主联系方式" align="center" prop="ownerPhone" width="120"> <el-table-column label="业主联系方式" align="center" prop="ownerPhone" width="120">