import { JSX } from 'preact'; import { useState, useEffect } from 'preact/hooks'; import { ScriptConfig, ScriptParameter, FavoriteList } from '@/types'; import { GM_setValue, GM_getValue } from '$'; import './ScriptCard.css'; interface ScriptCardProps { script: ScriptConfig; onExecute: (scriptId: string, parameters: Record) => void; onStop: (scriptId: string) => void; isRunning: boolean; progress?: number; favoriteList?: FavoriteList | null; favoriteListLoading?: boolean; favoriteListError?: string | null; onRetryFavoriteList?: () => void; } export function ScriptCard({ script, onExecute, onStop, isRunning, progress = 0, favoriteList, favoriteListLoading = false, favoriteListError, onRetryFavoriteList }: ScriptCardProps): JSX.Element { const [parameters, setParameters] = useState>({}); const [isExpanded, setIsExpanded] = useState(false); const [focusedFavoriteInputs, setFocusedFavoriteInputs] = useState>(new Set()); // 从GM存储加载参数 useEffect(() => { const storageKey = `bili_tasks_params_${script.id}`; const savedParams = GM_getValue(storageKey, '{}'); try { const parsedParams = JSON.parse(savedParams); const initialParams: Record = {}; script.parameters.forEach(param => { // 优先使用保存的参数,否则使用默认值 initialParams[param.key] = parsedParams[param.key] !== undefined ? parsedParams[param.key] : param.defaultValue; }); setParameters(initialParams); } catch (error) { console.warn('Failed to load saved parameters:', error); // 如果解析失败,使用默认值 const initialParams: Record = {}; script.parameters.forEach(param => { initialParams[param.key] = param.defaultValue; }); setParameters(initialParams); } }, [script.id, script.parameters]); // 保存参数到GM存储 useEffect(() => { if (Object.keys(parameters).length > 0) { const storageKey = `bili_tasks_params_${script.id}`; GM_setValue(storageKey, JSON.stringify(parameters)); } }, [parameters, script.id]); const handleParameterChange = (key: string, value: any) => { setParameters(prev => ({ ...prev, [key]: value })); }; const handleExecute = () => { // 验证必填参数 const missingParams = script.parameters .filter(param => param.required && !parameters[param.key]) .map(param => param.label); if (missingParams.length > 0) { alert(`请填写必填参数: ${missingParams.join(', ')}`); return; } onExecute(script.id, parameters); }; const handleStop = () => { onStop(script.id); }; // 判断参数是否为收藏夹ID const isFavoriteIdParameter = (param: ScriptParameter): boolean => { return param.type === 'number' && (param.label.includes('收藏夹ID') || param.key.toLowerCase().includes('favorite')); }; // 获取排序后的收藏夹选项 const getFavoriteOptions = () => { if (!favoriteList?.list) return []; return favoriteList.list .map(fav => ({ value: `${fav.title}(${fav.id})`, // value设置为显示格式 id: fav.id, // 保留原始ID用于提取 title: fav.title })) .sort((a, b) => a.value.localeCompare(b.value)); }; // 根据ID查找收藏夹标题 const getFavoriteTitleById = (id: number): string | null => { if (!favoriteList?.list) return null; const favorite = favoriteList.list.find(fav => fav.id === id); return favorite ? favorite.title : null; }; // 格式化显示值(失去焦点时使用) const formatDisplayValue = (value: number): string => { if (!value) return ''; const title = getFavoriteTitleById(value); return title ? `${title}(${value})` : value.toString(); }; // 从格式化字符串中提取ID const extractIdFromFormattedValue = (formattedValue: string): number | null => { // 尝试直接解析为数字 const directNumber = parseInt(formattedValue); if (!isNaN(directNumber) && directNumber.toString() === formattedValue) { return directNumber; } // 尝试从 "标题(ID)" 格式中提取ID const match = formattedValue.match(/\((\d+)\)$/); if (match) { return parseInt(match[1]); } return null; }; // 处理收藏夹输入框焦点事件 const handleFavoriteInputFocus = (paramKey: string) => { setFocusedFavoriteInputs(prev => new Set(prev).add(paramKey)); }; const handleFavoriteInputBlur = (paramKey: string) => { setFocusedFavoriteInputs(prev => { const newSet = new Set(prev); newSet.delete(paramKey); return newSet; }); }; // 获取输入框显示值 const getFavoriteInputDisplayValue = (paramKey: string, value: number): string => { const isFocused = focusedFavoriteInputs.has(paramKey); if (isFocused || value === undefined || value === null) { return value !== undefined && value !== null ? value.toString() : ''; } return formatDisplayValue(value); }; // 通用清空按钮组件 const renderClearButton = (paramKey: string, hasValue: boolean, className: string = '') => { if (!hasValue || isRunning) return null; return ( ); }; const renderParameterInput = (param: ScriptParameter) => { const value = parameters[param.key]; switch (param.type) { case 'text': return (
handleParameterChange(param.key, (e.target as HTMLInputElement).value)} placeholder={param.placeholder} disabled={isRunning} class="script-input" /> {renderClearButton(param.key, value !== undefined && value !== null && value !== '' && value.toString().trim() !== '')}
); case 'number': // 如果是收藏夹ID参数,渲染为可编辑的下拉选择框 if (isFavoriteIdParameter(param)) { const favoriteOptions = getFavoriteOptions(); const displayValue = getFavoriteInputDisplayValue(param.key, value); return (
{ const inputValue = (e.target as HTMLInputElement).value; const extractedId = extractIdFromFormattedValue(inputValue); if (extractedId !== null) { handleParameterChange(param.key, extractedId); } }} onFocus={() => handleFavoriteInputFocus(param.key)} onBlur={() => handleFavoriteInputBlur(param.key)} placeholder={param.placeholder || '输入收藏夹ID或从下拉列表选择'} disabled={isRunning} class="script-input favorite-input" list={`favorites-${script.id}-${param.key}`} /> {renderClearButton(param.key, value !== undefined && value !== null && value !== '', 'clear-button-favorite')}
{favoriteOptions.map(option => ( {favoriteListLoading && (
正在加载收藏夹列表...
)} {favoriteListError && (
加载失败: {favoriteListError} {onRetryFavoriteList && ( )}
)}
); } // 普通数字输入框 return (
handleParameterChange(param.key, Number((e.target as HTMLInputElement).value))} placeholder={param.placeholder} disabled={isRunning} class="script-input" /> {renderClearButton(param.key, value !== undefined && value !== null && value !== '')}
); case 'boolean': return ( ); case 'select': return ( ); case 'textarea': return (