From eacb7016b6d6b38f826ef695f03ccd235cd489e0 Mon Sep 17 00:00:00 2001 From: tangly1024 Date: Fri, 15 May 2026 21:35:25 +0800 Subject: [PATCH 01/11] fix(cache): use tmpdir when .next/cache/notion is not writable (Netlify) Netlify Functions use a read-only /var/task; mkdir on .next/cache/notion throws ENOENT/EROFS. Fall back to os.tmpdir()/notionnext-notion-cache. Optional override: NOTION_NEXT_NOTION_CACHE_DIR. Co-authored-by: Cursor --- lib/cache/build_session.js | 62 ++++++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/lib/cache/build_session.js b/lib/cache/build_session.js index 7d0593e1b1d..838c4b7cad0 100644 --- a/lib/cache/build_session.js +++ b/lib/cache/build_session.js @@ -1,21 +1,66 @@ import fs from 'fs' +import os from 'os' import path from 'path' -const NOTION_CACHE_ROOT = path.join(process.cwd(), '.next', 'cache', 'notion') -const BUILD_SESSION_FILE = path.join(NOTION_CACHE_ROOT, 'build-session.json') +/** 惰性解析:构建/本地可写 `.next/cache/notion`;Serverless(如 Netlify)只读盘则落到 `os.tmpdir()` */ +let cachedNotionCacheRoot = null + +function resolveNotionCacheRoot() { + if (cachedNotionCacheRoot) { + return cachedNotionCacheRoot + } + + const fromEnv = process.env.NOTION_NEXT_NOTION_CACHE_DIR + if (fromEnv) { + const dir = path.resolve(fromEnv) + fs.mkdirSync(dir, { recursive: true }) + cachedNotionCacheRoot = dir + return cachedNotionCacheRoot + } + + const primary = path.join(process.cwd(), '.next', 'cache', 'notion') + try { + fs.mkdirSync(primary, { recursive: true }) + cachedNotionCacheRoot = primary + return cachedNotionCacheRoot + } catch (err) { + const code = err && err.code + if ( + code === 'ENOENT' || + code === 'EROFS' || + code === 'EACCES' || + code === 'EPERM' + ) { + const fallback = path.join(os.tmpdir(), 'notionnext-notion-cache') + fs.mkdirSync(fallback, { recursive: true }) + cachedNotionCacheRoot = fallback + if (process.env.NODE_ENV !== 'test') { + console.warn( + '[NotionNext] Notion file cache root (read-only deploy, using tmpdir):', + fallback + ) + } + return cachedNotionCacheRoot + } + throw err + } +} function sanitizeSessionId(sessionId) { return String(sessionId || 'default').replace(/[^a-z0-9_-]/gi, '_') } export function getNotionCacheRoot() { - fs.mkdirSync(NOTION_CACHE_ROOT, { recursive: true }) - return NOTION_CACHE_ROOT + return resolveNotionCacheRoot() } export function getBuildSessionId() { + const buildSessionFile = path.join( + getNotionCacheRoot(), + 'build-session.json' + ) try { - const raw = fs.readFileSync(BUILD_SESSION_FILE, 'utf8') + const raw = fs.readFileSync(buildSessionFile, 'utf8') const parsed = JSON.parse(raw) if (parsed?.sessionId) { return sanitizeSessionId(parsed.sessionId) @@ -26,5 +71,10 @@ export function getBuildSessionId() { } export function getBuildSessionPath(...parts) { - return path.join(getNotionCacheRoot(), 'sessions', getBuildSessionId(), ...parts) + return path.join( + getNotionCacheRoot(), + 'sessions', + getBuildSessionId(), + ...parts + ) } From 3effec4f08aa5c9dd3cc91a3caa19829eea44041 Mon Sep 17 00:00:00 2001 From: tangly1024 Date: Fri, 15 May 2026 21:41:37 +0800 Subject: [PATCH 02/11] docs(deploy): EdgeOne Node compatibility checklist (nvmrc vs platform) Co-authored-by: Cursor --- DEPLOYMENT.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 3320744735e..889fd42ff6b 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -205,6 +205,18 @@ EdgeOne 构建阶段会按仓库中的 `.nvmrc` 切换 Node 版本。若控制 2. **自建仓库**:在 EdgeOne 控制台选择与 `.nvmrc` **完全一致**的 Node 版本;仍失败时检查构建日志是否仍在读取旧的 `.nvmrc`(需推送后再构建)。 3. `package.json` 中 `engines.node` 为 `>=20 <25`,在 Node 20 系列内均可构建;关键是 **构建环境实际安装的版本**能解析 `.nvmrc`。 +**兼容尝试(按顺序,任选其一即可,不必全做):** + +| 尝试 | 做法 | +|------|------| +| A. 对齐补丁号 | 打开 EdgeOne **项目设置 → Node.js 版本**,选择与仓库根目录 **`.nvmrc` 完全一致**的一项(例如均为 `20.18.0`),保存后 **重新部署**。 | +| B. 改自建 fork | 若平台下拉列表**没有**当前 `.nvmrc` 里的补丁号:把 fork 里的 `.nvmrc` 改成平台**已有**的某一档(仍为 Node 20 即可),推送后再构建。 | +| C. 清缓存 / 换分支 | 确认构建日志里 **Switching node** 读到的版本已更新;关闭「使用构建缓存」或触发一次无缓存构建,排除旧 `.nvmrc` 缓存。 | +| D. `engines` 报错时 | 若日志是 **Yarn does not satisfy engine** 而非 **Switching node**:在构建环境设 `YARN_IGNORE_ENGINES=1`(仅当确为 engines 校验问题时使用)。 | +| E. 不要用「只写 major」** | 少数平台不支持 `.nvmrc` 仅写 `20`;若遇解析错误,改为 **`20.18.0`** 这类完整补丁号。 | + +说明:Next.js 14 与当前依赖不要求锁在某一补丁版本;**兼容的核心是「平台实际能装上的 Node」与「`.nvmrc` / 控制台选择」一致**。 + 构建命令与静态导出等与其它平台相同,按需配置环境变量(至少 `NOTION_PAGE_ID`)。 ## Docker 部署 From 6b4b2495a43629f17e070c86ada1ec6092d799b5 Mon Sep 17 00:00:00 2001 From: tangly1024 Date: Fri, 15 May 2026 21:42:54 +0800 Subject: [PATCH 03/11] docs(deploy): EdgeOne ENOSPC during yarn install troubleshooting Co-authored-by: Cursor --- DEPLOYMENT.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 889fd42ff6b..6904d5b3fa9 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -219,6 +219,21 @@ EdgeOne 构建阶段会按仓库中的 `.nvmrc` 切换 Node 版本。若控制 构建命令与静态导出等与其它平台相同,按需配置环境变量(至少 `NOTION_PAGE_ID`)。 +### `yarn install` 报 `ENOSPC: no space left on device` + +与 **Node 版本** 无关(日志里已出现 `Now, we're on node version v20.18.0` 即表示切换成功)。错误 **`ENOSPC`** 表示构建机 **磁盘或 `/dev/shm`(内存盘)空间不足**,在安装依赖从 cache 拷贝到 `node_modules` 时写满。 + +**可尝试:** + +1. **向 EdgeOne / 腾讯云工单反馈**:说明构建任务在 `yarn install` 阶段 `ENOSPC`,申请更大构建盘或确认是否为平台侧临时配额。 +2. **减少单次写入体积**(若控制台支持自定义安装命令): + - 使用 `yarn install --frozen-lockfile --prefer-offline` 且**开启依赖缓存**(命中缓存时少下载);或 + - 在构建前增加 `yarn cache clean`(会多下载,仅当怀疑缓存损坏时尝试,**不一定**缓解 ENOSPC)。 +3. **自定义 Yarn 缓存目录**(若平台文档支持挂载更大分区):设置环境变量 **`YARN_CACHE_FOLDER`** 到有足够剩余空间的路径(具体以 EdgeOne 构建环境说明为准)。 +4. **换构建方案**:在磁盘更大的 CI(如 GitHub Actions)完成 `yarn build` / `next build`,将产物同步到 EdgeOne(仅静态托管),绕过 Pages 内置构建机容量限制。 + +仓库本身无法通过改 `package.json` 消除平台磁盘上限;根本解决依赖 **构建环境配额** 或 **外置构建**。 + ## Docker 部署 ### Dockerfile From 56360fc6f7a0119365377f734bd549a913394956 Mon Sep 17 00:00:00 2001 From: tangly1024 Date: Fri, 15 May 2026 21:47:13 +0800 Subject: [PATCH 04/11] docs(deploy): EdgeOne ENOSPC optional cache-off and ENABLE_CACHE note Co-authored-by: Cursor --- DEPLOYMENT.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 6904d5b3fa9..3e98939fc38 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -232,6 +232,14 @@ EdgeOne 构建阶段会按仓库中的 `.nvmrc` 切换 Node 版本。若控制 3. **自定义 Yarn 缓存目录**(若平台文档支持挂载更大分区):设置环境变量 **`YARN_CACHE_FOLDER`** 到有足够剩余空间的路径(具体以 EdgeOne 构建环境说明为准)。 4. **换构建方案**:在磁盘更大的 CI(如 GitHub Actions)完成 `yarn build` / `next build`,将产物同步到 EdgeOne(仅静态托管),绕过 Pages 内置构建机容量限制。 +**备选:怀疑「构建/文件缓存」把空间占满时** + +| 做法 | 适用阶段 | 说明 | +|------|----------|------| +| **关闭 EdgeOne「依赖/构建缓存」再构建** | `yarn install` 前后 | 若平台在恢复缓存后又解压一份 `node_modules`,可能出现「缓存 + 工作区」双份占用 **`/dev/shm`**。在控制台**关闭缓存恢复**后重试(构建会变慢,但有时能腾出空间;视平台实现而定)。 | +| **`YARN_CACHE_FOLDER` 指到大磁盘路径** | `yarn install` | 避免 Yarn 默认缓存与仓库同落在小容量分区;路径以 **EdgeOne 官方文档** 为准。 | +| **构建环境变量 `ENABLE_CACHE=false`** | **`next build` 阶段** | 关闭 Notion 数据**读**盘缓存为主,会**增加 Notion 请求与耗时**;**锁文件/会话目录仍可能写入**。对日志里 **`yarn install` 拷贝 `node_modules` 即失败**的 ENOSPC **通常无效**,因尚未执行到 Next 构建。 | + 仓库本身无法通过改 `package.json` 消除平台磁盘上限;根本解决依赖 **构建环境配额** 或 **外置构建**。 ## Docker 部署 From 6e89404ef3a083ae2c5d4c3081891009108d7a8d Mon Sep 17 00:00:00 2001 From: tangly1024 Date: Fri, 15 May 2026 21:51:23 +0800 Subject: [PATCH 05/11] feat(theme): add ThoughtLite scaffold and migration plan (#3987) Co-authored-by: Cursor --- docs/themes/README.en.md | 1 + docs/themes/README.md | 1 + docs/themes/THOUGHTLITE.md | 27 ++ .../THOUGHTLITE_MIGRATION_PLAN.zh-CN.md | 89 +++++ themes/thoughtlite/components/Announcement.js | 32 ++ themes/thoughtlite/components/BlogItem.js | 83 ++++ .../thoughtlite/components/BlogListArchive.js | 36 ++ themes/thoughtlite/components/BlogListPage.js | 62 +++ .../thoughtlite/components/BlogListScroll.js | 75 ++++ themes/thoughtlite/components/Catalog.js | 94 +++++ themes/thoughtlite/components/Footer.js | 27 ++ themes/thoughtlite/components/Header.js | 28 ++ themes/thoughtlite/components/MenuItemDrop.js | 57 +++ themes/thoughtlite/components/MenuList.js | 73 ++++ themes/thoughtlite/components/PostLock.js | 63 +++ themes/thoughtlite/components/PostMeta.js | 51 +++ .../components/RecentCommentListForExample.js | 62 +++ themes/thoughtlite/components/SearchInput.js | 91 +++++ themes/thoughtlite/components/SideBar.js | 126 ++++++ themes/thoughtlite/components/TitleBar.js | 48 +++ themes/thoughtlite/config.js | 19 + themes/thoughtlite/index.js | 363 ++++++++++++++++++ themes/thoughtlite/style.js | 17 + 23 files changed, 1525 insertions(+) create mode 100644 docs/themes/THOUGHTLITE.md create mode 100644 docs/themes/THOUGHTLITE_MIGRATION_PLAN.zh-CN.md create mode 100644 themes/thoughtlite/components/Announcement.js create mode 100644 themes/thoughtlite/components/BlogItem.js create mode 100644 themes/thoughtlite/components/BlogListArchive.js create mode 100644 themes/thoughtlite/components/BlogListPage.js create mode 100644 themes/thoughtlite/components/BlogListScroll.js create mode 100644 themes/thoughtlite/components/Catalog.js create mode 100644 themes/thoughtlite/components/Footer.js create mode 100644 themes/thoughtlite/components/Header.js create mode 100644 themes/thoughtlite/components/MenuItemDrop.js create mode 100644 themes/thoughtlite/components/MenuList.js create mode 100644 themes/thoughtlite/components/PostLock.js create mode 100644 themes/thoughtlite/components/PostMeta.js create mode 100644 themes/thoughtlite/components/RecentCommentListForExample.js create mode 100644 themes/thoughtlite/components/SearchInput.js create mode 100644 themes/thoughtlite/components/SideBar.js create mode 100644 themes/thoughtlite/components/TitleBar.js create mode 100644 themes/thoughtlite/config.js create mode 100644 themes/thoughtlite/index.js create mode 100644 themes/thoughtlite/style.js diff --git a/docs/themes/README.en.md b/docs/themes/README.en.md index fd6ccd040f0..1cab2403ed8 100644 --- a/docs/themes/README.en.md +++ b/docs/themes/README.en.md @@ -11,6 +11,7 @@ This directory centralizes documentation for all themes, including design intent | Claude | [CLAUDE.en.md](./CLAUDE.en.md) | [CLAUDE.md](./CLAUDE.md) | | Endspace | [ENDSPACE.en.md](./ENDSPACE.en.md) | [ENDSPACE.md](./ENDSPACE.md) | | Fuwari | [FUWARI.md](./FUWARI.md) | [FUWARI.md](./FUWARI.md) | +| ThoughtLite (WIP) | [THOUGHTLITE.md](./THOUGHTLITE.md) | [THOUGHTLITE.md](./THOUGHTLITE.md) (plan: [THOUGHTLITE_MIGRATION_PLAN.zh-CN.md](./THOUGHTLITE_MIGRATION_PLAN.zh-CN.md)) | ## Maintenance Rules diff --git a/docs/themes/README.md b/docs/themes/README.md index e8cb18d9dd5..d4f68c736c2 100644 --- a/docs/themes/README.md +++ b/docs/themes/README.md @@ -11,6 +11,7 @@ | Claude | [CLAUDE.md](./CLAUDE.md) | [CLAUDE.en.md](./CLAUDE.en.md) | | Endspace | [ENDSPACE.md](./ENDSPACE.md) | [ENDSPACE.en.md](./ENDSPACE.en.md) | | Fuwari | [FUWARI.md](./FUWARI.md) | [FUWARI.md](./FUWARI.md) | +| ThoughtLite(移植中) | [THOUGHTLITE.md](./THOUGHTLITE.md) | 见 [THOUGHTLITE.md](./THOUGHTLITE.md)(计划:[THOUGHTLITE_MIGRATION_PLAN.zh-CN.md](./THOUGHTLITE_MIGRATION_PLAN.zh-CN.md)) | ## 维护约定 diff --git a/docs/themes/THOUGHTLITE.md b/docs/themes/THOUGHTLITE.md new file mode 100644 index 00000000000..5dea71bd1cd --- /dev/null +++ b/docs/themes/THOUGHTLITE.md @@ -0,0 +1,27 @@ +# ThoughtLite(NotionNext 移植中) + +中文 | 任务计划:[THOUGHTLITE_MIGRATION_PLAN.zh-CN.md](./THOUGHTLITE_MIGRATION_PLAN.zh-CN.md) + +## 上游参考 + +- 源码:[tuyuritio/astro-theme-thought-lite](https://github.com/tuyuritio/astro-theme-thought-lite)(Astro + Svelte,**GPL-3.0**) +- 预览:[thought-lite.ttio.workers.dev](https://thought-lite.ttio.workers.dev/) +- 需求跟踪:[notionnext-org/NotionNext#3987](https://github.com/notionnext-org/NotionNext/issues/3987) + +## 当前状态 + +- 工作分支:**`feat/theme-thoughtlite`** +- 主题目录:**`themes/thoughtlite/`** — 基于 `themes/example` 的**可运行骨架**(配置前缀 `THOUGHTLITE_*`),用于先跑通 NotionNext 数据流;**视觉尚未对齐**上游 ThoughtLite。 + +## 本地试用 + +```bash +# .env.local 或环境变量 +NEXT_PUBLIC_THEME=thoughtlite +``` + +或使用 `?theme=thoughtlite`(需主题已在构建扫描列表中)。 + +## 许可证说明 + +合并正式版前请阅读迁移计划 **§0**(GPL 与 MIT 兼容性),并在 PR 中写明结论。 diff --git a/docs/themes/THOUGHTLITE_MIGRATION_PLAN.zh-CN.md b/docs/themes/THOUGHTLITE_MIGRATION_PLAN.zh-CN.md new file mode 100644 index 00000000000..9eb8ec3c587 --- /dev/null +++ b/docs/themes/THOUGHTLITE_MIGRATION_PLAN.zh-CN.md @@ -0,0 +1,89 @@ +# ThoughtLite 主题迁移任务计划(NotionNext) + +关联需求:[Issue #3987 · 新主题 ThoughtLite](https://github.com/notionnext-org/NotionNext/issues/3987) + +| 项目 | 链接 | +|------|------| +| 上游源码(Astro + Svelte + Tailwind) | [tuyuritio/astro-theme-thought-lite](https://github.com/tuyuritio/astro-theme-thought-lite) | +| 在线预览 | [thought-lite.ttio.workers.dev](https://thought-lite.ttio.workers.dev/) | +| 本仓库工作分支 | `feat/theme-thoughtlite` | +| 当前骨架目录 | `themes/thoughtlite/`(由 `themes/example` 复制并重命名配置前缀,**可运行、非最终视觉**) | + +--- + +## 0. 许可证与合规(必须先确认) + +上游仓库为 **GPL-3.0**。NotionNext 主仓库为 **MIT**。 + +- **直接复制**上游 `.astro` / `.svelte` 等源码进本仓库,可能触发 **GPL 与 MIT 的兼容性**问题,需在合并前由维护者确认策略(例如:主题单独以 GPL 子目录说明、或仅「参考设计」用 React 重写而不复制 GPL 文本等)。 +- 请在 **PR 描述与 `docs/themes/THOUGHTLITE.md`** 中写明许可证结论后再合入 `main`。 + +--- + +## 1. 与 Fuwari(Astro → Next)迁移的相同注意点 + +对照 [`docs/themes/FUWARI.md`](./FUWARI.md) 与 [主题迁移指南(中文)](../THEME_MIGRATION_GUIDE.zh-CN.md): + +1. **技术栈**:上游为 **Astro + Svelte**,NotionNext 主题为 **Next.js Pages Router + React**;不能「直接挂载」Astro 工程,只能 **移植视觉与信息架构**。 +2. **数据契约**:仅使用 NotionNext 传入的 `props`(`siteInfo`、`posts`、`post`、`customNav`、`customMenu`、`notice`、`tagOptions` 等),见迁移指南 §3。 +3. **必接能力**:菜单(含 `CUSTOM_MENU`)、公告 `NotionPage`、深浅色 `useGlobal`、文章 TOC/评论/分享/版权/相邻篇、侧栏挂件与 `rightAreaSlot`、联系邮箱 `handleEmailClick` / `resolveContactEmail` 约定(迁移指南 §7)。 +4. **配置**:使用 `siteConfig('THOUGHTLITE_*', default, CONFIG)`,集中在 `themes/thoughtlite/config.js`,避免魔法常量。 +5. **目录隔离**:不引用其他主题目录组件;通用能力用 `@/components/*`。 + +--- + +## 2. 分阶段任务(建议顺序) + +### Phase A — 骨架与联调(当前分支目标) + +- [x] 新建分支 `feat/theme-thoughtlite`。 +- [x] 建立 `themes/thoughtlite`(example 骨架 + `THOUGHTLITE_*` 配置前缀 + `id='theme-thoughtlite'`)。 +- [ ] 本地 `NEXT_PUBLIC_THEME=thoughtlite`(或 URL `?theme=thoughtlite`)跑通:首页、文章、归档、分类、标签、搜索、404。 +- [ ] `yarn lint` / `yarn type-check` 针对本主题目录无新增错误。 + +### Phase B — 版式对齐 ThoughtLite + +- [ ] 阅读上游 `src/layouts`、`src/pages` 与首页「时间线 / Latest」结构,画出与 NotionNext 路由的映射表(`/`、`/[prefix]/[slug]` 等)。 +- [ ] 替换顶栏导航:风格贴近上游(Home / Note / Jotting / About 等需映射到 Notion 的 Page/Post 或 `customNav`)。 +- [ ] 首页:实现上游式 **列表时间线**(可用 `posts` + `publishDate` / `lastEditedDate` 排序),再接 `Pagination` / 滚动列表模式。 +- [ ] `style.js`:用 CSS 变量固化 ThoughtLite 色板与圆角(参考上游 Tailwind 主题 token)。 + +### Phase C — 文章与侧栏 + +- [ ] 文章页:正文 `NotionPage`、封面、元信息密度对齐上游文章页。 +- [ ] 侧栏/页脚:按需保留或简化;接入统计、广告、插件位(与 Fuwari 侧栏模块对齐思路)。 +- [ ] 可选:上游 **Swup** 类全页过渡在 Next 中成本高,可用 `framer-motion` 或弱化动效替代。 + +### Phase D — 上架主仓库清单 + +- [ ] `docs/themes/THOUGHTLITE.md`(及英文若有)写清功能、配置项、上游致谢与许可证。 +- [ ] `docs/themes/README.md` 导航表更新。 +- [ ] 按 [主题迁移指南 §8](../THEME_MIGRATION_GUIDE.zh-CN.md) 提交 `public/images/themes-preview/thoughtlite.png` / `.webp` 与 `conf/themeSwitch.manifest.js` 条目。 +- [ ] 发起 PR,关联 **关闭 #3987**。 + +--- + +## 3. 风险与依赖 + +| 风险 | 缓解 | +|------|------| +| GPL / MIT | 见 §0,PR 前书面结论。 | +| 上游用 **pnpm + Svelte**,与本仓库 **yarn + React** 不一致 | 仅借鉴 UI,不混用包管理器。 | +| i18n | 与全局 `next.config` `locales` 一致;上游多语言文案迁到 `lib/lang` 或主题内字典。 | +| 构建体积 | 勿引入过大依赖;图标优先已有 Iconify / Heroicons 体系。 | + +--- + +## 4. 验证命令(开发自检) + +```bash +yarn dev +# 浏览器访问: http://localhost:3000/?theme=thoughtlite + +yarn build +yarn type-check +``` + +--- + +维护者可按 Phase 勾选推进;每阶段结束建议 squash 或清晰 commit message 便于审阅。 diff --git a/themes/thoughtlite/components/Announcement.js b/themes/thoughtlite/components/Announcement.js new file mode 100644 index 00000000000..9ffba0e9c0f --- /dev/null +++ b/themes/thoughtlite/components/Announcement.js @@ -0,0 +1,32 @@ +import { useGlobal } from '@/lib/global' +import dynamic from 'next/dynamic' + +const NotionPage = dynamic(() => import('@/components/NotionPage')) + +/** + * 公告模块 + * 其实就是一篇文章 + * @param {*} param0 + * @returns + */ +const Announcement = ({ post, className }) => { + const { locale } = useGlobal() + if (!post || Object.keys(post).length === 0) { + return <> + } + return ( + + ) +} +export default Announcement diff --git a/themes/thoughtlite/components/BlogItem.js b/themes/thoughtlite/components/BlogItem.js new file mode 100644 index 00000000000..33bffc6ec8a --- /dev/null +++ b/themes/thoughtlite/components/BlogItem.js @@ -0,0 +1,83 @@ +import LazyImage from '@/components/LazyImage' +import NotionIcon from '@/components/NotionIcon' +import TwikooCommentCount from '@/components/TwikooCommentCount' +import { siteConfig } from '@/lib/config' +import SmartLink from '@/components/SmartLink' +import CONFIG from '../config' + +/** + * 博客列表的单个卡片 + * @param {*} param0 + * @returns + */ +const BlogItem = ({ post }) => { + const showPageCover = + siteConfig('THOUGHTLITE_POST_LIST_COVER', null, CONFIG) && + post?.pageCoverThumbnail + + return ( +
+
+

+ + {siteConfig('POST_TITLE_ICON') && ( + + )} + {post?.title} + +

+ +
+ by{' '} + + {siteConfig('AUTHOR')} + {' '} + on {post.date?.start_date || post.createdTime} + + {post.category && ( + <> + | + + {post.category} + + + )} + {/* | */} + {/* 2 Comments */} +
+ + {!post.results && ( +

+ {post.summary} +

+ )} + {/* 搜索结果 */} + {post.results && ( +

+ {post.results.map((r, index) => ( + {r} + ))} +

+ )} +
+ {/* 图片封面 */} + {showPageCover && ( +
+ + + +
+ )} +
+ ) +} + +export default BlogItem diff --git a/themes/thoughtlite/components/BlogListArchive.js b/themes/thoughtlite/components/BlogListArchive.js new file mode 100644 index 00000000000..f38434b6bce --- /dev/null +++ b/themes/thoughtlite/components/BlogListArchive.js @@ -0,0 +1,36 @@ +import SmartLink from '@/components/SmartLink' + +/** + * 博客归档列表;仅归档页面使用 + * 按照日期将文章分组 + * @param {*} param0 + * @returns + */ +export default function BlogListArchive({ archiveTitle, archivePosts }) { + return ( +
+
+ {archiveTitle} +
+ +
    + {archivePosts[archiveTitle].map(post => { + return ( +
  • +
    + {post?.publishDay}   + + {post.title} + +
    +
  • + ) + })} +
+
+ ) +} diff --git a/themes/thoughtlite/components/BlogListPage.js b/themes/thoughtlite/components/BlogListPage.js new file mode 100644 index 00000000000..45d56fbd6fd --- /dev/null +++ b/themes/thoughtlite/components/BlogListPage.js @@ -0,0 +1,62 @@ +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import SmartLink from '@/components/SmartLink' +import { useRouter } from 'next/router' +import CONFIG from '../config' +import BlogItem from './BlogItem' +/** + * 使用分页插件的博客列表 + * @param {*} props + * @returns + */ +export const BlogListPage = props => { + const { page = 1, posts, postCount } = props + const { locale, NOTION_CONFIG } = useGlobal() + const router = useRouter() + const totalPage = Math.ceil( + postCount / siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG) + ) + const currentPage = +page + + const showPrev = currentPage > 1 + const showNext = page < totalPage + const pagePrefix = router.asPath + .split('?')[0] + .replace(/\/page\/[1-9]\d*/, '') + .replace(/\/$/, '') + .replace('.html', '') + + const showPageCover = siteConfig('THOUGHTLITE_POST_LIST_COVER', null, CONFIG) + + return ( +
+
+ {posts?.map(post => ( + + ))} +
+ +
+ + {locale.PAGINATION.PREV} + + + {locale.PAGINATION.NEXT} + +
+
+ ) +} diff --git a/themes/thoughtlite/components/BlogListScroll.js b/themes/thoughtlite/components/BlogListScroll.js new file mode 100644 index 00000000000..db6eb8e4a5c --- /dev/null +++ b/themes/thoughtlite/components/BlogListScroll.js @@ -0,0 +1,75 @@ +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import throttle from 'lodash.throttle' +import { useCallback, useEffect, useRef, useState } from 'react' +import CONFIG from '../config' +import BlogItem from './BlogItem' +/** + * 使用滚动无限加载的博客列表 + * @param {*} props + * @returns + */ +export const BlogListScroll = props => { + const { posts } = props + const { locale, NOTION_CONFIG } = useGlobal() + const [page, updatePage] = useState(1) + const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG) + + let hasMore = false + const postsToShow = posts + ? Object.assign(posts).slice(0, POSTS_PER_PAGE * page) + : [] + + if (posts) { + const totalCount = posts.length + hasMore = page * POSTS_PER_PAGE < totalCount + } + const handleGetMore = () => { + if (!hasMore) return + updatePage(page + 1) + } + + const targetRef = useRef(null) + + // 监听滚动自动分页加载 + const scrollTrigger = useCallback( + throttle(() => { + const scrollS = window.scrollY + window.outerHeight + const clientHeight = targetRef + ? targetRef.current + ? targetRef.current.clientHeight + : 0 + : 0 + if (scrollS > clientHeight + 100) { + handleGetMore() + } + }, 500) + ) + const showPageCover = siteConfig('THOUGHTLITE_POST_LIST_COVER', null, CONFIG) + + useEffect(() => { + window.addEventListener('scroll', scrollTrigger) + + return () => { + window.removeEventListener('scroll', scrollTrigger) + } + }) + + return ( +
+ {postsToShow?.map(post => ( + + ))} + +
+ {' '} + {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '} +
+
+ ) +} diff --git a/themes/thoughtlite/components/Catalog.js b/themes/thoughtlite/components/Catalog.js new file mode 100644 index 00000000000..5f546b89e25 --- /dev/null +++ b/themes/thoughtlite/components/Catalog.js @@ -0,0 +1,94 @@ +import throttle from 'lodash.throttle' +import { uuidToId } from 'notion-utils' +import { useCallback, useEffect, useRef, useState } from 'react' + +/** + * 目录导航组件 + * @param toc + * @returns {JSX.Element} + * @constructor + */ +const Catalog = ({ toc }) => { + // 监听滚动事件 + useEffect(() => { + window.addEventListener('scroll', actionSectionScrollSpy) + actionSectionScrollSpy() + return () => { + window.removeEventListener('scroll', actionSectionScrollSpy) + } + }, []) + + // 目录自动滚动 + const tRef = useRef(null) + const tocIds = [] + + // 同步选中目录事件 + const [activeSection, setActiveSection] = useState(null) + const throttleMs = 200 + const actionSectionScrollSpy = useCallback( + throttle(() => { + const sections = document.getElementsByClassName('notion-h') + let prevBBox = null + let currentSectionId = activeSection + for (let i = 0; i < sections.length; ++i) { + const section = sections[i] + if (!section || !(section instanceof Element)) continue + if (!currentSectionId) { + currentSectionId = section.getAttribute('data-id') + } + const bbox = section.getBoundingClientRect() + const prevHeight = prevBBox ? bbox.top - prevBBox.bottom : 0 + const offset = Math.max(150, prevHeight / 4) + // GetBoundingClientRect returns values relative to viewport + if (bbox.top - offset < 0) { + currentSectionId = section.getAttribute('data-id') + prevBBox = bbox + continue + } + // No need to continue loop, if last element has been detected + break + } + setActiveSection(currentSectionId) + const index = tocIds.indexOf(currentSectionId) || 0 + tRef?.current?.scrollTo({ top: 28 * index, behavior: 'smooth' }) + }, throttleMs) + ) + + // 无目录就直接返回空 + if (!toc || toc.length < 1) { + return <> + } + + return ( +
+
+ +
+
+ ) +} + +export default Catalog diff --git a/themes/thoughtlite/components/Footer.js b/themes/thoughtlite/components/Footer.js new file mode 100644 index 00000000000..029a31b76f2 --- /dev/null +++ b/themes/thoughtlite/components/Footer.js @@ -0,0 +1,27 @@ +import { BeiAnGongAn } from '@/components/BeiAnGongAn' +import BeiAnSite from '@/components/BeiAnSite' +import CopyRightDate from '@/components/CopyRightDate' +import DarkModeButton from '@/components/DarkModeButton' +import PoweredBy from '@/components/PoweredBy' + +export const Footer = props => { + return ( +
+ + +
+ +
+ {/* 右侧链接 */} + {/* Privacy Policy */} +
+ {' '} + + +
+ +
+
+
+ ) +} diff --git a/themes/thoughtlite/components/Header.js b/themes/thoughtlite/components/Header.js new file mode 100644 index 00000000000..486fb79a2c1 --- /dev/null +++ b/themes/thoughtlite/components/Header.js @@ -0,0 +1,28 @@ +import { siteConfig } from '@/lib/config' +import SmartLink from '@/components/SmartLink' +import { MenuList } from './MenuList' + +/** + * 网站顶部 + * LOGO 和 菜单 + * @returns + */ +export const Header = props => { + return ( +
+
+ + {siteConfig('TITLE')} + +
+ {/* 右侧文字 */} +
+
+ + {/* 菜单 */} + +
+ ) +} diff --git a/themes/thoughtlite/components/MenuItemDrop.js b/themes/thoughtlite/components/MenuItemDrop.js new file mode 100644 index 00000000000..198c2b00fb6 --- /dev/null +++ b/themes/thoughtlite/components/MenuItemDrop.js @@ -0,0 +1,57 @@ +import SmartLink from '@/components/SmartLink' +import { useState } from 'react' + +/** + * 支持下拉二级的菜单 + * @param {*} param0 + * @returns + */ +export const MenuItemDrop = ({ link }) => { + const [show, changeShow] = useState(false) + const hasSubMenu = link?.subMenus?.length > 0 + + return ( +
  • changeShow(true)} + onMouseOut={() => changeShow(false)}> + {!hasSubMenu && ( +
    + + {link?.icon && } {link?.name} + {hasSubMenu && } + +
    + )} + + {hasSubMenu && ( +
    + {link?.icon && } {link?.name} + +
    + )} + + {/* 子菜单 */} + {hasSubMenu && ( +
      + {link.subMenus.map((sLink, index) => { + return ( +
    • + + + {link?.icon &&   } + {sLink.title} + + +
    • + ) + })} +
    + )} +
  • + ) +} diff --git a/themes/thoughtlite/components/MenuList.js b/themes/thoughtlite/components/MenuList.js new file mode 100644 index 00000000000..4bda71859f8 --- /dev/null +++ b/themes/thoughtlite/components/MenuList.js @@ -0,0 +1,73 @@ +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import CONFIG from '../config' +import { MenuItemDrop } from './MenuItemDrop' + +/** + * 导航菜单列表 + * @param {*} props + * @returns + */ +export const MenuList = props => { + const { customNav, customMenu } = props + const { locale } = useGlobal() + + let links = [ + { + id: 1, + icon: 'fas fa-search', + name: locale.NAV.SEARCH, + href: '/search', + show: siteConfig('THOUGHTLITE_MENU_SEARCH', null, CONFIG) + }, + { + id: 2, + icon: 'fas fa-archive', + name: locale.NAV.ARCHIVE, + href: '/archive', + show: siteConfig('THOUGHTLITE_MENU_ARCHIVE', null, CONFIG) + }, + { + id: 3, + icon: 'fas fa-folder', + name: locale.COMMON.CATEGORY, + href: '/category', + show: siteConfig('THOUGHTLITE_MENU_CATEGORY', null, CONFIG) + }, + { + id: 4, + icon: 'fas fa-tag', + name: locale.COMMON.TAGS, + href: '/tag', + show: siteConfig('THOUGHTLITE_MENU_TAG', null, CONFIG) + } + ] + + if (customNav) { + links = links.concat(customNav) + } + + // 如果 开启自定义菜单,则不再使用 Page生成菜单。 + if (siteConfig('CUSTOM_MENU')) { + links = customMenu + } + + if (!links || links.length === 0) { + return null + } + + return ( + + ) +} diff --git a/themes/thoughtlite/components/PostLock.js b/themes/thoughtlite/components/PostLock.js new file mode 100644 index 00000000000..ca20fc7767c --- /dev/null +++ b/themes/thoughtlite/components/PostLock.js @@ -0,0 +1,63 @@ +import { useGlobal } from '@/lib/global' +import { useEffect, useRef } from 'react' + +/** + * 文章锁;通过此组件校验密码访问文章 + * @param {password, validPassword} props + * @param password 正确的密码 + * @param validPassword(bool) 回调函数,校验正确回调入参为true + * @returns + */ +export const PostLock = props => { + const { validPassword } = props + const { locale } = useGlobal() + + const submitPassword = () => { + const p = document.getElementById('password') + if (!validPassword(p?.value)) { + const tips = document.getElementById('tips') + if (tips) { + tips.innerHTML = '' + tips.innerHTML = `
    ${locale.COMMON.PASSWORD_ERROR}
    ` + } + } + } + const passwordInputRef = useRef(null) + useEffect(() => { + // 选中密码输入框并将其聚焦 + passwordInputRef.current.focus() + }, []) + + return ( +
    +
    +
    {locale.COMMON.ARTICLE_LOCK_TIPS}
    +
    + { + if (e.key === 'Enter') { + submitPassword() + } + }} + ref={passwordInputRef} // 绑定ref到passwordInputRef变量 + className='outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg font-light leading-10 text-black dark:bg-gray-500 bg-gray-50'> +
    + +  {locale.COMMON.SUBMIT} + +
    +
    +
    +
    +
    + ) +} diff --git a/themes/thoughtlite/components/PostMeta.js b/themes/thoughtlite/components/PostMeta.js new file mode 100644 index 00000000000..ebbb14b05aa --- /dev/null +++ b/themes/thoughtlite/components/PostMeta.js @@ -0,0 +1,51 @@ +import { useGlobal } from '@/lib/global' +import { formatDateFmt } from '@/lib/utils/formatDate' +import SmartLink from '@/components/SmartLink' +/** + * 文章详情的元信息 + * 标题、作者、分类、标签、创建日期等等。 + */ +export const PostMeta = props => { + const { post } = props + const { locale } = useGlobal() + + return ( +
    +
    + {post?.type !== 'Page' && ( + <> + + + {post?.category} + + | + + )} + + {post?.type !== 'Page' && ( + <> + + {post?.publishDay} + + | + + {locale.COMMON.LAST_EDITED_TIME}: {post?.lastEditedDay} + + | + + +   + + + + )} +
    +
    + ) +} diff --git a/themes/thoughtlite/components/RecentCommentListForExample.js b/themes/thoughtlite/components/RecentCommentListForExample.js new file mode 100644 index 00000000000..0fbd075efc2 --- /dev/null +++ b/themes/thoughtlite/components/RecentCommentListForExample.js @@ -0,0 +1,62 @@ +import { siteConfig } from '@/lib/config' +import { RecentComments } from '@waline/client' +import SmartLink from '@/components/SmartLink' +import { useEffect, useState } from 'react' + +/** + * 最近评论列表 + * 基于Waline实现 + * @see https://waline.js.org/guide/get-started.html + * @param {*} props + * @returns + */ +const RecentCommentListForExample = props => { + const [comments, updateComments] = useState([]) + const [onLoading, changeLoading] = useState(true) + useEffect(() => { + RecentComments({ + serverURL: siteConfig('COMMENT_WALINE_SERVER_URL'), + count: 5 + }).then(({ comments }) => { + changeLoading(false) + updateComments(comments) + }) + }, []) + + return ( + <> + {onLoading && ( +
    + Loading... + +
    + )} + {!onLoading && comments && comments.length === 0 && ( +
    No Comments
    + )} + {!onLoading && + comments && + comments.length > 0 && + comments.map(comment => ( +
    +
    +
    + + --{comment.nick} + +
    +
    + ))} + + ) +} + +export default RecentCommentListForExample diff --git a/themes/thoughtlite/components/SearchInput.js b/themes/thoughtlite/components/SearchInput.js new file mode 100644 index 00000000000..043f2961480 --- /dev/null +++ b/themes/thoughtlite/components/SearchInput.js @@ -0,0 +1,91 @@ +import { useGlobal } from '@/lib/global' +import { useRouter } from 'next/router' +import { useImperativeHandle, useRef, useState } from 'react' + +let lock = false + +/** + * 搜索输入框 + * @param {*} param0 + * @returns + */ +const SearchInput = ({ currentTag, keyword, cRef }) => { + const { locale } = useGlobal() + const router = useRouter() + const searchInputRef = useRef(null) + useImperativeHandle(cRef, () => { + return { + focus: () => { + searchInputRef?.current?.focus() + } + } + }) + const handleSearch = () => { + const key = searchInputRef.current.value + if (key && key !== '') { + router.push({ pathname: '/search/' + key }).then(r => { + }) + } else { + router.push({ pathname: '/' }).then(r => { + }) + } + } + const handleKeyUp = (e) => { + if (e.keyCode === 13) { // 回车 + handleSearch(searchInputRef.current.value) + } else if (e.keyCode === 27) { // ESC + cleanSearch() + } + } + const cleanSearch = () => { + searchInputRef.current.value = '' + setShowClean(false) + } + function lockSearchInput () { + lock = true + } + + function unLockSearchInput () { + lock = false + } + const [showClean, setShowClean] = useState(false) + const updateSearchKey = (val) => { + if (lock) { + return + } + searchInputRef.current.value = val + if (val) { + setShowClean(true) + } else { + setShowClean(false) + } + } + + return
    + updateSearchKey(e.target.value)} + defaultValue={keyword || ''} + /> + +
    + +
    + + {(showClean && +
    + +
    + )} +
    +} + +export default SearchInput diff --git a/themes/thoughtlite/components/SideBar.js b/themes/thoughtlite/components/SideBar.js new file mode 100644 index 00000000000..cebf59f9a6f --- /dev/null +++ b/themes/thoughtlite/components/SideBar.js @@ -0,0 +1,126 @@ +import Live2D from '@/components/Live2D' +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import dynamic from 'next/dynamic' +import SmartLink from '@/components/SmartLink' +import CONFIG from '../config' +import Announcement from './Announcement' +import Catalog from './Catalog' +const ExampleRecentComments = dynamic( + () => import('./RecentCommentListForExample') +) + +/** + * 侧边栏 + */ +export const SideBar = props => { + const { locale } = useGlobal() + const { latestPosts, categoryOptions, notice, post } = props + // 评论相关 + const COMMENT_WALINE_SERVER_URL = siteConfig( + 'COMMENT_WALINE_SERVER_URL', + false + ) + const COMMENT_WALINE_RECENT = siteConfig('COMMENT_WALINE_RECENT', false) + + // 文章详情页特殊布局 + const HIDDEN_NOTIFICATION = + post && siteConfig('THOUGHTLITE_ARTICLE_HIDDEN_NOTIFICATION', false, CONFIG) + + // 文章详情页左右布局改为上下布局 + const LAYOUT_VERTICAL = + post && siteConfig('THOUGHTLITE_ARTICLE_LAYOUT_VERTICAL', false, CONFIG) + + return ( + <> + {/* 目录 */} + {post?.toc && post?.toc.length > 2 && ( + + )} + + {/* 分类 */} + + + {/* 最新文章 */} + + + {/* 公告 */} + {/* 公告栏 */} + {!HIDDEN_NOTIFICATION && } + + {/* 最近评论 */} + {COMMENT_WALINE_SERVER_URL && COMMENT_WALINE_RECENT && ( + + )} + + {/* 宠物挂件 */} + + + ) +} diff --git a/themes/thoughtlite/components/TitleBar.js b/themes/thoughtlite/components/TitleBar.js new file mode 100644 index 00000000000..d088751bd0c --- /dev/null +++ b/themes/thoughtlite/components/TitleBar.js @@ -0,0 +1,48 @@ +import NotionIcon from '@/components/NotionIcon' +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import CONFIG from '../config' + +/** + * 标题栏 + */ +export default function TitleBar(props) { + const { post } = props + const { fullWidth, siteInfo } = useGlobal() + + const title = post?.title || siteConfig('TITLE') + const description = post?.description || siteConfig('AUTHOR') + const headerImage = post?.pageCoverThumbnail + ? post.pageCoverThumbnail + : siteInfo?.pageCover + + const TITLE_BG = siteConfig('THOUGHTLITE_TITLE_IMAGE', false, CONFIG) + + return ( + <> + {/* 标题栏 */} + {!fullWidth && ( +
    +

    + {siteConfig('POST_TITLE_ICON') && ( + + )} + {title} +

    +

    + {description} +

    + {TITLE_BG && ( + <> + {/* eslint-disable-next-line @next/next/no-img-element */} + + + )} +
    + )} + + ) +} diff --git a/themes/thoughtlite/config.js b/themes/thoughtlite/config.js new file mode 100644 index 00000000000..a1082433216 --- /dev/null +++ b/themes/thoughtlite/config.js @@ -0,0 +1,19 @@ +/** + * 主题配置文件 + */ +const CONFIG = { + // 菜单配置 + THOUGHTLITE_MENU_CATEGORY: true, // 显示分类 + THOUGHTLITE_MENU_TAG: true, // 显示标签 + THOUGHTLITE_MENU_ARCHIVE: true, // 显示归档 + THOUGHTLITE_MENU_SEARCH: true, // 显示搜索 + + THOUGHTLITE_POST_LIST_COVER: true, // 列表显示文章封面 + + THOUGHTLITE_TITLE_IMAGE: false, // 标题栏,是否背景图片 + + // 文章页面布局 + THOUGHTLITE_ARTICLE_LAYOUT_VERTICAL: false, // 文章详情,左右布局改为上下布局 + THOUGHTLITE_ARTICLE_HIDDEN_NOTIFICATION: false // 文章详情隐藏公告 +} +export default CONFIG diff --git a/themes/thoughtlite/index.js b/themes/thoughtlite/index.js new file mode 100644 index 00000000000..2fc2ee0bfe2 --- /dev/null +++ b/themes/thoughtlite/index.js @@ -0,0 +1,363 @@ +'use client' + +/** + * ThoughtLite(进行中) + * + * 当前为基于 `themes/example` 的可运行骨架,便于在 `NEXT_PUBLIC_THEME=thoughtlite` 下联调数据契约。 + * 视觉与交互需按 `docs/themes/THOUGHTLITE_MIGRATION_PLAN.zh-CN.md` 对照上游 [ThoughtLite](https://github.com/tuyuritio/astro-theme-thought-lite) 逐步替换。 + * + * 关联:[#3987](https://github.com/notionnext-org/NotionNext/issues/3987) + */ + +import Comment from '@/components/Comment' +import replaceSearchResult from '@/components/Mark' +import NotionPage from '@/components/NotionPage' +import ShareBar from '@/components/ShareBar' +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import { isBrowser } from '@/lib/utils' +import { Transition } from '@headlessui/react' +import SmartLink from '@/components/SmartLink' +import { useRouter } from 'next/router' +import { useEffect } from 'react' +import BlogListArchive from './components/BlogListArchive' +import { BlogListPage } from './components/BlogListPage' +import { BlogListScroll } from './components/BlogListScroll' +import { Footer } from './components/Footer' +import { Header } from './components/Header' +import { PostLock } from './components/PostLock' +import { PostMeta } from './components/PostMeta' +import SearchInput from './components/SearchInput' +import { SideBar } from './components/SideBar' +import TitleBar from './components/TitleBar' +import CONFIG from './config' +import { Style } from './style' + +/** + * 基础布局框架 + * 1.其它页面都嵌入在LayoutBase中 + * 2.采用左右两侧布局,移动端使用顶部导航栏 + * @returns {JSX.Element} + * @constructor + */ +const LayoutBase = props => { + const { children, post } = props + const { onLoading, fullWidth, locale } = useGlobal() + + // 文章详情页左右布局改为上下布局 + const LAYOUT_VERTICAL = + post && siteConfig('THOUGHTLITE_ARTICLE_LAYOUT_VERTICAL', false, CONFIG) + + // 网站左右布局颠倒 + const LAYOUT_SIDEBAR_REVERSE = siteConfig('LAYOUT_SIDEBAR_REVERSE', false) + + return ( +
    + +} + +export { Style } From 5d4b178425b72800577d3a08d29ac1222934531d Mon Sep 17 00:00:00 2001 From: tangly1024 Date: Fri, 15 May 2026 22:08:15 +0800 Subject: [PATCH 06/11] feat(theme-thoughtlite): ThoughtLite-style header, timeline home, article layout Co-authored-by: Cursor --- docs/themes/THOUGHTLITE.md | 2 +- themes/thoughtlite/components/BlogItem.js | 66 ++++-- themes/thoughtlite/components/BlogListPage.js | 32 ++- .../thoughtlite/components/BlogListScroll.js | 82 +++++--- themes/thoughtlite/components/Footer.js | 16 +- themes/thoughtlite/components/Header.js | 45 ++-- themes/thoughtlite/components/HomeTimeline.js | 49 +++++ themes/thoughtlite/components/LatestCard.js | 28 +++ themes/thoughtlite/components/MenuItemDrop.js | 29 ++- themes/thoughtlite/components/MenuList.js | 15 +- themes/thoughtlite/components/PostMeta.js | 21 +- themes/thoughtlite/components/SideBar.js | 21 +- themes/thoughtlite/components/TitleBar.js | 72 ++++--- themes/thoughtlite/config.js | 11 +- themes/thoughtlite/index.js | 151 +++++++++----- themes/thoughtlite/style.js | 194 +++++++++++++++++- 16 files changed, 635 insertions(+), 199 deletions(-) create mode 100644 themes/thoughtlite/components/HomeTimeline.js create mode 100644 themes/thoughtlite/components/LatestCard.js diff --git a/docs/themes/THOUGHTLITE.md b/docs/themes/THOUGHTLITE.md index 5dea71bd1cd..0b191283944 100644 --- a/docs/themes/THOUGHTLITE.md +++ b/docs/themes/THOUGHTLITE.md @@ -11,7 +11,7 @@ ## 当前状态 - 工作分支:**`feat/theme-thoughtlite`** -- 主题目录:**`themes/thoughtlite/`** — 基于 `themes/example` 的**可运行骨架**(配置前缀 `THOUGHTLITE_*`),用于先跑通 NotionNext 数据流;**视觉尚未对齐**上游 ThoughtLite。 +- 主题目录:**`themes/thoughtlite/`** — 已接入 **ThoughtLite 取向** 的顶栏、CSS 变量主题色、首页 **Latest 卡片**、**按日发布日时间线**、文章页 **卡片式标题区 + 正文 prose 包裹**、侧栏 **仅文章页显示**(可配置)、评论区 **SSR 关闭** 等;仍可按计划继续微调像素级对齐上游 Astro 版。 ## 本地试用 diff --git a/themes/thoughtlite/components/BlogItem.js b/themes/thoughtlite/components/BlogItem.js index 33bffc6ec8a..02334db937c 100644 --- a/themes/thoughtlite/components/BlogItem.js +++ b/themes/thoughtlite/components/BlogItem.js @@ -7,14 +7,55 @@ import CONFIG from '../config' /** * 博客列表的单个卡片 - * @param {*} param0 - * @returns + * @param {{ post: object, variant?: 'default' | 'timeline' }} props */ -const BlogItem = ({ post }) => { +const BlogItem = ({ post, variant = 'default' }) => { const showPageCover = + variant === 'default' && siteConfig('THOUGHTLITE_POST_LIST_COVER', null, CONFIG) && post?.pageCoverThumbnail + if (variant === 'timeline') { + return ( +
    +
    + + {siteConfig('POST_TITLE_ICON') && ( + + + + )} + {post?.title} + + {post?.type !== 'Page' && post?.category && ( + + #{post.category} + + )} +
    + {post.summary && !post.results ? ( +

    + {post.summary} +

    + ) : null} + {post.results ? ( +

    + {post.results.map((r, index) => ( + {r} + ))} +

    + ) : null} +
    + +
    +
    + ) + } + return (
    @@ -22,7 +63,7 @@ const BlogItem = ({ post }) => {

    + className='text-[var(--tl-text)] text-xl md:text-2xl no-underline hover:underline'> {siteConfig('POST_TITLE_ICON') && ( )} @@ -30,11 +71,10 @@ const BlogItem = ({ post }) => {

    -
    +
    by{' '} - - {siteConfig('AUTHOR')} - {' '} + {siteConfig('AUTHOR')} + {' '} on {post.date?.start_date || post.createdTime} {post.category && ( @@ -42,30 +82,26 @@ const BlogItem = ({ post }) => { | + className='text-[var(--tl-muted)] hover:underline'> {post.category} )} - {/* | */} - {/* 2 Comments */}
    {!post.results && ( -

    +

    {post.summary}

    )} - {/* 搜索结果 */} {post.results && ( -

    +

    {post.results.map((r, index) => ( {r} ))}

    )}
    - {/* 图片封面 */} {showPageCover && (
    diff --git a/themes/thoughtlite/components/BlogListPage.js b/themes/thoughtlite/components/BlogListPage.js index 45d56fbd6fd..3facec24233 100644 --- a/themes/thoughtlite/components/BlogListPage.js +++ b/themes/thoughtlite/components/BlogListPage.js @@ -4,13 +4,20 @@ import SmartLink from '@/components/SmartLink' import { useRouter } from 'next/router' import CONFIG from '../config' import BlogItem from './BlogItem' +import HomeTimeline from './HomeTimeline' + /** * 使用分页插件的博客列表 * @param {*} props * @returns */ export const BlogListPage = props => { - const { page = 1, posts, postCount } = props + const { + page = 1, + posts, + postCount, + useTimeline: useTimelineProp + } = props const { locale, NOTION_CONFIG } = useGlobal() const router = useRouter() const totalPage = Math.ceil( @@ -28,13 +35,26 @@ export const BlogListPage = props => { const showPageCover = siteConfig('THOUGHTLITE_POST_LIST_COVER', null, CONFIG) + const useTimeline = + useTimelineProp ?? + (siteConfig('THOUGHTLITE_HOME_TIMELINE', true, CONFIG) && + !props.category && + !props.tag && + !props.keyword && + !router?.query?.s && + (router.pathname === '/' || router.pathname === '/page/[page]')) + return (
    -
    - {posts?.map(post => ( - - ))} -
    + {useTimeline ? ( + + ) : ( +
    + {posts?.map(post => ( + + ))} +
    + )}
    { - const { posts } = props + const { posts, useTimeline: useTimelineProp } = props + const router = useRouter() const { locale, NOTION_CONFIG } = useGlobal() const [page, updatePage] = useState(1) const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG) - let hasMore = false const postsToShow = posts - ? Object.assign(posts).slice(0, POSTS_PER_PAGE * page) + ? Object.assign([], posts).slice(0, POSTS_PER_PAGE * page) : [] - if (posts) { - const totalCount = posts.length - hasMore = page * POSTS_PER_PAGE < totalCount - } - const handleGetMore = () => { - if (!hasMore) return - updatePage(page + 1) - } + const hasMore = Boolean(posts?.length) && page * POSTS_PER_PAGE < posts.length + + const handleGetMore = useCallback(() => { + updatePage(p => { + const total = posts?.length ?? 0 + if (p * POSTS_PER_PAGE >= total) return p + return p + 1 + }) + }, [posts, POSTS_PER_PAGE]) const targetRef = useRef(null) - // 监听滚动自动分页加载 - const scrollTrigger = useCallback( - throttle(() => { - const scrollS = window.scrollY + window.outerHeight - const clientHeight = targetRef - ? targetRef.current - ? targetRef.current.clientHeight - : 0 - : 0 - if (scrollS > clientHeight + 100) { - handleGetMore() - } - }, 500) + const scrollTrigger = useMemo( + () => + throttle(() => { + const scrollS = window.scrollY + window.outerHeight + const clientHeight = targetRef.current?.clientHeight ?? 0 + if (scrollS > clientHeight + 100) { + handleGetMore() + } + }, 500), + [handleGetMore] ) - const showPageCover = siteConfig('THOUGHTLITE_POST_LIST_COVER', null, CONFIG) useEffect(() => { window.addEventListener('scroll', scrollTrigger) - return () => { + scrollTrigger.cancel() window.removeEventListener('scroll', scrollTrigger) } - }) + }, [scrollTrigger]) + + const showPageCover = siteConfig('THOUGHTLITE_POST_LIST_COVER', null, CONFIG) + + const useTimeline = + useTimelineProp ?? + (siteConfig('THOUGHTLITE_HOME_TIMELINE', true, CONFIG) && + !props.category && + !props.tag && + !props.keyword && + !router?.query?.s && + (router.pathname === '/' || router.pathname === '/page/[page]')) return (
    - {postsToShow?.map(post => ( - - ))} + {useTimeline ? ( + + ) : ( +
    + {postsToShow?.map(post => ( + + ))} +
    + )}
    { +export const Footer = () => { return ( -