新增
This commit is contained in:
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user