364 lines
15 KiB
TypeScript
364 lines
15 KiB
TypeScript
'use client';
|
||
|
||
import { useState, useEffect } from 'react';
|
||
import { useRouter } from 'next/navigation';
|
||
|
||
export default function ActivatePage() {
|
||
const router = useRouter();
|
||
const [initialAdmin, setInitialAdmin] = useState<{
|
||
email: string;
|
||
username: string;
|
||
password: string | null;
|
||
mustChangePassword: boolean;
|
||
passwordSource?: string | null;
|
||
} | null>(null);
|
||
const [licenseKey, setLicenseKey] = useState('');
|
||
const [loading, setLoading] = useState(false);
|
||
const [checking, setChecking] = useState(true);
|
||
const [error, setError] = useState('');
|
||
const [activated, setActivated] = useState(false);
|
||
const [showFreeTrialModal, setShowFreeTrialModal] = useState(false);
|
||
const [customerName, setCustomerName] = useState('');
|
||
const [contactInfo, setContactInfo] = useState('');
|
||
|
||
// 页面加载时检查数据库中是否已有许可证
|
||
useEffect(() => {
|
||
const checkLicense = async () => {
|
||
try {
|
||
const res = await fetch('/api/license/check');
|
||
const data = await res.json();
|
||
|
||
if (data.activated) {
|
||
// 数据库中已有许可证,cookie 已自动恢复,重定向到首页
|
||
router.push('/');
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
console.error('检查许可证状态失败:', error);
|
||
} finally {
|
||
setChecking(false);
|
||
}
|
||
};
|
||
|
||
checkLicense();
|
||
}, [router]);
|
||
|
||
// 格式化许可证密钥输入(自动添加横杠)
|
||
const formatLicenseKey = (value: string) => {
|
||
const cleaned = value.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||
const parts = [];
|
||
for (let i = 0; i < cleaned.length && i < 16; i += 4) {
|
||
parts.push(cleaned.slice(i, i + 4));
|
||
}
|
||
return parts.join('-');
|
||
};
|
||
|
||
const handleKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
setLicenseKey(formatLicenseKey(e.target.value));
|
||
setError('');
|
||
};
|
||
|
||
// 激活许可证
|
||
const handleActivate = async () => {
|
||
if (!licenseKey || licenseKey.length !== 19) {
|
||
setError('请输入完整的许可证密钥');
|
||
return;
|
||
}
|
||
|
||
setLoading(true);
|
||
setError('');
|
||
|
||
try {
|
||
const res = await fetch('/api/license/activate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ licenseKey }),
|
||
});
|
||
|
||
const data = await res.json();
|
||
|
||
if (data.success) {
|
||
setInitialAdmin(data.initialAdmin || null);
|
||
setActivated(true);
|
||
} else {
|
||
setError(data.error || '激活失败,请检查许可证密钥');
|
||
}
|
||
} catch {
|
||
setError('网络错误,请检查网络连接');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
// 打开免费试用弹窗
|
||
const openFreeTrialModal = () => {
|
||
setCustomerName('');
|
||
setContactInfo('');
|
||
setShowFreeTrialModal(true);
|
||
};
|
||
|
||
// 免费试用
|
||
const handleFreeTrial = async () => {
|
||
const trimmedName = customerName.trim();
|
||
const trimmedContact = contactInfo.trim();
|
||
|
||
if (!trimmedName) {
|
||
setError('请输入客户名称');
|
||
return;
|
||
}
|
||
if (!trimmedContact) {
|
||
setError('请输入联系方式');
|
||
return;
|
||
}
|
||
|
||
setLoading(true);
|
||
setError('');
|
||
setShowFreeTrialModal(false);
|
||
|
||
try {
|
||
const res = await fetch('/api/license/activate-free', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
customerName: trimmedName,
|
||
contactInfo: trimmedContact
|
||
}),
|
||
});
|
||
|
||
let data;
|
||
try {
|
||
data = await res.json();
|
||
} catch (jsonError) {
|
||
// 如果响应不是JSON(可能是HTML重定向),显示详细错误
|
||
const text = await res.text();
|
||
console.error('激活响应不是JSON:', text.substring(0, 200));
|
||
setError(`服务器返回了无效响应(${res.status}),请检查网络连接`);
|
||
return;
|
||
}
|
||
|
||
if (data.success) {
|
||
setInitialAdmin(data.initialAdmin || null);
|
||
setActivated(true);
|
||
} else {
|
||
// 处理各种错误消息格式
|
||
let errorMsg = data.error || '激活失败,请检查输入信息';
|
||
if (errorMsg === 'Required' || errorMsg.includes('required')) {
|
||
errorMsg = '请填写完整的客户信息(客户名称和联系方式都是必填项)';
|
||
}
|
||
setError(errorMsg);
|
||
}
|
||
} catch (error) {
|
||
console.error('激活请求失败:', error);
|
||
setError('网络错误,请检查网络连接');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
// 检查中或已激活,显示加载状态
|
||
if (checking) {
|
||
return (
|
||
<div className="min-h-screen bg-slate-50 dark:bg-slate-950 flex items-center justify-center p-4">
|
||
<div className="text-center">
|
||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-slate-700 dark:border-slate-300 mb-4"></div>
|
||
<p className="text-slate-600 dark:text-slate-400">正在检查许可证状态...</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 激活成功页面
|
||
if (activated) {
|
||
return (
|
||
<div className="min-h-screen bg-slate-50 dark:bg-slate-950 flex items-center justify-center p-4">
|
||
<div className="w-full max-w-md">
|
||
<div className="bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-800 shadow-sm p-6">
|
||
<div className="text-center mb-6">
|
||
<div className="w-10 h-10 bg-emerald-50 dark:bg-emerald-900/20 rounded-full flex items-center justify-center mx-auto mb-4 border border-emerald-100 dark:border-emerald-900/40">
|
||
<svg className="w-5 h-5 text-emerald-600 dark:text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||
</svg>
|
||
</div>
|
||
<h1 className="text-xl font-semibold text-slate-900 dark:text-white">激活成功</h1>
|
||
<p className="text-sm text-slate-500 dark:text-slate-400 mt-2">系统已准备就绪</p>
|
||
</div>
|
||
|
||
{initialAdmin ? (
|
||
<>
|
||
<div className="bg-slate-50 dark:bg-slate-800/60 border border-slate-200 dark:border-slate-700 rounded-lg p-4 mb-6">
|
||
<h3 className="text-sm font-medium text-slate-800 dark:text-slate-200 mb-3">初始管理员账号</h3>
|
||
<div className="space-y-2 text-sm">
|
||
<div className="flex justify-between gap-3">
|
||
<span className="text-slate-600 dark:text-slate-400">邮箱:</span>
|
||
<span className="font-mono font-medium text-slate-900 dark:text-white break-all text-right">{initialAdmin.email}</span>
|
||
</div>
|
||
{initialAdmin.password ? (
|
||
<div className="flex justify-between gap-3">
|
||
<span className="text-slate-600 dark:text-slate-400">密码:</span>
|
||
<span className="font-mono font-medium text-slate-900 dark:text-white break-all text-right">{initialAdmin.password}</span>
|
||
</div>
|
||
) : initialAdmin.passwordSource === 'environment' ? (
|
||
<p className="text-slate-700 dark:text-slate-300">初始密码已由部署配置提供,系统不会在页面或日志中回显明文。</p>
|
||
) : (
|
||
<p className="text-slate-700 dark:text-slate-300">管理员账号已存在,请使用现有管理员密码登录。</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-100 dark:border-amber-800 rounded-lg p-3 mb-6">
|
||
<p className="text-xs text-amber-700 dark:text-amber-400">
|
||
{initialAdmin.password
|
||
? '初始密码只在当前页面展示一次,首次登录后将强制修改密码。'
|
||
: '首次登录后将强制修改密码。'}
|
||
</p>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className="bg-slate-50 dark:bg-slate-800/60 border border-slate-200 dark:border-slate-700 rounded-lg p-4 mb-6">
|
||
<p className="text-sm text-slate-700 dark:text-slate-300">系统已激活,请使用现有管理员账号登录。</p>
|
||
</div>
|
||
)}
|
||
|
||
<button
|
||
onClick={() => router.push('/login')}
|
||
className="w-full py-3 bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 rounded-lg font-medium hover:bg-slate-800 dark:hover:bg-white transition-colors"
|
||
>
|
||
前往登录
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-slate-50 dark:bg-slate-950 flex items-center justify-center p-4">
|
||
<div className="w-full max-w-md">
|
||
<div className="bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-800 shadow-sm p-6">
|
||
{/* Logo */}
|
||
<div className="text-center mb-8">
|
||
<h1 className="text-xl font-semibold text-slate-900 dark:text-white">FLNXI 素材系统</h1>
|
||
<p className="text-sm text-slate-500 dark:text-slate-400 mt-2">请激活您的许可证</p>
|
||
</div>
|
||
|
||
{/* 许可证输入 */}
|
||
<div className="mb-6">
|
||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||
许可证密钥
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={licenseKey}
|
||
onChange={handleKeyChange}
|
||
placeholder="XXXX-XXXX-XXXX-XXXX"
|
||
className="w-full px-4 py-3 border border-slate-300 dark:border-slate-700 rounded-lg focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-700 focus:border-slate-900 dark:focus:border-slate-500 text-center text-base font-mono bg-white dark:bg-slate-800 text-slate-900 dark:text-white"
|
||
maxLength={19}
|
||
disabled={loading}
|
||
/>
|
||
</div>
|
||
|
||
{/* 错误提示 */}
|
||
{error && (
|
||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-600 text-sm">
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
{/* 激活按钮 */}
|
||
<button
|
||
onClick={handleActivate}
|
||
disabled={loading || licenseKey.length !== 19}
|
||
className="w-full py-3 bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 rounded-lg font-medium hover:bg-slate-800 dark:hover:bg-white disabled:bg-slate-300 dark:disabled:bg-slate-700 disabled:cursor-not-allowed transition-colors"
|
||
>
|
||
{loading ? '激活中...' : '激活许可证'}
|
||
</button>
|
||
|
||
{/* 分隔线 */}
|
||
<div className="my-6 flex items-center">
|
||
<div className="flex-1 border-t border-slate-200 dark:border-slate-700"></div>
|
||
<span className="px-4 text-sm text-slate-400">或</span>
|
||
<div className="flex-1 border-t border-slate-200 dark:border-slate-700"></div>
|
||
</div>
|
||
|
||
{/* 免费试用 */}
|
||
<button
|
||
onClick={openFreeTrialModal}
|
||
disabled={loading}
|
||
className="w-full py-3 border border-slate-300 dark:border-slate-700 text-slate-700 dark:text-slate-300 rounded-lg font-medium hover:bg-slate-50 dark:hover:bg-slate-800 disabled:cursor-not-allowed transition-colors"
|
||
>
|
||
免费试用
|
||
</button>
|
||
|
||
{/* 免费版说明 */}
|
||
<p className="mt-4 text-center text-xs text-slate-400">
|
||
免费版限制:10 个用户、100 个素材
|
||
</p>
|
||
</div>
|
||
|
||
{/* 联系信息 */}
|
||
<p className="mt-6 text-center text-sm text-slate-500">
|
||
需要购买许可证?请联系我们
|
||
</p>
|
||
</div>
|
||
|
||
{/* 免费试用弹窗 */}
|
||
{showFreeTrialModal && (
|
||
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
|
||
<form
|
||
className="bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-800 shadow-sm p-6 w-full max-w-sm mx-4"
|
||
onSubmit={(e) => {
|
||
e.preventDefault();
|
||
handleFreeTrial();
|
||
}}
|
||
>
|
||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">申请免费试用</h3>
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||
客户名称 <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={customerName}
|
||
onChange={(e) => setCustomerName(e.target.value)}
|
||
placeholder="请输入您的公司或个人名称"
|
||
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-700 rounded-lg focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-700 focus:border-slate-900 dark:focus:border-slate-500 bg-white dark:bg-slate-800 text-slate-900 dark:text-white"
|
||
autoFocus
|
||
required
|
||
/>
|
||
</div>
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||
联系方式 <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={contactInfo}
|
||
onChange={(e) => setContactInfo(e.target.value)}
|
||
placeholder="手机号/微信/邮箱"
|
||
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-700 rounded-lg focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-700 focus:border-slate-900 dark:focus:border-slate-500 bg-white dark:bg-slate-800 text-slate-900 dark:text-white"
|
||
required
|
||
/>
|
||
<p className="mt-1 text-xs text-slate-500">用于许可证管理和售后服务</p>
|
||
</div>
|
||
<div className="flex gap-3">
|
||
<button
|
||
onClick={() => setShowFreeTrialModal(false)}
|
||
className="flex-1 py-2 border border-slate-300 dark:border-slate-700 text-slate-700 dark:text-slate-300 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors"
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
onClick={handleFreeTrial}
|
||
disabled={!customerName.trim() || !contactInfo.trim()}
|
||
className="flex-1 py-2 bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 rounded-lg hover:bg-slate-800 dark:hover:bg-white disabled:bg-slate-300 dark:disabled:bg-slate-700 disabled:cursor-not-allowed transition-colors"
|
||
>
|
||
确认申请
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|