154 lines
4.1 KiB
TypeScript
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}
|
|
/>
|
|
</>
|
|
)
|
|
}
|