将项目文件整理到 refer 目录

This commit is contained in:
2026-07-03 22:26:00 +08:00
parent 49880e785f
commit 9f57af4d37
1223 changed files with 98 additions and 0 deletions
@@ -0,0 +1,223 @@
import { Settings2, TrendingDown, RadioTower } from 'lucide-react'
import { motion } from 'framer-motion'
import { storage } from '@/lib/storage'
// ===== 卡片尺寸 =====
export type CardSize = 'mini' | 'normal' | 'large' | 'hidden'
export function loadCardSize(): CardSize {
const v = storage.screenerCardSize.get('normal')
if (v === 'mini' || v === 'normal' || v === 'large' || v === 'hidden') return v
return 'normal'
}
const CARD_STYLES: Record<CardSize, {
wrap: string
card: string
name: string
count: string
desc: string
icon: string
}> = {
mini: {
wrap: 'gap-1',
card: 'inline-flex items-center gap-1 px-2 py-0.5 rounded-full',
name: 'text-[10px]',
count: 'text-[11px]',
desc: '',
icon: 'h-3 w-3',
},
normal: {
wrap: 'gap-2',
card: 'relative inline-flex items-center gap-2 pl-3 pr-12 py-1.5 rounded-lg',
name: 'text-xs',
count: 'text-xs',
desc: 'text-[10px] text-muted leading-tight mt-0.5 line-clamp-1 max-w-[120px]',
icon: 'h-3.5 w-3.5',
},
large: {
wrap: 'gap-2',
card: 'relative inline-flex flex-col items-start pl-3.5 pr-12 py-2.5 rounded-btn min-w-[100px]',
name: 'text-xs',
count: 'text-lg font-mono font-bold tabular-nums',
desc: 'text-[10px] text-muted leading-tight mt-0.5 line-clamp-2 max-w-[140px]',
icon: 'h-3.5 w-3.5',
},
hidden: {
wrap: '',
card: '',
name: '',
count: '',
desc: '',
icon: '',
},
}
export { CARD_STYLES }
/** 获取卡片容器的 flex-wrap gap 样式 */
export function cardWrapCls(size: CardSize): string {
return `flex flex-wrap ${CARD_STYLES[size].wrap}`
}
// ===== 来源标签 =====
const SRC_MAP: Record<string, string> = { builtin: '内置', custom: '自定义', ai: 'AI' }
const BADGE_CLS_MAP: Record<string, string> = {
builtin: 'bg-secondary/10 text-muted border-border',
ai: 'bg-purple-500/10 text-purple-400 border-purple-500/30',
custom: 'bg-amber-400/10 text-amber-400 border-amber-400/30',
}
// ===== 策略卡片 =====
interface StrategyCardProps {
name: string
description?: string
source?: string
active: boolean
count?: number
/** 今日曾命中总数 */
everMatched?: number
/** 今日已失效数 (曾命中 - 当前命中) */
expiredCount?: number
loading: boolean
cardSize: CardSize
onRun: () => void
disabled: boolean
onSettings: () => void
/** 是否已加入策略监控 */
monitored?: boolean
/** 切换策略监控 (点击 RadioTower 图标) */
onToggleMonitor?: () => void
}
export function StrategyCard({
name, description, source, active, count, expiredCount,
loading, cardSize,
onRun, disabled, onSettings, monitored, onToggleMonitor,
}: StrategyCardProps) {
const cs = CARD_STYLES[cardSize]
const activeCls = active
? 'border-accent/50 bg-accent/10 shadow-[0_0_10px_rgba(59,130,246,0.1)]'
: 'border-border bg-surface hover:border-accent/40 hover:bg-accent/[0.03]'
const countCls = count === 0
? 'text-muted'
: 'bg-gradient-to-r from-amber-400 to-orange-500 bg-clip-text text-transparent'
const srcLabel = cardSize === 'mini' ? (SRC_MAP[source ?? ''] ?? '内') : (SRC_MAP[source ?? ''] ?? '内置')
const badgeCls = BADGE_CLS_MAP[source ?? 'builtin'] ?? BADGE_CLS_MAP.builtin
// 失效数 > 0 时显示
const hasExpired = expiredCount != null && expiredCount > 0
return (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.12, ease: [0.16, 1, 0.3, 1] }}
className={`${cs.card} border transition-all duration-150 text-left group ${activeCls}`}
>
{cardSize === 'large' ? (
<>
<button onClick={onRun} disabled={disabled}
className="flex flex-col items-start cursor-pointer disabled:opacity-50 disabled:cursor-wait w-full">
<div className="flex items-center gap-1.5 max-w-full">
<span className={`text-[9px] px-1 py-px rounded border font-medium leading-tight shrink-0 ${badgeCls}`}>{srcLabel}</span>
<span className="text-xs font-medium truncate text-foreground">{name}</span>
</div>
{description && (
<span className="text-[10px] text-muted leading-tight mt-0.5 line-clamp-1">{description}</span>
)}
{count != null && (
<div className="mt-1.5 flex items-center gap-2">
<div className="flex items-center gap-1">
<span className={`text-sm font-mono font-bold tabular-nums ${countCls}`}>{count}</span>
<span className="text-[10px] text-muted"></span>
</div>
{hasExpired && (
<div className="flex items-center gap-0.5 px-1.5 py-0.5 rounded bg-red-500/8 border border-red-500/15">
<TrendingDown className="h-2.5 w-2.5 text-red-400" />
<span className="text-[10px] font-mono font-medium text-red-400">{expiredCount}</span>
</div>
)}
</div>
)}
{loading && <div className="mt-1 h-4 w-10 rounded bg-elevated animate-pulse" />}
</button>
<button onClick={(e) => { e.stopPropagation(); onSettings() }}
className="absolute top-1.5 right-1.5 p-0.5 rounded hover:bg-elevated transition-colors cursor-pointer" title="策略设置">
<Settings2 className="h-3 w-3 text-muted hover:text-accent transition-colors" />
</button>
{onToggleMonitor && (
<button onClick={(e) => { e.stopPropagation(); onToggleMonitor() }}
className="absolute top-1.5 right-7 p-0.5 rounded hover:bg-elevated transition-colors cursor-pointer" title={monitored ? '取消策略监控' : '开启策略监控'}>
<RadioTower className={`relative h-3 w-3 transition-colors ${monitored ? 'text-accent' : 'text-muted hover:text-accent'}`} />
{monitored && <span className="absolute inset-0 rounded animate-ping bg-accent/20" />}
</button>
)}
</>
) : cardSize === 'normal' ? (
<>
<button onClick={onRun} disabled={disabled}
className="flex flex-col items-start cursor-pointer disabled:opacity-50 disabled:cursor-wait min-w-0">
<div className="flex items-center gap-1.5 min-w-0">
<span className={`text-[9px] px-1 py-px rounded border font-medium leading-tight shrink-0 ${badgeCls}`}>{srcLabel}</span>
<span className="text-xs font-medium truncate text-foreground">{name}</span>
{count != null && (
<span className={`text-xs font-mono font-bold tabular-nums shrink-0 ${countCls}`}>{count}</span>
)}
{loading && <span className="w-5 h-3 rounded bg-elevated animate-pulse shrink-0" />}
</div>
<div className="flex items-center gap-1.5 mt-0.5">
{description && (
<span className="text-[10px] text-muted leading-tight line-clamp-1 max-w-[120px]">{description}</span>
)}
{hasExpired && (
<span className="text-[9px] font-mono text-red-400/80">{'-' + expiredCount}</span>
)}
</div>
</button>
<button onClick={(e) => { e.stopPropagation(); onSettings() }}
className="absolute top-1.5 right-1.5 p-0.5 rounded hover:bg-elevated transition-colors cursor-pointer" title="策略设置">
<Settings2 className="h-3 w-3 text-muted hover:text-accent transition-colors" />
</button>
{onToggleMonitor && (
<button onClick={(e) => { e.stopPropagation(); onToggleMonitor() }}
className="absolute top-1.5 right-7 p-0.5 rounded hover:bg-elevated transition-colors cursor-pointer" title={monitored ? '取消策略监控' : '开启策略监控'}>
<RadioTower className={`relative h-3 w-3 transition-colors ${monitored ? 'text-accent' : 'text-muted hover:text-accent'}`} />
{monitored && <span className="absolute inset-0 rounded animate-ping bg-accent/20" />}
</button>
)}
</>
) : (
/* mini */
<>
<button onClick={onRun} disabled={disabled}
className="flex items-center gap-1 cursor-pointer disabled:opacity-50 disabled:cursor-wait">
<span className="text-[8px] px-0.5 rounded bg-secondary/10 text-muted border border-border font-medium leading-tight">{srcLabel}</span>
<span className="text-[10px] font-medium whitespace-nowrap text-foreground">{name}</span>
{count != null && (
<span className={`text-xs font-mono font-bold tabular-nums ${countCls}`}>{count}</span>
)}
{hasExpired && (
<span className="text-[9px] font-mono text-red-400/70">{'-' + expiredCount}</span>
)}
{loading && <span className="w-4 h-2.5 rounded bg-elevated animate-pulse" />}
</button>
{onToggleMonitor && (
<button onClick={(e) => { e.stopPropagation(); onToggleMonitor() }}
className="relative p-0.5 rounded hover:bg-elevated transition-colors cursor-pointer" title={monitored ? '取消策略监控' : '开启策略监控'}>
<RadioTower className={`h-3 w-3 transition-colors ${monitored ? 'text-accent' : 'text-muted hover:text-accent'}`} />
{monitored && <span className="absolute inset-0 rounded animate-ping bg-accent/20" />}
</button>
)}
<button onClick={(e) => { e.stopPropagation(); onSettings() }}
className="p-0.5 rounded hover:bg-elevated transition-colors cursor-pointer" title="策略设置">
<Settings2 className="h-3 w-3 text-muted hover:text-accent transition-colors" />
</button>
</>
)}
</motion.div>
)
}