diff --git a/frontend-v2/lib/catalog/catalog.ts b/frontend-v2/lib/catalog/catalog.ts index b2bbca2..4083494 100644 --- a/frontend-v2/lib/catalog/catalog.ts +++ b/frontend-v2/lib/catalog/catalog.ts @@ -103,7 +103,9 @@ export const NovelListProps = z.object({ description: z.string().optional(), chapterCount: z.number().optional(), tags: z.array(z.string()).optional(), - })), + })).optional(), + children: z.array(z.any()).optional(), + emptyMessage: z.string().optional(), className: z.string().optional(), }); @@ -150,6 +152,114 @@ export const SuggestionButtonsProps = z.object({ className: z.string().optional(), }); +// ============ Novel 相关组件 ============ + +export const NovelItemProps = z.object({ + id: z.union([z.string(), z.number()]), + title: z.string(), + author: z.string().optional(), + description: z.string().optional(), + chapterCount: z.number().optional(), + tags: z.array(z.string()).optional(), + coverUrl: z.string().optional(), + status: z.string().optional(), + isVip: z.boolean().optional(), + onClick: z.string().optional(), + className: z.string().optional(), +}); + +export const CountProps = z.object({ + value: z.number(), + label: z.string().optional(), + variant: z.enum(['default', 'primary', 'success', 'warning']).optional(), + className: z.string().optional(), +}); + +export const RefreshButtonProps = z.object({ + label: z.string().optional(), + loading: z.boolean().optional(), + onClick: z.string().optional(), + disabled: z.boolean().optional(), + variant: z.enum(['default', 'primary']).optional(), + className: z.string().optional(), +}); + +// ============ 筛选器相关组件 ============ + +export const FiltersProps = z.object({ + children: z.array(z.any()).optional(), + className: z.string().optional(), +}); + +export const HeaderProps = z.object({ + title: z.string().optional(), + subtitle: z.string().optional(), + actions: z.array(z.any()).optional(), + className: z.string().optional(), +}); + +export const CategoryFilterProps = z.object({ + categories: z.array(z.object({ + id: z.union([z.string(), z.number()]), + name: z.string(), + count: z.number().optional(), + })).optional(), + selected: z.union([z.string(), z.number()]).optional(), + onSelect: z.string().optional(), + className: z.string().optional(), +}); + +export const SearchInputProps = z.object({ + placeholder: z.string().optional(), + value: z.string().optional(), + onSearch: z.string().optional(), + onInputChange: z.string().optional(), + disabled: z.boolean().optional(), + className: z.string().optional(), +}); + +// ============ 缺失组件 ============ + +export const SearchBarProps = z.object({ + placeholder: z.string().optional(), + value: z.string().optional(), + onSearch: z.string().optional(), + onClear: z.string().optional(), + disabled: z.boolean().optional(), + showClearButton: z.boolean().optional(), + className: z.string().optional(), +}); + +export const FilterSectionProps = z.object({ + title: z.string().optional(), + children: z.array(z.any()).optional(), + collapsible: z.boolean().optional(), + defaultCollapsed: z.boolean().optional(), + className: z.string().optional(), +}); + +export const NovelGridProps = z.object({ + columns: z.number().optional(), + gap: z.number().optional(), + children: z.array(z.any()).optional(), + responsive: z.boolean().optional(), + className: z.string().optional(), +}); + +export const PaginationProps = z.object({ + currentPage: z.number(), + totalPages: z.number(), + onPageChange: z.string().optional(), + showFirstLast: z.boolean().optional(), + maxVisiblePages: z.number().optional(), + className: z.string().optional(), +}); + +export const SearchSectionProps = z.object({ + children: z.array(z.any()).optional(), + className: z.string().optional(), +}); + // 创建 catalog const catalog = defineCatalog(schema, { components: { @@ -225,6 +335,54 @@ const catalog = defineCatalog(schema, { props: SuggestionButtonsProps, description: '建议按钮组组件,用于快速选择预定义消息', }, + 'novel-item': { + props: NovelItemProps, + description: '小说列表项组件,用于展示单个小说的卡片', + }, + count: { + props: CountProps, + description: '计数显示组件,用于显示数量统计', + }, + 'refresh-button': { + props: RefreshButtonProps, + description: '刷新按钮组件,支持加载状态', + }, + filters: { + props: FiltersProps, + description: '筛选器容器组件,用于包装筛选相关元素', + }, + header: { + props: HeaderProps, + description: '头部组件,用于页面标题和操作区', + }, + 'category-filter': { + props: CategoryFilterProps, + description: '分类筛选组件,支持单选分类', + }, + 'search-input': { + props: SearchInputProps, + description: '搜索输入组件,支持搜索和输入事件', + }, + 'search-bar': { + props: SearchBarProps, + description: '搜索栏组件,包含搜索输入框和搜索按钮', + }, + 'filter-section': { + props: FilterSectionProps, + description: '筛选区域容器组件,用于包装分类筛选等筛选元素', + }, + 'novel-grid': { + props: NovelGridProps, + description: '网格布局容器组件,用于展示小说卡片', + }, + 'pagination': { + props: PaginationProps, + description: '分页组件,显示页码按钮和翻页控制', + }, + 'search-section': { + props: SearchSectionProps, + description: '搜索区域容器组件,用于包装搜索输入和刷新按钮', + }, }, }); diff --git a/frontend-v2/lib/registry.tsx b/frontend-v2/lib/registry.tsx index 39a6b4d..c5c6bba 100644 --- a/frontend-v2/lib/registry.tsx +++ b/frontend-v2/lib/registry.tsx @@ -28,6 +28,18 @@ import catalog, { LoginPanelProps, McpStatusProps, SuggestionButtonsProps, + NovelItemProps, + CountProps, + RefreshButtonProps, + FiltersProps, + HeaderProps, + CategoryFilterProps, + SearchInputProps, + SearchBarProps, + FilterSectionProps, + NovelGridProps, + PaginationProps, + SearchSectionProps, } from './catalog/catalog'; // ============ 基础组件 ============ @@ -252,31 +264,42 @@ const TranslationResult = ({ props }: BaseComponentProps>) => ( +const NovelList = ({ props, emit, children }: BaseComponentProps>) => (
- {props.novels?.map((novel) => ( -
emit('selectNovel')} - className="bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 p-4 hover:shadow-md hover:border-blue-300 dark:hover:border-blue-600 transition-all cursor-pointer" - > -

{novel.title}

- {novel.author &&

By {novel.author}

} - {novel.description &&

{novel.description}

} -
- {novel.chapterCount && ( - - {novel.chapterCount} chapters - - )} - {novel.tags?.map((tag) => ( - - {tag} - - ))} + {/* 如果有 children,直接渲染 children */} + {children && children.length > 0 ? ( + children + ) : props.novels && props.novels.length > 0 ? ( + // 否则渲染 novels 数组 + props.novels.map((novel) => ( +
emit('selectNovel')} + className="bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 p-4 hover:shadow-md hover:border-blue-300 dark:hover:border-blue-600 transition-all cursor-pointer" + > +

{novel.title}

+ {novel.author &&

By {novel.author}

} + {novel.description &&

{novel.description}

} +
+ {novel.chapterCount && ( + + {novel.chapterCount} chapters + + )} + {novel.tags?.map((tag) => ( + + {tag} + + ))} +
+ )) + ) : ( + // 空状态 +
+ {props.emptyMessage || '暂无小说'}
- ))} + )}
); @@ -443,6 +466,469 @@ const SuggestionButtons = ({ props }: BaseComponentProps>) => { + const handleClick = () => { + if (props.onClick) { + emit(props.onClick); + } + }; + + return ( +
+
+ {props.coverUrl && ( +
+ {props.title} +
+ )} +
+
+

{props.title}

+ {props.isVip && ( + + 👑 VIP + + )} +
+ {props.author &&

By {props.author}

} + {props.description &&

{props.description}

} +
+ {props.status && ( + + {props.status === 'completed' ? '已完结' : props.status === 'ongoing' ? '连载中' : props.status} + + )} + {props.chapterCount && ( + + {props.chapterCount} 章 + + )} + {props.tags?.map((tag) => ( + + {tag} + + ))} +
+
+
+
+ ); +}; + +const Count = ({ props }: BaseComponentProps>) => { + const variantClasses: Record = { + default: 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200', + primary: 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200', + success: 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200', + warning: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200', + }; + const variant = props.variant || 'default'; + + return ( +
+ {props.label && {props.label}:} + {props.value} +
+ ); +}; + +const RefreshButton = ({ props, emit }: BaseComponentProps>) => { + const handleClick = () => { + if (!props.disabled && !props.loading && props.onClick) { + emit(props.onClick); + } + }; + + return ( + + ); +}; + +// ============ 筛选器相关组件实现 ============ + +const Filters = ({ props, children }: BaseComponentProps>) => ( +
+ {children} +
+); + +const Header = ({ props, children }: BaseComponentProps>) => ( +
+
+
+ {props.title &&

{props.title}

} + {props.subtitle &&

{props.subtitle}

} +
+ {props.actions && props.actions.length > 0 && ( +
+ {props.actions.map((action: any, i: number) => ( + {action} + ))} +
+ )} +
+
+); + +const CategoryFilter = ({ props, emit }: BaseComponentProps>) => { + const selected = props.selected; + + const handleSelect = (category: any) => { + if (props.onSelect) { + emit(props.onSelect); + } + }; + + return ( +
+ {props.categories?.map((category) => { + const isSelected = selected === category.id; + return ( + + ); + })} +
+ ); +}; + +const SearchInput = ({ props, emit }: BaseComponentProps>) => { + const [value, setValue] = useState(props.value || ''); + + const handleSubmit = () => { + if (props.onSearch) { + emit(props.onSearch); + } + }; + + const handleInputChange = (newValue: string) => { + setValue(newValue); + if (props.onInputChange) { + emit(props.onInputChange); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleSubmit(); + } + }; + + return ( +
+ handleInputChange(e.target.value)} + onKeyDown={handleKeyDown} + disabled={props.disabled} + className="w-full pl-10 pr-12 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50" + /> + + + + +
+ ); +}; + +// ============ 缺失组件实现 ============ + +const SearchBar = ({ props, emit }: BaseComponentProps>) => { + const [value, setValue] = useState(props.value || ''); + + const handleSearch = () => { + if (props.onSearch && value.trim()) { + emit(props.onSearch); + } + }; + + const handleClear = () => { + setValue(''); + if (props.onClear) { + emit(props.onClear); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleSearch(); + } + }; + + const showClear = props.showClearButton !== false && value.length > 0; + + return ( +
+
+ setValue(e.target.value)} + onKeyDown={handleKeyDown} + disabled={props.disabled} + className="w-full pl-10 pr-24 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50" + /> + + + +
+ {showClear && ( + + )} + +
+
+
+ ); +}; + +const SearchSection = ({ props, children }: BaseComponentProps>) => ( +
+ {children} +
+); + +const FilterSection = ({ props, children }: BaseComponentProps>) => { + const [isCollapsed, setIsCollapsed] = useState(props.defaultCollapsed || false); + + return ( +
+
+ {props.title && ( +

{props.title}

+ )} + {props.collapsible && ( + + )} +
+ {!isCollapsed && ( +
+ {children} +
+ )} +
+ ); +}; + +const NovelGrid = ({ props, children }: BaseComponentProps>) => { + const columns = props.columns || 3; + const gap = props.gap || 4; + + // 响应式布局 + const gridClass = props.responsive !== false + ? `grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-${columns}` + : `grid grid-cols-${columns}`; + + const gapClass = gap === 1 ? 'gap-1' + : gap === 2 ? 'gap-2' + : gap === 3 ? 'gap-3' + : gap === 4 ? 'gap-4' + : gap === 6 ? 'gap-6' + : `gap-[${gap}px]`; + + return ( +
+ {children} +
+ ); +}; + +const Pagination = ({ props, emit }: BaseComponentProps>) => { + const currentPage = props.currentPage; + const totalPages = props.totalPages; + const maxVisible = props.maxVisiblePages || 5; + + const getPageNumbers = () => { + const pages: (number | string)[] = []; + const showFirstLast = props.showFirstLast !== false; + + if (showFirstLast && currentPage > 1) { + pages.push('first'); + } + + let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2)); + let endPage = Math.min(totalPages, startPage + maxVisible - 1); + + if (endPage - startPage + 1 < maxVisible) { + startPage = Math.max(1, endPage - maxVisible + 1); + } + + if (startPage > 1) { + pages.push(1); + if (startPage > 2) pages.push('...'); + } + + for (let i = startPage; i <= endPage; i++) { + pages.push(i); + } + + if (endPage < totalPages) { + if (endPage < totalPages - 1) pages.push('...'); + pages.push(totalPages); + } + + if (showFirstLast && currentPage < totalPages) { + pages.push('last'); + } + + return pages; + }; + + const handlePageChange = (page: number) => { + if (page >= 1 && page <= totalPages && page !== currentPage && props.onPageChange) { + emit(props.onPageChange); + } + }; + + const pages = getPageNumbers(); + + return ( +
+ {pages.map((page, idx) => { + if (page === '...') { + return ( + + ... + + ); + } + + if (page === 'first') { + return ( + + ); + } + + if (page === 'last') { + return ( + + ); + } + + const isActive = page === currentPage; + return ( + + ); + })} +
+ ); +}; + // ============ 创建并导出注册表 ============ const { registry } = defineRegistry(catalog, { @@ -468,6 +954,24 @@ const { registry } = defineRegistry(catalog, { 'login-panel': LoginPanel, 'mcp-status': McpStatus, 'suggestion-buttons': SuggestionButtons, + + // Novel 相关组件 + 'novel-item': NovelItem, + count: Count, + 'refresh-button': RefreshButton, + + // 筛选器相关组件 + filters: Filters, + header: Header, + 'category-filter': CategoryFilter, + 'search-input': SearchInput, + + // 缺失组件 + 'search-bar': SearchBar, + 'filter-section': FilterSection, + 'novel-grid': NovelGrid, + 'pagination': Pagination, + 'search-section': SearchSection, }, });