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

154 lines
4.1 KiB
TypeScript

'use client'
/**
* RestrictedToggle 组件
*
* 受限功能开关组件,集成 PRO 徽章和点击拦截逻辑
* 当功能受限时显示禁用状态,点击时触发升级弹窗
*
* @created 2025-12-16
* @component
*/
import { useState } from 'react'
import { useTheme, cn } from '@/lib/ui-styles'
import ProBadge from './ProBadge'
import UpgradeModal from './UpgradeModal'
import type { FeatureKey } from '@/lib/license/types'
interface RestrictedToggleProps {
/** 开关标签 */
label: string
/** 功能键,用于检查是否受限 */
featureKey: FeatureKey
/** 当前值 */
value: boolean
/** 值变化回调 */
onChange: (value: boolean) => void
/** 功能描述(可选) */
description?: string
/** 是否受限(由父组件传入,基于许可证状态) */
isRestricted: boolean
/** 自定义类名 */
className?: string
}
export default function RestrictedToggle({
label,
featureKey,
value,
onChange,
description,
isRestricted,
className,
}: RestrictedToggleProps) {
const theme = useTheme()
const [showUpgradeModal, setShowUpgradeModal] = useState(false)
const handleClick = () => {
if (isRestricted) {
setShowUpgradeModal(true)
} else {
onChange(!value)
}
}
return (
<>
<div
className={cn(
'flex items-center justify-between py-3',
className
)}
data-testid="restricted-toggle"
data-restricted={isRestricted}
>
{/* 标签和描述 */}
<div className="flex-1 min-w-0 pr-4">
<div className="flex items-center gap-2">
<span
className={cn(
'text-sm font-medium',
theme === 'dark' ? 'text-slate-200' : 'text-slate-700',
isRestricted && 'opacity-60'
)}
>
{label}
</span>
{isRestricted && <ProBadge size="small" />}
</div>
{description && (
<p
className={cn(
'text-xs mt-1',
theme === 'dark' ? 'text-slate-400' : 'text-slate-500',
isRestricted && 'opacity-60'
)}
>
{description}
</p>
)}
{isRestricted && (
<p
className={cn(
'text-xs mt-1',
'text-amber-600 dark:text-amber-400'
)}
data-testid="restricted-hint"
>
升级到付费版解锁此功能
</p>
)}
</div>
{/* 开关 */}
<button
type="button"
role="switch"
aria-checked={value}
aria-disabled={isRestricted}
onClick={handleClick}
className={cn(
'relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent',
'transition-colors duration-200 ease-in-out',
'focus:outline-none focus:ring-2 focus:ring-offset-2',
isRestricted
? cn(
// 受限状态样式
'cursor-not-allowed opacity-50',
value
? 'bg-slate-400 dark:bg-slate-600'
: 'bg-slate-300 dark:bg-slate-700',
'focus:ring-amber-500'
)
: cn(
// 正常状态样式
'cursor-pointer',
value
? 'bg-indigo-600 dark:bg-indigo-500'
: 'bg-slate-200 dark:bg-slate-700',
'focus:ring-indigo-500'
)
)}
data-testid="toggle-button"
>
<span
className={cn(
'pointer-events-none inline-block h-5 w-5 transform rounded-full',
'bg-white shadow ring-0 transition duration-200 ease-in-out',
value ? 'translate-x-5' : 'translate-x-0'
)}
/>
</button>
</div>
{/* 升级弹窗 */}
<UpgradeModal
isOpen={showUpgradeModal}
onClose={() => setShowUpgradeModal(false)}
featureKey={featureKey}
/>
</>
)
}