221 lines
7.4 KiB
TypeScript
221 lines
7.4 KiB
TypeScript
'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<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)
|
|
}
|
|
}
|
|
|
|
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 (
|
|
<main className="min-h-[100dvh] overflow-hidden bg-white text-[#111827]">
|
|
<div className="bg-black text-white">
|
|
<div className="mx-auto flex h-16 max-w-[430px] items-center px-5">
|
|
<div className="min-w-0">
|
|
<div className="truncate text-[18px] font-semibold tracking-tight">由壹点「素材库」</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<section className="mx-auto max-w-[430px] px-5 py-8">
|
|
<div className="overflow-hidden rounded-[26px] border border-[#e3eaf3] bg-white p-6 shadow-[0_18px_48px_rgba(15,23,42,0.10)]">
|
|
<div className="text-[14px] font-semibold text-[#1677ff]">由壹点工作台</div>
|
|
<h1 className="mt-3 text-[28px] font-semibold leading-tight tracking-[-0.01em] text-[#101828]">正在打开页面</h1>
|
|
<p className="mt-3 text-[14px] font-medium leading-6 text-[#667085]">{message}</p>
|
|
|
|
<div className="mt-6 rounded-[22px] bg-[#f6f8fb] p-4">
|
|
<div className="flex items-center justify-between gap-4">
|
|
<span className="text-[15px] font-semibold text-[#101828]">准备页面内容</span>
|
|
<span className="text-[14px] font-semibold text-[#1677ff]">马上进入</span>
|
|
</div>
|
|
<div className="mt-4 h-1.5 overflow-hidden rounded-full bg-[#e8f1ff]">
|
|
<div className="h-full w-2/3 animate-pulse rounded-full bg-[#1677ff]" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-7 rounded-[18px] border border-[#eef2f7] bg-white p-5">
|
|
<div className="text-[15px] font-semibold text-[#7b8494]">业务工作台</div>
|
|
<div className="mt-6 h-3 rounded-full bg-[#eef3f9]" />
|
|
<div className="mt-3 h-3 w-3/4 rounded-full bg-[#eef3f9]" />
|
|
<div className="mt-8 space-y-5">
|
|
<div className="flex items-center gap-3">
|
|
<span className="h-4 w-4 rounded-full border border-[#e3eaf3]" />
|
|
<span className="h-3 flex-1 rounded-full bg-[#f1f5f9]" />
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className="h-4 w-4 rounded-full border border-[#e3eaf3]" />
|
|
<span className="h-3 flex-1 rounded-full bg-[#f1f5f9]" />
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className="h-4 w-4 rounded-full border border-[#e3eaf3]" />
|
|
<span className="h-3 w-2/3 rounded-full bg-[#f1f5f9]" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
)
|
|
}
|