refactor(tenant): 优化租户相关代码

This commit is contained in:
2025-07-16 22:41:56 +08:00
parent 112c27e49c
commit 87bcf33940
10 changed files with 69 additions and 200 deletions

View File

@@ -5,19 +5,32 @@ export type * from './type'
const BASE_URL = '/auth'
/** @desc 账号登录 */
export function accountLogin(req: T.AccountLoginReq) {
return http.post<T.LoginResp>(`${BASE_URL}/login`, req)
const login = (req: T.AccountLoginReq | T.PhoneLoginReq | T.EmailLoginReq, tenantCode?: string) => {
const headers = {}
if (tenantCode) {
headers['X-Tenant-Code'] = tenantCode
}
return http.requestNative({
url: `${BASE_URL}/login`,
data: req,
method: 'post',
headers,
})
}
/** @desc 手机号登录 */
export function phoneLogin(req: T.PhoneLoginReq) {
return http.post<T.LoginResp>(`${BASE_URL}/login`, req)
/** @desc 号登录 */
export function accountLogin(req: T.AccountLoginReq, tenantCode?: string) {
return login(req, tenantCode)
}
/** @desc 邮箱登录 */
export function emailLogin(req: T.EmailLoginReq) {
return http.post<T.LoginResp>(`${BASE_URL}/login`, req)
export function emailLogin(req: T.EmailLoginReq, tenantCode?: string) {
return login(req, tenantCode)
}
/** @desc 手机号登录 */
export function phoneLogin(req: T.PhoneLoginReq, tenantCode?: string) {
return login(req, tenantCode)
}
/** @desc 三方账号登录 */

View File

@@ -335,6 +335,7 @@ export interface BasicConfig {
SITE_TITLE: string
SITE_COPYRIGHT: string
SITE_BEIAN: string
TENANT_ENABLED: boolean
}
/** 基础配置类型 */

View File

@@ -1,6 +1,5 @@
import type * as T from './type'
import http from '@/utils/http'
import type { TenantCommon } from '@/utils/tenant'
export type * from './type'
@@ -31,11 +30,6 @@ export function deleteTenant(id: string) {
return http.del(`${BASE_URL}/${id}`)
}
/** @desc 多租户通用信息查询 */
export const getTenantCommon = () => {
return http.get<TenantCommon>(`${BASE_URL}/common`)
}
/** @desc 修改租户管理员密码 */
export const updateTenantAdminUserPwd = (data: any, id: string) => {
return http.put(`${BASE_URL}/${id}/admin/pwd`, data)

View File

@@ -1,168 +0,0 @@
<template>
<a-modal
v-model:visible="visible"
title="选择租户"
hide-cancel
:closable="false"
:mask-closable="false"
:ok-loading="okLoading"
draggable
:width="width >= 500 ? 500 : '90%'"
@before-ok="confirm"
>
<div class="scrollable-container">
<a-radio-group v-model="tenantId" direction="vertical" class="scrollable-container-radio-group" @change="value => { setTenantId(value) }">
<a-radio :value="0" class="scrollable-container-radio">
<template #radio="{ checked }">
<a-space
align="start"
class="custom-radio-card"
:class="{ 'custom-radio-card-checked': checked }"
>
<div className="custom-radio-card-mask">
<div className="custom-radio-card-mask-dot" />
</div>
系统默认租户
</a-space>
</template>
</a-radio>
<template v-for="item in tenantList" :key="item.id">
<a-radio :value="item.id" class="scrollable-container-radio">
<template #radio="{ checked }">
<a-space
align="start"
class="custom-radio-card"
:class="{ 'custom-radio-card-checked': checked }"
>
<div className="custom-radio-card-mask">
<div className="custom-radio-card-mask-dot" />
</div>
{{ item.name }}
</a-space>
</template>
</a-radio>
</template>
</a-radio-group>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core'
import { Message } from '@arco-design/web-vue'
import { getTenantId, setTenantId } from '@/utils/tenant'
import { getTenantCommon } from '@/apis/tenant/management'
const { width } = useWindowSize()
const visible = ref(false)
const okLoading = ref(false)
const tenantList = ref()
const tenantId = ref(getTenantId())
const confirm = async () => {
if (!getTenantId()) {
Message.error('请确认需要登录的租户')
return false
} else {
visible.value = true
}
}
onMounted(async () => {
const tenantCommon = await getTenantCommon()
// 需要开启了多租户才显示租户选择款
if (tenantCommon && tenantCommon.data.isEnabled) {
if (tenantCommon.data.availableList.length > 0) {
const hostname = window.location.hostname
for (const item of tenantCommon.data.availableList) {
// 如果有域名匹配则直接设置对应租户
if (item.domain === hostname) {
setTenantId(item.id)
return false
}
}
} else {
// 如果后台没有配置租户则是默认系统租户
setTenantId(0)
return false
}
tenantList.value = tenantCommon.data.availableList
visible.value = true
}
})
</script>
<style scoped lang="scss">
.tenant-select{
width: 100%;
padding: 10px 0;
}
.scrollable-container {
height: 300px;
overflow-y: auto;
scrollbar-width: none;
text-align: center;
&-radio-group{
width: 90%
}
&-radio{
padding: 7px 0
}
}
.custom-radio-card {
padding: 10px 16px;
border: 1px solid var(--color-border-2);
border-radius: 4px;
width: 100%;
box-sizing: border-box;
white-space: nowrap;
display: flex;
align-items: center;
}
.custom-radio-card-mask {
height: 14px;
width: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 100%;
border: 1px solid var(--color-border-2);
box-sizing: border-box;
}
.custom-radio-card-mask-dot {
width: 8px;
height: 8px;
border-radius: 100%;
}
.custom-radio-card-title {
color: var(--color-text-1);
font-size: 14px;
font-weight: bold;
margin-bottom: 8px;
}
.custom-radio-card:hover,
.custom-radio-card-checked,
.custom-radio-card:hover .custom-radio-card-mask,
.custom-radio-card-checked .custom-radio-card-mask{
border-color: rgb(var(--primary-6));
}
.custom-radio-card-checked {
background-color: var(--color-primary-light-1);
}
.custom-radio-card:hover .custom-radio-card-title,
.custom-radio-card-checked .custom-radio-card-title {
color: rgb(var(--primary-6));
}
.custom-radio-card-checked .custom-radio-card-mask-dot {
background-color: rgb(var(--primary-6));
}
</style>

View File

@@ -68,6 +68,7 @@ const storeSetup = () => {
siteConfig.SITE_TITLE = resMap.get('SITE_TITLE')
siteConfig.SITE_COPYRIGHT = resMap.get('SITE_COPYRIGHT')
siteConfig.SITE_BEIAN = resMap.get('SITE_BEIAN')
siteConfig.TENANT_ENABLED = resMap.get('TENANT_ENABLED') === 'true'
document.title = resMap.get('SITE_TITLE')
document
.querySelector('link[rel="shortcut icon"]')
@@ -122,6 +123,10 @@ const storeSetup = () => {
const getForRecord = () => {
return siteConfig.SITE_BEIAN
}
const getTenantEnabled = () => {
return siteConfig.TENANT_ENABLED
}
return {
...toRefs(settingConfig),
...toRefs(siteConfig),
@@ -138,6 +143,7 @@ const storeSetup = () => {
getTitle,
getCopyright,
getForRecord,
getTenantEnabled,
}
}

View File

@@ -50,22 +50,22 @@ const storeSetup = () => {
}
// 登录
const accountLogin = async (req: AccountLoginReq) => {
const res = await accountLoginApi({ ...req, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.ACCOUNT })
const accountLogin = async (req: AccountLoginReq, tenantCode?: string) => {
const res = await accountLoginApi({ ...req, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.ACCOUNT }, tenantCode)
setToken(res.data.token)
token.value = res.data.token
}
// 邮箱登录
const emailLogin = async (req: EmailLoginReq) => {
const res = await emailLoginApi({ ...req, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.EMAIL })
const emailLogin = async (req: EmailLoginReq, tenantCode?: string) => {
const res = await emailLoginApi({ ...req, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.EMAIL }, tenantCode)
setToken(res.data.token)
token.value = res.data.token
}
// 手机号登录
const phoneLogin = async (req: PhoneLoginReq) => {
const res = await phoneLoginApi({ ...req, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.PHONE })
const phoneLogin = async (req: PhoneLoginReq, tenantCode?: string) => {
const res = await phoneLoginApi({ ...req, clientId: import.meta.env.VITE_CLIENT_ID, authType: AuthTypeConstants.PHONE }, tenantCode)
setToken(res.data.token)
token.value = res.data.token
}

View File

@@ -52,17 +52,14 @@ const handleError = (msg: string) => {
http.interceptors.request.use(
(config: AxiosRequestConfig) => {
const token = getToken()
if (!config.headers) {
config.headers = {}
}
if (token) {
if (!config.headers) {
config.headers = {}
}
config.headers.Authorization = `Bearer ${token}`
}
const tenantId = getTenantId()
if (tenantId) {
if (!config.headers) {
config.headers = {}
}
config.headers['X-Tenant-Id'] = tenantId
}
return config

View File

@@ -8,6 +8,9 @@
size="large"
@submit="handleLogin"
>
<a-form-item v-if="tenantEnabled" field="tenantCode" hide-label>
<a-input v-model="tenantCode" placeholder="请输入租户编码" allow-clear />
</a-form-item>
<a-form-item field="username" hide-label>
<a-input v-model="form.username" placeholder="请输入用户名" allow-clear />
</a-form-item>
@@ -40,12 +43,17 @@
<script setup lang="ts">
import { type FormInstance, Message } from '@arco-design/web-vue'
import { useStorage } from '@vueuse/core'
import { computed } from 'vue'
import { getImageCaptcha } from '@/apis/common'
import { useTabsStore, useUserStore } from '@/stores'
import { useAppStore, useTabsStore, useUserStore } from '@/stores'
import { encryptByRsa } from '@/utils/encrypt'
const appStore = useAppStore()
const tenantEnabled = computed(() => appStore.getTenantEnabled())
const loginConfig = useStorage('login-config', {
rememberMe: true,
tenantCode: '',
username: 'admin', // 演示默认值
password: 'admin123', // 演示默认值
// username: debug ? 'admin' : '', // 演示默认值
@@ -64,6 +72,7 @@ const form = reactive({
uuid: '',
expired: false,
})
const tenantCode = ref()
const rules: FormInstance['rules'] = {
username: [{ required: true, message: '请输入用户名' }],
password: [{ required: true, message: '请输入密码' }],
@@ -119,10 +128,11 @@ const handleLogin = async () => {
password: encryptByRsa(form.password) || '',
captcha: form.captcha,
uuid: form.uuid,
})
}, tenantCode.value)
tabsStore.reset()
const { redirect, ...othersQuery } = router.currentRoute.value.query
const { rememberMe } = loginConfig.value
loginConfig.value.tenantCode = rememberMe ? tenantCode.value : ''
loginConfig.value.username = rememberMe ? form.username : ''
// 如果有重定向参数,解码并直接跳转到完整路径

View File

@@ -8,6 +8,9 @@
size="large"
@submit="handleLogin"
>
<a-form-item v-if="tenantEnabled" field="tenantCode" hide-label>
<a-input v-model="tenantCode" placeholder="请输入租户编码" allow-clear />
</a-form-item>
<a-form-item field="email" hide-label>
<a-input v-model="form.email" placeholder="请输入邮箱" allow-clear />
</a-form-item>
@@ -40,16 +43,21 @@
<script setup lang="ts">
import { type FormInstance, Message } from '@arco-design/web-vue'
import { computed } from 'vue'
import type { BehaviorCaptchaReq } from '@/apis'
// import { type BehaviorCaptchaReq, getEmailCaptcha } from '@/apis'
import { useTabsStore, useUserStore } from '@/stores'
import { useAppStore, useTabsStore, useUserStore } from '@/stores'
import * as Regexp from '@/utils/regexp'
const appStore = useAppStore()
const tenantEnabled = computed(() => appStore.getTenantEnabled())
const formRef = ref<FormInstance>()
const form = reactive({
email: '',
captcha: '',
})
const tenantCode = ref()
const rules: FormInstance['rules'] = {
email: [
@@ -69,7 +77,7 @@ const handleLogin = async () => {
const isInvalid = await formRef.value?.validate()
if (isInvalid) return
loading.value = true
await userStore.emailLogin(form)
await userStore.emailLogin(form, tenantCode.value)
tabsStore.reset()
const { redirect, ...othersQuery } = router.currentRoute.value.query

View File

@@ -8,6 +8,9 @@
size="large"
@submit="handleLogin"
>
<a-form-item v-if="tenantEnabled" field="tenantCode" hide-label>
<a-input v-model="tenantCode" placeholder="请输入租户编码" allow-clear />
</a-form-item>
<a-form-item field="phone" hide-label>
<a-input v-model="form.phone" placeholder="请输入手机号" :max-length="11" allow-clear />
</a-form-item>
@@ -41,15 +44,20 @@
<script setup lang="ts">
import { type FormInstance, Message } from '@arco-design/web-vue'
// import type { BehaviorCaptchaReq } from '@/apis'
import { computed } from 'vue'
import { type BehaviorCaptchaReq, getSmsCaptcha } from '@/apis'
import { useTabsStore, useUserStore } from '@/stores'
import { useAppStore, useTabsStore, useUserStore } from '@/stores'
import * as Regexp from '@/utils/regexp'
const appStore = useAppStore()
const tenantEnabled = computed(() => appStore.getTenantEnabled())
const formRef = ref<FormInstance>()
const form = reactive({
phone: '',
captcha: '',
})
const tenantCode = ref()
const rules: FormInstance['rules'] = {
phone: [
@@ -69,7 +77,7 @@ const handleLogin = async () => {
if (isInvalid) return
try {
loading.value = true
await userStore.phoneLogin(form)
await userStore.phoneLogin(form, tenantCode.value)
tabsStore.reset()
const { redirect, ...othersQuery } = router.currentRoute.value.query