WuYongJie f646b71813
Some checks failed
CI Check / check (push) Has been cancelled
chore: 初始化本地仓库并同步最近改动
2025-11-18 17:50:16 +08:00

305 lines
9.9 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, useState, Suspense } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { signIn } from 'next-auth/react'
import Image from 'next/image'
interface Department {
id: string
name: string
}
function CompleteProfileContent() {
const router = useRouter()
const searchParams = useSearchParams()
const [department, setDepartment] = useState('')
const [departments, setDepartments] = useState<Department[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState('')
const [userInfo, setUserInfo] = useState<any>(null)
// 系统设置
const [loginLogoUrl, setLoginLogoUrl] = useState(process.env.NEXT_PUBLIC_DEFAULT_LOGIN_LOGO_URL || '')
const [siteName, setSiteName] = useState('由壹点3D素材')
useEffect(() => {
// 加载系统设置
const loadSettings = async () => {
try {
const response = await fetch('/api/system-settings')
if (response.ok) {
const settings = await response.json()
setLoginLogoUrl(settings.loginLogoUrl || loginLogoUrl)
setSiteName(settings.siteName || siteName)
}
} catch (error) {
console.error('加载系统设置失败:', error)
}
}
// 加载部门列表
const loadDepartments = async () => {
try {
const response = await fetch('/api/departments')
if (response.ok) {
const data = await response.json()
setDepartments(data || [])
}
} catch (error) {
console.error('加载部门列表失败:', error)
}
}
// 从URL参数获取用户信息
const userInfoParam = searchParams.get('userInfo')
if (userInfoParam) {
try {
const info = JSON.parse(decodeURIComponent(userInfoParam))
setUserInfo(info)
} catch (error) {
console.error('解析用户信息失败:', error)
setError('无效的用户信息,请重新登录')
}
} else {
setError('缺少用户信息,请重新登录')
}
loadSettings()
loadDepartments()
}, [searchParams])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
if (!department) {
setError('请选择部门')
return
}
setLoading(true)
setError('') // 清空错误信息
setSuccess('') // 清空成功信息
try {
// 调用API完善用户信息(只需要选择部门)
const response = await fetch('/api/dingtalk/complete-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: userInfo?.id,
department,
}),
})
const data = await response.json()
if (!response.ok || !data.success) {
throw new Error(data.message || '信息提交失败')
}
// 如果是账号关联(关联到已有账号),提示用户使用原账号密码登录
if (data.isAccountLink) {
setSuccess('账号关联成功!正在跳转到登录页面,您可以使用原账号密码登录或继续使用钉钉扫码登录。')
setTimeout(() => {
// 智能跳转:保持在内网或外网环境
if (typeof window !== 'undefined' && window.location.hostname.includes('192.168.')) {
window.location.href = `${window.location.origin}/login`
} else {
router.push('/login')
}
}, 3000)
return
}
// 检查新用户是否已激活
if (data.user && !data.user.isActive) {
setSuccess('信息提交成功!您的账号需要管理员激活后才能登录使用。')
setTimeout(() => {
// 智能跳转:保持在内网或外网环境
if (typeof window !== 'undefined' && window.location.hostname.includes('192.168.')) {
window.location.href = `${window.location.origin}/login`
} else {
router.push('/login')
}
}, 3000)
return
}
// 已激活用户:使用 NextAuth 自动登录
const result = await signIn('credentials', {
email: data.user.email,
password: 'dingtalk_auto_login',
redirect: false,
})
if (result?.error) {
throw new Error('登录失败')
}
// 智能跳转:保持在内网或外网环境
if (typeof window !== 'undefined' && window.location.hostname.includes('192.168.')) {
window.location.href = window.location.origin
} else {
router.push('/')
router.refresh()
}
} catch (error: any) {
console.error('提交失败:', error)
setError(error.message || '提交失败,请稍后再试')
} finally {
setLoading(false)
}
}
if (error && !userInfo) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-primary-100 p-4">
<div className="max-w-md w-full bg-white rounded-2xl shadow-xl p-8">
<div className="text-center">
<div className="mb-4">
<svg className="mx-auto h-12 w-12 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<h2 className="text-xl font-semibold mb-2 text-red-600">出错了</h2>
<p className="text-gray-600 mb-4">{error}</p>
<button
onClick={() => router.push('/login')}
className="btn-primary"
>
返回登录
</button>
</div>
</div>
</div>
)
}
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-primary-100 p-4">
<div className="max-w-md w-full">
{/* Logo */}
<div className="text-center mb-8">
<div className="flex justify-center mb-4">
<Image
src={loginLogoUrl}
alt={siteName}
width={80}
height={80}
unoptimized
className="rounded-apple shadow-apple"
/>
</div>
<h1 className="text-3xl font-bold text-gray-900">完善个人信息</h1>
<p className="text-gray-600 mt-2">首次登录,请完善您的用户信息</p>
</div>
{/* Form */}
<div className="card-apple p-8">
{userInfo && (
<div className="mb-6 p-4 bg-blue-50 rounded-lg border border-blue-200">
<div className="flex items-center gap-3">
{userInfo.avatar && (
<Image
src={userInfo.avatar}
alt={userInfo.realName}
width={48}
height={48}
className="rounded-full"
unoptimized
/>
)}
<div>
<p className="text-sm text-gray-600">钉钉账号</p>
<p className="font-medium text-gray-900">{userInfo.realName}</p>
</div>
</div>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-apple text-sm">
{error}
</div>
)}
{success && (
<div className="bg-green-50 border border-green-200 text-green-600 px-4 py-3 rounded-apple text-sm">
{success}
</div>
)}
{/* 显示钉钉用户名(只读) */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
用户名
</label>
<div className="input-apple bg-gray-50 text-gray-700 cursor-not-allowed">
{userInfo?.username || userInfo?.realName || '加载中...'}
</div>
<p className="mt-1 text-xs text-gray-500">使用您的钉钉姓名作为系统用户名</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
所属部门 *
</label>
<select
value={department}
onChange={(e) => setDepartment(e.target.value)}
className="input-apple"
required
>
<option value="">请选择部门</option>
{departments.map((dept) => (
<option key={dept.id} value={dept.name}>
{dept.name}
</option>
))}
</select>
</div>
<button
type="submit"
className="w-full btn-primary"
disabled={loading}
>
{loading ? '提交中...' : '完成并登录'}
</button>
</form>
<div className="mt-6 text-center">
<button
onClick={() => router.push('/login')}
className="text-sm text-gray-600 hover:text-gray-900"
>
返回登录
</button>
</div>
</div>
</div>
</div>
)
}
export default function CompleteProfilePage() {
return (
<Suspense fallback={
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-primary-100">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-primary-500 border-t-transparent"></div>
<p className="mt-4 text-gray-600">加载中...</p>
</div>
</div>
}>
<CompleteProfileContent />
</Suspense>
)
}