mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 11:37:31 +08:00
- 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.
88 lines
2.6 KiB
TypeScript
88 lines
2.6 KiB
TypeScript
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 }
|
||
}
|