From e4595f281fb214c8c081a4e29cba5929b6f314e5 Mon Sep 17 00:00:00 2001 From: p7lr8khyn <2113553527@qq.com> Date: Sun, 19 Oct 2025 20:36:59 +0800 Subject: [PATCH] Update App.js --- apps/sodo-search/src/App.js | 145 +++++++++++++++++++++++++----------- 1 file changed, 103 insertions(+), 42 deletions(-) diff --git a/apps/sodo-search/src/App.js b/apps/sodo-search/src/App.js index 6debd87..b8d436a 100644 --- a/apps/sodo-search/src/App.js +++ b/apps/sodo-search/src/App.js @@ -5,54 +5,74 @@ import PopupModal from './components/PopupModal'; import SearchIndex from './search-index.js'; import i18nLib from '@tryghost/i18n'; +/** + * 搜索应用的主组件 - 负责管理整个搜索功能的布局和状态 + */ export default class App extends React.Component { constructor(props) { super(props); + // ==================== 国际化设置 ==================== + // 设置语言环境,默认为英语 const i18nLanguage = this.props.locale || 'en'; const i18n = i18nLib(i18nLanguage, 'search'); - const dir = i18n.dir() || 'ltr'; + const dir = i18n.dir() || 'ltr'; // 文字方向(左到右或右到左) + // ==================== 搜索索引初始化 ==================== + // 创建搜索索引实例,用于处理搜索逻辑 const searchIndex = new SearchIndex({ - adminUrl: props.adminUrl, - apiKey: props.apiKey, - dir: dir + adminUrl: props.adminUrl, // 管理后台URL + apiKey: props.apiKey, // API密钥 + dir: dir // 文字方向 }); + // ==================== 组件状态初始化 ==================== this.state = { - searchIndex, - showPopup: false, - indexStarted: false, - indexComplete: false, - t: i18n.t, - dir: dir, - scrollbarWidth: 0 + searchIndex, // 搜索索引实例 + showPopup: false, // 是否显示搜索弹窗 + indexStarted: false, // 是否开始构建索引 + indexComplete: false, // 索引构建是否完成 + t: i18n.t, // 国际化翻译函数 + dir: dir, // 文字方向 + scrollbarWidth: 0 // 滚动条宽度(用于防止布局偏移) }; + // 创建输入框的引用,用于直接操作DOM元素 this.inputRef = React.createRef(); } + /** + * 组件挂载后立即执行 + */ componentDidMount() { + // 计算并保存滚动条宽度 const scrollbarWidth = this.getScrollbarWidth(); this.setState({scrollbarWidth}); + // 初始化搜索设置 this.initSetup(); } + /** + * 组件更新时执行,主要用于处理弹窗显示/隐藏时的滚动条逻辑 + */ componentDidUpdate(_prevProps, prevState) { + // ==================== 弹窗显示状态变化处理 ==================== if (prevState.showPopup !== this.state.showPopup) { - /** Remove background scroll when popup is opened */ + /** 当弹窗打开时移除背景滚动,防止页面抖动 */ try { if (this.state.showPopup) { - /** When modal is opened, store current overflow and set as hidden */ + /** 当弹窗打开时,保存当前的overflow状态并设置为hidden */ this.bodyScroll = window.document?.body?.style?.overflow; this.bodyMargin = window.getComputedStyle(document.body).getPropertyValue('margin-right'); window.document.body.style.overflow = 'hidden'; + + // 如果有滚动条且内容高度超过视口,调整右边距防止布局偏移 if (this.state.scrollbarWidth && document.body.scrollHeight > window.innerHeight) { window.document.body.style.marginRight = `calc(${this.bodyMargin} + ${this.state.scrollbarWidth}px)`; } } else { - /** When the modal is hidden, reset overflow property for body */ + /** 当弹窗隐藏时,恢复body的overflow属性 */ window.document.body.style.overflow = this.bodyScroll || ''; if (!this.bodyMargin || this.bodyMargin === '0px') { window.document.body.style.marginRight = ''; @@ -61,74 +81,93 @@ export default class App extends React.Component { } } } catch (e) { - /** Ignore any errors for scroll handling */ + /** 忽略滚动处理中的任何错误 */ } } + // ==================== 弹窗关闭时清空搜索值 ==================== if (this.state.showPopup !== prevState?.showPopup && !this.state.showPopup) { this.setState({ searchValue: '' }); } + // ==================== 弹窗打开时初始化搜索索引 ==================== if (this.state.showPopup && !this.state.indexStarted) { this.setupSearchIndex(); } } + /** + * 设置搜索索引 - 异步初始化搜索功能 + */ async setupSearchIndex() { this.setState({ - indexStarted: true + indexStarted: true // 标记索引构建开始 }); - await this.state.searchIndex.init(); + await this.state.searchIndex.init(); // 等待索引初始化完成 this.setState({ - indexComplete: true + indexComplete: true // 标记索引构建完成 }); } + /** + * 组件卸载前的清理工作 + */ componentWillUnmount() { - /**Clear timeouts and event listeners on unmount */ + /** 清除超时和事件监听器 */ window.removeEventListener('hashchange', this.hashHandler, false); window.removeEventListener('keydown', this.handleKeyDown, false); } + /** + * 初始化搜索设置 + */ initSetup() { - // Listen to preview mode changes + // 监听预览模式变化 this.handleSearchUrl(); this.addKeyboardShortcuts(); this.setupCustomTriggerButton(); + + // 哈希变化处理器(用于URL路由) this.hashHandler = () => { this.handleSearchUrl(); }; window.addEventListener('hashchange', this.hashHandler, false); } - // User for adding trailing margin to prevent layout shift when popup appears + /** + * 计算滚动条宽度 - 用于防止弹窗出现时的布局偏移 + */ getScrollbarWidth() { - // Create a temporary div + // 创建临时div元素 const div = document.createElement('div'); div.style.visibility = 'hidden'; - div.style.overflow = 'scroll'; // forcing scrollbar to appear + div.style.overflow = 'scroll'; // 强制显示滚动条 document.body.appendChild(div); - // Calculate the width difference + // 计算宽度差异(这就是滚动条的宽度) const scrollbarWidth = div.offsetWidth - div.clientWidth; - // Clean up + // 清理临时元素 document.body.removeChild(div); return scrollbarWidth; } - /** Setup custom trigger buttons handling on page */ + /** + * 设置页面上的自定义触发按钮处理 + */ setupCustomTriggerButton() { - // Handler for custom buttons + // 自定义按钮的点击处理器 this.clickHandler = (event) => { event.preventDefault(); this.setState({ - showPopup: true + showPopup: true // 显示搜索弹窗 }); + // ==================== 焦点管理技巧 ==================== + // 创建临时输入元素来解决焦点问题 const tmpElement = document.createElement('input'); tmpElement.style.opacity = '0'; tmpElement.style.position = 'fixed'; @@ -136,12 +175,14 @@ export default class App extends React.Component { document.body.appendChild(tmpElement); tmpElement.focus(); + // 延迟后将焦点转移到真正的搜索输入框 setTimeout(() => { this.inputRef.current.focus(); document.body.removeChild(tmpElement); }, 150); }; + // 获取所有自定义触发按钮并添加点击事件 this.customTriggerButtons = this.getCustomTriggerButtons(); this.customTriggerButtons.forEach((customTriggerButton) => { customTriggerButton.removeEventListener('click', this.clickHandler); @@ -149,27 +190,39 @@ export default class App extends React.Component { }); } + /** + * 获取页面上的自定义触发按钮 + */ getCustomTriggerButtons() { - const customTriggerSelector = '[data-ghost-search]'; + const customTriggerSelector = '[data-ghost-search]'; // 通过data属性选择按钮 return document.querySelectorAll(customTriggerSelector) || []; } + /** + * 处理搜索URL - 当URL哈希为/#search时自动打开搜索弹窗 + */ handleSearchUrl() { const [path] = window.location.hash.substr(1).split('?'); if (path === '/search' || path === '/search/') { this.setState({ showPopup: true }); + // 替换历史状态,移除哈希部分 window.history.replaceState('', document.title, window.location.pathname); } } + /** + * 添加快捷键支持 - Cmd+K 打开搜索 + */ addKeyboardShortcuts() { const customTriggerButtons = this.getCustomTriggerButtons(); if (!customTriggerButtons?.length) { return; } + this.handleKeyDown = (e) => { + // 检测 Cmd+K (Mac) 或 Ctrl+K (Windows) if (e.key === 'k' && e.metaKey) { this.setState({ showPopup: true @@ -182,19 +235,26 @@ export default class App extends React.Component { document.addEventListener('keydown', this.handleKeyDown); } + /** + * 渲染组件 - 这里是整个搜索应用的布局入口 + */ render() { return ( + /** + * 使用Context API提供全局状态给所有子组件 + * 这是React中管理全局状态的常用模式 + */ {}, - dispatch: (action, data) => { + page: 'search', // 当前页面标识 + showPopup: this.state.showPopup, // 弹窗显示状态 + adminUrl: this.props.adminUrl, // 管理后台URL + stylesUrl: this.props.stylesUrl, // 样式文件URL + searchIndex: this.state.searchIndex, // 搜索索引实例 + indexComplete: this.state.indexComplete, // 索引完成状态 + searchValue: this.state.searchValue, // 搜索关键词 + inputRef: this.inputRef, // 输入框引用 + onAction: () => {}, // 动作处理器 + dispatch: (action, data) => { // 状态更新函数 if (action === 'update') { this.setState({ ...this.state, @@ -202,11 +262,12 @@ export default class App extends React.Component { }); } }, - t: this.state.t, - dir: this.state.dir + t: this.state.t, // 翻译函数 + dir: this.state.dir // 文字方向 }}> + {/* 渲染搜索弹窗组件 - 这是主要的UI组件 */} ); } -} +} \ No newline at end of file -- 2.34.1