diff --git a/code/docusaurus/.trae/documents/bookmarks_import_plan.md b/code/docusaurus/.trae/documents/bookmarks_import_plan.md new file mode 100644 index 00000000..200b3178 --- /dev/null +++ b/code/docusaurus/.trae/documents/bookmarks_import_plan.md @@ -0,0 +1,48 @@ +# 书签导入与整理计划 + +## 需求分析 +用户希望将浏览器书签文件 `D:\bookmarks_2026_5_26.html` 中的内容导入并整理到 `docs/resources.mdx` 文件中。 + +## 书签文件结构分析 + +从读取的书签文件中识别出以下主要分组: + +### 第一层分组 +1. **AI 智能助手** - 包含 ChatGPT、腾讯元宝、v0 等 +2. **前端开发** - 包含多个子文件夹: + - 图标库 (expo icons, iconfont, icones, lucide) + - 前端模板库 (uiverse, codepen, htmlrev, lottiefiles, mockuphone) + - TinyMCE 相关 + - Uniapp 相关 + - 前端组件库 (Arco Design) + - React Native 第三方库 + - 插画库 (undraw, storyset, manypixels) + - React Native 开发相关 +3. **Python** - PaddlePaddle 相关 + +## 实现方案 + +### 步骤 +1. 创建一个 Node.js 脚本来解析书签 HTML 文件 +2. 将解析结果转换为资源导航所需的分组格式 +3. 更新 `docs/resources.mdx` 文件 + +### 修改文件 +- `docs/resources.mdx` - 更新为解析后的书签内容 + +## 输出格式 +使用现有的 `SearchableLinkGrid` 组件格式,保持分组结构清晰。 + +## 过滤规则 +1. 跳过重复链接 +2. 清理过长的标题(超过50字符截断) +3. 提取有意义的描述信息 + +## 风险评估 +- 文件较大(50KB+),需要处理大量数据 +- 部分链接可能失效,建议用户后续验证 + +## 测试计划 +1. 解析书签文件 +2. 生成新的 resources.mdx +3. 启动开发服务器验证显示效果 \ No newline at end of file diff --git a/code/docusaurus/.trae/documents/search_filter_plan.md b/code/docusaurus/.trae/documents/search_filter_plan.md new file mode 100644 index 00000000..32f28c17 --- /dev/null +++ b/code/docusaurus/.trae/documents/search_filter_plan.md @@ -0,0 +1,102 @@ +# 资源导航搜索过滤功能实现计划 + +## 需求分析 + +用户希望在 `docs/resources.mdx` 文件中添加一个搜索框,实现本地过滤功能,可以根据关键词过滤显示的资源卡片。 + +## 现有代码结构分析 + +### 当前文件结构 + +* `docs/resources.mdx` - 资源导航页面,使用 LinkGrid 和 LinkCard 组件 + +* `src/components/Link/LinkCard.tsx` - 单个链接卡片组件 + +* `src/components/Link/LinkGrid.tsx` - 卡片网格容器组件 + +### 当前实现方式 + +`LinkGrid` 组件直接渲染所有子元素,没有过滤功能。 + +## 实现方案 + +### 方案选择 + +创建一个新的带搜索功能的组件 `SearchableLinkGrid`,包含: + +1. 搜索输入框 +2. 过滤逻辑(基于 title 和 description) +3. 集成现有 LinkCard 组件 + +### 修改文件列表 + +| 文件 | 操作 | 说明 | +| -------------------------------------------- | -- | ------------ | +| `src/components/Link/SearchableLinkGrid.tsx` | 新建 | 带搜索过滤的网格组件 | +| `docs/resources.mdx` | 修改 | 引入新组件并重构内容结构 | + +## 实现步骤 + +### 步骤 1: 创建 SearchableLinkGrid 组件 + +* 使用 React useState 管理搜索关键词 + +* 使用 useMemo 进行过滤计算 + +* 支持按 title 和 description 进行模糊搜索 + +### 步骤 2: 修改 resources.mdx + +* 引入新组件 + +* 将现有 LinkCard 包装在 SearchableLinkGrid 中 + +## 技术实现细节 + +### 搜索过滤逻辑 + +```typescript +// 过滤逻辑 +const filteredCards = useMemo(() => { + if (!searchTerm) return children + const lowerTerm = searchTerm.toLowerCase() + return // 过滤匹配的卡片 +}, [searchTerm, children]) +``` + +### 组件设计 + +* 搜索框位于网格上方 + +* 实时过滤,无需点击搜索按钮 + +* 支持中英文搜索 + +## 风险评估 + +### 潜在风险 + +1. **性能问题**:如果卡片数量很大,过滤可能有性能问题 + + * 缓解:使用 useMemo 缓存过滤结果 + +2. **样式冲突**:新组件可能与现有样式冲突 + + * 缓解:使用独立的 CSS 模块 + +3. **React 版本兼容性**:需要确保使用 React 19 兼容的写法 + + * 缓解:使用标准 React hooks + +## 依赖检查 + +* 项目已安装 React 19 和相关类型定义 + +* 使用现有样式系统 (CSS Modules) + +## 测试计划 + +1. 启动开发服务器验证功能 +2. 测试搜索功能是否正常工作 +3. 验证样式是否正确显示 + diff --git a/code/docusaurus/docs/resources.mdx b/code/docusaurus/docs/resources.mdx new file mode 100644 index 00000000..c008365c --- /dev/null +++ b/code/docusaurus/docs/resources.mdx @@ -0,0 +1,333 @@ +--- +id: resources +title: 资源导航 +sidebar_position: false +hide_table_of_contents: true +--- + +import SearchableLinkGrid from '@site/src/components/Link/SearchableLinkGrid'; + + \ No newline at end of file diff --git a/code/docusaurus/docusaurus.config.ts b/code/docusaurus/docusaurus.config.ts index 9cc56f74..0ecc2c62 100644 --- a/code/docusaurus/docusaurus.config.ts +++ b/code/docusaurus/docusaurus.config.ts @@ -108,6 +108,11 @@ const config: Config = { position: 'left', sidebarId: 'DatabaseSidebar', }, + { + to: '/docs/resources', + label: '资源导航', + position: 'left', + }, { to: '/blog', label: '博客', @@ -160,6 +165,9 @@ const config: Config = { maxHeadingLevel: 5, }, } satisfies Preset.ThemeConfig, + plugins: [ + 'docusaurus-plugin-sass', + ], } export default config diff --git a/code/docusaurus/package.json b/code/docusaurus/package.json index d720c95b..e8d5b6f2 100644 --- a/code/docusaurus/package.json +++ b/code/docusaurus/package.json @@ -25,9 +25,11 @@ "@docusaurus/theme-common": "^3.9.2", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", + "docusaurus-plugin-sass": "^0.2.6", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "sass": "^1.100.0" }, "devDependencies": { "@antfu/eslint-config": "^6.7.3", diff --git a/code/docusaurus/pnpm-lock.yaml b/code/docusaurus/pnpm-lock.yaml index 22fc19e5..6dc9a63c 100644 --- a/code/docusaurus/pnpm-lock.yaml +++ b/code/docusaurus/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: clsx: specifier: ^2.0.0 version: 2.1.1 + docusaurus-plugin-sass: + specifier: ^0.2.6 + version: 0.2.6(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.6.3))(sass@1.100.0)(webpack@5.104.1) prism-react-renderer: specifier: ^2.3.0 version: 2.4.1(react@19.2.3) @@ -38,6 +41,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.2.3(react@19.2.3) + sass: + specifier: ^1.100.0 + version: 1.100.0 devDependencies: '@antfu/eslint-config': specifier: ^6.7.3 @@ -1556,6 +1562,94 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} + '@parcel/watcher-android-arm64@2.5.6': + resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.6': + resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.6': + resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.6': + resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.6': + resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.6': + resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.6': + resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.6': + resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.6': + resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.6': + resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.6': + resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} + engines: {node: '>= 10.0.0'} + '@pkgr/core@0.2.9': resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -2346,6 +2440,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + chrome-trace-event@1.0.4: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} @@ -2727,6 +2825,10 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} @@ -2750,6 +2852,12 @@ packages: resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} engines: {node: '>=6'} + docusaurus-plugin-sass@0.2.6: + resolution: {integrity: sha512-2hKQQDkrufMong9upKoG/kSHJhuwd+FA3iAe/qzS/BmWpbIpe7XKmq5wlz4J5CJaOPu4x+iDJbgAxZqcoQf0kg==} + peerDependencies: + '@docusaurus/core': ^2.0.0-beta || ^3.0.0-alpha + sass: ^1.30.0 + dom-converter@0.2.0: resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} @@ -3622,6 +3730,9 @@ packages: engines: {node: '>=16.x'} hasBin: true + immutable@5.1.5: + resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -4312,6 +4423,9 @@ packages: no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-emoji@2.2.0: resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} engines: {node: '>=18'} @@ -5082,6 +5196,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + recma-build-jsx@1.0.0: resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} @@ -5241,6 +5359,32 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sass-loader@16.0.8: + resolution: {integrity: sha512-hcov4ZwZJIGbEuyNr9EmiTmZueyrxSToE6GOzoZnq5JM7ecRO7ttyvilPn+VmRsqiP16+VYZzVnGZj/hzZgKBA==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@rspack/core': 0.x || ^1.0.0 || ^2.0.0-0 + node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + sass: ^1.3.0 + sass-embedded: '*' + webpack: ^5.0.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + node-sass: + optional: true + sass: + optional: true + sass-embedded: + optional: true + webpack: + optional: true + + sass@1.100.0: + resolution: {integrity: sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==} + engines: {node: '>=20.19.0'} + hasBin: true + sax@1.4.3: resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} @@ -8344,6 +8488,67 @@ snapshots: '@opentelemetry/api@1.9.0': {} + '@parcel/watcher-android-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-x64@2.5.6': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.6': + optional: true + + '@parcel/watcher-win32-arm64@2.5.6': + optional: true + + '@parcel/watcher-win32-ia32@2.5.6': + optional: true + + '@parcel/watcher-win32-x64@2.5.6': + optional: true + + '@parcel/watcher@2.5.6': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.3 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.6 + '@parcel/watcher-darwin-arm64': 2.5.6 + '@parcel/watcher-darwin-x64': 2.5.6 + '@parcel/watcher-freebsd-x64': 2.5.6 + '@parcel/watcher-linux-arm-glibc': 2.5.6 + '@parcel/watcher-linux-arm-musl': 2.5.6 + '@parcel/watcher-linux-arm64-glibc': 2.5.6 + '@parcel/watcher-linux-arm64-musl': 2.5.6 + '@parcel/watcher-linux-x64-glibc': 2.5.6 + '@parcel/watcher-linux-x64-musl': 2.5.6 + '@parcel/watcher-win32-arm64': 2.5.6 + '@parcel/watcher-win32-ia32': 2.5.6 + '@parcel/watcher-win32-x64': 2.5.6 + optional: true + '@pkgr/core@0.2.9': {} '@pnpm/config.env-replace@1.1.0': {} @@ -9301,6 +9506,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + chrome-trace-event@1.0.4: {} ci-info@3.9.0: {} @@ -9652,6 +9861,9 @@ snapshots: destroy@1.2.0: {} + detect-libc@2.1.2: + optional: true + detect-node@2.1.0: {} detect-port@1.6.1: @@ -9675,6 +9887,17 @@ snapshots: dependencies: '@leichtgewicht/ip-codec': 2.0.5 + docusaurus-plugin-sass@0.2.6(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.6.3))(sass@1.100.0)(webpack@5.104.1): + dependencies: + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.6.3) + sass: 1.100.0 + sass-loader: 16.0.8(sass@1.100.0)(webpack@5.104.1) + transitivePeerDependencies: + - '@rspack/core' + - node-sass + - sass-embedded + - webpack + dom-converter@0.2.0: dependencies: utila: 0.4.0 @@ -10804,6 +11027,8 @@ snapshots: image-size@2.0.2: {} + immutable@5.1.5: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -11703,6 +11928,9 @@ snapshots: lower-case: 2.0.2 tslib: 2.8.1 + node-addon-api@7.1.1: + optional: true + node-emoji@2.2.0: dependencies: '@sindresorhus/is': 4.6.0 @@ -12532,6 +12760,8 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@5.0.0: {} + recma-build-jsx@1.0.0: dependencies: '@types/estree': 1.0.8 @@ -12743,6 +12973,21 @@ snapshots: safer-buffer@2.1.2: {} + sass-loader@16.0.8(sass@1.100.0)(webpack@5.104.1): + dependencies: + neo-async: 2.6.2 + optionalDependencies: + sass: 1.100.0 + webpack: 5.104.1 + + sass@1.100.0: + dependencies: + chokidar: 5.0.0 + immutable: 5.1.5 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.6 + sax@1.4.3: {} scheduler@0.27.0: {} diff --git a/code/docusaurus/src/components/Link/LinkCard.tsx b/code/docusaurus/src/components/Link/LinkCard.tsx new file mode 100644 index 00000000..f372e7bf --- /dev/null +++ b/code/docusaurus/src/components/Link/LinkCard.tsx @@ -0,0 +1,61 @@ +import React from 'react' +import styles from './styles.module.css' + +export interface LinkCardProps { + title: string + description: string + href: string + icon?: string +} + +export default function LinkCard({ + title, + description, + href, + icon, +}: LinkCardProps) { + let favicon = icon + + if (!favicon) { + try { + const hostname = new URL(href).hostname + favicon = `https://www.google.com/s2/favicons?domain=${hostname}&sz=128` + } + catch { + favicon = undefined + } + } + + return ( + +
+ {favicon + ? ( + {title} + ) + : ( +
+ {title.charAt(0)} +
+ )} + + + {title} + +
+ +
+ {description} +
+
+ ) +} diff --git a/code/docusaurus/src/components/Link/LinkGrid.tsx b/code/docusaurus/src/components/Link/LinkGrid.tsx new file mode 100644 index 00000000..561b40e6 --- /dev/null +++ b/code/docusaurus/src/components/Link/LinkGrid.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import styles from './styles.module.css' + +interface Props { + children: React.ReactNode +} + +export default function LinkGrid({ children }: Props) { + return ( +
+ {children} +
+ ) +} diff --git a/code/docusaurus/src/components/Link/SearchableLinkGrid.tsx b/code/docusaurus/src/components/Link/SearchableLinkGrid.tsx new file mode 100644 index 00000000..e78578ac --- /dev/null +++ b/code/docusaurus/src/components/Link/SearchableLinkGrid.tsx @@ -0,0 +1,98 @@ +import React, { useState, useMemo } from 'react' +import LinkCard, { LinkCardProps } from './LinkCard' +import styles from './styles.module.css' + +export interface GroupedLinkCardProps { + groupName: string + items: LinkCardProps[] +} + +interface SearchableLinkGridProps { + groups: GroupedLinkCardProps[] +} + +export default function SearchableLinkGrid({ groups }: SearchableLinkGridProps) { + const [searchTerm, setSearchTerm] = useState('') + + const filteredGroups = useMemo(() => { + if (!searchTerm.trim()) return groups + + const lowerTerm = searchTerm.toLowerCase() + return groups.map(group => ({ + ...group, + items: group.items.filter( + item => + item.title.toLowerCase().includes(lowerTerm) || + item.description.toLowerCase().includes(lowerTerm), + ), + })).filter(group => group.items.length > 0) + }, [searchTerm, groups]) + + const totalItems = groups.reduce((sum, group) => sum + group.items.length, 0) + const filteredTotalItems = filteredGroups.reduce((sum, group) => sum + group.items.length, 0) + + return ( +
+
+
+ + + + + setSearchTerm(e.target.value)} + className={styles.searchInput} + /> +
+ + {filteredTotalItems} / {totalItems} 项 + +
+ + {filteredGroups.length > 0 ? ( + filteredGroups.map((group, groupIndex) => ( +
+

{group.groupName}

+
+ {group.items.map((item, itemIndex) => ( + + ))} +
+
+ )) + ) : ( +
+ + + + + +

未找到匹配的资源

+
+ )} +
+ ) +} \ No newline at end of file diff --git a/code/docusaurus/src/components/Link/styles.module.css b/code/docusaurus/src/components/Link/styles.module.css new file mode 100644 index 00000000..790f618e --- /dev/null +++ b/code/docusaurus/src/components/Link/styles.module.css @@ -0,0 +1,231 @@ +.grid { + display: grid; + gap: 16px; + margin-top: 16px; + + /* 默认桌面4列 */ + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +/* 平板 */ +@media (max-width: 1200px) { + .grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +/* 小平板 */ +@media (max-width: 996px) { + .grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +/* 手机 */ +@media (max-width: 576px) { + .grid { + grid-template-columns: 1fr; + } +} + +/* 卡片 */ + +.card { + display: flex; + flex-direction: column; + + padding: 18px; + + border-radius: 14px; + border: 1px solid var(--ifm-color-emphasis-200); + + background: var(--ifm-card-background-color); + + color: inherit; + text-decoration: none; + + transition: + transform 0.2s ease, + border-color 0.2s ease, + box-shadow 0.2s ease; +} + +.card:hover { + color: inherit; + text-decoration: none; + + transform: translateY(-3px); + + border-color: var(--ifm-color-primary); + + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08); +} + +/* 标题区域 */ + +.header { + display: flex; + align-items: center; + gap: 12px; +} + +/* 图标 */ + +.icon { + width: 32px; + height: 32px; + + object-fit: contain; + + flex-shrink: 0; +} + +.placeholder { + width: 40px; + height: 40px; + + border-radius: 10px; + + display: flex; + align-items: center; + justify-content: center; + + background: var(--ifm-color-primary); + color: white; + + font-size: 18px; + font-weight: 700; + + flex-shrink: 0; +} + +/* 标题 */ + +.title { + font-size: 17px; + font-weight: 600; + line-height: 1.4; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* 描述 */ + +.description { + margin-top: 14px; + + font-size: 14px; + line-height: 1.6; + + color: var(--ifm-color-emphasis-700); + + overflow: hidden; + + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +/* 搜索容器 */ + +.searchContainer { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 24px; + width: 100%; +} + +.searchWrapper { + flex: 1; + display: flex; + align-items: center; + background: var(--ifm-card-background-color); + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 12px; + padding: 2px 2px 2px 16px; + transition: + border-color 0.2s ease, + box-shadow 0.2s ease; +} + +.searchWrapper:focus-within { + border-color: var(--ifm-color-primary); + box-shadow: 0 0 0 3px var(--ifm-color-primary-alpha-10); +} + +.searchIcon { + color: var(--ifm-color-emphasis-400); + margin-right: 12px; + flex-shrink: 0; +} + +.searchInput { + flex: 1; + width: 100%; + padding: 12px 16px 12px 0; + border: none; + border-radius: 0 10px 10px 0; + font-size: 15px; + background: transparent; + color: var(--ifm-font-color-base); +} + +.searchInput:focus { + outline: none; +} + +.searchInput::placeholder { + color: var(--ifm-color-emphasis-500); +} + +.searchCount { + font-size: 13px; + color: var(--ifm-color-emphasis-500); + white-space: nowrap; + padding: 6px 12px; + background: var(--ifm-color-emphasis-100); + border-radius: 20px; +} + +/* 分组样式 */ + +.group { + margin-bottom: 32px; +} + +.group:last-child { + margin-bottom: 0; +} + +.groupTitle { + font-size: 18px; + font-weight: 700; + color: var(--ifm-color-emphasis-800); + margin-bottom: 20px; + padding-left: 14px; + border-left: 4px solid var(--ifm-color-primary); +} + +/* 无结果状态 */ + +.noResults { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + color: var(--ifm-color-emphasis-600); +} + +.noResultsIcon { + color: var(--ifm-color-emphasis-400); + margin-bottom: 16px; +} + +.noResults p { + margin: 0; + font-size: 15px; +}