245 lines
8.6 KiB
Vue
245 lines
8.6 KiB
Vue
<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>
|