flnxi-Wu/components/LicenseInfo.tsx
2026-05-25 14:52:24 +08:00

240 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 } from 'react';
import ActivationModal from './ui/ActivationModal';
interface LicenseData {
type: 'FREE' | 'STANDARD' | 'PRO';
status: 'active' | 'expired' | 'grace';
expiresAt: string | null;
licenseKey: string | null;
usage: {
users: { current: number; max: number };
materials: { current: number; max: number };
};
features: {
customTemplates: boolean;
departmentIsolation: boolean;
intranetPenetration: boolean;
};
lastVerified: string;
}
const LICENSE_TYPE_NAMES = {
FREE: '免费版',
STANDARD: '标准版',
PRO: '专业版',
};
const STATUS_NAMES = {
active: '正常',
expired: '已过期',
grace: '宽限期',
};
export default function LicenseInfo() {
const [license, setLicense] = useState<LicenseData | null>(null);
const [loading, setLoading] = useState(true);
const [showActivationModal, setShowActivationModal] = useState(false);
useEffect(() => {
fetchLicenseInfo();
}, []);
const fetchLicenseInfo = async () => {
try {
const res = await fetch('/api/license/info');
if (res.ok) {
const data = await res.json();
setLicense(data);
}
} catch (error) {
console.error('获取许可证信息失败:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="bg-white dark:bg-slate-800 rounded-lg border dark:border-slate-700 p-6">
<div className="animate-pulse">
<div className="h-4 bg-gray-200 dark:bg-slate-700 rounded w-1/4 mb-4"></div>
<div className="h-8 bg-gray-200 dark:bg-slate-700 rounded w-1/2"></div>
</div>
</div>
);
}
if (!license) {
return (
<div className="bg-white dark:bg-slate-800 rounded-lg border dark:border-slate-700 p-6">
<p className="text-gray-500 dark:text-slate-400">无法获取许可证信息</p>
</div>
);
}
const isExpiringSoon =
license.expiresAt &&
new Date(license.expiresAt).getTime() - Date.now() < 30 * 24 * 60 * 60 * 1000;
return (
<div className="bg-white dark:bg-slate-800 rounded-lg border dark:border-slate-700">
{/* 头部 */}
<div className="p-6 border-b dark:border-slate-700">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium text-gray-900 dark:text-slate-100">许可证信息</h3>
<p className="text-sm text-gray-500 dark:text-slate-400 mt-1">
最后验证: {new Date(license.lastVerified).toLocaleString()}
</p>
{license.licenseKey && (
<div className="mt-2">
<p className="text-xs text-gray-500 dark:text-slate-400 mb-1">激活码</p>
<p className="text-sm font-mono font-medium text-gray-900 dark:text-slate-100 bg-gray-50 dark:bg-slate-700 px-3 py-1.5 rounded border dark:border-slate-600 inline-block">
{license.licenseKey}
</p>
</div>
)}
</div>
<div className="flex items-center gap-2">
<span
className={`px-3 py-1 rounded-full text-sm font-medium ${
license.type === 'PRO'
? 'bg-purple-100 text-purple-700'
: license.type === 'STANDARD'
? 'bg-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-700'
}`}
>
{LICENSE_TYPE_NAMES[license.type]}
</span>
<span
className={`px-3 py-1 rounded-full text-sm font-medium ${
license.status === 'active'
? 'bg-green-100 text-green-700'
: license.status === 'grace'
? 'bg-yellow-100 text-yellow-700'
: 'bg-red-100 text-red-700'
}`}
>
{STATUS_NAMES[license.status]}
</span>
</div>
</div>
</div>
{/* 到期提醒 */}
{isExpiringSoon && license.expiresAt && (
<div className="px-6 py-3 bg-yellow-50 dark:bg-yellow-900/20 border-b border-yellow-100 dark:border-yellow-800">
<p className="text-sm text-yellow-700 dark:text-yellow-400">
⚠️ 许可证将于 {new Date(license.expiresAt).toLocaleDateString()} 到期,请及时续期
</p>
</div>
)}
{/* 使用统计 */}
<div className="p-6 border-b dark:border-slate-700">
<h4 className="text-sm font-medium text-gray-700 dark:text-slate-300 mb-4">使用情况</h4>
<div className="grid grid-cols-2 gap-4">
{/* 用户数 */}
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-600 dark:text-slate-400">用户数</span>
<span className="text-gray-900 dark:text-slate-100">
{license.usage.users.current} / {license.usage.users.max === -1 ? '无限' : license.usage.users.max}
</span>
</div>
{license.usage.users.max !== -1 && (
<div className="h-2 bg-gray-100 dark:bg-slate-700 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${
license.usage.users.current / license.usage.users.max > 0.9
? 'bg-red-500'
: 'bg-blue-500'
}`}
style={{
width: `${Math.min(100, (license.usage.users.current / license.usage.users.max) * 100)}%`,
}}
/>
</div>
)}
</div>
{/* 素材数 */}
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-600 dark:text-slate-400">素材数</span>
<span className="text-gray-900 dark:text-slate-100">
{license.usage.materials.current} / {license.usage.materials.max === -1 ? '无限' : license.usage.materials.max}
</span>
</div>
{license.usage.materials.max !== -1 && (
<div className="h-2 bg-gray-100 dark:bg-slate-700 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${
license.usage.materials.current / license.usage.materials.max > 0.9
? 'bg-red-500'
: 'bg-blue-500'
}`}
style={{
width: `${Math.min(100, (license.usage.materials.current / license.usage.materials.max) * 100)}%`,
}}
/>
</div>
)}
</div>
</div>
</div>
{/* 功能列表 */}
<div className="p-6">
<h4 className="text-sm font-medium text-gray-700 dark:text-slate-300 mb-4">功能权限</h4>
<div className="space-y-2">
<FeatureItem label="自定义模板" enabled={license.features.customTemplates} />
<FeatureItem label="部门隔离" enabled={license.features.departmentIsolation} />
<FeatureItem label="内网穿透" enabled={license.features.intranetPenetration} />
</div>
</div>
{/* 升级/激活按钮 */}
{license.type !== 'PRO' && (
<div className="px-6 pb-6 space-y-3">
<button
onClick={() => setShowActivationModal(true)}
className="w-full py-2.5 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors font-medium"
>
输入激活码
</button>
<button
onClick={() => window.open('https://www.flnxi.com/pricing', '_blank')}
className="w-full py-2.5 bg-white dark:bg-slate-700 text-indigo-600 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-800 rounded-lg hover:bg-indigo-50 dark:hover:bg-slate-600 transition-colors font-medium"
>
购买授权 →
</button>
</div>
)}
{/* 激活码输入弹窗 */}
<ActivationModal
isOpen={showActivationModal}
onClose={() => setShowActivationModal(false)}
onSuccess={fetchLicenseInfo}
/>
</div>
);
}
function FeatureItem({ label, enabled }: { label: string; enabled: boolean }) {
return (
<div className="flex items-center justify-between py-1">
<span className="text-sm text-gray-600 dark:text-slate-400">{label}</span>
{enabled ? (
<span className="text-green-600 dark:text-green-400">✓</span>
) : (
<span className="text-gray-300 dark:text-slate-600">✗</span>
)}
</div>
);
}