This commit is contained in:
Your Name
2026-03-13 14:08:52 +08:00
parent 08dd9cd307
commit eb283f008b
667 changed files with 8425 additions and 27053 deletions
+220 -56
View File
@@ -6,41 +6,79 @@
<image-contain :src="config.login_image" :width="400" height="100%" />
</div>
<div
class="login-form bg-body flex flex-col justify-center px-10 py-10 md:w-[400px] w-[375px] flex-none mx-auto"
class="login-form bg-body flex flex-col justify-center px-10 py-10 md:w-[420px] w-[380px] flex-none mx-auto"
>
<div class="text-center text-3xl font-medium mb-8">{{ config.web_name }}</div>
<el-form ref="formRef" :model="formData" size="large" :rules="rules">
<el-form-item prop="account">
<el-input
v-model="formData.account"
placeholder="请输入账号"
@keyup.enter="handleEnter"
>
<template #prepend>
<icon name="el-icon-User" size="16" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
ref="passwordRef"
v-model="formData.password"
show-password
placeholder="请输入密码"
@keyup.enter="handleLogin"
>
<template #prepend>
<icon name="el-icon-Lock" size="16" />
</template>
</el-input>
</el-form-item>
</el-form>
<div class="mb-5">
<el-checkbox v-model="remAccount" label="记住账号"></el-checkbox>
<!-- 企业微信自动授权中 -->
<div v-if="wxWorkAutoLogin" class="text-center py-10">
<el-icon class="is-loading mb-4" :size="40" color="var(--el-color-primary)">
<Loading />
</el-icon>
<div class="text-gray-500">企业微信授权登录中...</div>
</div>
<el-button type="primary" size="large" :loading="isLock" @click="lockLogin">
登录
</el-button>
<template v-else>
<!-- 登录方式切换标签 -->
<div v-if="wxWorkEnabled" class="flex justify-center mb-6">
<el-radio-group v-model="loginMode" size="large">
<el-radio-button value="account">账号登录</el-radio-button>
<el-radio-button value="wxwork">企业微信</el-radio-button>
</el-radio-group>
</div>
<!-- 账号密码登录 -->
<template v-if="loginMode === 'account'">
<el-form ref="formRef" :model="formData" size="large" :rules="rules">
<el-form-item prop="account">
<el-input
v-model="formData.account"
placeholder="请输入账号"
@keyup.enter="handleEnter"
>
<template #prepend>
<icon name="el-icon-User" size="16" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
ref="passwordRef"
v-model="formData.password"
show-password
placeholder="请输入密码"
@keyup.enter="handleLogin"
>
<template #prepend>
<icon name="el-icon-Lock" size="16" />
</template>
</el-input>
</el-form-item>
</el-form>
<div class="mb-5">
<el-checkbox v-model="remAccount" label="记住账号"></el-checkbox>
</div>
<el-button type="primary" size="large" :loading="isLock" @click="lockLogin">
登录
</el-button>
</template>
<!-- 企业微信扫码登录非企业微信内浏览器 -->
<template v-if="loginMode === 'wxwork'">
<div class="wxwork-qrcode-wrap">
<div v-if="wxWorkLoading" class="text-center py-10">
<el-icon class="is-loading" :size="32" color="var(--el-color-primary)">
<Loading />
</el-icon>
<div class="mt-2 text-gray-400 text-sm">加载企业微信扫码...</div>
</div>
<div v-else id="wxwork_qrcode_container" class="wxwork-qrcode"></div>
</div>
<div class="text-center text-sm text-gray-400 mt-4">
请使用企业微信扫描二维码登录
</div>
</template>
</template>
</div>
</div>
</div>
@@ -50,8 +88,10 @@
<script lang="ts" setup>
import type { FormInstance, InputInstance } from 'element-plus'
import { computed, onMounted, reactive, ref, shallowRef } from 'vue'
import { computed, nextTick, onMounted, ref, shallowRef, watch } from 'vue'
import { Loading } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { ACCOUNT_KEY } from '@/enums/cacheEnums'
import { PageEnum } from '@/enums/pageEnum'
import { useLockFn } from '@/hooks/useLockFn'
@@ -59,6 +99,7 @@ import LayoutFooter from '@/layout/components/footer.vue'
import useAppStore from '@/stores/modules/app'
import useUserStore from '@/stores/modules/user'
import cache from '@/utils/cache'
import { getWorkWechatConfig } from '@/api/user'
const passwordRef = shallowRef<InputInstance>()
const formRef = shallowRef<FormInstance>()
@@ -73,51 +114,154 @@ const formData = reactive({
password: ''
})
const rules = {
account: [
{
required: true,
message: '请输入账号',
trigger: ['blur']
}
],
password: [
{
required: true,
message: '请输入密码',
trigger: ['blur']
}
]
account: [{ required: true, message: '请输入账号', trigger: ['blur'] }],
password: [{ required: true, message: '请输入密码', trigger: ['blur'] }]
}
// 回车按键监听
// 企业微信相关
const loginMode = ref<'account' | 'wxwork'>('account')
const wxWorkEnabled = ref(false)
const wxWorkConfig = ref<{ corp_id: string; agent_id: string }>({ corp_id: '', agent_id: '' })
const wxWorkAutoLogin = ref(false)
const wxWorkLoading = ref(false)
const isInWxWork = () => /wxwork/i.test(navigator.userAgent)
const getRedirectUri = () => {
return window.location.origin + window.location.pathname
}
const handleEnter = () => {
if (!formData.password) {
return passwordRef.value?.focus()
}
handleLogin()
}
// 登录处理
const handleLogin = async () => {
await formRef.value?.validate()
// 记住账号,缓存
cache.set(ACCOUNT_KEY, {
remember: remAccount.value,
account: remAccount.value ? formData.account : ''
})
await userStore.login(formData)
const {
query: { redirect }
} = route
redirectAfterLogin()
}
const { isLock, lockFn: lockLogin } = useLockFn(handleLogin)
const redirectAfterLogin = () => {
const { query: { redirect } } = route
const path = typeof redirect === 'string' ? redirect : PageEnum.INDEX
router.push(path)
}
const { isLock, lockFn: lockLogin } = useLockFn(handleLogin)
onMounted(() => {
// 企业微信 code 登录
const handleWxWorkCodeLogin = async (code: string) => {
wxWorkAutoLogin.value = true
try {
await userStore.workWechatLogin(code)
redirectAfterLogin()
} catch (error: any) {
console.error('企业微信登录失败:', error)
ElMessage.error(error?.msg || error?.message || '企业微信登录失败,请使用账号密码登录')
wxWorkAutoLogin.value = false
loginMode.value = 'account'
}
}
// 加载企业微信扫码 JS SDK 并渲染二维码
const renderWxWorkQRCode = async () => {
if (!wxWorkConfig.value.corp_id) return
wxWorkLoading.value = true
// 动态加载企业微信扫码 JS
if (!(window as any).WwLogin) {
await new Promise<void>((resolve, reject) => {
const script = document.createElement('script')
script.src = 'https://wwcdn.weixin.qq.com/node/wework/wwopen/js/wwLogin-1.2.7.js'
script.onload = () => resolve()
script.onerror = () => reject(new Error('加载企业微信JS失败'))
document.head.appendChild(script)
})
}
await nextTick()
wxWorkLoading.value = false
await nextTick()
const redirectUri = encodeURIComponent(getRedirectUri())
new (window as any).WwLogin({
id: 'wxwork_qrcode_container',
appid: wxWorkConfig.value.corp_id,
agentid: wxWorkConfig.value.agent_id,
redirect_uri: redirectUri,
lang: 'zh',
state: 'admin_login',
href: '',
self_redirect: false,
})
}
// 切换到企业微信扫码时自动渲染
watch(loginMode, (val) => {
if (val === 'wxwork') {
nextTick(() => renderWxWorkQRCode())
}
})
onMounted(async () => {
// 恢复记住账号
const value = cache.get(ACCOUNT_KEY)
if (value?.remember) {
remAccount.value = value.remember
formData.account = value.account
}
// 检查 URL 中是否有企业微信回调 code
const urlParams = new URLSearchParams(window.location.search)
const wxCode = urlParams.get('code')
const wxState = urlParams.get('state')
if (wxCode && wxState === 'admin_login') {
// 清除 URL 中的 code 参数(避免刷新重复使用)
const cleanUrl = window.location.pathname + (route.query.redirect ? `?redirect=${route.query.redirect}` : '')
window.history.replaceState({}, '', cleanUrl)
handleWxWorkCodeLogin(wxCode)
return
}
// 获取企业微信配置
try {
const res = await getWorkWechatConfig()
if (res?.enabled) {
wxWorkEnabled.value = true
wxWorkConfig.value = { corp_id: res.corp_id, agent_id: res.agent_id }
// 在企业微信内:自动跳转 OAuth 授权
if (isInWxWork()) {
const redirectUri = encodeURIComponent(getRedirectUri())
const authUrl =
`https://open.weixin.qq.com/connect/oauth2/authorize` +
`?appid=${res.corp_id}` +
`&redirect_uri=${redirectUri}` +
`&response_type=code` +
`&scope=snsapi_privateinfo` +
`&agentid=${res.agent_id}` +
`&state=admin_login` +
`#wechat_redirect`
window.location.href = authUrl
wxWorkAutoLogin.value = true
return
}
}
} catch (e) {
console.warn('获取企业微信配置失败:', e)
}
})
</script>
@@ -126,7 +270,27 @@ onMounted(() => {
background-image: url('./images/login_bg.png');
@apply min-h-screen bg-no-repeat bg-center bg-cover;
.login-card {
height: 400px;
height: auto;
min-height: 400px;
}
}
.wxwork-qrcode-wrap {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.wxwork-qrcode {
width: 340px;
height: 400px;
overflow: hidden;
:deep(iframe) {
width: 340px !important;
height: 400px !important;
border: none;
}
}
</style>