'use client' import { useEffect, useRef, useState } from 'react' declare global { interface Window { dd?: any } } type DingtalkEntryClientProps = { corpId: string redirectPath: string } function loadDingtalkJsApi() { return new Promise((resolve, reject) => { if (window.dd?.runtime?.permission) { resolve() return } const existing = document.querySelector('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((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 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) } } function navigateDingtalkH5Login(authCode: string, redirectPath: string) { const params = new URLSearchParams({ code: authCode, redirect: redirectPath, source: 'dingtalk-entry-v2', }) window.location.replace(`/api/dingtalk/h5-login?${params.toString()}`) } async function hasActiveSession() { try { const response = await fetch('/api/auth/session', { cache: 'no-store', credentials: 'include', }) if (!response.ok) return false const data = await response.json().catch(() => null) // 品牌中心外部人员也可能已经登录素材库;只有品牌中心成员才可跳过钉钉免登。 return data?.user?.canAccessBrandCenter === true } catch { return false } } export default function DingtalkEntryClient({ corpId, redirectPath }: DingtalkEntryClientProps) { const startedRef = useRef(false) const [message, setMessage] = useState('正在打开页面...') useEffect(() => { if (startedRef.current) return startedRef.current = true const run = async () => { if (!corpId) { setMessage('请返回钉钉,从最新通知重新打开。') return } if (!/DingTalk/i.test(window.navigator.userAgent || '')) { setMessage('请在钉钉客户端内打开通知。') return } try { setMessage('正在打开页面...') // 绩效通知必须重新走 workNotice 免登,不能因为浏览器里已有素材库会话 // 就直接打开普通会话。否则外部人员会被素材库会话带回默认素材库页面, // 也无法把本次通知对应的 surveyId/targetId 绑定到评分权限。 const isPerformanceSurveyNotice = redirectPath === '/performance-survey' || redirectPath.startsWith('/performance-survey/') if (!isPerformanceSurveyNotice && await hasActiveSession()) { window.location.replace(redirectPath) return } await loadDingtalkJsApi() const authCode = await requestDingtalkAuthCode(corpId) navigateDingtalkH5Login(authCode, redirectPath) } catch (error) { console.error('[Dingtalk Entry] 打开页面失败:', error) setMessage('请返回钉钉,从最新通知重新打开。') } } void run() }, [corpId, redirectPath]) return (
由壹点「素材库」
由壹点工作台

正在打开页面

{message}

准备页面内容 马上进入
业务工作台
) }