import type { WidgetComponentConfig, WidgetComponentType, SidebarConfig, } from "@/types/config"; import { sidebarConfig } from "@/config"; /** * 组件映射表 - 将组件类型映射到实际的组件路径 */ export const WIDGET_COMPONENT_MAP = { profile: "@components/sidebar/profile.astro", announcement: "@components/sidebar/announcement.astro", categories: "@components/sidebar/categories.astro", tags: "@components/sidebar/tags.astro", toc: "@components/sidebar/toc.astro", statistics: "@components/sidebar/statistics.astro", custom: null, // 自定义组件需要在配置中指定路径 } as const; /** * 组件管理器类 * 负责管理侧边栏组件的动态加载、排序和渲染 */ export class WidgetManager { private config: SidebarConfig; constructor(config: SidebarConfig = sidebarConfig) { this.config = config; } /** * 获取配置 */ getConfig(): SidebarConfig { return this.config; } /** * 获取指定侧边栏上的组件列表 * @param side 侧边栏位置:'left' | 'right' */ getComponentsBySide(side: "left" | "right"): WidgetComponentConfig[] { return this.config.components[side] || []; } /** * 根据位置获取组件列表 * @param position 组件位置:'top' | 'sticky' */ getComponentsByPosition(position: "top" | "sticky"): WidgetComponentConfig[] { const left = this.getComponentsBySideAndPosition("left", position); const right = this.getComponentsBySideAndPosition("right", position); // Note: This might return duplicates if left/right logic overlaps, but used for enabled types check return [...left, ...right]; } /** * 根据侧边栏和位置获取组件列表 * @param side 侧边栏位置:'left' | 'right' | 'middle' * @param position 组件位置:'top' | 'sticky' */ getComponentsBySideAndPosition( side: "left" | "right" | "middle", position: "top" | "sticky", ): WidgetComponentConfig[] { const leftComponents = (this.config.components.left || []).filter(c => c.position === position); const rightComponents = (this.config.components.right || []).filter(c => c.position === position); if (side === "left") { // Left sidebar includes Right components on Tablet (merged) return [...leftComponents, ...rightComponents]; } if (side === "right") { // Right sidebar only shows Right components (Desktop only) return rightComponents; } if (side === "middle") { // Middle sidebar includes all components return [...leftComponents, ...rightComponents]; } return []; } /** * 获取组件的CSS类名 * @param component 组件配置 * @param index 组件在列表中的索引 * @param side 当前渲染的侧边栏位置 */ getComponentClass(component: WidgetComponentConfig, index: number, side: "left" | "right" | "middle"): string { const classes: string[] = []; // 基础响应式隐藏配置 (用户配置的) if (component.responsive?.hidden) { component.responsive.hidden.forEach((device) => { switch (device) { case "mobile": classes.push("hidden md:block"); break; case "tablet": classes.push("md:hidden lg:block"); break; case "desktop": classes.push("lg:hidden"); break; } }); } // 自动布局逻辑 const isFromLeft = (this.config.components.left || []).includes(component); const isFromRight = (this.config.components.right || []).includes(component); if (side === "left") { if (isFromRight && !isFromLeft) { // 如果是右侧组件在左侧栏渲染(平板模式),则仅在平板显示 classes.push("hidden md:block lg:hidden"); } // 左侧组件默认显示 } return classes.join(" "); } /** * 获取组件的内联样式 * @param component 组件配置 * @param index 组件在列表中的索引 */ getComponentStyle(component: WidgetComponentConfig, index: number): string { const styles: string[] = []; // 添加自定义样式 if (component.style) { styles.push(component.style); } return styles.join("; "); } /** * 检查组件是否应该折叠 * @param component 组件配置 * @param itemCount 组件内容项数量 */ isCollapsed(component: WidgetComponentConfig, itemCount: number): boolean { if (!component.responsive?.collapseThreshold) { return false; } return itemCount >= component.responsive.collapseThreshold; } /** * 获取组件的路径 * @param componentType 组件类型 */ getComponentPath(componentType: WidgetComponentType): string | null { return WIDGET_COMPONENT_MAP[componentType]; } /** * 检查指定侧边栏是否具有实际可显示的内容 * @param side 侧边栏位置:'left' | 'right' * @param headings 页面标题列表,用于判断特殊组件是否显示 */ hasContentOnSide(side: "left" | "right", headings: any[] = []): boolean { const components = this.getComponentsBySide(side); if (components.length === 0) return false; // 只要有一个组件能显示内容,侧边栏就不是空的 return components.some((component) => { // TOC 组件只有在有标题时才显示 if (component.type === "toc") { return headings && headings.length > 0; } // 其他组件暂认为始终有内容 return true; }); } /** * 更新组件配置 * @param newConfig 新的配置 */ updateConfig(newConfig: Partial): void { this.config = { ...this.config, ...newConfig }; } /** * 添加新组件 * @param component 组件配置 * @param side 侧边栏位置 */ addComponent(component: WidgetComponentConfig, side: "left" | "right"): void { if (!this.config.components[side]) { this.config.components[side] = []; } this.config.components[side].push(component); } /** * 移除组件 * @param componentType 组件类型 */ removeComponent(componentType: WidgetComponentType): void { if (this.config.components.left) { this.config.components.left = this.config.components.left.filter( (component) => component.type !== componentType, ); } if (this.config.components.right) { this.config.components.right = this.config.components.right.filter( (component) => component.type !== componentType, ); } } /** * 重新排序组件 * @param side 侧边栏 * @param oldIndex 旧索引 * @param newIndex 新索引 */ reorderComponent(side: "left" | "right", oldIndex: number, newIndex: number): void { const list = this.config.components[side]; if (!list) return; if (oldIndex >= 0 && oldIndex < list.length && newIndex >= 0 && newIndex < list.length) { const [moved] = list.splice(oldIndex, 1); list.splice(newIndex, 0, moved); } } /** * 检查组件是否应该在侧边栏中渲染 * @param componentType 组件类型 */ isSidebarComponent(componentType: WidgetComponentType): boolean { return true; } /** * 获取页面中的标题列表 * @returns 格式化后的标题数组 */ getPageHeadings() { if (typeof document === "undefined") return []; return Array.from(document.querySelectorAll("h1, h2, h3, h4, h5, h6")) .filter((h) => h.id) .map((h) => ({ depth: parseInt(h.tagName.substring(1)), slug: h.id, text: (h.textContent || "").replace(/#+\s*$/, ""), })); } /** * 获取网格布局相关的类名 * @param headings 页面标题列表 */ getGridLayout(headings: any[] = []) { const hasLeftComponents = this.hasContentOnSide("left", headings); const hasRightComponents = this.hasContentOnSide("right", headings); const hasAnyComponents = hasLeftComponents || hasRightComponents; // Desktop: Left if hasLeft, Right if hasRight const hasLeftSidebar = hasLeftComponents; const hasRightSidebar = hasRightComponents; // 动态网格布局类名 const gridCols = ` grid-cols-1 ${hasAnyComponents ? "md:grid-cols-[17.5rem_1fr]" : "md:grid-cols-1"} ${ hasLeftSidebar && hasRightSidebar ? "lg:grid-cols-[17.5rem_1fr_17.5rem]" : hasLeftSidebar ? "lg:grid-cols-[17.5rem_1fr]" : hasRightSidebar ? "lg:grid-cols-[1fr_17.5rem]" : "lg:grid-cols-1" } `.trim().replace(/\s+/g, " "); // 左侧侧边栏容器类名 // Mobile: Hidden // Tablet: Visible if hasAnyComponents (merged) // Desktop: Visible if hasLeftSidebar const leftSidebarClass = ` mb-0 col-span-1 hidden ${hasAnyComponents ? "md:block md:max-w-70" : ""} ${hasLeftSidebar ? "lg:block lg:max-w-70 lg:col-start-1 lg:col-end-2 lg:row-start-1 lg:row-end-2" : "lg:hidden"} `.trim().replace(/\s+/g, " "); // 右侧侧边栏容器类名 // Mobile: Hidden // Tablet: Hidden // Desktop: Visible if hasRightSidebar const rightSidebarClass = ` mb-0 col-span-1 hidden md:hidden ${ hasRightSidebar ? hasLeftSidebar ? "lg:block lg:max-w-70 lg:col-start-3 lg:col-end-4 lg:row-start-1 lg:row-end-2" : "lg:block lg:max-w-70 lg:col-start-2 lg:col-end-3 lg:row-start-1 lg:row-end-2" : "lg:hidden" } `.trim().replace(/\s+/g, " "); // 移动端 Footer 类名 // Always 1 col on mobile // 2 cols on tablet if sidebar is present const mobileFooterClass = ` footer col-span-1 onload-animation-up block lg:hidden transition-swup-fade ${hasAnyComponents ? "md:col-span-2" : "md:col-span-1"} `.trim().replace(/\s+/g, " "); // 移动端侧边栏类名 const middleSidebarClass = ` col-span-1 block md:hidden ${!hasAnyComponents ? "hidden" : ""} `.trim().replace(/\s+/g, " "); // 主内容区域类名 const mainContentClass = ` overflow-hidden w-full col-span-1 row-start-1 row-end-2 ${hasAnyComponents ? "md:col-start-2 md:col-end-3 md:row-start-1 md:row-end-2" : "md:col-span-1"} ${ hasLeftSidebar && hasRightSidebar ? "lg:col-start-2 lg:col-end-3 lg:row-start-1 lg:row-end-2" : hasLeftSidebar ? "lg:col-start-2 lg:col-end-3 lg:row-start-1 lg:row-end-2" : hasRightSidebar ? "lg:col-start-1 lg:col-end-2 lg:row-start-1 lg:row-end-2" : "lg:col-span-1" } `.trim().replace(/\s+/g, " "); return { hasLeftSidebar, hasRightSidebar, hasAnyComponents, gridCols, leftSidebarClass, rightSidebarClass, mainContentClass, mobileFooterClass, middleSidebarClass, }; } } /** * 默认组件管理器实例 */ export const widgetManager = new WidgetManager(); /** * 工具函数:根据组件类型获取组件配置 * @param componentType 组件类型 */ export function getComponentConfig( componentType: WidgetComponentType, ): WidgetComponentConfig | undefined { const left = widgetManager.getConfig().components.left || []; const right = widgetManager.getConfig().components.right || []; return left.find((c) => c.type === componentType) || right.find((c) => c.type === componentType); } /** * 工具函数:检查组件是否启用 * @param componentType 组件类型 */ export function isComponentEnabled( componentType: WidgetComponentType, ): boolean { // 默认所有配置中存在的组件都视为启用 return !!getComponentConfig(componentType); } /** * 工具函数:获取所有启用的组件类型 */ export function getEnabledComponentTypes(): WidgetComponentType[] { const enabledComponents = widgetManager.getComponentsByPosition("top").concat( widgetManager.getComponentsByPosition("sticky") ); return enabledComponents.map((c) => c.type); } /** * 通用的点击外部关闭处理函数 * @param event 鼠标事件 * @param panelId 面板ID * @param ignoreIds 忽略的元素ID(按钮等),支持单个ID或ID数组 * @param action 关闭回调 */ export function onClickOutside( event: MouseEvent, panelId: string, ignoreIds: string | string[], action: () => void ) { if (typeof document === "undefined") { return; } const panel = document.getElementById(panelId); const target = event.target as HTMLElement; const ids = Array.isArray(ignoreIds) ? ignoreIds : [ignoreIds]; // 如果点击的是忽略元素或其内部元素,则不执行关闭操作 for (const id of ids) { if (target.closest(`#${id}`)) { return; } } // 如果面板存在且点击发生在面板外部,则执行关闭操作 if (panel && !panel.contains(target)) { action(); } }