From 386c0f77c79b68ef171d520e2cafa26f7b69b8d7 Mon Sep 17 00:00:00 2001 From: CNWei Date: Thu, 22 Oct 2026 20:27:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E5=AF=B9=E9=BD=90=20Alright=20?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E8=A7=86=E8=A7=89=E8=A7=84=E8=8C=83=EF=BC=88?= =?UTF-8?q?=E9=85=8D=E8=89=B2/=E5=AD=97=E4=BD=93/=E5=9B=BE=E6=A0=87/?= =?UTF-8?q?=E8=83=8C=E6=99=AF/=E5=8A=A8=E6=95=88=EF=BC=89=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E8=A1=A5=E5=85=85=E8=AE=BE=E8=AE=A1=E8=A7=84=E8=8C=83?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 24 +- ALRIGHT-DESIGN.md | 325 ++++ README.md | 297 ++-- stash-web/index.html | 6 + stash-web/src/App.css | 1485 ++++++++++++----- stash-web/src/App.tsx | 302 ++-- stash-web/src/assets/iconfont.woff2 | Bin 0 -> 52136 bytes stash-web/src/assets/site-bg.jpg | Bin 0 -> 28746 bytes stash-web/src/catalog.ts | 130 -- stash-web/src/components/BackToTop.tsx | 22 - stash-web/src/components/Sidebar.tsx | 84 - stash-web/src/components/SiteCard.tsx | 26 - stash-web/src/components/TopBar.tsx | 89 - stash-web/src/components/common/BackToTop.tsx | 33 + .../src/components/common/CountBadge.tsx | 11 + stash-web/src/components/common/Icon.tsx | 67 + stash-web/src/components/content/AppCard.tsx | 49 + stash-web/src/components/content/AppGrid.tsx | 24 + .../src/components/content/ArticleCard.tsx | 36 + .../components/content/ArticleDetailView.tsx | 79 + .../src/components/content/ArticleGrid.tsx | 19 + stash-web/src/components/content/CesCard.tsx | 37 + .../src/components/content/CesCardGrid.tsx | 19 + .../src/components/content/DetailView.tsx | 88 + .../src/components/content/GroupBlock.tsx | 20 + stash-web/src/components/content/HomeView.tsx | 67 + .../src/components/content/ItemDetailView.tsx | 122 ++ .../src/components/content/PosterCard.tsx | 36 + .../src/components/content/PosterGrid.tsx | 20 + .../src/components/content/RelatedList.tsx | 45 + .../src/components/content/SectionCard.tsx | 47 + .../src/components/header/HeaderInfoItem.tsx | 24 + .../src/components/header/SiteHeader.tsx | 58 + stash-web/src/components/icons.tsx | 37 - .../src/components/{ => search}/SearchBar.tsx | 14 +- .../src/components/sidebar/CategoryNav.tsx | 79 + .../components/{ => sidebar}/ProfileCard.tsx | 13 +- .../components/{ => sidebar}/ServicePanel.tsx | 12 +- stash-web/src/components/sidebar/Sidebar.tsx | 48 + stash-web/src/data/catalog.ts | 267 +++ stash-web/src/data/detail.ts | 75 + stash-web/src/data/filter.ts | 49 + stash-web/src/data/nav.ts | 54 + stash-web/src/data/types.ts | 190 +++ stash-web/src/iconfont.css | 74 + stash-web/src/index.css | 37 +- stash-web/src/main.tsx | 1 + templates/index2.html | 499 ++++++ templates/index3.html | 920 ++++++++++ templates/index4.html | 1378 +++++++++++++++ 50 files changed, 6344 insertions(+), 1094 deletions(-) create mode 100644 ALRIGHT-DESIGN.md create mode 100644 stash-web/src/assets/iconfont.woff2 create mode 100644 stash-web/src/assets/site-bg.jpg delete mode 100644 stash-web/src/catalog.ts delete mode 100644 stash-web/src/components/BackToTop.tsx delete mode 100644 stash-web/src/components/Sidebar.tsx delete mode 100644 stash-web/src/components/SiteCard.tsx delete mode 100644 stash-web/src/components/TopBar.tsx create mode 100644 stash-web/src/components/common/BackToTop.tsx create mode 100644 stash-web/src/components/common/CountBadge.tsx create mode 100644 stash-web/src/components/common/Icon.tsx create mode 100644 stash-web/src/components/content/AppCard.tsx create mode 100644 stash-web/src/components/content/AppGrid.tsx create mode 100644 stash-web/src/components/content/ArticleCard.tsx create mode 100644 stash-web/src/components/content/ArticleDetailView.tsx create mode 100644 stash-web/src/components/content/ArticleGrid.tsx create mode 100644 stash-web/src/components/content/CesCard.tsx create mode 100644 stash-web/src/components/content/CesCardGrid.tsx create mode 100644 stash-web/src/components/content/DetailView.tsx create mode 100644 stash-web/src/components/content/GroupBlock.tsx create mode 100644 stash-web/src/components/content/HomeView.tsx create mode 100644 stash-web/src/components/content/ItemDetailView.tsx create mode 100644 stash-web/src/components/content/PosterCard.tsx create mode 100644 stash-web/src/components/content/PosterGrid.tsx create mode 100644 stash-web/src/components/content/RelatedList.tsx create mode 100644 stash-web/src/components/content/SectionCard.tsx create mode 100644 stash-web/src/components/header/HeaderInfoItem.tsx create mode 100644 stash-web/src/components/header/SiteHeader.tsx delete mode 100644 stash-web/src/components/icons.tsx rename stash-web/src/components/{ => search}/SearchBar.tsx (81%) create mode 100644 stash-web/src/components/sidebar/CategoryNav.tsx rename stash-web/src/components/{ => sidebar}/ProfileCard.tsx (65%) rename stash-web/src/components/{ => sidebar}/ServicePanel.tsx (86%) create mode 100644 stash-web/src/components/sidebar/Sidebar.tsx create mode 100644 stash-web/src/data/catalog.ts create mode 100644 stash-web/src/data/detail.ts create mode 100644 stash-web/src/data/filter.ts create mode 100644 stash-web/src/data/nav.ts create mode 100644 stash-web/src/data/types.ts create mode 100644 stash-web/src/iconfont.css create mode 100644 templates/index2.html create mode 100644 templates/index3.html create mode 100644 templates/index4.html diff --git a/.gitignore b/.gitignore index 3a8cabc..24f0845 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,24 @@ +# ---------- Rust ---------- /target -.idea + +# ---------- 前端 ---------- +# 依赖与构建产物:dist 由 npm run build 生成,再被 build.rs 内嵌进二进制 +node_modules/ +stash-web/dist/ +stash-web/dist-ssr/ +*.tsbuildinfo +.vite/ + +# ---------- 生成的代码快照 ---------- +repomix-output.xml + +# ---------- IDE / 编辑器 ---------- +/.idea +*.iml +.vscode/ + +# ---------- 日志与系统文件 ---------- +*.log +.DS_Store +Thumbs.db +*.local diff --git a/ALRIGHT-DESIGN.md b/ALRIGHT-DESIGN.md new file mode 100644 index 0000000..a10fd0e --- /dev/null +++ b/ALRIGHT-DESIGN.md @@ -0,0 +1,325 @@ +# Alright 主题设计规范(Stash 前端) + +本文件记录 Stash 控制台在视觉上对齐 [Alright 主题](https://alright.demo.nicetheme.xyz/) 的各项标准, +用于后续新增组件、调整样式时保持一致。 + +- 参考站点: +- 生效范围:`stash-web/`(React 控制台) +- 令牌定义处:[`stash-web/src/index.css`](stash-web/src/index.css) +- 组件样式:[`stash-web/src/App.css`](stash-web/src/App.css) + +> 所有色值、字号、圆角均取自主题的 `:root` 设计变量,不是肉眼取色。 + +--- + +## 1. 设计令牌 + +### 1.1 灰阶 + +| 令牌 | 值 | 用途 | +| --- | --- | --- | +| `--gray-1` | `#f7f8fa` | 页面背景、浅底块 | +| `--gray-2` | `#f6f6f6` | 边框、次级浅底 | +| `--gray-3` | `#eeeeee` | hover 底色 | +| `--gray-4` | `#c9cdd4` | 分隔线、禁用态 | +| `--gray-5` | `#86909c` | 说明文字 | +| `--gray-6` | `#4e5969` | 次级文字 | +| `--gray-7` | `#2c343c` | 强调文字 | +| `--gray-8` | `#1d2129` | 主文字 | + +### 1.2 语义色 + +| 令牌 | 值 | 用途 | +| --- | --- | --- | +| `--color-primary` | `#3858f6` | 主色:链接、选中态、色条、图标 | +| `--color-primary` 10% | `rgba(56, 88, 246, 0.1)` | 主色浅底(`--primary-soft`) | +| `--color-danger` | `#ff3e3e` | 数量角标、错误提示 | +| `--color-warning` | `#ffba3a` | 橙色强调(CES 色条) | +| `--color-success` | `#4abd86` | 在线状态 | +| 渐变红 | `linear-gradient(to right, #ff6034, #ff3e3e)` | 热门标记 | +| HOT 渐变 | `#ff7b51 → #ff0e0e` | HOT 图形徽标 | +| 认证渐变 | `#ffb74a → #fd900f` | 头像认证角标 | + +### 1.3 圆角 + +| 令牌 | 值 | 用途 | +| --- | --- | --- | +| `--radius-xs` | `4px` | 小标签、徽标 | +| `--radius-sm` | `6px` | 小组件 | +| `--radius-main`(md) | `12px` | 卡片、面板 | +| `--radius-lg` | `24px` | 大容器 | +| `max` | `999px` | 胶囊、圆形 | + +### 1.4 阴影 + +主题的阴影非常克制,只有两层: + +```css +--shadow-card: 0 6px 16px rgba(27, 26, 49, 0.02); /* 常态 */ +--shadow-hover: 0 6px 16px rgba(27, 26, 49, 0.06); /* 悬停 */ +``` + +**注意**:应用条目、作者列表这类「行」不应该有阴影或描边。 + +### 1.5 字号阶梯 + +| 令牌 | 字号 | 行高 | 用途 | +| --- | --- | --- | --- | +| `xs` | 10px | 14px | 角标 | +| `sm` | 12px | 16px | 描述、元信息、HOT 旁注 | +| `md` | 14px | 20px | 正文、按钮、导航 | +| `lg` | 16px | 24px | 分区标题、条目标题 | +| `xl` | 24px | — | 详情页大标题 | + +字重:主题的「加粗」是 `500`,不是 700。 + +### 1.6 动效时长 + +| 令牌 | 值 | 用途 | +| --- | --- | --- | +| `base` | 0.3s | 展开/收起、淡入淡出 | +| `fast` | 0.2s | 颜色、背景变化 | +| enter | `ease-out` | 出现 | +| leave | `ease-in` | 消失 | + +--- + +## 2. 字体 + +| 角色 | 字体 | 说明 | +| --- | --- | --- | +| 正文 | **LXGW WenKai(霞鹜文楷)** | 与演示站同一份 webfont,`cdn.staticfile.net` | +| 回退 | `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif` | 字体加载失败时 | +| 数字(主题) | `Outfit-Medium` | 仅主题的 `font-price` / `font-number` 使用,Stash 未引入 | +| 等宽 | `ui-monospace, Consolas, monospace` | 路由地址等路径文本 | + +引入位置:[`stash-web/index.html`](stash-web/index.html) 的 ``, +CSS 变量 `--font-sans` / `--font-mono` 定义在 `index.css`。 + +--- + +## 3. 图标 + +### 3.1 图标字体 + +主题使用自建 iconfont(线性风格,Project id 4809463)。 +Stash 自托管了一份 **只含所需字形** 的子集: + +- 字体文件:[`stash-web/src/assets/iconfont.woff2`](stash-web/src/assets/iconfont.woff2) +- 样式:[`stash-web/src/iconfont.css`](stash-web/src/iconfont.css) +- 组件封装:[`stash-web/src/components/common/Icon.tsx`](stash-web/src/components/common/Icon.tsx) + +| 用途 | 主题字形 | 码点 | Stash 类名 | +| --- | --- | --- | --- | +| 搜索 | `search_3_line` | `\e942` | `.icon-search` | +| 用户 / 登录 | `user_3_line` | `\e966` | `.icon-user` | +| 重新扫描 | `refresh_3_line` | `\e93d` | `.icon-refresh` | +| 查看接口 | `link_line` | `\ea37` | `.icon-link` | +| 跳转 | `arrow_right_up_line` | `\e8be` | `.icon-external` | +| 浏览量 | `eye_line` | `\e8fd` | `.icon-eye` | +| 点赞 | `thumb_up_2_line` | `\e957` | `.icon-thumb-up` | +| 分享 | `share_3_line` | `\e946` | `.icon-share` | +| 清空搜索 | `close_line` | `\e8e4` | `.icon-close` | +| 分区更多 | `more_3_line` | `\e91f` | `.icon-more` | +| 回到顶部 | `arrow_to_up_line` | `\e8c1` | `.icon-arrow-up` | +| 返回 | `arrow_left_line` | `\e8b5` | `.icon-arrow-left` | +| 导航展开 | `arrow_down_line` | `\e8b7` | `.icon-arrow-down`(未使用,见下) | + +用法:``,尺寸用 `font-size`、颜色用 `color` 控制。 + +### 3.2 CSS 绘制的箭头 + +主题的**导航折叠箭头不是图标字体**,而是一个 6×6 的描边方块旋转而成: + +```css +.nav-link.has-children::after { + content: ''; + width: 6px; height: 6px; + border-left: 1px solid currentColor; + border-bottom: 1px solid currentColor; + transform: rotate(315deg); /* 展开时 135deg */ + opacity: 0.4; +} +``` + +--- + +## 4. 布局 + +### 4.1 整体结构 + +``` +┌──────────────────────────────────────────────┐ +│ SiteHeader(固定 60px,白底、底部 1px 分隔) │ +├──────────────────────────────────────────────┤ +│ 搜索框(固定,480×44,居中) │ +│ ┌──────────┬─────────────────────────────┐ │ +│ │ Sidebar │ 内容区(内部滚动) │ │ +│ │ 240px │ 分区卡片 / 明细页 │ │ +│ └──────────┴─────────────────────────────┘ │ +└──────────────────────────────────────────────┘ +``` + +| 项 | 值 | 说明 | +| --- | --- | --- | +| 版心宽度 | `1380px` | `--layout-max-width`,超宽屏居中 | +| 顶栏高度 | `60px` | `--top-nav-height` | +| 侧栏宽度 | `240px` | `--sidebar-width`(主题为 260px,Stash 收窄) | +| 栅格间距 | `20px` | 侧栏与内容区之间 | +| 内容区顶部让位 | `52px` | `--content-top-offset`,给悬浮搜索框 | +| 搜索框 | `480 × 44`,距顶 `76px` | 相对整页居中 | + +### 4.2 页面背景 + +主题在 body 上叠了一层固定背景图: + +```css +.site-background { + position: fixed; inset: 0 auto auto 0; + width: 100%; height: 65vh; + background: url('./assets/site-bg.jpg') top center / cover no-repeat; + pointer-events: none; +} +.site-background::after { /* 底部 80% 渐隐到页面底色 */ + content: ''; position: absolute; inset: auto 0 0 0; height: 80%; + background-image: linear-gradient(0deg, var(--bg-color) 0%, transparent 100%); +} +``` + +滚动时背景不动,形成轻微视差。 + +--- + +## 5. 组件规范 + +### 5.1 分区卡片(首页每一块) + +| 属性 | 值 | +| --- | --- | +| 容器 | 白底、`1px solid var(--border-color)`、圆角 12、内边距 20、`var(--shadow-card)` | +| 标题色条 | `4 × 16px`,圆角 2,分类色(本地/推荐 `#3858f6`、电影 `#ff6034`、CES `#ffba3a`) | +| 标题 | 16px / 500 | +| 数量角标 | 红色 `#ff3e3e`、10px 字号、圆角 8、贴在标题右上角(`top:-8px; right:-18px`) | +| 更多按钮 | `.icon-more`,16px,右对齐 | + +### 5.2 应用条目(对齐主题「推荐作者」) + +对应标记: + +```html + +
+ + +
+
+

