Files
zyt/admin/src/views/account/bind-work-wechat.vue
T
2026-04-07 18:13:03 +08:00

245 lines
8.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>