mateclaw/mateclaw-ui/src/composables/channels/useWecomBotAuth.ts
matevip 22894ac4b1 perf(channels): split Channels.vue, lazy-load modal, async locales, keep-alive route
- Extract create/edit modal into ChannelEditModal.vue (defineAsyncComponent),
  shrinking Channels.vue from 1438 to 370 lines and dropping ~30KB from the
  initial route chunk.
- Move side-effect logic into composables: useWeixinQrcodePoll (QR + 2s status
  poll, auto-cleanup) and useWecomBotAuth (lazy SDK script with module-level
  promise dedupe). Pure config-JSON helpers move to utils/channelConfigJson.ts.
- Switch i18n locales from static imports to dynamic import keyed by current
  locale; applyLocale becomes async to avoid first-render flicker.
- /channels route opts into keep-alive (meta.keepAlive=true). Channels.vue
  pauses status polling in onDeactivated and resumes in onActivated, with an
  isActive guard to prevent late-resolving timers from leaking after navigation.
- Initial load goes from serial 3-RTT to Promise.all + 4-card el-skeleton.
2026-04-28 11:08:36 +08:00

88 lines
2.6 KiB
TypeScript
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.

import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
const SDK_URL = 'https://wwcdn.weixin.qq.com/node/wework/js/wecom-aibot-sdk@0.1.0.min.js'
const SOURCE = 'mateclaw'
// Module-level guard: the SDK script tag is appended to <body> exactly once
// across all component instances and re-renders.
let sdkLoadPromise: Promise<void> | null = null
function loadSDK(): Promise<void> {
if ((window as any).WecomAIBotSDK) return Promise.resolve()
if (sdkLoadPromise) return sdkLoadPromise
sdkLoadPromise = new Promise<void>((resolve, reject) => {
const script = document.createElement('script')
script.src = SDK_URL
script.async = true
script.onload = () => resolve()
script.onerror = () => {
sdkLoadPromise = null // allow retry on next call
reject(new Error('WeCom SDK load failed'))
}
document.body.appendChild(script)
})
return sdkLoadPromise
}
export interface WecomBotAuthResult {
botid: string
secret: string
}
/**
* 企业微信扫码授权:动态加载官方 JS SDK弹出授权窗口回调 botid/secret。
*
* 拆出来的好处:
* - SDK 脚本只在用户真点"授权"按钮时才注入,不污染每个页面的 <head>
* - 多次点击不会重复 append <script>sdkLoadPromise 去重)
* - 弹窗组件 template 里只剩按钮 + loading 状态
*/
export function useWecomBotAuth(onSuccess: (r: WecomBotAuthResult) => void) {
const { t } = useI18n()
const loading = ref(false)
async function start() {
loading.value = true
try {
await loadSDK()
} catch {
ElMessage.error(t('channels.wecom.sdkFailed'))
loading.value = false
return
}
const sdk = (window as any).WecomAIBotSDK
if (!sdk) {
ElMessage.error(t('channels.wecom.sdkFailed'))
loading.value = false
return
}
loading.value = false
const result = sdk.openBotInfoAuthWindow({ source: SOURCE })
if (!result || typeof result.then !== 'function') return
result.then(
(bot: WecomBotAuthResult) => {
if (bot?.botid) {
onSuccess(bot)
ElMessage.success(t('channels.wecom.authSuccess'))
}
},
(error: { code: string; message: string }) => {
if (error?.code === 'WINDOW_BLOCKED') {
ElMessage.error(t('channels.wecom.windowBlocked'))
} else if (error?.code === 'CANCELLED') {
ElMessage.info(t('channels.wecom.authCancelled'))
} else {
ElMessage.error(t('channels.wecom.authFailed') + '' + (error?.message || error?.code || ''))
}
},
)
}
return { loading, start }
}