名字

+
描述
+
+
+``` + +| 元素 | 规格 | +| --- | --- | +| 行 | `display:flex; gap:15px`,**无底色、无描边、无阴影** | +| 头像 | 48×48,`border-radius:100%`,图片 `object-fit:cover` | +| 认证角标 | 10×10,绝对定位右下角,内联 `badge_5` 的 SVG | +| 名字 | 16px / 行高 24px / 500 | +| HOT 徽标 | 24×16 的**图片**(内联主题 `--hot-icon` SVG),不是文字 | +| 描述 | 12px / 行高 16px / `--text-muted`,单行省略 | +| 悬停 | 名字变主色,不加阴影 | + +> HOT 与认证角标只出现在 `hot: true` 的条目上,不是每个都有。 + +### 5.3 海报 / 图文 + +| 网格 | 列数 | 间距 | 比例 | +| --- | --- | --- | --- | +| 应用 | 4(≤1100px 变 2) | 12px | — | +| 电影海报 | 6(≤1100px 变 3) | 12px | 2:3 | +| 首页图文 | 4(≤1100px 变 2) | 12px | 4:3 | +| 明细页图文白卡 | 4(≤1100px 变 2) | 16px | 封面 16:10 | + +封面下方统一用「黑色渐变 → 透明」遮罩压住白色标题: + +```css +background: linear-gradient(to top, rgba(0,0,0,.85) 0%, rgba(0,0,0,.4) 60%, transparent 100%); +``` + +### 5.4 侧栏 + +| 模块 | 规格 | +| --- | --- | +| 用户卡 | 64px 圆形头像 + 认证角标、14px 名字、11–12px 角色/简介/属地 | +| 本地服务面板 | 标题 14px、状态胶囊、两行统计(`dt` 12px / `dd` 12px 等宽)、两个操作按钮 | +| 分类导航 | 行高 `9px 12px` 内边距、圆角 8、14px;子项 12px、缩进 12px | + +### 5.5 返回顶部 + +对应主题 `.scroll-toolbar .totop-button`: + +```css +.back-to-top { + position: fixed; top: 50%; right: 30px; + width: 42px; height: 42px; + border-radius: 50%; + background-color: var(--border-color); + color: var(--text-sub); + opacity: 0; visibility: hidden; /* 默认隐藏 */ + transform: translateY(-50%) translateY(15px); + transition: opacity .3s, visibility .3s, transform .3s; +} +.back-to-top.is-visible { + opacity: 1; visibility: visible; + transform: translateY(-50%); +} +``` + +滚动超过 4px 才出现,回到顶部后自动隐藏;窄屏改为 `right:15px; bottom:30%`。 + +--- + +## 6. 动效 + +| 效果 | 实现 | +| --- | --- | +| 视图切换(首页 ↔ 明细 ↔ 条目) | `view-in`:0.22s 淡入 + 上移 6px | +| 侧栏子菜单展开 | `grid-template-rows: 0fr → 1fr`,0.28s,全程 `overflow:hidden`(不产生滚动条) | +| 导航箭头 | `rotate(315deg) → 135deg`,0.3s | +| 分区卡片 / 海报 / 图文悬停 | `translateY(-2px)`,0.2s | +| 应用条目悬停 | 仅名字改为主色 | +| 回到顶部 | 淡入 + 上移 15px,0.3s | +| 刷新按钮加载 | `spin 0.9s linear infinite` | +| 内容区顶部遮罩 | 滚动后 0.2s 淡入 | + +--- + +## 7. 与演示站的有意差异 + +| 项 | 主题 | Stash | 原因 | +| --- | --- | --- | --- | +| 侧栏宽度 | 260px | 240px | 控制台信息密度略高 | +| 深色模式 | 支持 | 未实现 | 当前只做浅色主题 | +| 数字字体 | Outfit-Medium | 未引入 | 少一个外部字体请求 | +| 图标集 | 全量 446 个 | 13 个子集 | 控制体积(woff2 52KB) | +| 导航箭头位置 | 紧贴文字右侧 | 行末右对齐 | 视觉上更整齐 | + +--- + +## 8. 扩展指引 + +| 需求 | 改动位置 | +| --- | --- | +| 改主色 / 灰阶 / 圆角 | [`stash-web/src/index.css`](stash-web/src/index.css) 的 `:root` | +| 新增图标 | 把 codepoint 加进 [`stash-web/src/iconfont.css`](stash-web/src/iconfont.css),在 `Icon.tsx` 包一个组件 | +| 新增分类 | [`stash-web/src/data/catalog.ts`](stash-web/src/data/catalog.ts) 加一条 `Category`,首页与明细页自动适配 | +| 新增卡片类型 | 在 `data/types.ts` 增加 kind,`HomeView` / `DetailView` 各加一个分支 | +| 调整分区样式 | [`stash-web/src/App.css`](stash-web/src/App.css) 的「分区卡片」段落 | +| 换背景图 | 替换 `stash-web/src/assets/site-bg.jpg` | + +--- + +## 9. 变更记录 + +- 2026-10-22:按本规范完成整体换肤(配色、字体、图标、背景、应用条目、返回顶部), + 并将侧栏导航箭头改为与主题一致的 CSS 箭头。 diff --git a/README.md b/README.md index 123903d..bbc36ae 100644 --- a/README.md +++ b/README.md @@ -1,151 +1,112 @@ # Stash -Stash 是一个基于 Rust、Tokio 和 Axum 实现的本地静态页面托管工具。它既可以直接托管一个静态站点,也可以扫描托管目录下的多个前端项目,并通过内置导航页统一访问。 +Stash 是一个用 Rust 编写的本地静态页面托管工具。编译后的 React 控制台会被内嵌进二进制, +所以发布物只有一个 `stash` 可执行文件,不需要额外部署前端静态资源。 -## 核心功能 - -### 单站点托管 - -当托管根目录中存在 `index.html` 时,访问 `/` 会直接返回该页面。页面依赖的 JavaScript、CSS、图片、字体等资源继续通过静态文件服务加载,因此可以直接运行常见的纯静态前端项目。 - -### 多项目导航 - -当根目录没有 `index.html` 时,Stash 会扫描根目录下的一级子目录,并将包含 `index.html` 或 `index.htm` 的目录识别为前端项目。 - -- 自动排除以 `.` 开头的隐藏目录。 -- 项目按名称进行不区分大小写的升序排列。 -- 每个项目生成一个卡片链接,地址格式为 `/<项目目录名>/`。 -- 项目名称首字符会作为圆形标记显示。 -- 没有识别到项目时显示空状态提示。 -- 导航页采用 4 列、2 列、1 列的响应式布局,分别适配桌面、平板和手机。 -- 项目和目录列表在每次请求时重新扫描,新增项目通常无需重启服务。 -- 文件监听会在项目快照发生变化后主动通知已打开的导航页,无需手动刷新。 - -### 静态文件服务 - -Stash 使用 `tower-http` 的 `ServeDir` 托管根目录内容: - -- 自动为目录追加 `index.html`。 -- 根据文件扩展名返回对应的 MIME 类型。 -- 未找到的路径会返回内置的中文 404 页面,并显示请求路径。 -- 每次 HTTP 请求都会经过 Trace 层,可通过调整日志级别观察请求日志。 -- 内置控制台资源通过保留路径 `/__stash__/` 提供,避免与用户托管的项目文件冲突。 - -### 内置控制台 - -- 使用 Askama 在编译期渲染项目导航页和 404 页面。 -- 页面 HTML、CSS 和 JavaScript 分别维护,不再使用字符串替换拼接卡片。 -- 项目名称和请求路径由模板引擎自动进行 HTML 转义。 -- 项目目录名会进行 URL 路径段编码,再生成项目访问地址。 -- 少量 JavaScript 通过 SSE 订阅项目变化,并在不刷新整个页面的情况下更新卡片;脚本不可用时,服务端渲染的导航和手动 API 仍可正常使用。 - -### 同源 API - -服务提供两个只读 JSON 接口和一个 SSE 推送接口,可供导航页或同源前端调用。 - -### CORS - -默认允许任意来源、任意请求方法和任意请求头访问服务,适合本地联调。通过 `--no-cors` 可以关闭 CORS 标头注入。 - -### 服务管理 - -- 默认监听 `127.0.0.1:8080`。 -- 支持自定义绑定地址、端口和托管目录。 -- 默认托管目录 `./public` 不存在时会自动创建。 -- 用户显式指定的托管目录不存在时,会输出错误并退出。 -- 端口绑定失败、目录无效等错误会以清晰的错误信息返回。 -- 支持分别通过 `Ctrl+C` 以及 Unix 系统的 `SIGTERM` 触发优雅停机。 - -### 可复用核心库 - -`stash-core` 同时作为库导出 `Config`、`StashError`、`start`、`ServerHandle`、`scan_projects` 和 `AppProject`,既可由 `stash-cli` 使用,也可以被其他 Rust 程序嵌入或复用。 +它能做两件事:托管目录下的多个静态前端项目、给控制台提供同源 JSON / SSE 接口。 ## 快速开始 -### 环境要求 +```bash +# 1. 先构建前端(dist 会被内嵌,缺了它 cargo build 会直接失败) +cd stash-web +npm install +npm run build +cd .. -- Rust 工具链,需支持 Rust 2024 Edition。 -- Cargo。 +# 2. 编译并运行 +cargo run # 托管 ./public,监听 127.0.0.1:8080 -### 启动示例 +# 自定义目录 / 端口 / 对外暴露 +cargo run -- --root ./sites --port 3000 +cargo run -- --root ./sites --host 0.0.0.0 --port 3000 +``` + +启动后打开终端里打印的地址即可,例如 `http://127.0.0.1:8080/`。 +默认托管目录 `./public` 不存在时会自动创建。 + +环境要求: + +- Rust 工具链(2024 Edition) +- Node.js 24,推荐用 [Volta](https://volta.sh/) 管理;`stash-web/package.json` 里已用 `volta` 字段固定版本 + +## 工作方式 + +请求按下面的顺序命中: + +| 路径 | 处理方式 | +| --- | --- | +| `/api/*` | JSON / SSE 接口 | +| `/`、`/index.html` | 内嵌前端的 `index.html`(`no-cache`) | +| `/assets/**` | 内嵌前端的静态资源(文件名带 hash,长缓存) | +| `/favicon.svg` | 内嵌前端根目录文件 | +| 其他路径 | `ServeDir` 托管 `--root` 目录,目录自动补 `index.html` | +| 仍未命中 | 内置 404 页面(显示请求路径) | + +托管目录下的每个一级子目录,只要包含 `index.html` 或 `index.htm`,就会被识别为一个项目, +通过 `/<目录名>/` 访问。目录名会做 URL 路径段编码。 + +## 目录结构 + +```text +. +├── Cargo.toml # 单 crate(lib + bin),依赖清单 +├── Cargo.lock +├── build.rs # 把 stash-web/dist 同步到 $OUT_DIR/web +├── src/ # Rust 后端 +│ ├── main.rs # CLI 参数、启动横幅、停机信号 +│ ├── lib.rs # 库导出 +│ ├── config.rs # 服务配置 +│ ├── error.rs # 错误类型与 JSON 错误响应 +│ ├── server.rs # 路由、内嵌静态资源、SSE、生命周期 +│ ├── listing.rs # 托管目录扫描 +│ └── watcher.rs # 文件监听、防抖、快照广播 +├── stash-web/ # React + Vite + TypeScript 控制台 +│ ├── package.json # 含 volta 版本固定 +│ ├── vite.config.ts # 开发期 /api 代理到 127.0.0.1:8080 +│ └── src/ +└── public/ # 默认托管目录(可放多个前端项目) +``` + +`templates/` 与 `assets/` 是上一版 Askama 控制台留下的文件,当前已无任何代码引用, +可以直接删除。 + +## 前端怎么被内嵌 + +1. `npm run build` 产出 `stash-web/dist`; +2. `build.rs` 在编译 Rust 之前把 `dist` 复制到 `$OUT_DIR/web`(先清空,避免旧 hash 文件残留); +3. `src/server.rs` 用 `include_dir!("$OUT_DIR/web")` 在编译期把整棵目录树嵌进二进制; +4. 二进制运行时不再读取任何前端文件。 + +构建顺序不能颠倒:`dist` 不存在时 `build.rs` 会带着提示信息直接失败,而不是产出一个没有界面的二进制。 + +`build.rs` 声明了 `rerun-if-changed`,改动 `dist` 后重新 `cargo build` 会自动重新嵌入。 + +## 前端开发 ```bash -# 托管默认目录 ./public,监听 127.0.0.1:8080 -cargo run -p stash-cli - -# 托管指定目录并修改端口 -cargo run -p stash-cli -- --root ./sites --port 3000 - -# 允许局域网设备访问 -cargo run -p stash-cli -- --root ./sites --host 0.0.0.0 --port 3000 - -# 关闭 CORS -cargo run -p stash-cli -- --root ./sites --no-cors +cd stash-web +npm install +npm run dev # http://127.0.0.1:5173,/api 自动代理到后端 8080 +npm run build # 产物输出到 stash-web/dist +npm run lint # oxlint ``` -启动成功后访问终端中显示的地址即可,例如: - -```text -http://127.0.0.1:8080/ -``` - -### 构建发布版本 - -```bash -cargo build --release -p stash-cli -``` - -构建产物为 `target/release/stash`;Windows 下为 `target/release/stash.exe`。 - -## 托管目录示例 - -### 单个静态站点 - -```text -public/ -├── index.html -├── assets/ -│ ├── app.js -│ └── style.css -└── images/ - └── logo.png -``` - -此时访问 `/` 会直接展示 `public/index.html`。 - -### 多个前端项目 - -```text -public/ -├── admin-console/ -│ └── index.html -├── docs/ -│ └── index.html -└── landing-page/ - └── index.html -``` - -由于根目录没有 `index.html`,访问 `/` 时会展示项目导航页。对应访问地址为: - -```text -/admin-console/ -/docs/ -/landing-page/ -``` +开发时前后端分开跑:一个终端 `cargo run`,另一个终端 `npm run dev`。 +控制台的用户资料目前是占位数据(见 `stash-web/src/profile.ts`),后端尚未提供鉴权接口。 ## 命令行参数 | 参数 | 默认值 | 说明 | | --- | --- | --- | -| `-r, --root ` | `./public` | 静态页面托管根目录,支持相对路径和绝对路径 | -| `-p, --port ` | `8080` | HTTP 服务监听端口 | -| `--host ` | `127.0.0.1` | 监听地址;使用 `0.0.0.0` 可供局域网访问 | -| `--no-listing` | 关闭 | 设计用于关闭根目录项目导航页 | +| `-r, --root ` | `./public` | 静态页面托管根目录 | +| `-p, --port ` | `8080` | 监听端口 | +| `--host ` | `127.0.0.1` | 监听地址,`0.0.0.0` 供局域网访问 | | `--no-cors` | 关闭 | 关闭全局 CORS 标头注入 | -| `-h, --help` | - | 查看命令行帮助 | -| `-V, --version` | - | 查看程序版本 | +| `-h, --help` | - | 帮助 | +| `-V, --version` | - | 版本 | -注意:`--no-listing` 当前已由命令行解析并写入 `Config.directory_listing`,但根路径处理器尚未读取该配置。因此根目录没有 `index.html` 时,导航页目前仍会显示。 +`--no-listing` 参数目前仍能被解析,但由于控制台已改为内嵌前端,它不再影响任何行为。 ## HTTP API @@ -155,40 +116,25 @@ public/ GET /api/health ``` -响应: - ```json -{ - "status": "ok", - "service": "stash" -} +{ "status": "ok", "service": "stash" } ``` -### 获取项目列表 +### 项目列表 ```http GET /api/projects ``` -响应示例: - ```json { "projects": [ - { - "name": "admin-console", - "route_path": "/admin-console/" - }, - { - "name": "docs", - "route_path": "/docs/" - } + { "name": "admin-console", "route_path": "/admin-console/" }, + { "name": "docs", "route_path": "/docs/" } ] } ``` -该接口与导航页使用相同的项目扫描规则。即使托管根目录包含 `index.html`,接口仍会返回当前根目录下可识别的项目列表。 - ### 订阅项目变化 ```http @@ -196,60 +142,29 @@ GET /api/projects/events Accept: text/event-stream ``` -连接建立后,服务会立即发送当前项目快照,随后在文件监听检测到项目列表变化时推送新快照: +连接建立后立即推送一次当前快照,之后在项目列表变化时推送: ```text event: projects data: {"projects":[{"name":"docs","route_path":"/docs/"}]} ``` -服务端会递归监听托管目录,将短时间内的连续文件事件合并为一次扫描,并且只在项目名称或访问路径实际变化时广播。SSE 连接每 15 秒发送一次保活消息,浏览器断开后会自动重连。 - -## 项目结构 - -```text -. -├── Cargo.toml # Rust workspace 配置 -├── Cargo.lock # 依赖锁定文件 -├── README.md -├── stash-cli/ -│ ├── Cargo.toml -│ └── src/ -│ └── main.rs # CLI 参数解析、启动信息和停机信号 -└── stash-core/ - ├── Cargo.toml - ├── assets/ - │ ├── dashboard.css # 控制台与 404 页面样式 - │ └── dashboard.js # 项目列表刷新增强 - ├── src/ - │ ├── config.rs # 服务配置 - │ ├── error.rs # 错误类型与 HTTP 错误响应 - │ ├── lib.rs # 核心库导出 - │ ├── listing.rs # 项目扫描与页面渲染 - │ ├── server.rs # Axum 路由、SSE、静态托管与服务器生命周期 - │ └── watcher.rs # 文件监听、事件防抖和项目快照广播 - └── templates/ - ├── index.html # Askama 多项目导航页模板 - └── 404.html # Askama 404 页面模板 -``` +服务端递归监听托管目录,把短时间内的连续事件合并成一次扫描,只在项目名称或访问路径真正变化时广播; +每 15 秒发送一次保活消息,浏览器断开后自动重连。 ## 测试 -运行核心库测试: - ```bash -cargo test -p stash-core +cargo test # 目录扫描、隐藏目录过滤、排序、路径编码、文件监听广播 +cd stash-web && npm run build # tsc -b + vite build,同时做类型检查 ``` -当前测试覆盖项目目录扫描、隐藏目录过滤、一级目录限制、项目排序、项目路径编码、空状态渲染、项目内容 HTML 转义、404 页面渲染与路径转义,以及新增项目后的文件监听广播。 - ## 已知限制 -- 项目扫描只检查托管根目录的一级子目录,不递归查找更深层项目。 -- 根目录存在 `index.html` 时会优先进入单站点模式,不再展示项目导航页。 -- 静态服务只为目录自动追加 `index.html`;导航扫描虽然也识别 `index.htm`,但建议项目统一使用 `index.html`。 -- 未配置 SPA History API 回退。刷新前端路由等不存在的物理路径时,会返回 404 页面。 -- SSE 仅用于服务端向浏览器推送项目快照,不提供浏览器向服务端发送命令的双向通道。 -- 文件事件会经过约 150ms 防抖后再扫描,因此项目出现在导航页中可能有短暂延迟。 -- 启动横幅中显示的接口地址为 `/api/pages`,实际实现的项目列表接口是 `/api/projects`。 -- 服务没有身份认证机制,默认仅建议绑定 `127.0.0.1`。使用 `0.0.0.0` 暴露到局域网前,应确认目录内容适合被其他设备访问。 +- 控制台是单页应用但没有客户端路由,因此没有 SPA history 回退;刷新不存在的物理路径会返回 404。 +- 内嵌资源占用了 `/assets/` 前缀,托管目录下如果存在同名目录会被遮蔽。 +- `/` 固定由控制台占用,上一版「根目录有 `index.html` 就直接当单站点展示」的行为已取消; + 托管目录里的项目统一走 `/<目录名>/`。 +- 项目扫描只检查一级子目录,不递归查找更深层项目。 +- 文件事件经过约 150ms 防抖,新项目出现在控制台可能有短暂延迟。 +- 没有身份认证,默认只建议绑定 `127.0.0.1`;使用 `0.0.0.0` 暴露到局域网前请确认目录内容适合被其他设备访问。 diff --git a/stash-web/index.html b/stash-web/index.html index 25e12d7..88c1559 100644 --- a/stash-web/index.html +++ b/stash-web/index.html @@ -5,6 +5,12 @@ + + Stash · 本地应用工作区 diff --git a/stash-web/src/App.css b/stash-web/src/App.css index e5b31e2..ad0c908 100644 --- a/stash-web/src/App.css +++ b/stash-web/src/App.css @@ -1,4 +1,35 @@ -/* ============ 基础 ============ */ +/* ============================================================ + 页面背景:复刻 Alright 的 .site-background + ============================================================ */ + +.site-background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 65vh; + background-image: url('./assets/site-bg.jpg'); + background-size: cover; + background-position: top center; + background-repeat: no-repeat; + pointer-events: none; + z-index: 0; +} + +/* 底部 80% 高度做一层「底色 → 透明」的渐变,让图片自然融进页面背景 */ +.site-background::after { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 80%; + background-image: linear-gradient(0deg, var(--bg-color) 0%, rgba(0, 0, 0, 0) 100%); +} + +/* ============================================================ + 全局容器 + ============================================================ */ .app { height: 100%; @@ -6,6 +37,14 @@ flex-direction: column; } +/* 版心:顶栏与内容区共用,保证左右边界一?*/ +.layout-container { + width: 100%; + max-width: var(--layout-max-width); + margin: 0 auto; + padding: 0 20px; +} + .card-box { background-color: var(--card-bg); border: 1px solid var(--border-color); @@ -26,9 +65,11 @@ border: 0; } -/* ============ 顶栏 ============ */ +/* ============================================================ + 顶部栏:品牌 + 运行信息 + 操作图标 + ============================================================ */ -.topbar { +.site-header { position: fixed; top: 0; left: 0; @@ -39,36 +80,26 @@ z-index: 200; } -/* 顶栏内容与下方 .layout 使用同一套宽度与左右内边距, - 这样 logo、右侧图标才能和侧栏、内容区严格对齐 */ -.topbar-inner { - width: 100%; - max-width: var(--layout-max-width); +.site-header-inner { height: 100%; - margin: 0 auto; - padding: 0 24px 0 20px; display: flex; align-items: center; justify-content: space-between; gap: 16px; } -.brand { +.site-brand { display: flex; align-items: center; gap: 10px; - width: 180px; flex-shrink: 0; - font-size: 18px; - font-weight: 800; - color: #0f172a; } -.brand-mark { +.site-brand-mark { width: 32px; height: 32px; border-radius: 50%; - background-color: #0f172a; + background-color: var(--text-main); color: #fff; display: flex; align-items: center; @@ -77,85 +108,60 @@ font-weight: 700; } -.brand-name { +.site-brand-name { + font-size: 18px; + font-weight: 800; letter-spacing: 0.04em; + color: var(--text-main); } -/* 跑马灯 */ +/* 中间运行信息 */ -.ticker { - flex: 1; - max-width: 560px; - height: 36px; - margin: 0 20px; - overflow: hidden; - position: relative; +.header-info { display: flex; align-items: center; - -webkit-mask-image: linear-gradient( - to right, - transparent, - #000 10%, - #000 90%, - transparent - ); - mask-image: linear-gradient( - to right, - transparent, - #000 10%, - #000 90%, - transparent - ); -} - -.ticker-track { - display: flex; - align-items: center; - gap: 40px; - white-space: nowrap; - animation: ticker 26s linear infinite; -} - -.ticker:hover .ticker-track { - animation-play-state: paused; -} - -.ticker-item { - display: flex; - align-items: center; - gap: 6px; - font-size: 13.5px; + gap: 20px; + min-width: 0; + font-size: 14px; color: var(--text-sub); } -.ticker-tag { - padding: 2px 6px; +.header-info-item { + display: flex; + align-items: center; + gap: 6px; + white-space: nowrap; +} + +.header-info-tag { + padding: 2px 8px; border-radius: 4px; background-color: var(--primary-soft); color: var(--primary-color); - font-size: 11px; - font-weight: 600; + font-size: 12px; } -@keyframes ticker { - from { - transform: translateX(0); - } - to { - transform: translateX(-50%); - } +.header-info-tag.is-green { + background-color: rgba(74, 189, 134, 0.12); + color: var(--green); } -/* 顶栏操作 */ +.header-info-text { + overflow: hidden; + text-overflow: ellipsis; +} -.top-actions { +/* 右侧图标 */ + +.header-actions { display: flex; align-items: center; - gap: 12px; + gap: 16px; flex-shrink: 0; + color: var(--text-sub); } -.icon-button { +.header-icon-button { width: 34px; height: 34px; display: flex; @@ -163,41 +169,25 @@ justify-content: center; padding: 6px; border-radius: 50%; - color: #475569; cursor: pointer; transition: background-color 0.2s, color 0.2s; } -.icon-button:hover { - background-color: #f1f5f9; - color: var(--primary-color); +.header-icon-button:hover { + background-color: var(--border-strong); + color: var(--text-main); } -.icon-button svg { - width: 19px; - height: 19px; - stroke: currentColor; - fill: none; - stroke-width: 2; - stroke-linecap: round; - stroke-linejoin: round; +.header-icon-button .iconfont { + font-size: 19px; } -.icon-button[data-spinning='true'] svg, -.service-action[data-spinning='true'] svg { - animation: spin 0.9s linear infinite; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -/* ============ 搜索框 ============ */ +/* ============================================================ + 搜索框:固定悬浮在顶栏下方居? ============================================================ */ +/* 固定悬浮、相对整页居中,与设计稿一?*/ .search-row { position: fixed; top: 76px; @@ -218,13 +208,8 @@ .search-icon { flex-shrink: 0; - width: 16px; - height: 16px; + font-size: 16px; color: var(--text-sub); - stroke: currentColor; - fill: none; - stroke-width: 2; - stroke-linecap: round; } .search-input { @@ -234,7 +219,7 @@ border: none; outline: none; background: transparent; - font-size: 13.5px; + font-size: 14px; color: var(--text-main); } @@ -258,45 +243,42 @@ } .search-clear:hover { - background-color: #f1f5f9; + background-color: var(--border-strong); color: var(--text-main); } -.search-clear svg { - width: 13px; - height: 13px; - stroke: currentColor; - fill: none; - stroke-width: 2.4; - stroke-linecap: round; +.search-clear .iconfont { + font-size: 14px; } -/* ============ 双列布局 ============ */ +/* ============================================================ + 双列布局:左侧栏 + 右侧内容 + ============================================================ */ .layout { + position: relative; + z-index: 1; width: 100%; max-width: var(--layout-max-width); - /* 宽屏时整体居中,避免内容全部贴左 */ margin: var(--top-nav-height) auto 0; height: calc(100vh - var(--top-nav-height)); - padding: 36px 24px 0 20px; + padding: 36px 20px 0; display: grid; grid-template-columns: var(--sidebar-width) minmax(0, 1fr); gap: 20px; align-items: stretch; } -/* 左侧栏 */ - .sidebar { display: flex; flex-direction: column; gap: 16px; min-height: 0; - margin-top: 68px; + /* 与右侧首个分区卡片对?*/ + margin-top: var(--content-top-offset); } -/* 用户资料模块(数据占位,见 profile.ts) */ +/* ---------- 用户资料?---------- */ .profile-card { display: flex; @@ -316,17 +298,12 @@ border-radius: 50%; display: grid; place-items: center; - background: linear-gradient(135deg, #dbeafe, #bfdbfe); - color: #1d4ed8; + background: linear-gradient(135deg, #dfe4ff, #c9d0ff); + color: var(--primary-color); } -.profile-avatar svg { - width: 34px; - height: 34px; - stroke: currentColor; - fill: none; - stroke-width: 1.8; - stroke-linecap: round; +.profile-avatar .iconfont { + font-size: 34px; } .profile-badge { @@ -337,7 +314,7 @@ height: 20px; border-radius: 50%; border: 2px solid var(--card-bg); - background-color: #f59e0b; + background-color: var(--orange); color: #fff; display: grid; place-items: center; @@ -356,132 +333,24 @@ padding: 1px 8px; border: 1px solid var(--border-color); border-radius: 999px; - font-size: 11px; + font-size: 12px; color: var(--text-sub); } .profile-bio { margin: 2px 0 0; - font-size: 11.5px; + font-size: 12px; line-height: 1.55; color: var(--text-sub); } .profile-location { margin-top: 2px; - font-size: 11px; + font-size: 12px; color: var(--text-muted); } -.nav-card { - padding: 8px; - flex-shrink: 0; -} - -.nav-group { - list-style: none; -} - -.nav-item { - margin-bottom: 4px; -} - -.nav-item:last-child { - margin-bottom: 0; -} - -.nav-link { - width: 100%; - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - padding: 9px 12px; - border-radius: 8px; - font-size: 13.5px; - font-weight: 500; - color: var(--text-sub); - cursor: pointer; - text-align: left; - transition: - background-color 0.2s, - color 0.2s; -} - -.nav-link:hover, -.nav-link.active { - background-color: var(--primary-soft); - color: var(--primary-color); -} - -.nav-count { - padding: 1px 6px; - border-radius: 10px; - background-color: #f1f5f9; - color: var(--text-muted); - font-size: 11px; - font-weight: 600; -} - -.nav-trailing { - display: flex; - align-items: center; - gap: 8px; - flex-shrink: 0; -} - -.nav-link.active .nav-count, -.nav-link:hover .nav-count { - background-color: #fff; - color: var(--primary-color); -} - -.nav-arrow { - color: var(--text-muted); - font-weight: 700; - line-height: 1; - transition: transform 0.3s; -} - -.nav-item.open > .nav-link .nav-arrow { - transform: rotate(90deg); -} - -.submenu { - list-style: none; - padding-left: 12px; - max-height: 0; - overflow: hidden; - transition: max-height 0.3s cubic-bezier(0, 1, 0, 1); -} - -.nav-item.open > .submenu { - max-height: 320px; - overflow-y: auto; - transition: max-height 0.3s ease-in-out; - scrollbar-width: thin; -} - -.submenu .nav-link { - padding: 7px 12px; - font-size: 12.5px; -} - -.submenu .nav-link > span:first-child { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.nav-empty { - color: var(--text-muted); - cursor: default; - font-size: 12.5px; - padding: 7px 12px; - display: block; -} - -/* 本地服务面板:独立模块,不与导航项混排 */ +/* ---------- 本地服务面板 ---------- */ .service-panel { padding: 12px 14px; @@ -496,7 +365,7 @@ } .service-title { - font-size: 13px; + font-size: 14px; font-weight: 600; color: var(--text-main); } @@ -507,14 +376,14 @@ gap: 6px; padding: 2px 8px; border-radius: 999px; - background-color: #f1f5f9; - font-size: 11px; + background-color: var(--border-strong); + font-size: 12px; color: var(--text-sub); } .service-pill.is-on { background-color: rgba(34, 197, 94, 0.12); - color: #15803d; + color: var(--green); } .service-stats { @@ -540,14 +409,13 @@ margin: 0; overflow: hidden; font-family: var(--font-mono); - font-size: 11.5px; + font-size: 12px; font-weight: 600; color: var(--text-main); text-overflow: ellipsis; white-space: nowrap; } -/* 面板底部操作:重新扫描 / 查看接口 */ .service-actions { display: flex; gap: 8px; @@ -585,22 +453,15 @@ cursor: default; } -.service-action svg { - width: 13px; - height: 13px; - flex-shrink: 0; - stroke: currentColor; - fill: none; - stroke-width: 2; - stroke-linecap: round; - stroke-linejoin: round; +.service-action .iconfont { + font-size: 14px; } .status-dot { width: 8px; height: 8px; border-radius: 50%; - background-color: #cbd5e1; + background-color: var(--border-strong); flex-shrink: 0; transition: background-color 0.2s, @@ -608,35 +469,144 @@ } .status-dot.is-on { - background-color: #22c55e; + background-color: var(--green); box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.16); } -/* 右侧内容 */ +/* ---------- 分类导航 ---------- */ + +.nav-card { + padding: 8px; + flex-shrink: 0; +} + +.nav-group { + list-style: none; +} + +.nav-item { + margin-bottom: 4px; +} + +.nav-item:last-child { + margin-bottom: 0; +} + +.nav-link { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 9px 12px; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + color: var(--text-sub); + cursor: pointer; + text-align: left; + transition: + background-color 0.2s, + color 0.2s; +} + +.nav-link:hover, +.nav-link.active { + background-color: var(--primary-soft); + color: var(--primary-color); +} + +/* 折叠箭头:复刻主题的 6×6 边框 V 形,靠右对齐 */ +.nav-link.has-children::after { + content: ''; + flex-shrink: 0; + width: 6px; + height: 6px; + border-left: 1px solid currentColor; + border-bottom: 1px solid currentColor; + opacity: 0.4; + transform: rotate(315deg); + transition: + transform 0.3s, + opacity 0.3s; +} + +.nav-link.has-children:hover::after { + opacity: 0.7; +} + +.nav-item.open > .nav-link.has-children::after { + opacity: 0.7; + transform: rotate(135deg); +} + +/* 子菜单展开:用 grid-template-rows 0fr → 1fr 做高度动画, + 高度精确贴合内容,全程 overflow: hidden,不会出现滚动条 */ +.submenu-wrap { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 0.28s ease; +} + +.submenu-wrap.is-open { + grid-template-rows: 1fr; +} + +.submenu { + list-style: none; + min-height: 0; + padding-left: 12px; + overflow: hidden; +} + +.submenu .nav-link { + padding: 7px 12px; + font-size: 12px; +} + +.submenu .nav-link > span:first-child { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ============================================================ + 右侧内容? ============================================================ */ .content { + position: relative; display: flex; flex-direction: column; min-height: 0; - padding-top: 52px; + /* 让出悬浮的搜索框;侧栏与遮罩层都按同一个变量对?*/ + padding-top: var(--content-top-offset); +} + +/* 滚动后才淡入的顶部渐隐层? 卡片滚到容器顶边时被硬切会很难看,用一层背景色渐变把它柔化?*/ +.content-veil { + position: absolute; + top: var(--content-top-offset); + right: 0; + left: 0; + height: 32px; + pointer-events: none; + opacity: 0; + background: linear-gradient(to bottom, var(--bg-color) 0%, transparent 100%); + transition: opacity 0.2s ease; + z-index: 5; +} + +.content-veil.is-visible { + opacity: 1; } .content-scroll { flex: 1; min-height: 0; overflow-y: auto; - /* 顶部留出渐隐区,避免第一个分区标题被 mask 吃掉 */ - padding-top: 24px; padding-right: 4px; padding-bottom: 40px; scroll-behavior: smooth; - -webkit-mask-image: linear-gradient( - to bottom, - transparent 0, - #000 22px, - #000 100% - ); - mask-image: linear-gradient(to bottom, transparent 0, #000 22px, #000 100%); -ms-overflow-style: none; scrollbar-width: none; } @@ -646,103 +616,829 @@ width: 0; } -.section-heading { +/* ---------- 首页:分区卡?---------- */ + +.home-view { display: flex; - align-items: baseline; + flex-direction: column; + gap: 20px; +} + +.section-card { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 20px; + box-shadow: var(--shadow-card); +} + +.section-header { + display: flex; + align-items: center; justify-content: space-between; - gap: 12px; margin-bottom: 16px; } -.section-title { +.section-title-wrapper { display: flex; align-items: center; - font-size: 15px; - font-weight: 600; - color: var(--text-main); - line-height: 1; } -.section-title::before { - content: ''; +.section-bar { width: 4px; - height: 15px; + height: 16px; margin-right: 8px; border-radius: 2px; background-color: var(--primary-color); } -/* 右上角数量角标:粉色渐变胶囊 */ -.section-badge { - flex-shrink: 0; +/* 标题容器设为 relative,角标才能贴在标题右上角 */ +.section-title-box { + position: relative; display: inline-flex; align-items: center; +} + +.section-title { + font-size: 16px; + font-weight: 600; + line-height: 1.2; + color: var(--text-main); +} + +.count-badge { + position: absolute; + top: -8px; + right: -18px; + display: flex; + align-items: center; justify-content: center; - min-width: 26px; - height: 20px; - padding: 0 9px; - border-radius: 999px; - background-image: linear-gradient(90deg, #ff8a8a 0%, #f42a63 100%); - box-shadow: 0 2px 6px rgba(244, 42, 99, 0.28); + height: 16px; + min-width: 16px; + padding: 0 4px; + border-radius: 8px; + background-color: var(--danger); + box-shadow: 0 1px 3px rgba(255, 77, 79, 0.3); color: #fff; font-size: 12px; font-weight: 700; line-height: 1; - letter-spacing: 0.02em; } -.section-group + .section-group { - margin-top: 28px; +.section-more { + display: inline-flex; + align-items: center; + padding: 4px 6px; + border-radius: 4px; + color: var(--text-muted); + font-size: 16px; + line-height: 1; + cursor: pointer; + transition: + background-color 0.2s, + color 0.2s; } -.grid { +.section-more:hover { + background-color: var(--border-color); + color: var(--text-sub); +} + +.section-empty { + padding: 8px 0; + font-size: 14px; + color: var(--text-sub); +} + +/* ---------- 应用网格 ---------- */ + +.app-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.app-grid--detail { gap: 16px; } -.card { +.app-card { display: flex; align-items: center; - padding: 12px 14px; - border: 1px solid var(--border-color); - border-radius: 10px; - background-color: var(--card-bg); - box-shadow: var(--shadow-card); - transition: - transform 0.2s, - box-shadow 0.2s, - border-color 0.2s; - animation: card-in 0.28s ease backwards; + gap: 15px; + padding: 6px 0; + /* 和主题的作者条目一样:没有底色、描边、阴影,就是一行内容 */ + background: none; + border: 0; + color: inherit; } -.card:hover { - transform: translateY(-2px); - border-color: rgba(59, 130, 246, 0.35); - box-shadow: var(--shadow-hover); +.app-card:hover .app-card-name { + color: var(--primary-color); } -.card-icon { - width: 36px; - height: 36px; - margin-right: 12px; +/* 48px 圆形头像 + 右下角认证角标,对应主题 .flex-avatar / .avatar-badge.badge_5 */ +.app-card-avatar { + position: relative; + width: 48px; + height: 48px; + flex-shrink: 0; + border-radius: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--primary-soft); + color: var(--primary-color); + font-size: 18px; + font-weight: 700; +} + +/* 有封面图时直接铺满圆形头像 */ +.app-card-avatar img { + width: 100%; + height: 100%; + border-radius: inherit; + object-fit: cover; + display: block; +} + +.app-card-verify { + position: absolute; + right: 0; + bottom: 0; + width: 10px; + height: 10px; + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgZmlsbD0ibm9uZSI+PGNpcmNsZSBjeD0iMTgiIGN5PSIxOCIgcj0iMTgiIGZpbGw9InVybCgjYSkiLz48cGF0aCBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yOC4yNSAxMS44NDRhMi4yMzIgMi4yMzIgMCAwIDEgMCAzLjE1NkwxNy41ODggMjUuNjYyYTIuMzggMi4zOCAwIDAgMS0zLjM2NiAwbC01LjY0NC01LjY0NGEyLjIzMiAyLjIzMiAwIDAgMSAzLjE1Ni0zLjE1NWw0LjE3IDQuMTcgOS4xOS05LjE4OWEyLjIzMiAyLjIzMiAwIDAgMSAzLjE1NiAwWiIgY2xpcC1ydWxlPSJldmVub2RkIi8+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJhIiB4MT0iMzEuNzE0IiB4Mj0iLTMuNzMxIiB5MT0iLjg1NyIgeTI9IjExLjgzOSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiNGRkI3NEEiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNGRDkwMEYiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48L3N2Zz4="); + background-position: center; + background-repeat: no-repeat; + background-size: contain; +} + +.app-card-info { + display: flex; + flex-direction: column; + justify-content: center; + min-width: 0; + flex: 1 1 auto; + overflow: hidden; +} + +.app-card-title { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; +} + +.app-card-name { + font-size: 16px; + font-weight: 500; + line-height: 24px; + color: var(--text-main); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 名字后的 HOT 徽标:主题里是 24×16 的图形,不是文字 */ +.app-card-badge { + position: relative; + top: 2px; + display: inline-block; + flex-shrink: 0; + width: 24px; + height: 16px; + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MCIgaGVpZ2h0PSIyMSIgZmlsbD0ibm9uZSI+PHBhdGggZmlsbD0idXJsKCNhKSIgZD0iTTAgNGE0IDQgMCAwIDEgNC00aDQyYTQgNCAwIDAgMSA0IDR2MTNhNCA0IDAgMCAxLTQgNEg0YTQgNCAwIDAgMS00LTRWNFoiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNi4yODYgOS4zNDVoOC4wM1Y0LjE3NmgyLjI4NnYxMi42NDhoLTIuMjg3di01LjE2OUg2LjI4NnY1LjE2OUg0VjQuMTc2aDIuMjg2djUuMTY5Wk0zMi41MTkgMTEuNzA4YzAgLjc3LS4xMzIgMS40NzktLjM5NiAyLjEyNmE1LjAzOSA1LjAzOSAwIDAgMS0xLjEgMS42NzUgNS4wMiA1LjAyIDAgMCAxLTEuNjcgMS4xMDNBNS41MjYgNS41MjYgMCAwIDEgMjcuMjYgMTdoLTMuMDk1Yy0uNzU3IDAtMS40NTctLjEzLTIuMTAyLS4zODhhNS4wMiA1LjAyIDAgMCAxLTEuNjcxLTEuMTAyIDUuMTUyIDUuMTUyIDAgMCAxLTEuMTA4LTEuNjc2IDUuNTc2IDUuNTc2IDAgMCAxLS4zOTYtMi4xMjZWOS4yOTJjMC0uNzY1LjEzMi0xLjQ3LjM5Ni0yLjExN2E1LjAzIDUuMDMgMCAwIDEgMS4xMDgtMS42NzYgNS4wMjIgNS4wMjIgMCAwIDEgMS42Ny0xLjEwMkE1LjQ4NCA1LjQ4NCAwIDAgMSAyNC4xNjYgNGgzLjA5NWMuNzU2IDAgMS40NTQuMTMyIDIuMDkzLjM5N2E1LjAyMiA1LjAyMiAwIDAgMSAxLjY3IDEuMTAyIDQuOTIyIDQuOTIyIDAgMCAxIDEuMSAxLjY3NmMuMjY0LjY0Ny4zOTYgMS4zNTIuMzk2IDIuMTE3djIuNDE2Wm0tMi4yODctMi40MTZjMC0uNDUzLS4wNzMtLjg2Mi0uMjItMS4yMjZhMi41ODkgMi41ODkgMCAwIDAtLjYwNi0uOTQ0IDIuNTggMi41OCAwIDAgMC0uOTQxLS42MDggMy4xODYgMy4xODYgMCAwIDAtMS4yMDUtLjIyaC0zLjA5NWMtLjQ0NiAwLS44NTMuMDczLTEuMjIzLjIyYTIuNjgxIDIuNjgxIDAgMCAwLS45NC42MDhjLS4yNjUuMjU5LS40Ny41NzMtLjYxNi45NDQtLjE0MS4zNjQtLjIxMS43NzMtLjIxMSAxLjIyNnYyLjQxNmMwIC40NTMuMDcuODY1LjIxIDEuMjM1LjE0Ny4zNjUuMzUyLjY4LjYxNi45NDQuMjY0LjI1OC41NzguNDYxLjk0MS42MDhhMy40IDMuNCAwIDAgMCAxLjIyMy4yMTJoMy4wNzhjLjQ0NSAwIC44NS0uMDcgMS4yMTMtLjIxMi4zNy0uMTQ3LjY4Ni0uMzUuOTUtLjYwOC4yNjQtLjI2NS40NjYtLjU4LjYwNy0uOTQ0LjE0Ni0uMzcuMjItLjc4Mi4yMi0xLjIzNVY5LjI5MlpNNDYgNi40N2gtNS4wMzl2MTAuMzU0aC0yLjI4NlY2LjQ2OWgtNS4wNDhWNC4xNzZINDZWNi40N1oiLz48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImEiIHgxPSI0NC4wNDgiIHgyPSI5LjEwNyIgeTE9Ii41IiB5Mj0iMjYuMjc2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agc3RvcC1jb2xvcj0iI0ZGN0I1MSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0ZGMEUwRSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjwvc3ZnPg=="); + background-position: center; + background-repeat: no-repeat; + background-size: contain; +} + +.app-card-desc { + font-size: 12px; + line-height: 16px; + color: var(--text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ---------- 海报网格 ---------- */ + +.poster-grid { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 12px; +} + +.poster-grid--detail { + gap: 14px; +} + +.poster-card { + position: relative; + display: block; + width: 100%; + margin: 0; + padding: 0; + border: none; + aspect-ratio: 2 / 3; border-radius: 8px; - background-color: #f1f5f9; + overflow: hidden; + background-color: var(--border-strong); + text-align: left; + font: inherit; + cursor: pointer; + transition: transform 0.2s ease; +} + +.poster-card:hover { + transform: translateY(-2px); +} + +.poster-cover { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.poster-caption { + position: absolute; + right: 0; + bottom: 0; + left: 0; + padding: 20px 8px 8px; + background: linear-gradient( + to top, + rgba(0, 0, 0, 0.85) 0%, + rgba(0, 0, 0, 0.4) 60%, + transparent 100% + ); + color: #fff; +} + +.poster-title { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + font-size: 12px; + font-weight: 500; + line-height: 1.3; +} + +/* ---------- 首页图文网格 ---------- */ + +.article-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.article-card { + position: relative; + display: block; + width: 100%; + margin: 0; + padding: 0; + border: none; + aspect-ratio: 4 / 3; + border-radius: 6px; + overflow: hidden; + background-color: var(--border-strong); + text-align: left; + font: inherit; + cursor: pointer; + transition: transform 0.2s ease; +} + +.article-card:hover { + transform: translateY(-2px); +} + +.article-cover { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.article-caption { + position: absolute; + right: 0; + bottom: 0; + left: 0; + padding: 16px 6px 6px; + background: linear-gradient( + to top, + rgba(0, 0, 0, 0.8) 0%, + rgba(0, 0, 0, 0.3) 70%, + transparent 100% + ); + color: #fff; +} + +.article-title { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + font-size: 12px; + line-height: 1.3; +} + +/* ---------- 明细?---------- */ + +.detail-view { + display: flex; + flex-direction: column; + gap: 20px; +} + +.detail-nav { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 20px; +} + +.detail-back { + display: inline-flex; + align-items: center; + gap: 4px; + color: var(--primary-color); + font-size: 14px; + font-weight: 500; + cursor: pointer; +} + +.detail-back:hover { + text-decoration: underline; +} + +.detail-breadcrumb { + font-size: 12px; + color: var(--text-muted); +} + +.detail-hero { + display: flex; + flex-direction: column; + align-items: center; + padding: 12px 0 4px; + text-align: center; +} + +.detail-hero-icon { + width: 44px; + height: 44px; + margin-bottom: 8px; + border-radius: 50%; + background-color: var(--primary-soft); color: var(--primary-color); display: flex; align-items: center; justify-content: center; + font-size: 24px; +} + +.detail-hero-title { + font-size: 24px; + font-weight: 700; + color: var(--text-main); +} + +.detail-hero-count { + margin-top: 4px; + font-size: 14px; + color: var(--text-muted); +} + +/* 分组块:小标?+ 该组网格 */ +.group-block { + margin-bottom: 12px; +} + +.group-title { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + font-size: 16px; font-weight: 600; + color: var(--text-main); +} + +.group-title::before { + content: ''; + width: 3px; + height: 14px; + border-radius: 2px; + background-color: var(--primary-color); +} + +/* 明细页图文白?*/ +.ces-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px; +} + +.ces-card { + display: flex; + flex-direction: column; + width: 100%; + padding: 0; + overflow: hidden; + border: 1px solid var(--border-color); + border-radius: 10px; + background-color: var(--card-bg); + box-shadow: var(--shadow-card); + text-align: left; + font: inherit; + cursor: pointer; + transition: + transform 0.2s, + box-shadow 0.2s; +} + +.ces-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-hover); +} + +.ces-cover { + width: 100%; + aspect-ratio: 16 / 10; + object-fit: cover; + display: block; + background-color: var(--border-color); +} + +.ces-body { + display: flex; + flex-direction: column; + gap: 4px; + padding: 12px; +} + +.ces-title { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + font-size: 14px; + font-weight: 500; + line-height: 1.4; + color: var(--text-main); +} + +.ces-desc { + display: block; + margin-top: 2px; + font-size: 12px; + color: var(--text-muted); +} + +/* ============================================================ + 文章详情页(点击图文卡片进入? ============================================================ */ + +.article-head { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + padding: 4px 0; + text-align: center; +} + +.article-heading { + max-width: 760px; + font-size: 24px; + font-weight: 700; + line-height: 1.4; + color: var(--text-main); +} + +.article-meta { + display: flex; + align-items: center; + font-size: 12px; + color: var(--text-muted); +} + +.article-meta-item { + display: inline-flex; + align-items: center; +} + +.article-meta-dot { + width: 3px; + height: 3px; + margin: 0 8px; + border-radius: 50%; + background-color: var(--border-strong); +} + +.article-body { + padding: 32px 40px 40px; +} + +.article-body-text { + margin-bottom: 16px; + font-size: 14px; + line-height: 1.9; + color: var(--text-sub); +} + +.article-body-heading { + margin: 28px 0 16px; + font-size: 16px; + font-weight: 700; + color: var(--text-main); +} + +.article-body-image { + display: block; + width: 100%; + margin: 20px 0; + border-radius: 4px; + background-color: var(--border-color); +} + +/* ============================================================ + 条目详情页(点击应用卡片进入? ============================================================ */ + +/* 明细 / 条目详情共用同一套打开动画,和点击 ••?的表现保持一?*/ +.detail-view { + animation: view-in 0.22s ease both; +} + +@keyframes view-in { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: none; + } +} + +.item-card { + display: flex; + flex-direction: column; + align-items: center; + padding: 24px 28px; +} + +.item-title { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + font-size: 24px; + font-weight: 600; + color: var(--text-main); +} + +.item-edit { + font-size: 14px; + font-weight: 400; + color: var(--text-muted); + cursor: default; +} + +/* 主图:应用是方形头像,电影是竖版海报 */ +.item-cover { + display: flex; + align-items: center; + justify-content: center; + margin: 18px 0 10px; + overflow: hidden; + background-color: var(--primary-soft); + color: var(--primary-color); +} + +.item-cover--square { + width: 96px; + height: 96px; + border-radius: 12px; +} + +.item-cover--portrait { + width: 179px; + aspect-ratio: 2 / 3; + border-radius: 8px; +} + +.item-cover img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.item-cover-letter { + font-size: 38px; + font-weight: 700; +} + +.item-tags { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 8px; +} + +.item-tag { + padding: 2px 10px; + border-radius: 999px; + background-color: var(--primary-soft); + color: var(--primary-color); + font-size: 12px; +} + +.item-quote { + display: flex; + gap: 6px; + width: 100%; + margin-top: 18px; + text-align: left; +} + +.item-quote-mark { + flex-shrink: 0; + font-size: 32px; + font-weight: 700; + line-height: 1; + color: rgba(255, 62, 62, 0.35); +} + +.item-quote-text { + font-size: 14px; + line-height: 1.7; + color: var(--text-sub); +} + +.item-desc { + width: 100%; + margin-top: 10px; + font-size: 14px; + line-height: 1.7; + color: var(--text-sub); + text-align: left; +} + +.item-actions { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + margin-top: 18px; +} + +.item-stat { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + min-width: 56px; + height: 36px; + padding: 0 12px; + border-radius: 8px; + background-color: var(--bg-color); + color: var(--text-sub); + font-size: 12px; +} + +.item-stat .iconfont { + font-size: 16px; +} + +.item-stat em { + font-style: normal; + font-weight: 600; + color: var(--text-main); +} + +.item-stat--icon { + cursor: pointer; +} + +.item-stat--icon:hover { + background-color: var(--border-color); +} + +.item-jump { + flex: 1; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + height: 36px; + border-radius: 8px; + background-color: var(--primary-color); + color: #fff; + font-size: 14px; + transition: background-color 0.2s; +} + +.item-jump:hover { + background-color: #2c47d8; +} + +.item-jump:disabled { + opacity: 0.55; + cursor: default; +} + +.item-jump .iconfont { + font-size: 14px; +} + +.item-banner { + width: 100%; + margin-top: 18px; + aspect-ratio: 3 / 1; + border-radius: 10px; + object-fit: cover; + background-color: var(--border-color); +} + +/* 详情页「推荐阅读?*/ + +.related-card { + padding: 20px; +} + +.related-title { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; + font-size: 16px; + font-weight: 600; + color: var(--text-main); +} + +.related-title-icon { + font-size: 16px; +} + +.related-grid { + list-style: none; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px 24px; +} + +.related-item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 4px; + border-radius: 8px; + text-align: left; + cursor: pointer; + transition: background-color 0.2s; +} + +.related-item:hover { + background-color: var(--bg-color); +} + +.related-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--primary-soft); + color: var(--primary-color); + font-size: 14px; + font-weight: 700; flex-shrink: 0; } -.card-body { +.related-avatar img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.related-info { display: flex; flex-direction: column; min-width: 0; } -.card-title { +.related-name { font-size: 14px; font-weight: 600; color: var(--text-main); @@ -751,43 +1447,36 @@ text-overflow: ellipsis; } -.card-desc { +.related-desc { margin-top: 2px; - font-size: 11.5px; - color: var(--text-sub); + font-size: 12px; + color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -/* 路径类描述用等宽字体,普通文案保持无衬线 */ -.card-desc.is-mono { - font-family: var(--font-mono); -} - -@keyframes card-in { - from { - opacity: 0; - transform: translateY(6px); - } - to { - opacity: 1; - transform: translateY(0); +@media (max-width: 900px) { + .related-grid { + grid-template-columns: minmax(0, 1fr); } } -/* 空状态 / 错误 */ +/* ============================================================ + 提示?/ 回到顶部 + ============================================================ */ .notice { display: flex; flex-direction: column; gap: 6px; - padding: 36px 24px; + margin-bottom: 20px; + padding: 24px; border: 1px dashed var(--border-color); - border-radius: var(--radius-main); - background-color: rgba(255, 255, 255, 0.6); + border-radius: 12px; + background-color: rgba(255, 255, 255, 0.7); color: var(--text-sub); - font-size: 13px; + font-size: 14px; } .notice strong { @@ -796,23 +1485,14 @@ color: var(--text-main); } -.notice code { - padding: 1px 5px; - border-radius: 5px; - background-color: #eef2f7; - font-family: var(--font-mono); - font-size: 12px; - color: var(--text-main); -} - .notice-error { border-style: solid; - border-color: #f3c6c6; - background-color: #fff7f7; + border-color: rgba(255, 62, 62, 0.25); + background-color: #fff5f5; } .notice-error strong { - color: #b42318; + color: var(--danger); } .notice-action { @@ -822,11 +1502,8 @@ border: 1px solid var(--border-color); border-radius: 8px; background-color: var(--card-bg); - font-size: 13px; + font-size: 14px; cursor: pointer; - transition: - border-color 0.2s, - color 0.2s; } .notice-action:hover { @@ -834,49 +1511,78 @@ color: var(--primary-color); } -/* 回到顶部 */ - +/* 返回顶部:复刻 Alright 的 .totop-button(42px 圆形浅灰底), + 默认隐藏,内容区滚动后才淡入 */ .back-to-top { position: fixed; + top: 50%; right: 30px; - bottom: 30px; - width: 40px; - height: 40px; + width: 42px; + height: 42px; display: flex; align-items: center; justify-content: center; border-radius: 50%; - background-color: #e2e8f0; - color: #1e293b; - box-shadow: var(--shadow-card); + border: 1px solid var(--border-color); + background-color: var(--border-color); + color: var(--text-sub); cursor: pointer; - z-index: 90; + z-index: 99; + opacity: 0; + visibility: hidden; + transform: translateY(-50%) translateY(15px); transition: + opacity 0.3s ease, + visibility 0.3s ease, + transform 0.3s ease, background-color 0.2s, - transform 0.2s; + color 0.2s; +} + +.back-to-top.is-visible { + opacity: 1; + visibility: visible; + transform: translateY(-50%); } .back-to-top:hover { - background-color: #cbd5e1; - transform: translateY(-2px); + background-color: var(--border-strong); + border-color: var(--border-strong); + color: var(--text-main); } -.back-to-top svg { - width: 18px; - height: 18px; - stroke: currentColor; - fill: none; - stroke-width: 2.5; - stroke-linecap: round; - stroke-linejoin: round; +.back-to-top .iconfont { + font-size: 20px; } -/* ============ 响应式 ============ */ +/* 刷新按钮转圈 */ +.service-action[data-spinning='true'] .iconfont { + animation: spin 0.9s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* ============================================================ + 响应? ============================================================ */ @media (max-width: 1100px) { :root { --sidebar-width: 200px; } + + .app-grid, + .article-grid, + .ces-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .poster-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } } @media (max-width: 900px) { @@ -885,26 +1591,22 @@ min-height: 100vh; } - .topbar { - position: sticky; - } - - .topbar-inner { + .layout-container { padding: 0 16px; } - .brand { - width: auto; + .site-header { + position: sticky; } - .ticker { + .header-info { display: none; } .search-row { position: static; - top: auto; left: auto; + top: auto; transform: none; width: auto; height: 44px; @@ -926,14 +1628,25 @@ padding-top: 0; } + .content-veil { + display: none; + } + .content-scroll { overflow: visible; -webkit-mask-image: none; mask-image: none; } + /* 窄屏沿用主题的右下侧定位,隐藏/显示的位移单独处理 */ .back-to-top { - right: 20px; - bottom: 20px; + top: auto; + right: 15px; + bottom: 30%; + transform: translateY(15px); + } + + .back-to-top.is-visible { + transform: translateY(0); } } diff --git a/stash-web/src/App.tsx b/stash-web/src/App.tsx index 377589f..c8c6297 100644 --- a/stash-web/src/App.tsx +++ b/stash-web/src/App.tsx @@ -1,119 +1,178 @@ -import { useMemo, useRef, useState } from 'react' -import { BackToTop } from './components/BackToTop' -import { SearchBar } from './components/SearchBar' -import { Sidebar } from './components/Sidebar' -import { SiteCard } from './components/SiteCard' -import { TopBar } from './components/TopBar' -import { MOCK_SECTIONS, type CatalogCard, type CatalogSection } from './catalog' +import { useEffect, useMemo, useRef, useState } from 'react' +import { BackToTop } from './components/common/BackToTop' +import { ArticleDetailView } from './components/content/ArticleDetailView' +import { DetailView } from './components/content/DetailView' +import { HomeView } from './components/content/HomeView' +import { ItemDetailView } from './components/content/ItemDetailView' +import { SiteHeader } from './components/header/SiteHeader' +import { SearchBar } from './components/search/SearchBar' +import { Sidebar } from './components/sidebar/Sidebar' +import { buildCategories } from './data/catalog' +import { + appDetailModel, + posterDetailModel, + posterToRelatedItem, + toRelatedItem, +} from './data/detail' +import { filterCategories } from './data/filter' +import type { NavTarget } from './data/nav' +import { + findAppItem, + findArticleItem, + findPosterItem, + relatedAppItems, + relatedPosterItems, +} from './data/types' import { useProjects } from './useProjects' import './App.css' -function initialOf(name: string): string { - const [first] = Array.from(name.trim()) - return first ? first.toUpperCase() : '#' -} +/** + * 当前展示的视图: + * - home:首页分区卡片流 + * - detail:某个分类(可指定分组)的明细 + * - item:单个条目(应用或电影)的详情,returnTo 记录返回目标 + */ +type View = + | { type: 'home' } + | { type: 'detail'; categoryId: string; groupId?: string } + | { type: 'item'; categoryId: string; itemId: string; returnTo: View } export default function App() { const { status, projects, error, live, refreshing, refresh } = useProjects() const [keyword, setKeyword] = useState('') + const [view, setView] = useState({ type: 'home' }) + const [scrolled, setScrolled] = useState(false) const searchRef = useRef(null) + const scrollRef = useRef(null) - // 真实项目放最前面,占位分类跟在后面 - const sections = useMemo(() => { - const localCards: CatalogCard[] = projects.map((project) => ({ - id: project.route_path, - title: project.name, - desc: project.route_path, - href: project.route_path, - initial: initialOf(project.name), - mono: true, - })) + const categories = useMemo(() => buildCategories(projects), [projects]) + const visibleCategories = useMemo( + () => filterCategories(categories, keyword), + [categories, keyword], + ) - return [{ id: 'local', title: '本地服务', cards: localCards }, ...MOCK_SECTIONS] - }, [projects]) + const detailView = view.type === 'detail' ? view : undefined + const itemView = view.type === 'item' ? view : undefined - const query = keyword.trim().toLowerCase() - const searching = query !== '' + const detailCategory = + detailView !== undefined + ? categories.find((category) => category.id === detailView.categoryId) + : undefined - // 搜索时过滤卡片,并丢掉没有命中的分区 - const sectionsToRender = useMemo(() => { - if (!searching) return sections + const itemCategory = + itemView !== undefined + ? categories.find((category) => category.id === itemView.categoryId) + : undefined - return sections - .map((section) => ({ - ...section, - cards: section.cards.filter( - (card) => - card.title.toLowerCase().includes(query) || - card.desc.toLowerCase().includes(query), - ), - })) - .filter((section) => section.cards.length > 0) - }, [sections, searching, query]) + // 应用与电影共用一个详情模型,这里按分类类型做映射 + const detailModel = useMemo(() => { + if (itemView === undefined || itemCategory === undefined) return undefined - const noMatch = searching && sectionsToRender.length === 0 + if (itemCategory.kind === 'apps') { + const item = findAppItem(itemCategory, itemView.itemId) + return item !== undefined ? appDetailModel(item) : undefined + } + + if (itemCategory.kind === 'posters') { + const poster = findPosterItem(itemCategory, itemView.itemId) + return poster !== undefined ? posterDetailModel(poster) : undefined + } + + return undefined + }, [itemView, itemCategory]) + + const relatedItems = useMemo(() => { + if (itemView === undefined || itemCategory === undefined) return [] + + if (itemCategory.kind === 'apps') { + return relatedAppItems(itemCategory, itemView.itemId, 4).map(toRelatedItem) + } + + if (itemCategory.kind === 'posters') { + return relatedPosterItems(itemCategory, itemView.itemId, 4).map(posterToRelatedItem) + } + + return [] + }, [itemView, itemCategory]) + + // 切换视图时把内容区滚回顶部 + useEffect(() => { + scrollRef.current?.scrollTo({ top: 0 }) + }, [view]) + + // 桌面端滚动作在内容容器里,窄屏滚的是窗口: + // 两处都要驱动「已滚动」状态(顶部渐隐层 + 返回顶部按钮共用) + useEffect(() => { + const sync = () => { + const inner = scrollRef.current?.scrollTop ?? 0 + const next = Math.max(inner, window.scrollY) > 4 + setScrolled((current) => (current === next ? current : next)) + } + + window.addEventListener('scroll', sync, { passive: true }) + return () => window.removeEventListener('scroll', sync) + }, []) + + const openDetail = (categoryId: string) => setView({ type: 'detail', categoryId }) + + const openItem = (categoryId: string, itemId: string, returnTo: View) => + setView({ type: 'item', categoryId, itemId, returnTo }) + + const navigate = (target: NavTarget) => + setView({ type: 'detail', categoryId: target.categoryId, groupId: target.groupId }) + + const goHome = () => setView({ type: 'home' }) + + const searching = keyword.trim() !== '' + + /** + * 条目详情按分类类型分流: + * 图文走长文版式,应用 / 电影走通用的条目版式。 + */ + function renderItemView() { + if (itemView === undefined) return null + if (itemCategory === undefined) { + return

