mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 11:58:34 +08:00
Reported issue: click 'scan to create' -> button momentarily flickers
loading -> button re-enables but no QR shows up -> blank for 1-2 seconds
-> QR suddenly appears. Looks broken even though it works.
Root cause: loading.value flipped back to false the moment the begin HTTP
call returned (sessionId in hand), but the actual QR image only arrives on
the first status poll, which the existing code waited a full 2 seconds
for. Between begin completing and the first poll firing the UI was a
disabled button + nothing.
Three coordinated changes:
- useFeishuAppRegister and useDingTalkAppRegister: keep loading.value true
through begin AND across the polls, only flip false when the QR image
is actually populated (or a terminal failure status arrives). Also run
an immediate first poll right after begin instead of waiting for the
setInterval tick — usually the first poll already has the rendered QR
for dingtalk, and pushes the feishu user roughly 2 seconds closer.
- ChannelEditModal: same-sized loading placeholder (min-height 240px,
matching the QR card) that renders when loading is true and no QR is
in hand. CSS spinner ring tinted with the channel brand color (feishu
indigo, dingtalk blue) and a new
channels.{feishu,dingtalk}Register.qrcodeLoading hint. The placeholder
swaps to the real image with no layout shift.
- i18n: new qrcodeLoading key in zh-CN and en-US for both flows.
Net effect: click to spinner-visible is ~50ms; the user is never staring
at a frozen button-without-content again.
119 lines
3.8 KiB
TypeScript
119 lines
3.8 KiB
TypeScript
import { ref, onBeforeUnmount } from 'vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
import { ElMessage } from 'element-plus'
|
||
import { channelApi } from '@/api'
|
||
|
||
export type DingTalkRegisterStatus = '' | 'waiting' | 'confirmed' | 'expired' | 'denied'
|
||
|
||
export interface DingTalkRegisterResult {
|
||
clientId: string
|
||
clientSecret: string
|
||
}
|
||
|
||
/**
|
||
* 钉钉"一键应用注册"前端状态机:
|
||
* 1. POST /dingtalk/register/begin → sessionId(后端起 worker,开始 5s 轮询 /poll)
|
||
* 2. 立即触发一次 status 轮询,之后每 2s 轮询 → 拿到 qrcode_img 渲染 / confirmed 时回调
|
||
* 3. 终态(confirmed / expired / denied)后停止轮询
|
||
*
|
||
* `loading` 状态从用户点击开始一直保持 true,直到 QR 真正可见才置 false ——
|
||
* 期间的 UI 会显示 spinner 占位块,避免出现"按钮已恢复但 QR 还没来"的死页面错觉。
|
||
*/
|
||
export function useDingTalkAppRegister(onConfirmed: (r: DingTalkRegisterResult) => void) {
|
||
const { t } = useI18n()
|
||
const qrcodeUrl = ref('')
|
||
const loading = ref(false)
|
||
const status = ref<DingTalkRegisterStatus>('')
|
||
|
||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||
let confirmedFired = false
|
||
|
||
function stopPolling() {
|
||
if (pollTimer) {
|
||
clearInterval(pollTimer)
|
||
pollTimer = null
|
||
}
|
||
}
|
||
|
||
function reset() {
|
||
stopPolling()
|
||
qrcodeUrl.value = ''
|
||
status.value = ''
|
||
confirmedFired = false
|
||
}
|
||
|
||
async function start() {
|
||
reset()
|
||
loading.value = true
|
||
|
||
let sessionId = ''
|
||
try {
|
||
const res: any = await channelApi.dingtalkRegisterBegin()
|
||
sessionId = res?.data?.session_id || res?.session_id || ''
|
||
if (!sessionId) {
|
||
loading.value = false
|
||
ElMessage.error(t('channels.dingtalkRegister.startFailed'))
|
||
return
|
||
}
|
||
status.value = 'waiting'
|
||
} catch {
|
||
loading.value = false
|
||
ElMessage.error(t('channels.dingtalkRegister.startFailed'))
|
||
return
|
||
}
|
||
|
||
// Single poll body. Used both for the immediate first call (no 2s wait) and
|
||
// the subsequent setInterval — DingTalk backend polls every 5s, so the
|
||
// first qrcode_img usually appears in our 2nd or 3rd poll.
|
||
const pollOnce = async () => {
|
||
try {
|
||
const res: any = await channelApi.dingtalkRegisterStatus(sessionId)
|
||
const data = res?.data || res || {}
|
||
const s = (data.status as DingTalkRegisterStatus) || 'waiting'
|
||
|
||
const img = data.qrcode_img || data.qrcode_url
|
||
if (img && qrcodeUrl.value !== img) {
|
||
qrcodeUrl.value = img
|
||
loading.value = false // QR is now visible, button can recover
|
||
}
|
||
status.value = s
|
||
|
||
if (s === 'confirmed') {
|
||
if (confirmedFired) return
|
||
confirmedFired = true
|
||
stopPolling()
|
||
loading.value = false
|
||
const clientId = data.client_id || ''
|
||
const clientSecret = data.client_secret || ''
|
||
if (clientId && clientSecret) {
|
||
onConfirmed({ clientId, clientSecret })
|
||
ElMessage.success(t('channels.dingtalkRegister.confirmed'))
|
||
}
|
||
return
|
||
}
|
||
|
||
if (s === 'expired') {
|
||
stopPolling()
|
||
loading.value = false
|
||
ElMessage.warning(t('channels.dingtalkRegister.expired'))
|
||
} else if (s === 'denied') {
|
||
stopPolling()
|
||
loading.value = false
|
||
ElMessage.warning(t('channels.dingtalkRegister.denied'))
|
||
}
|
||
} catch {
|
||
// Silent — transient network errors should not abort the loop.
|
||
}
|
||
}
|
||
|
||
// Immediate first poll, then 2s interval. begin() should already have stored
|
||
// the QR URL, so the very first poll typically already returns it.
|
||
await pollOnce()
|
||
pollTimer = setInterval(pollOnce, 2000)
|
||
}
|
||
|
||
onBeforeUnmount(stopPolling)
|
||
|
||
return { qrcodeUrl, loading, status, start, reset, stopPolling }
|
||
}
|