flnxi-Wu/app/activate/page.tsx
2026-05-25 14:52:24 +08:00

364 lines
15 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 { 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>
);
}