gongxiangp2/app/dingtalk-entry/DingtalkEntryClient.tsx
2026-07-07 14:07:48 +08:00

167 lines
4.8 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.

'use client'
import { useEffect, useRef, useState } from 'react'
import { signIn, useSession } from 'next-auth/react'
declare global {
interface Window {
dd?: any
}
}
type DingtalkEntryClientProps = {
corpId: string
redirectPath: string
}
function loadDingtalkJsApi() {
return new Promise<void>((resolve, reject) => {
if (window.dd?.runtime?.permission) {
resolve()
return
}
const existing = document.querySelector<HTMLScriptElement>('script[data-dingtalk-jsapi="true"]')
if (existing) {
existing.addEventListener('load', () => resolve(), { once: true })
existing.addEventListener('error', () => reject(new Error('钉钉JSAPI加载失败')), { once: true })
return
}
const script = document.createElement('script')
script.src = 'https://g.alicdn.com/dingding/dingtalk-jsapi/3.0.39/dingtalk.open.js'
script.async = true
script.dataset.dingtalkJsapi = 'true'
script.onload = () => resolve()
script.onerror = () => reject(new Error('钉钉JSAPI加载失败'))
document.head.appendChild(script)
})
}
function requestDingtalkAuthCode(corpId: string) {
return new Promise<string>((resolve, reject) => {
const dd = window.dd
if (!dd?.ready || !dd?.runtime?.permission?.requestAuthCode) {
reject(new Error('当前页面未检测到钉钉免登环境'))
return
}
const timer = window.setTimeout(() => {
reject(new Error('钉钉免登初始化超时'))
}, 15000)
if (dd.error) {
dd.error((error: unknown) => {
window.clearTimeout(timer)
reject(error instanceof Error ? error : new Error(formatDingtalkError(error)))
})
}
dd.ready(() => {
window.clearTimeout(timer)
dd.runtime.permission.requestAuthCode({
corpId,
onSuccess: (result: { code?: string }) => {
const code = String(result?.code || '').trim()
if (!code) {
reject(new Error('钉钉未返回免登授权码'))
return
}
resolve(code)
},
onFail: (error: unknown) => {
reject(error instanceof Error ? error : new Error(formatDingtalkError(error)))
},
})
})
})
}
function formatDingtalkError(error: unknown) {
if (!error) return '钉钉免登失败'
if (typeof error === 'string') return error
if (error instanceof Error) return error.message
const record = error as Record<string, unknown>
const message =
record.message ||
record.errorMessage ||
record.errMsg ||
record.errmsg ||
record.submsg
if (message) return String(message)
try {
return JSON.stringify(error)
} catch {
return String(error)
}
}
export default function DingtalkEntryClient({ corpId, redirectPath }: DingtalkEntryClientProps) {
const { status } = useSession()
const startedRef = useRef(false)
const [message, setMessage] = useState('正在通过钉钉确认身份...')
useEffect(() => {
if (status === 'authenticated') {
window.location.replace(redirectPath)
}
}, [redirectPath, status])
useEffect(() => {
if (startedRef.current || status === 'loading' || status === 'authenticated') return
startedRef.current = true
const run = async () => {
if (!corpId) {
setMessage('钉钉免登配置缺少 CorpId,请联系管理员检查应用配置。')
return
}
if (!/DingTalk/i.test(window.navigator.userAgent || '')) {
setMessage('请在钉钉客户端内重新打开通知。')
return
}
try {
setMessage('正在加载钉钉免登组件...')
await loadDingtalkJsApi()
setMessage('正在向钉钉获取免登授权码...')
const authCode = await requestDingtalkAuthCode(corpId)
setMessage('正在写入素材库登录状态...')
const result = await signIn('credentials', {
loginType: 'dingtalk_inapp',
authCode,
redirect: false,
})
if (result?.error) {
setMessage(`钉钉免登失败:${result.error}`)
return
}
setMessage('登录成功,正在进入考核调研...')
window.location.replace(redirectPath)
} catch (error) {
setMessage(`钉钉免登失败:${formatDingtalkError(error)}`)
}
}
void run()
}, [corpId, redirectPath, status])
return (
<main className="flex min-h-screen items-center justify-center bg-slate-950 px-6 text-slate-50">
<section className="w-full max-w-sm text-center">
<div className="mx-auto mb-5 h-10 w-10 animate-spin rounded-full border-4 border-white/25 border-t-sky-400" />
<h1 className="mb-3 text-2xl font-semibold">正在进入素材库</h1>
<p className="text-sm leading-7 text-slate-300">{message}</p>
</section>
</main>
)
}