diff --git a/.gitignore b/.gitignore index 628d67c..f211e46 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ hookify.*.local.md # statisticService runtime logs (created by logger when running tests) backend/services/statisticService/**/logs/ backend/services/statisticService/**/logs/*.log + +# superpowers +.superpowers \ No newline at end of file diff --git a/docs/superpowers/plans/2026-06-10-square-stargalaxy-component.md b/docs/superpowers/plans/2026-06-10-square-stargalaxy-component.md new file mode 100644 index 0000000..0781c53 --- /dev/null +++ b/docs/superpowers/plans/2026-06-10-square-stargalaxy-component.md @@ -0,0 +1,1021 @@ +# StarGalaxy 组件实现计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在 square 页面「星河」tab 渲染一个 3D 倾斜椭圆轨道 + 9 item 顺时针旋转 + TOP 1-3 颁奖台的排行榜组件 + +**Architecture:** 三层拆分 — `index.vue`(容器/数据/装饰)+ `PodiumCard.vue`(TOP 1-3 大卡)+ `ScatteredRanks.vue`(TOP 4-12 9 个散落 item)+ `config.js`(9 slot 位置公式)。单组 `@keyframes orbit` 配合 9 个不同 `animation-delay` 实现旋转。复用现有 `getHotRankingApi` 和 `getAssetCoverRealUrl`。 + +**Tech Stack:** Vue 3 Composition API + uni-app + CSS keyframes(uni-app 跨端支持 `transform`)。**无单元测试框架**(项目 package.json 只有 vuex),通过 H5 端 `npm run dev:h5` 可视化验证。 + +**Spec:** `docs/superpowers/specs/2026-06-10-square-stargalaxy-component-design.md` + +--- + +## 文件结构 + +| 路径 | 类型 | 职责 | +|----|----|----| +| `frontend/pages/square/components/StarGalaxy/index.vue` | 新建 | 容器组件:数据加载、装饰层、3D 椭圆轨道 SVG、TOP 1-3 颁奖台 + ScatteredRanks 编排 | +| `frontend/pages/square/components/StarGalaxy/PodiumCard.vue` | 新建 | TOP 1-3 大卡(钻石渐变外框 + cover + 下方 TOP N 标签 + 可选皇冠) | +| `frontend/pages/square/components/StarGalaxy/ScatteredRanks.vue` | 新建 | TOP 4-12 9 个散落 item(cover + 上方 TOP N 标签) | +| `frontend/pages/square/components/StarGalaxy/config.js` | 新建 | 9 slot 位置/translate/scale 公式 + KEYFRAMES 常量 | +| `frontend/pages/square/square.vue` | 修改 | 在「星河」tab 分支渲染 ``,并设置 onShow 重置 activeContentTab 为 "xinghe" | + +每个新文件职责单一,文件之间通过 props/emit 通信。 + +--- + +## Task 1: 创建 config.js + +**Files:** +- Create: `frontend/pages/square/components/StarGalaxy/config.js` + +- [ ] **Step 1.1: 创建目录** + +```bash +mkdir -p frontend/pages/square/components/StarGalaxy +``` + +- [ ] **Step 1.2: 写入 config.js** + +```js +// StarGalaxy 组件配置常量 +// 9 个散落 item 沿 65° 倾斜椭圆轨道排列 +// slot 0 = 最前(底部,TOP 4 起始位置),slot 4-5 = 最后(顶部) + +export const RING = { + cx: 187, // 椭圆圆心 x + cy: 510, // 椭圆圆心 y + rx: 130, // 水平半径 + ry: 55, // 垂直半径(cos(65°) ≈ 0.423,模拟向后倾 65°) + startAngle: 180, // 起始角:slot 0 在正下方 + step: 40, // 间隔角(顺时针 = 负方向 → step = -40 在 CSS 中) +} + +// item 固定尺寸(label 在 cover 上方) +export const ITEM = { + width: 46, // cover + label 宽度 + labelHeight: 14, // 顶部 label 高度 + coverHeight: 56, // 底部 cover 高度 + gap: 2, // label 与 cover 之间的间距 +} +// total: 14 + 2 + 56 = 72 + +// TOP 6 / TOP 11 推到边缘,避免与 TOP 5/7、TOP 10/12 重叠 +// TOP 6 推到屏幕右侧 (321, 488),TOP 11 推到屏幕左侧 (8, 488) +export const OVERRIDES = { + 6: { x: 321, y: 488 }, + 11: { x: 8, y: 488 }, +} + +// 计算 y 在椭圆前/后位置的比例(0 = 最后, 1 = 最前) +function yFactor(y) { + // y=458 (back) → 0; y=565 (front) → 1 + return Math.max(0, Math.min(1, (y - 458) / 107)) +} + +// 生成 9 个 item 的位置配置 +export function generateRingPositions() { + return Array.from({ length: 9 }, (_, i) => { + const rank = i + 4 + const alpha = (RING.startAngle + i * RING.step) * Math.PI / 180 + const baseX = RING.cx + RING.rx * Math.sin(alpha) - ITEM.width / 2 + const baseY = RING.cy - RING.ry * Math.cos(alpha) - (ITEM.labelHeight + ITEM.gap + ITEM.coverHeight) / 2 + const ovr = OVERRIDES[rank] + const y = ovr?.y ?? baseY + const x = ovr?.x ?? baseX + const f = yFactor(y) + return { + rank, + x, + y, + scale: 0.75 + 0.40 * f, // 0.75 → 1.15 + zIndex: Math.round(f * 10), // 0 → 10 + } + }) +} + +// 单组 @keyframes(CSS 模板字符串) +// translate 值是相对 slot 0 中心 (164, 530) 的偏移 +export const ORBIT_KEYFRAMES = ` +@keyframes orbit { + 0% { transform: translate(0,0) scale(1.15); } + 11.11% { transform: translate(84px,-13px) scale(1.05); } + 22.22% { transform: translate(157px,-43px) scale(0.95); } + 33.33% { transform: translate(113px,-83px) scale(0.85); } + 44.44% { transform: translate(45px,-107px) scale(0.75); } + 55.55% { transform: translate(-45px,-107px) scale(0.75); } + 66.66% { transform: translate(-113px,-83px) scale(0.85); } + 77.77% { transform: translate(-156px,-43px) scale(0.95); } + 88.88% { transform: translate(-84px,-13px) scale(1.05); } + 100% { transform: translate(0,0) scale(1.15); } +} + +@keyframes crownPulse { + 0%, 100% { transform: translateX(-50%) scale(1); } + 50% { transform: translateX(-50%) scale(1.15); } +} +` + +// 各 slot 对应 ring-item 类的 r0..r8 的 delay(负值让 item 起始位置 = slot) +export const RING_DELAYS = [0, -4, -8, -12, -16, -20, -24, -28, -32] +``` + +- [ ] **Step 1.3: 验证文件创建** + +```bash +ls -la frontend/pages/square/components/StarGalaxy/config.js +``` + +Expected: 文件存在,~80 行 + +- [ ] **Step 1.4: 提交** + +```bash +cd frontend +git add pages/square/components/StarGalaxy/config.js +git commit -m "feat(stargalaxy): add ring position config and orbit keyframes" +``` + +--- + +## Task 2: 创建 PodiumCard.vue + +**Files:** +- Create: `frontend/pages/square/components/StarGalaxy/PodiumCard.vue` + +- [ ] **Step 2.1: 写入 PodiumCard.vue** + +```vue + + + + + +``` + +- [ ] **Step 2.2: 验证文件创建** + +```bash +ls -la frontend/pages/square/components/StarGalaxy/PodiumCard.vue +wc -l frontend/pages/square/components/StarGalaxy/PodiumCard.vue +``` + +Expected: ~150 行 + +- [ ] **Step 2.3: 提交** + +```bash +cd frontend +git add pages/square/components/StarGalaxy/PodiumCard.vue +git commit -m "feat(stargalaxy): add PodiumCard for TOP 1-3 with gold/silver/bronze labels" +``` + +--- + +## Task 3: 创建 ScatteredRanks.vue + +**Files:** +- Create: `frontend/pages/square/components/StarGalaxy/ScatteredRanks.vue` + +- [ ] **Step 3.1: 写入 ScatteredRanks.vue** + +```vue + + + + + + + +``` + +- [ ] **Step 3.2: 验证** + +```bash +ls -la frontend/pages/square/components/StarGalaxy/ScatteredRanks.vue +``` + +- [ ] **Step 3.3: 提交** + +```bash +cd frontend +git add pages/square/components/StarGalaxy/ScatteredRanks.vue +git commit -m "feat(stargalaxy): add ScatteredRanks with 9 ring items + 36s orbit animation" +``` + +--- + +## Task 4: 创建 StarGalaxy/index.vue 容器 + +**Files:** +- Create: `frontend/pages/square/components/StarGalaxy/index.vue` + +- [ ] **Step 4.1: 写入 index.vue** + +```vue + + + + + +``` + +- [ ] **Step 4.2: 验证文件结构** + +```bash +ls -la frontend/pages/square/components/StarGalaxy/ +``` + +Expected: 4 files (config.js, PodiumCard.vue, ScatteredRanks.vue, index.vue) + +- [ ] **Step 4.3: 提交** + +```bash +cd frontend +git add pages/square/components/StarGalaxy/index.vue +git commit -m "feat(stargalaxy): add container with data loading, decoration, podium + scattered orchestration" +``` + +--- + +## Task 5: 在 square.vue 中集成 StarGalaxy + +**Files:** +- Modify: `frontend/pages/square/square.vue:42-49`(添加星河分支) + +- [ ] **Step 5.1: 导入 StarGalaxy 组件** + +在 `square.vue` 顶部 import 区域(约第 71 行附近)添加: + +```js +import StarGalaxy from "./components/StarGalaxy/index.vue"; +``` + +- [ ] **Step 5.2: 添加星河分支** + +在 `` 之前,添加新的星河分支(替换原 `` 注释块): + +```vue + + + + + + + + + +``` + +- [ ] **Step 5.3: 验证 square.vue 语法** + +```bash +cd frontend +npx --no-install eslint pages/square/square.vue 2>/dev/null || echo "无 eslint,跳过" +``` + +- [ ] **Step 5.4: 提交** + +```bash +cd frontend +git add pages/square/square.vue +git commit -m "feat(square): wire StarGalaxy component into 星河 tab" +``` + +--- + +## Task 6: H5 端可视化验证 + +**Files:** 无(仅验证) + +- [ ] **Step 6.1: 启动 H5 调试服务** + +```bash +cd frontend +npm run dev:h5 +``` + +Expected: 浏览器打开 `http://localhost:8080`(或类似端口),square 页面正常加载。 + +- [ ] **Step 6.2: 进入「星河」tab 视觉验证** + +手动检查清单: +- [ ] 顶部「★ 星河 ★」标题可见 +- [ ] 3 张 TOP 1-3 颁奖台卡片显示,TOP 1 在中央最大,TOP 2 在左上,TOP 3 在右上 +- [ ] TOP 1 顶部有皇冠,cover 下方有金渐变「TOP 1」标签 +- [ ] TOP 2 下方有银渐变「TOP 2」标签 +- [ ] TOP 3 下方有铜渐变「TOP 3」标签 +- [ ] 下方椭圆轨道上有 9 个 item(TOP 4-12)排成圆环 +- [ ] TOP 4 在最前(最大),TOP 8/9 在最后(最小) +- [ ] 9 个 item 顺时针缓慢旋转(36s 一圈) +- [ ] 点击任一 item 跳到 asset-detail 页 + +- [ ] **Step 6.3: 检查动画** + +- [ ] 打开 DevTools 观察 .ring-item 的 transform 在变化 +- [ ] 旋转应该是匀速、连续、无卡顿 +- [ ] 前后 item 的大小差异明显(TOP 4 比 TOP 8 大约 1.5 倍) + +- [ ] **Step 6.4: 检查错误状态** + +在 DevTools 中断网(Network → Offline),刷新页面,应看到「加载失败,点击重试」+ 重试按钮。恢复网络点击重试,数据应正常加载。 + +- [ ] **Step 6.5: 提交验证记录(如有问题修复)** + +```bash +cd frontend +git status +# 如有修改: +git add -A +git commit -m "fix(stargalaxy): visual verification fixes" || echo "无修改需提交" +``` + +--- + +## Task 7: 跨端兼容性烟测 + +**Files:** 无(仅验证) + +- [ ] **Step 7.1: 验证关键 CSS 兼容性** + +确认以下 CSS 属性能在 uni-app 跨端运行(小程序、H5、APP): +- `transform: scale()` + `transform-origin: center` — ✅ uni-app 全支持 +- `filter: blur()` — ✅ H5 + APP 支持,小程序需 `enable-backdrop-filter` +- `radial-gradient` — ✅ H5 + APP 支持,小程序有限制 +- `@keyframes` + `animation` — ✅ uni-app 全支持 + +如果小程序有问题,对应的 @media 块需要适配或使用条件编译。 + +- [ ] **Step 7.2: 文档记录** + +在 `frontend/pages/square/components/StarGalaxy/README.md` 写组件使用说明: + +```markdown +# StarGalaxy 组件 + +square 页「星河」tab 的排行榜组件。 + +## 视觉特征 + +- 65° 倾斜椭圆轨道 +- TOP 1-3 颁奖台(cover + 下方标签 + 钻石外框) +- TOP 4-12 9 个散落 item(cover + 上方标签) +- 9 item 顺时针 36s 旋转一圈 +- 近大远小(scale 0.75→1.15) + +## 数据来源 + +`getHotRankingApi("displaying", null, 1, 12)` — 与「星榜」共用 API,取前 12 条。 + +## 文件 + +- `index.vue` — 容器 +- `PodiumCard.vue` — TOP 1-3 大卡 +- `ScatteredRanks.vue` — TOP 4-12 9 散落 item +- `config.js` — 位置公式和 keyframes + +## 可调参数 + +- 椭圆倾斜角(config.js: RING.ry)— 改 ry 调整倾斜度 +- 旋转周期(ScatteredRanks.vue: animation duration)— 改 36s 调整速度 +- 近大远小范围(keyframes scale 0.75→1.15)— 改 0.75/1.15 调整 + +## 可访问性 + +- `prefers-reduced-motion: reduce` 时关闭旋转动画 +- label 文字使用高对比度的 #FFFABD + 红色 text-shadow +``` + +```bash +cd frontend +git add pages/square/components/StarGalaxy/README.md +git commit -m "docs(stargalaxy): add component README" +``` + +--- + +## 验收清单(最终) + +- [ ] Task 1-5 全部完成并提交 +- [ ] Task 6.2 视觉验证清单全部 ✅ +- [ ] Task 6.3 动画流畅 +- [ ] Task 6.4 错误状态可重试 +- [ ] Task 7 README 文档完成 +- [ ] 跨端验证(至少 H5 通过;小程序/APP 在后续 PR 验证) +- [ ] 代码无 ESLint 错误(如有 ESLint 配置) +- [ ] 所有提交信息遵循 `feat/fix/docs/...` Conventional Commits 规范 + +--- + +## 风险与回退 + +| 风险 | 回退方案 | +|----|----| +| 旋转动画在低端机卡顿 | 在 keyframes 中加 `transform: translateZ(0)` 强制 GPU 加速 | +| 小程序不支持 `radial-gradient` | 改用 `background-image: linear-gradient` 模拟或接受色彩降级 | +| 椭圆轨道 SVG 在小程序不显示 | 用纯 CSS border 模拟椭圆装饰 | +| TOP 6/11 推到边缘与不同屏幕宽度冲突 | 在 config.js 用百分比 + 媒体查询自适应 | + +--- + +## 后续 PR(v2 增强,不在本计划范围) + +- 缩略图懒加载优化(``) +- 9 item 悬停暂停动画 +- 减少运动偏好支持进一步增强 +- 双指捏合缩放查看详情 diff --git a/docs/superpowers/specs/2026-06-10-square-stargalaxy-component-design.md b/docs/superpowers/specs/2026-06-10-square-stargalaxy-component-design.md new file mode 100644 index 0000000..0549ea8 --- /dev/null +++ b/docs/superpowers/specs/2026-06-10-square-stargalaxy-component-design.md @@ -0,0 +1,276 @@ +# 星河(StarGalaxy)组件设计 + +> **日期**: 2026-06-10 +> **状态**: 待评审 +> **目的**: 为 square 页面的「星河」tab 设计一个 3D 倾斜椭圆轨道 + 顺时针旋转的排行榜组件 +> **参考文档**: `docs/figma-analysis-xiaohongshu-stars.md`(Figma 节点 94:78「星河新版本」) + +--- + +## 1. 背景 + +square 页面现有 3 个内容 tab(星河 / 星榜 / 广场),其中「星榜」是 12 行的横向列表样式(`HotCategoryBlock.vue`)。本次新增「星河」tab,沿用 Figma 中的双层结构: + +- **上层**:TOP 1-3 颁奖台(cover + 下方标签 + 钻石渐变外框) +- **下层**:TOP 4-12 散落在 65° 倾斜椭圆轨道上,绕中心 3D 旋转 + +设计追求饭圈甜美梦幻风格(粉红渐变 + 玻璃拟态 + 暖黄光晕 + 渐变描边),不显示用户名/点赞数(保持 cover 纯净)。 + +--- + +## 2. 文件结构 + +``` +frontend/pages/square/components/StarGalaxy/ +├── index.vue ← 容器:数据加载、装饰层、TOP 1-3 + ScatteredRanks 编排 +├── PodiumCard.vue ← TOP 1-3 颁奖台卡片(cover + 下方 TOP N 标签 + 钻石外框) +├── ScatteredRanks.vue ← TOP 4-12 9 个散落 item(cover + 上方 TOP N 标签) +└── config.js ← 9 slot 位置/translate/scale 公式 +``` + +`square.vue` 改动:在星河 tab 分支中渲染 ``,复用现有 `handleCardClick`(单击跳详情 + 双击点赞)。 + +--- + +## 3. 组件职责 + +| 组件 | 职责 | 内部状态 | 输入 Props | 事件 | +|----|----|----|----|----| +| `StarGalaxy/index.vue` | 数据加载(`getHotRankingApi`)、装饰层渲染、3D 椭圆轨道 SVG、布局编排 | `items`、`loading` | — | `cardClick(item)` | +| `PodiumCard.vue` | 单张 TOP 1-3 大卡:钻石渐变外框 + cover + 下方 TOP N 标签 + 可选皇冠 | — | `rank: 4\|5\|6` 隐式;`item: HotRankingItem`、`size: {w,h}` | `click(item)` | +| `ScatteredRanks.vue` | TOP 4-12 共 9 个 item 散落在椭圆轨道 | — | `items: HotRankingItem[]` (length=9) | `cardClick(item)` | +| `config.js` | 9 个 slot 的绝对位置/translate/scale/zIndex 公式 | — | — | — | + +--- + +## 4. 数据流 + +``` +StarGalaxy/index.vue (onMounted) + │ + ├── getHotRankingApi("displaying", null, 1, 12) + │ │ + │ └── res.data.items[0..11] + │ + ├── Promise.all → getAssetCoverRealUrl(item.cover_url) // 复用 HotCategoryBlock 的模式 + │ + └── items 拆成两段: + ├── items.slice(0,3) → PodiumCard × 3 + └── items.slice(3,12) → ScatteredRanks(9 个 item) +``` + +事件冒泡:所有 card 点击 → `emit('cardClick', item)` → `square.vue` 的 `handleCardClick`(已有,含单击跳详情 + 双击点赞)。 + +--- + +## 5. 视觉设计 + +### 5.1 容器尺寸 +- 总宽 750rpx × 高约 1440rpx +- 适配 iPhone 标准 (375×812) +- 整体分上下两层:上层颁奖台 y=48~700,下层椭圆轨道 y=800~1440 + +### 5.2 装饰层(背景 overlay) +- 粉红渐变:`linear-gradient(179deg, #FFE5E5 0%, #F3A0A1 0%, #FF9C9C 86%, #FF2024 100%)`(覆盖 square 现有渐变之上) +- 樱花粉光晕:圆形 `#F3D3E3` + `filter: blur(60px)`,居中(top 580) +- 暖黄光晕:圆形 `#FFFABD` + `filter: blur(50px)`,top 50 +- ⚠️ 不动 square.vue 现有渐变背景,星河只画自己的装饰层 + +### 5.3 PodiumCard 三个实例 + +| 排名 | 位置 (left, top) | 卡片尺寸 | 标签尺寸 | 装饰图 | +|----|----|----|----|----| +| TOP 1 | (50%, 400) translateX(-50%) | 240×260 (cover 240×260) | 192×44 (金渐变) | 钻石外框 + 皇冠 | +| TOP 2 | (60, 120) | 200×200 (cover 200×200) | 156×36 (银渐变) | 钻石外框 | +| TOP 3 | (470, 150) | 192×192 (cover 192×192) | 156×36 (铜渐变) | 钻石外框 | + +**PodiumCard 内部层级**(从下到上): +``` +┌─ 钻石渐变外框(filter: blur(8-12px),不规则圆角 8px 22px 8px 19px) +│ ├─ 藏品主图(不规则圆角 6px 20px 6px 17px) +│ ├─ 青绿色高光 overlay(180deg #53F4D3 → 透明) +│ └─ 钻石渐变边框层(4px 渐变描边) +└─ TOP N 标签(绝对定位 bottom: -4px,居中) +``` + +**皇冠**(仅 TOP 1):绝对定位 `top: -44px; left: 50%; transform: translateX(-50%)`,font-size: 44rpx,2s 循环 pulse 动画。 + +### 5.4 ScatteredRanks 9 个 slot 位置 + +9 个 item 在 65° 倾斜椭圆上,等角度 40° 间隔(顺时针),起始角 180°(TOP 4 在正下方 = 最前)。 + +椭圆参数: +- 圆心 (cx, cy) = (187, 510) +- 水平半径 rx = 130 +- 垂直半径 ry = 55(cos(65°) ≈ 0.423,模拟向后倾斜 65°) +- 起始角 startAngle = 180°(slot 0 在正下方) +- 间隔角 step = -40°(顺时针 = 负方向) + +| Slot | Rank | 中心 (x, y) | 渲染尺寸 (w×h) | scale | z-index | +|----|----|----|----|----|----| +| 0 | TOP 4 | (187, 565) | 46×70 | 1.15 | 10 | +| 1 | TOP 5 | (271, 552) | 46×70 | 1.05 | 9 | +| 2 | TOP 6 | (321, 488) | 46×70 | 0.95 | 6 | +| 3 | TOP 7 | (300, 482) | 46×70 | 0.85 | 3 | +| 4 | TOP 8 | (232, 458) | 46×70 | 0.75 | 0 | +| 5 | TOP 9 | (142, 458) | 46×70 | 0.75 | 0 | +| 6 | TOP 10 | (74, 482) | 46×70 | 0.85 | 3 | +| 7 | TOP 11 | (8, 488) | 46×70 | 0.95 | 6 | +| 8 | TOP 12 | (80, 518) | 46×70 | 1.05 | 9 | + +**OVERRIDES**:TOP 6 / TOP 11 推到边缘 (8, 488) 和 (321, 488),避免与 TOP 5/7、TOP 12/10 重叠。 + +### 5.5 单个 item 结构(ScatteredRanks) + +尺寸 46×70 = label 14 + gap 2 + cover 56: + +```vue + + TOP 4 + + +``` + +- **label**(顶部 14rpx): + - 背景:`radial-gradient(ellipse, #C8E6FF, #fff 50%, #4D9AF8)`(蓝白钻石) + - TOP 4 label 用金渐变:`#FFFFFF, #FFFABD 30%, #4D9AF8 100%` + - 圆角 7rpx + - 文字:14rpx Baloo Bhai(fallback monospace),色 `#FFFABD`,`text-shadow: -1px 1px 2px rgba(206,9,9,0.84)` +- **cover**(下方 56rpx): + - 圆角 5rpx + - box-shadow:`3px 3px 6px rgba(198,13,13,0.45)`(按 scale 调整) + +### 5.6 颜色 / 渐变 / 阴影 + +| Token | 值 | 用途 | +|----|----|----| +| 主背景 | `linear-gradient(179deg, #FFE5E5 0%, #F3A0A1 0%, #FF9C9C 86%, #FF2024 100%)` | 装饰层渐变 | +| 樱花粉光晕 | `#F3D3E3` + `blur(60px)` | 椭圆中心光晕 | +| 暖黄光晕 | `#FFFABD` + `blur(50px)` | 顶部装饰光晕 | +| 钻石渐变(外框) | `radial-gradient(ellipse at -10% 5%, #86BEFF, #FF3939 32%, #88FFCE 59%, #4D9AF8)` | PodiumCard 描边 | +| 蓝白钻石(标签) | `radial-gradient(ellipse, #C8E6FF, #fff 50%, #4D9AF8)` | 9 item label | +| 金渐变(TOP 4 label) | `radial-gradient(ellipse, #FFFFFF, #FFFABD 30%, #4D9AF8 100%)` | TOP 4 label | +| 金渐变(TOP 1 标签) | `radial-gradient(ellipse, #FFD700, #FFF6A8 30%, #DAA520 100%)` | TOP 1 标签 | +| 银渐变(TOP 2 标签) | `radial-gradient(ellipse, #C0C0C0, #E8E8E8 50%, #7A7A7A)` | TOP 2 标签 | +| 铜渐变(TOP 3 标签) | `radial-gradient(ellipse, #CD7F32, #E8A45C 50%, #A0522D)` | TOP 3 标签 | +| TOP 数字字色 | `#FFFABD` + `text-shadow: -1px 1px 2px rgba(206,9,9,0.84)` | label 文字 | +| 卡片阴影 | `3px 3px 6px rgba(198,13,13,0.45)`(随 scale 微调) | cover 阴影 | + +### 5.7 字体 + +| 字体 | 用途 | Fallback | +|----|----|----| +| Baloo Bhai | label 数字(TOP 4、TOP 5 等) | monospace | +| HYQiHei | 不使用(已移除用户名显示) | PingFang | +| C800 | 不使用(已移除 TOP 排名文字) | system bold sans | + +--- + +## 6. 动画设计 + +### 6.1 9 item 顺时针旋转 + +``` +@keyframes orbit { + 0% { transform: translate(0,0) scale(1.15); } /* slot 0: front */ + 11.11% { transform: translate(84px,-13px) scale(1.05); } /* slot 1 */ + 22.22% { transform: translate(157px,-43px) scale(0.95); } /* slot 2 */ + 33.33% { transform: translate(113px,-83px) scale(0.85); } /* slot 3 */ + 44.44% { transform: translate(45px,-107px) scale(0.75); } /* slot 4 */ + 55.55% { transform: translate(-45px,-107px) scale(0.75); } /* slot 5 */ + 66.66% { transform: translate(-113px,-83px) scale(0.85); } /* slot 6 */ + 77.77% { transform: translate(-156px,-43px) scale(0.95); } /* slot 7 */ + 88.88% { transform: translate(-84px,-13px) scale(1.05); } /* slot 8 */ + 100% { transform: translate(0,0) scale(1.15); } /* loop */ +} +``` + +- 总周期 36s,4s 一步 +- 线性 timing,匀速旋转 +- infinite 循环 + +### 6.2 9 个 item 的 delay 错开 + +``` +.ring-item { animation: orbit 36s linear infinite; } +.r0 { animation-delay: 0s; } /* TOP 4 起始 slot 0 */ +.r1 { animation-delay: -4s; } /* TOP 5 起始 slot 1 */ +.r2 { animation-delay: -8s; } /* TOP 6 起始 slot 2 */ +... +.r8 { animation-delay: -32s; } /* TOP 12 起始 slot 8 */ +``` + +负值 delay 让每个 item 起始位置 = 对应的 slot,9 个 item 静态分布在 9 个 slot 上。 + +### 6.3 皇冠脉冲 + +```css +@keyframes crownPulse { + 0%, 100% { transform: translateX(-50%) scale(1); } + 50% { transform: translateX(-50%) scale(1.15); } +} +.crown { animation: crownPulse 2s ease-in-out infinite; } +``` + +### 6.4 可访问性 + +```css +@media (prefers-reduced-motion: reduce) { + .ring-item { animation: none; } + .crown { animation: none; } + /* item 用 inline transform 写到元素上,作为静态位置 */ +} +``` + +--- + +## 7. 状态管理 + +| 状态 | 显示 | +|----|----| +| Loading | 12 个骨架占位(3 大 + 9 小)+ shimmer 动画 | +| Success | 完整布局 | +| Empty (items.length < 3) | 「数据加载中」+ 重试按钮 | +| Error | 居中提示「加载失败,点击重试」+ 重试按钮 | + +--- + +## 8. 交互细节 + +- **单击 PodiumCard / ScatteredRank 缩略图**:emit('cardClick', item) → square.vue 已有逻辑 → 跳 `pages/asset-detail/asset-detail` +- **双击**:同样走 square.vue 的 `handleCardClick` → `doubleTapLike` + 波纹动画 +- **TOP 1 奖牌/皇冠**:CSS keyframes 微脉冲(scale 1.0 → 1.15 → 1.0,2s 循环) + +--- + +## 9. 性能/可访问性 + +- 图片懒加载用 uni-app ``(与现有 HotCategoryBlock 一致) +- `prefers-reduced-motion: reduce` 关闭旋转和脉冲 +- 9 item 动画使用 GPU 加速(transform) +- 椭圆轨道 SVG 装饰层 `pointer-events: none` + +--- + +## 10. 风险与备选 + +| 风险 | 缓解 | +|----|----| +| 旋转动画在低端机上卡顿 | 可降级到 60s 周期;或加 `will-change: transform` | +| 椭圆轨道的虚线 SVG 在某些小程序不渲染 | 提供 `v-if` 兜底,仅 H5 显示 | +| 数据 < 9 条时 ScatteredRanks 渲染异常 | 用 v-for 配合 length 校验,< 9 时只渲染实际条数 | + +--- + +## 11. 验收标准 + +1. ✅ square 页点击「星河」tab,显示 12 个 item 的 3D 排行榜 +2. ✅ TOP 1-3 在上方颁奖台位置,cover 下方有金/银/铜 TOP N 标签 +3. ✅ TOP 4-12 在下方椭圆轨道,9 个 item 等大 46×70,标签在 cover 上方 +4. ✅ 9 item 顺时针连续旋转(36s 一圈) +5. ✅ 近大远小(scale 0.75→1.15)表达 3D 透视 +6. ✅ 点击任一卡片跳 asset-detail +7. ✅ 双击点赞 + 波纹动画 +8. ✅ Loading / Empty / Error 状态都正常显示 +9. ✅ `prefers-reduced-motion: reduce` 时关闭旋转 +10. ✅ H5 + 小程序 + APP 三端视觉一致 diff --git a/frontend/pages/square/components/HotCategoryBlock.vue b/frontend/pages/square/components/HotCategoryBlock.vue index 26a8c10..34926e7 100644 --- a/frontend/pages/square/components/HotCategoryBlock.vue +++ b/frontend/pages/square/components/HotCategoryBlock.vue @@ -42,8 +42,8 @@ :key="item.id || index" class="grid-card" :class="{ - 'grid-card-top': index < 3, - [`grid-card-top-${index + 1}`]: index < 3, + [`grid-card-top-${index + 1}`]: index < 5, + 'grid-card-top-other': index >= 5, }" @click="handleCardClick(item)" > @@ -147,9 +147,33 @@ const tabs = [ { key: "hot", label: "热度榜", - icon: "/static/square/rementubiao.png", - iconWidth: 32, - iconHeight: 40, + icon: "/static/square/galaxy/dianzanbang.png", + iconWidth: 64, + iconHeight: 72, + fetch: () => getHotRankingApi("displaying", null, 1, 11), + }, + { + key: "new", + label: "活跃榜", + icon: "/static/square/galaxy/huoyuebang.png", + iconWidth: 64, + iconHeight: 72, + fetch: () => getHotRankingApi("displaying", null, 1, 11), + }, + { + key: "trending", + label: "曝光榜", + icon: "/static/square/galaxy/baoguangbang.png", + iconWidth: 64, + iconHeight: 72, + fetch: () => getHotRankingApi("displaying", null, 1, 11), + }, + { + key: "trending", + label: "同城榜", + icon: "/static/square/galaxy/tongchengbang.png", + iconWidth: 64, + iconHeight: 72, fetch: () => getHotRankingApi("displaying", null, 1, 11), }, ]; @@ -279,7 +303,7 @@ onUnmounted(() => { }); -