分类不存在

+ } + + if (itemCategory.kind === 'articles') { + const article = findArticleItem(itemCategory, itemView.itemId) + if (article === undefined) { + return

条目不存在

+ } - function renderLocalPlaceholder() { - if (status === 'error') { return ( -
- 连不上 Stash 服务 -

{error}

-

- 请确认后端已启动(默认 127.0.0.1:8080),开发时 Vite 会把{' '} - /api 代理到后端。 -

- -
+ setView(itemView.returnTo)} + /> ) } - if (status === 'loading') { - return ( -
- 正在读取项目列表… -

首次加载会扫描一次托管目录。

-
- ) + if (detailModel === undefined) { + return

条目不存在

} return ( -
- 这里还没有项目 -

- 在托管根目录下创建子目录并放入 index.html,刷新后即可在这里看到它。 -

-
- ) - } - - function renderSection(section: CatalogSection) { - return ( -
-
-

{section.title}

- {section.cards.length} -
- - {section.cards.length > 0 ? ( -
- {section.cards.map((card, index) => ( - - ))} -
- ) : ( - renderLocalPlaceholder() - )} -
+ setView(itemView.returnTo)} + onOpenItem={(itemId) => setView({ ...itemView, itemId })} + /> ) } return (
-
- + ) } diff --git a/stash-web/src/assets/iconfont.woff2 b/stash-web/src/assets/iconfont.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..4ca9e6fd1d7e2027c002ddf7510f5924d203e668 GIT binary patch literal 52136 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 F007T$p)LRb literal 0 HcmV?d00001 diff --git a/stash-web/src/assets/site-bg.jpg b/stash-web/src/assets/site-bg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..44f1755e380ba95ec452b0790f5924f8c65064b9 GIT binary patch literal 28746 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 { - // 桌面端滚动的是内容容器,窄屏滚动的是窗口 - document.getElementById('content-scroll')?.scrollTo({ top: 0, behavior: 'smooth' }) - window.scrollTo({ top: 0, behavior: 'smooth' }) - } - - return ( - - ) -} diff --git a/stash-web/src/components/Sidebar.tsx b/stash-web/src/components/Sidebar.tsx deleted file mode 100644 index 09dead0..0000000 --- a/stash-web/src/components/Sidebar.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { useState } from 'react' -import type { Project } from '../api' -import { MOCK_NAV_GROUPS, MOCK_NAV_PRIMARY } from '../catalog' -import { ProfileCard } from './ProfileCard' -import { ServicePanel } from './ServicePanel' - -type SidebarProps = { - projects: Project[] - live: boolean - refreshing: boolean - onRefresh: () => void -} - -export function Sidebar({ projects, live, refreshing, onRefresh }: SidebarProps) { - const [openGroups, setOpenGroups] = useState([]) - const [activeId, setActiveId] = useState('common') - - const toggle = (id: string) => { - setOpenGroups((previous) => - previous.includes(id) ? previous.filter((item) => item !== id) : [...previous, id], - ) - } - - const isOpen = (id: string) => openGroups.includes(id) - - return ( - - ) -} diff --git a/stash-web/src/components/SiteCard.tsx b/stash-web/src/components/SiteCard.tsx deleted file mode 100644 index f7db7c0..0000000 --- a/stash-web/src/components/SiteCard.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { CatalogCard } from '../catalog' - -type SiteCardProps = { - card: CatalogCard - index: number -} - -export function SiteCard({ card, index }: SiteCardProps) { - return ( - - - - {card.title} - {card.desc} - - - ) -} diff --git a/stash-web/src/components/TopBar.tsx b/stash-web/src/components/TopBar.tsx deleted file mode 100644 index d430298..0000000 --- a/stash-web/src/components/TopBar.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useMemo } from 'react' -import type { Project } from '../api' -import { LoginIcon, SearchIcon } from './icons' - -type TopBarProps = { - projects: Project[] - live: boolean - onFocusSearch: () => void -} - -type TickerItem = { - tag: string - text: string -} - -export function TopBar({ projects, live, onFocusSearch }: TopBarProps) { - return ( -
-
- - - STASH - - - - -
- - {/* TODO(auth): 后端登录接口就绪后接上,目前仅作入口占位 */} - -
-
-
- ) -} - -/** 顶栏跑马灯:展示连接状态、项目数量与前几个项目名。 */ -function Ticker({ projects, live }: { projects: Project[]; live: boolean }) { - const items = useMemo(() => buildTickerItems(projects, live), [projects, live]) - - // 复制一份实现无缝循环,动画位移为 -50% - const loop = [...items, ...items] - - return ( - - ) -} - -function buildTickerItems(projects: Project[], live: boolean): TickerItem[] { - const items: TickerItem[] = [ - { tag: '状态', text: live ? '实时同步已连接' : '等待服务连接' }, - { tag: '项目', text: `共 ${projects.length} 个本地应用` }, - ] - - if (projects.length === 0) { - items.push({ - tag: '提示', - text: '把静态站点放进托管目录,并保留 index.html,就会出现在这里', - }) - return items - } - - for (const project of projects.slice(0, 6)) { - items.push({ tag: '本地', text: project.name }) - } - - return items -} diff --git a/stash-web/src/components/common/BackToTop.tsx b/stash-web/src/components/common/BackToTop.tsx new file mode 100644 index 0000000..49d7f8f --- /dev/null +++ b/stash-web/src/components/common/BackToTop.tsx @@ -0,0 +1,33 @@ +import { ArrowUpIcon } from './Icon' + +/** + * 回到顶部:复刻 Alright 主题的 .scroll-toolbar .totop-button。 + * + * - 42px 圆形、浅灰底,固定在右侧(bottom: 50%) + * - 默认隐藏,只有内容区滚动后才淡入,回到顶部后再淡出 + */ +type BackToTopProps = { + /** 内容区是否已滚动(由 App 传入) */ + visible: boolean +} + +export function BackToTop({ visible }: BackToTopProps) { + const handleClick = () => { + // 桌面端滚动的是内容容器,窄屏滚动的是窗口,两处都滚一遍 + document.getElementById('content-scroll')?.scrollTo({ top: 0, behavior: 'smooth' }) + window.scrollTo({ top: 0, behavior: 'smooth' }) + } + + return ( + + ) +} diff --git a/stash-web/src/components/common/CountBadge.tsx b/stash-web/src/components/common/CountBadge.tsx new file mode 100644 index 0000000..8248eaf --- /dev/null +++ b/stash-web/src/components/common/CountBadge.tsx @@ -0,0 +1,11 @@ +/** + * 数量角标:贴在分区标题右上角的小红标。 + * 纯展示组件,只接收数字,宽度由内容撑开(两位数不会变形)。 + */ +type CountBadgeProps = { + count: number +} + +export function CountBadge({ count }: CountBadgeProps) { + return {count} +} diff --git a/stash-web/src/components/common/Icon.tsx b/stash-web/src/components/common/Icon.tsx new file mode 100644 index 0000000..4753d76 --- /dev/null +++ b/stash-web/src/components/common/Icon.tsx @@ -0,0 +1,67 @@ +/** + * 通用图标组件。 + * + * 全部使用 Alright 主题的 iconfont 线性图标(见 src/iconfont.css), + * 尺寸与颜色由使用处的 CSS 控制(font-size / color), + * 因此同一套图标可以复用在按钮、统计条、导航等不同位置。 + */ + +type IconProps = { + /** 附加类名,便于局部覆盖字号或颜色 */ + className?: string +} + +const base = (name: string, className?: string) => + `iconfont icon-${name}${className !== undefined ? ` ${className}` : ''}` + +export function SearchIcon({ className }: IconProps) { + return