This commit is contained in:
Your Name
2026-04-07 18:13:03 +08:00
parent a780356908
commit fdf714f833
397 changed files with 15086 additions and 1043 deletions
@@ -0,0 +1,244 @@
<template>
<div class="bind-wx flex flex-col">
<div class="flex-1 flex items-center justify-center">
<div class="bind-wx-card bg-body rounded-md px-10 py-10 w-[480px]">
<div class="text-center text-2xl font-medium mb-2">绑定企业微信</div>
<div class="text-center text-gray-500 text-sm mb-8">
根据安全策略需绑定企业微信账号后方可使用管理后台
</div>
<div v-if="wxWorkAutoAuth" 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>
<template v-else>
<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_bind_qrcode_container" class="wxwork-qrcode mx-auto"></div>
<div class="text-center text-sm text-gray-400 mt-4">
请使用企业微信扫描二维码完成绑定
</div>
</template>
<div class="mt-8 flex justify-center gap-4">
<el-button @click="handleLogout">退出登录</el-button>
</div>
</div>
</div>
<layout-footer />
</div>
</template>
<script lang="ts" setup>
import { Loading } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
import LayoutFooter from '@/layout/components/footer.vue'
import { bindWorkWechat, getWorkWechatConfig } from '@/api/user'
import { PageEnum } from '@/enums/pageEnum'
import useUserStore from '@/stores/modules/user'
import { getToken } from '@/utils/auth'
import { attachWecomOAuthMessageCapture } from '@/utils/wecomOauthPostMessage'
/** 与 user/setting.vue 一致,便于可信域名只配置一种回调 URL */
const WX_BIND_STATE = 'bind_wxwork'
/** 企微 JSSDK 扫码后可能通过 postMessage 触发整页跳转;若在 await 前 replaceState 清掉 query,二次进入 onMounted 会拿不到 code,误走扫码初始化并取消 getConfig */
const WX_BIND_OAUTH_LOCK = 'like_admin_wx_bind_oauth'
const router = useRouter()
const userStore = useUserStore()
const wxWorkLoading = ref(false)
const wxWorkAutoAuth = ref(false)
const wxConfig = ref<{ corp_id: string; agent_id: string }>({ corp_id: '', agent_id: '' })
const isInWxWork = () => /wxwork/i.test(navigator.userAgent)
const getRedirectUri = () => window.location.origin + window.location.pathname + '?bind_wxwork=1'
/** 从 search 与 hash 中解析 OAuth 参数(少数网关/回跳会把参数放在 hash 后) */
function getOAuthQuery(): URLSearchParams {
const merged = new URLSearchParams(window.location.search)
const hash = window.location.hash || ''
const hashQuery = hash.includes('?') ? hash.split('?').slice(1).join('?') : ''
if (hashQuery) {
new URLSearchParams(hashQuery).forEach((v, k) => {
if (!merged.has(k)) merged.set(k, v)
})
}
return merged
}
const handleLogout = () => {
userStore.logout()
}
const afterBindSuccess = async () => {
try {
await userStore.getUserInfo()
} catch {
// 忽略:后续路由仍会拉取
}
if (userStore.isPaw === 0) {
router.replace('/change-password')
return
}
router.replace(PageEnum.INDEX)
}
const submitBindCode = async (code: string) => {
wxWorkAutoAuth.value = true
try {
await bindWorkWechat({ code })
// 仅成功后再清 query;放 finally 会在失败时也清掉,用户一刷新 code 就没了、也无法 F5 重试
window.history.replaceState({}, '', window.location.pathname)
await afterBindSuccess()
} catch (e: any) {
console.error(e)
ElMessage.error(e?.msg || e?.message || '绑定失败')
wxWorkAutoAuth.value = false
}
}
let detachWecomOAuthCapture: (() => void) | null = null
const renderQr = async () => {
if (!wxConfig.value.corp_id) return
wxWorkLoading.value = true
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_bind_qrcode_container',
appid: wxConfig.value.corp_id,
agentid: wxConfig.value.agent_id,
redirect_uri: redirectUri,
lang: 'zh',
state: WX_BIND_STATE,
href: '',
self_redirect: false
})
}
onMounted(async () => {
// 与 axios 拦截器竞态:首屏若有请求先返回 10,可能在 onMounted 执行前就 router.replace 清掉 query;同步快照尽量早读
let oauthCodeSnapshot = ''
try {
oauthCodeSnapshot = (new URLSearchParams(window.location.search).get('code') || '').trim()
} catch {
oauthCodeSnapshot = ''
}
if (!getToken()) {
ElMessage.error('请先登录')
router.push(PageEnum.LOGIN)
return
}
const urlParams = getOAuthQuery()
const wxCode = oauthCodeSnapshot || (urlParams.get('code') || '').trim()
const wxStateRaw = urlParams.get('state')
const wxState = wxStateRaw != null ? String(wxStateRaw).trim() : ''
const legacyStateOk = wxState === 'admin_bind_wx'
const stateOk =
wxState === WX_BIND_STATE ||
legacyStateOk ||
wxState.toLowerCase() === WX_BIND_STATE.toLowerCase()
const bindFlagOk = urlParams.get('bind_wxwork') === '1'
// 扫码组件回跳有时只带 code,不带 state / bind_wxwork;若仍严格校验则刷新后永远进不了绑定逻辑
const bindPageImplicitOk =
!!wxCode && wxState === '' && router.currentRoute.value.path === '/bind-work-wechat'
if (wxCode && (stateOk || bindFlagOk || bindPageImplicitOk)) {
if (sessionStorage.getItem(WX_BIND_OAUTH_LOCK) === wxCode) {
return
}
sessionStorage.setItem(WX_BIND_OAUTH_LOCK, wxCode)
try {
await submitBindCode(wxCode)
} finally {
sessionStorage.removeItem(WX_BIND_OAUTH_LOCK)
}
return
}
try {
const res: any = await getWorkWechatConfig()
if (!res?.enabled || !res.corp_id) {
ElMessage.error('企业微信未配置,请联系管理员')
return
}
wxConfig.value = { corp_id: res.corp_id, agent_id: res.agent_id }
if (isInWxWork()) {
wxWorkAutoAuth.value = true
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=${WX_BIND_STATE}` +
`#wechat_redirect`
window.location.href = authUrl
return
}
detachWecomOAuthCapture?.()
detachWecomOAuthCapture = attachWecomOAuthMessageCapture({
pathIncludes: 'bind-work-wechat',
lockKey: WX_BIND_OAUTH_LOCK,
wxBindState: WX_BIND_STATE,
onCode: (code) => submitBindCode(code)
})
await renderQr()
} catch (e) {
console.error(e)
ElMessage.error('获取企业微信配置失败')
}
})
onUnmounted(() => {
detachWecomOAuthCapture?.()
detachWecomOAuthCapture = null
})
</script>
<style lang="scss" scoped>
.bind-wx {
background-image: url('./images/login_bg.png');
@apply min-h-screen bg-no-repeat bg-center bg-cover;
}
.wxwork-qrcode {
width: 340px;
height: 400px;
overflow: hidden;
:deep(iframe) {
width: 340px !important;
height: 400px !important;
border: none;
}
}
</style>
+10 -4
View File
@@ -145,13 +145,16 @@ const handleLogin = async () => {
account: remAccount.value ? formData.account : ''
})
const result: any = await userStore.login(formData)
// 检查是否需要修改密码
if (result.is_paw === 0) {
router.push('/change-password')
return
}
if (result.need_bind_work_wechat) {
router.push('/bind-work-wechat')
return
}
redirectAfterLogin()
}
@@ -168,13 +171,16 @@ const handleWxWorkCodeLogin = async (code: string) => {
wxWorkAutoLogin.value = true
try {
const result: any = await userStore.workWechatLogin(code)
// 检查是否需要修改密码
if (result.is_paw === 0) {
router.push('/change-password')
return
}
if (result.need_bind_work_wechat) {
router.push('/bind-work-wechat')
return
}
redirectAfterLogin()
} catch (error: any) {
console.error('企业微信登录失败:', error)