feat(deploy): production deployment setup and fixes
- Debian-based api image (bookworm-slim), docker/debian mirrors, prisma binaryTargets for openssl 3.0 - nginx: admin SPA under /admin, TLS via acme.sh (ZeroSSL) + auto-renewal cron, http->https redirect - prisma: add origin_goods.delisted migration, sync missing schema (good_image/tag_font_color/good_tags), fix users.createdAt Timestamptz - api: CORS wildcard reflection, helmet CORP cross-origin, price backfill in persistProductDetail, categoryIcon ancestor fallback, mediaByColor per-color gallery in public goods detail - admin: /admin base path (vite + router) - import-data.mjs: udt_name casting, serial sequence advance fix
This commit is contained in:
@@ -1,30 +1,30 @@
|
||||
# Homepage Navigation and Carousel Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make homepage CTAs navigate in the current page, automate homepage carousels, preserve the hero visual as a capped image, add product-center gutters, and temporarily hide two navigation menus.
|
||||
|
||||
**Architecture:** Keep behavior local to the existing Vue components. Use native current-page navigation for CTAs, lifecycle-managed timers for the step carousel, a duplicated CSS marquee for customer cases, and responsive CSS custom properties for product-center gutters.
|
||||
|
||||
**Tech Stack:** Nuxt 4, Vue 3 Composition API, Tailwind CSS, Vitest, Vue Test Utils
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Lock navigation behavior
|
||||
- [x] Add failing tests for current-page CTA and product-card navigation.
|
||||
- [x] Replace new-tab links and inert CTA buttons with immediate current-page navigation.
|
||||
- [x] Hide recommendation and solution navigation entries while retaining implementation code.
|
||||
|
||||
### Task 2: Automate homepage rotating content
|
||||
- [x] Add failing timer tests for the four-step carousel.
|
||||
- [x] Add a lifecycle-managed automatic step interval.
|
||||
- [x] Animate the duplicated customer-case track continuously and add reduced-motion behavior.
|
||||
|
||||
### Task 3: Improve desktop layout
|
||||
- [x] Keep `/主视觉.png` as an `<img>` with intrinsic aspect ratio and a capped rendered size.
|
||||
- [x] Add responsive left and right gutters to the product center.
|
||||
|
||||
### Task 4: Verify and document
|
||||
- [x] Update homepage, product-center, structure, and Agent reference docs.
|
||||
- [x] Run website tests and production build.
|
||||
- [x] Skip browser automation per the user's request; cover navigation, timing, hero image, country alignment, and bottom action behavior with Vitest.
|
||||
# Homepage Navigation and Carousel Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make homepage CTAs navigate in the current page, automate homepage carousels, preserve the hero visual as a capped image, add product-center gutters, and temporarily hide two navigation menus.
|
||||
|
||||
**Architecture:** Keep behavior local to the existing Vue components. Use native current-page navigation for CTAs, lifecycle-managed timers for the step carousel, a duplicated CSS marquee for customer cases, and responsive CSS custom properties for product-center gutters.
|
||||
|
||||
**Tech Stack:** Nuxt 4, Vue 3 Composition API, Tailwind CSS, Vitest, Vue Test Utils
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Lock navigation behavior
|
||||
- [x] Add failing tests for current-page CTA and product-card navigation.
|
||||
- [x] Replace new-tab links and inert CTA buttons with immediate current-page navigation.
|
||||
- [x] Hide recommendation and solution navigation entries while retaining implementation code.
|
||||
|
||||
### Task 2: Automate homepage rotating content
|
||||
- [x] Add failing timer tests for the four-step carousel.
|
||||
- [x] Add a lifecycle-managed automatic step interval.
|
||||
- [x] Animate the duplicated customer-case track continuously and add reduced-motion behavior.
|
||||
|
||||
### Task 3: Improve desktop layout
|
||||
- [x] Keep `/主视觉.png` as an `<img>` with intrinsic aspect ratio and a capped rendered size.
|
||||
- [x] Add responsive left and right gutters to the product center.
|
||||
|
||||
### Task 4: Verify and document
|
||||
- [x] Update homepage, product-center, structure, and Agent reference docs.
|
||||
- [x] Run website tests and production build.
|
||||
- [x] Skip browser automation per the user's request; cover navigation, timing, hero image, country alignment, and bottom action behavior with Vitest.
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
# Product Center Backend Integration Implementation Plan
|
||||
|
||||
**Goal:** Make product-center countries, categories, tag filters, and their icons fully driven by backend configuration while matching the Figma assets.
|
||||
|
||||
**Architecture:** Store Figma-derived icons as versioned API static assets and update existing database records through an idempotent Prisma script. The website resolves backend-relative asset URLs against `backendUrl` and builds filter controls from backend tag-group membership rather than sort-order guesses or tag-name allowlists.
|
||||
|
||||
**Tech Stack:** NestJS, Prisma, PostgreSQL, Nuxt 4, Vue 3, Vitest, Jest.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Backend Icon Assets and Configuration Script
|
||||
|
||||
- [x] Export country and top-level category icons from Figma into `apps/api/public/product-center`.
|
||||
- [x] Serve committed API assets under `/assets/`.
|
||||
- [x] Add an idempotent Prisma script that updates matching country/category icon fields.
|
||||
- [x] Run the script against the configured database and verify public API values.
|
||||
|
||||
### Task 2: Website Backend Asset Resolution
|
||||
|
||||
- [x] Add failing tests for absolute, data, and backend-relative icon URLs.
|
||||
- [x] Implement URL resolution in the product-center data mapper.
|
||||
- [x] Verify country and category components render API-provided icons.
|
||||
|
||||
### Task 3: Backend-Driven Tag Groups
|
||||
|
||||
- [x] Add failing component tests using backend group IDs/names.
|
||||
- [x] Pass tag groups into `ProductTagFilter` and derive logistics/process groups from backend metadata.
|
||||
- [x] Remove sort-order and tag-name allowlist assumptions.
|
||||
|
||||
### Task 4: End-to-End Verification
|
||||
|
||||
- [x] Run website and API tests/builds.
|
||||
- [x] Verify public endpoints contain durable icon URLs.
|
||||
- [x] Browser-test icons and filters on `/product-center`.
|
||||
# Product Center Backend Integration Implementation Plan
|
||||
|
||||
**Goal:** Make product-center countries, categories, tag filters, and their icons fully driven by backend configuration while matching the Figma assets.
|
||||
|
||||
**Architecture:** Store Figma-derived icons as versioned API static assets and update existing database records through an idempotent Prisma script. The website resolves backend-relative asset URLs against `backendUrl` and builds filter controls from backend tag-group membership rather than sort-order guesses or tag-name allowlists.
|
||||
|
||||
**Tech Stack:** NestJS, Prisma, PostgreSQL, Nuxt 4, Vue 3, Vitest, Jest.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Backend Icon Assets and Configuration Script
|
||||
|
||||
- [x] Export country and top-level category icons from Figma into `apps/api/public/product-center`.
|
||||
- [x] Serve committed API assets under `/assets/`.
|
||||
- [x] Add an idempotent Prisma script that updates matching country/category icon fields.
|
||||
- [x] Run the script against the configured database and verify public API values.
|
||||
|
||||
### Task 2: Website Backend Asset Resolution
|
||||
|
||||
- [x] Add failing tests for absolute, data, and backend-relative icon URLs.
|
||||
- [x] Implement URL resolution in the product-center data mapper.
|
||||
- [x] Verify country and category components render API-provided icons.
|
||||
|
||||
### Task 3: Backend-Driven Tag Groups
|
||||
|
||||
- [x] Add failing component tests using backend group IDs/names.
|
||||
- [x] Pass tag groups into `ProductTagFilter` and derive logistics/process groups from backend metadata.
|
||||
- [x] Remove sort-order and tag-name allowlist assumptions.
|
||||
|
||||
### Task 4: End-to-End Verification
|
||||
|
||||
- [x] Run website and API tests/builds.
|
||||
- [x] Verify public endpoints contain durable icon URLs.
|
||||
- [x] Browser-test icons and filters on `/product-center`.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,75 +1,75 @@
|
||||
# 产品中心 Figma 还原功能规格
|
||||
|
||||
## 目标
|
||||
|
||||
将 Figma 文件“印美达官网 Copy”节点 `1124:1633` 的产品中心页面还原到 `apps/website` 的 `/product-center` 二级页。1440px 桌面视口以设计稿为唯一视觉基准,同时保留现有真实商品接口和完整筛选交互。
|
||||
|
||||
## 设计基准
|
||||
|
||||
- Figma 文件 key:`c967cRetA5krsgM5jh0XPo`
|
||||
- 节点:`1124:1633`
|
||||
- 设计画布:1440 x 1602
|
||||
- 桌面结构:80px 顶部导航、240px 左侧分类栏、1200px 主内容区。
|
||||
- 主内容依次包含国家筛选、物流/工艺筛选与搜索、已选标签、四列商品网格和底部分页。
|
||||
- 颜色、字号、间距、边框、圆角、图标尺寸、卡片比例和选中态按 Figma 设计上下文实现。
|
||||
|
||||
## 页面行为
|
||||
|
||||
### 默认状态
|
||||
|
||||
- 页面首次进入时呈现设计稿中的示例选择状态:女式服装、T恤、包邮、烫画。
|
||||
- 国家筛选默认选中“全部”。
|
||||
- 默认筛选必须映射到后端返回的真实分类和标签,不使用硬编码 ID;当目标项不存在时保持对应维度未选择,页面仍可正常加载。
|
||||
|
||||
### 筛选与搜索
|
||||
|
||||
- 分类、国家、物流、工艺、关键词搜索和已选标签删除继续驱动现有商品查询。
|
||||
- 分类树支持展开与收起;桌面保持常驻侧栏。
|
||||
- 国家选项显示接口返回的国旗和名称。
|
||||
- 搜索支持按钮提交和回车提交。
|
||||
- 改变筛选条件后回到第一页并刷新商品数据。
|
||||
|
||||
### 商品与分页
|
||||
|
||||
- 商品卡片展示真实商品图、名称、标签、国家和起售价。
|
||||
- 1440px 视口每行展示四张卡片,尺寸与设计稿一致。
|
||||
- 分页展示总数、页码、前后翻页、每页数量和页码跳转;所有操作使用真实分页状态。
|
||||
- 加载、空数据和接口失败时保留明确且不破坏布局的反馈状态。
|
||||
|
||||
## 响应式规则
|
||||
|
||||
- 1440px 及常规桌面宽度优先保证设计稿还原精度。
|
||||
- 平板和移动端将左侧分类栏折叠为可开关抽屉。
|
||||
- 国家和筛选项允许横向浏览,不压缩到不可读尺寸。
|
||||
- 商品网格根据容器宽度从四列降为三列、两列和单列。
|
||||
- 移动端分页允许换行或精简页码,但保留总数、翻页与当前页信息。
|
||||
- 任意支持宽度下不得出现文字、控件或商品卡片重叠。
|
||||
|
||||
## 组件边界
|
||||
|
||||
- `product-center.vue`:页面数据编排、默认筛选初始化和响应式侧栏状态。
|
||||
- `ProductSidebar.vue`:桌面分类树与移动端抽屉内容。
|
||||
- `ProductCountryFilter.vue`:国家胶囊筛选。
|
||||
- `ProductTagFilter.vue` / `ProductFilterBar.vue`:下拉筛选、已选标签和搜索。
|
||||
- `ProductGrid.vue` / `ProductCard.vue`:商品网格、卡片和状态反馈。
|
||||
- `ProductPagination.vue`:分页、每页数量和跳转。
|
||||
- `useProductCenter.ts`:真实接口状态、筛选映射和查询行为;不承载视觉规则。
|
||||
|
||||
## 权限评估
|
||||
|
||||
本功能是公开官网页面的视觉和交互调整,不新增管理操作、不读取额外敏感数据,也不改变后端资源权限,因此无需新增权限控制。
|
||||
|
||||
## 测试与验收
|
||||
|
||||
- 使用 Vitest/Vue Test Utils 覆盖默认筛选映射、筛选交互、搜索提交、标签删除和分页事件。
|
||||
- 运行 website 的类型检查、测试和生产构建。
|
||||
- 启动本地 Nuxt 服务,在 1440px 桌面及典型平板、手机视口进行浏览器截图验证。
|
||||
- 桌面截图与 Figma 对照检查导航、侧栏、筛选区、商品卡片、分页的尺寸和位置。
|
||||
- 验证真实接口成功、空数据和失败场景不破坏页面结构。
|
||||
|
||||
## 文档范围
|
||||
|
||||
- 更新 `docs/references/structs.md` 记录产品中心组件职责。
|
||||
- 更新产品中心详细参考文档和 `skills` 中对应项目使用说明。
|
||||
- 本次为已有功能的设计调整,不更新 README 快速开始。
|
||||
|
||||
# 产品中心 Figma 还原功能规格
|
||||
|
||||
## 目标
|
||||
|
||||
将 Figma 文件“印美达官网 Copy”节点 `1124:1633` 的产品中心页面还原到 `apps/website` 的 `/product-center` 二级页。1440px 桌面视口以设计稿为唯一视觉基准,同时保留现有真实商品接口和完整筛选交互。
|
||||
|
||||
## 设计基准
|
||||
|
||||
- Figma 文件 key:`c967cRetA5krsgM5jh0XPo`
|
||||
- 节点:`1124:1633`
|
||||
- 设计画布:1440 x 1602
|
||||
- 桌面结构:80px 顶部导航、240px 左侧分类栏、1200px 主内容区。
|
||||
- 主内容依次包含国家筛选、物流/工艺筛选与搜索、已选标签、四列商品网格和底部分页。
|
||||
- 颜色、字号、间距、边框、圆角、图标尺寸、卡片比例和选中态按 Figma 设计上下文实现。
|
||||
|
||||
## 页面行为
|
||||
|
||||
### 默认状态
|
||||
|
||||
- 页面首次进入时呈现设计稿中的示例选择状态:女式服装、T恤、包邮、烫画。
|
||||
- 国家筛选默认选中“全部”。
|
||||
- 默认筛选必须映射到后端返回的真实分类和标签,不使用硬编码 ID;当目标项不存在时保持对应维度未选择,页面仍可正常加载。
|
||||
|
||||
### 筛选与搜索
|
||||
|
||||
- 分类、国家、物流、工艺、关键词搜索和已选标签删除继续驱动现有商品查询。
|
||||
- 分类树支持展开与收起;桌面保持常驻侧栏。
|
||||
- 国家选项显示接口返回的国旗和名称。
|
||||
- 搜索支持按钮提交和回车提交。
|
||||
- 改变筛选条件后回到第一页并刷新商品数据。
|
||||
|
||||
### 商品与分页
|
||||
|
||||
- 商品卡片展示真实商品图、名称、标签、国家和起售价。
|
||||
- 1440px 视口每行展示四张卡片,尺寸与设计稿一致。
|
||||
- 分页展示总数、页码、前后翻页、每页数量和页码跳转;所有操作使用真实分页状态。
|
||||
- 加载、空数据和接口失败时保留明确且不破坏布局的反馈状态。
|
||||
|
||||
## 响应式规则
|
||||
|
||||
- 1440px 及常规桌面宽度优先保证设计稿还原精度。
|
||||
- 平板和移动端将左侧分类栏折叠为可开关抽屉。
|
||||
- 国家和筛选项允许横向浏览,不压缩到不可读尺寸。
|
||||
- 商品网格根据容器宽度从四列降为三列、两列和单列。
|
||||
- 移动端分页允许换行或精简页码,但保留总数、翻页与当前页信息。
|
||||
- 任意支持宽度下不得出现文字、控件或商品卡片重叠。
|
||||
|
||||
## 组件边界
|
||||
|
||||
- `product-center.vue`:页面数据编排、默认筛选初始化和响应式侧栏状态。
|
||||
- `ProductSidebar.vue`:桌面分类树与移动端抽屉内容。
|
||||
- `ProductCountryFilter.vue`:国家胶囊筛选。
|
||||
- `ProductTagFilter.vue` / `ProductFilterBar.vue`:下拉筛选、已选标签和搜索。
|
||||
- `ProductGrid.vue` / `ProductCard.vue`:商品网格、卡片和状态反馈。
|
||||
- `ProductPagination.vue`:分页、每页数量和跳转。
|
||||
- `useProductCenter.ts`:真实接口状态、筛选映射和查询行为;不承载视觉规则。
|
||||
|
||||
## 权限评估
|
||||
|
||||
本功能是公开官网页面的视觉和交互调整,不新增管理操作、不读取额外敏感数据,也不改变后端资源权限,因此无需新增权限控制。
|
||||
|
||||
## 测试与验收
|
||||
|
||||
- 使用 Vitest/Vue Test Utils 覆盖默认筛选映射、筛选交互、搜索提交、标签删除和分页事件。
|
||||
- 运行 website 的类型检查、测试和生产构建。
|
||||
- 启动本地 Nuxt 服务,在 1440px 桌面及典型平板、手机视口进行浏览器截图验证。
|
||||
- 桌面截图与 Figma 对照检查导航、侧栏、筛选区、商品卡片、分页的尺寸和位置。
|
||||
- 验证真实接口成功、空数据和失败场景不破坏页面结构。
|
||||
|
||||
## 文档范围
|
||||
|
||||
- 更新 `docs/references/structs.md` 记录产品中心组件职责。
|
||||
- 更新产品中心详细参考文档和 `skills` 中对应项目使用说明。
|
||||
- 本次为已有功能的设计调整,不更新 README 快速开始。
|
||||
|
||||
|
||||
@@ -1,110 +1,110 @@
|
||||
# Product Center Figma Restoration Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Restore Figma node `1124:1633` in the Nuxt product center while preserving real API-backed filtering, search, and pagination.
|
||||
|
||||
**Architecture:** Keep `useProductCenter` as the query/state boundary and restyle the existing focused Vue components. Add a small default-filter resolver so Figma's initial state is derived from backend names rather than IDs, then compose desktop and mobile layouts in the existing page.
|
||||
|
||||
**Tech Stack:** Nuxt 4, Vue 3 Composition API, TypeScript, CSS, Vitest, Vue Test Utils, Playwright/browser QA.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add Website Test Infrastructure
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/website/package.json`
|
||||
- Create: `apps/website/vitest.config.ts`
|
||||
- Create: `apps/website/test/setup.ts`
|
||||
|
||||
- [ ] Add Vitest, Vue Test Utils, happy-dom, and a scoped `test` script.
|
||||
- [ ] Configure Nuxt aliases and the Vue plugin for component tests.
|
||||
- [ ] Run `pnpm --filter @inkreach/website test --run` and confirm the empty suite exits successfully.
|
||||
|
||||
### Task 2: Resolve the Figma Default Filter State
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/website/app/composables/useProductCenter.ts`
|
||||
- Create: `apps/website/test/useProductCenter.test.ts`
|
||||
|
||||
- [ ] Write failing tests for resolving `女式服装`, `T恤`, `包邮`, and `烫画` by display name while tolerating missing entries.
|
||||
- [ ] Run the focused test and confirm it fails because the resolver does not exist.
|
||||
- [ ] Implement and export a pure name-based resolver, then apply its result once after categories and tags load.
|
||||
- [ ] Run the focused test and confirm it passes without hard-coded backend IDs.
|
||||
|
||||
### Task 3: Restore Sidebar and Filter Controls
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/website/app/components/product/ProductSidebar.vue`
|
||||
- Modify: `apps/website/app/components/product/ProductCountryFilter.vue`
|
||||
- Modify: `apps/website/app/components/product/ProductTagFilter.vue`
|
||||
- Modify: `apps/website/app/components/product/ProductFilterBar.vue`
|
||||
- Create: `apps/website/test/product-filters.test.ts`
|
||||
|
||||
- [ ] Write failing component tests for category selection, country selection, dropdown selection, tag removal, and enter/button search submission.
|
||||
- [ ] Run the focused tests and verify the missing Figma interactions or markup cause failure.
|
||||
- [ ] Implement the 240px category tree, country pills, filter pills, selected chips, and 220px search control using semantic buttons and inputs.
|
||||
- [ ] Add mobile drawer-compatible props and events without duplicating sidebar content.
|
||||
- [ ] Run focused tests and confirm all filter events pass.
|
||||
|
||||
### Task 4: Restore Product Cards and Grid
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/website/app/components/product/ProductCard.vue`
|
||||
- Modify: `apps/website/app/components/product/ProductCardSkeleton.vue`
|
||||
- Modify: `apps/website/app/components/product/ProductGrid.vue`
|
||||
- Create: `apps/website/test/product-grid.test.ts`
|
||||
|
||||
- [ ] Write failing tests for image/name/tag/country/price rendering and loading/empty states.
|
||||
- [ ] Run the focused tests and confirm Figma card structure expectations fail.
|
||||
- [ ] Implement 270 x 382 desktop cards with 270px media, 18px title, grouped tag colors, and Figma price typography.
|
||||
- [ ] Implement the four/three/two/one-column responsive grid and stable skeleton/empty layouts.
|
||||
- [ ] Run focused tests and confirm all card and grid states pass.
|
||||
|
||||
### Task 5: Restore Pagination
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/website/app/components/product/ProductPagination.vue`
|
||||
- Create: `apps/website/test/product-pagination.test.ts`
|
||||
|
||||
- [ ] Write failing tests for total text, visible page tokens, previous/next, page-size selection, and page jump.
|
||||
- [ ] Run the focused test and verify the expected Figma pagination controls fail.
|
||||
- [ ] Implement the compact 32px pagination controls with responsive wrapping and real emitted values.
|
||||
- [ ] Run focused tests and confirm pagination behavior passes.
|
||||
|
||||
### Task 6: Compose the Restored Page
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/website/app/pages/product-center.vue`
|
||||
- Modify: `apps/website/app/components/AppHeader.vue`
|
||||
- Create: `apps/website/test/product-center-page.test.ts`
|
||||
|
||||
- [ ] Write a failing page-level test for Figma section order, default-state initialization, sidebar drawer trigger, and clearing filters.
|
||||
- [ ] Run the focused test and confirm the current layout fails.
|
||||
- [ ] Compose the 80px header, fixed-width desktop sidebar, 1200px content region, filter rows, four-column grid, ad capsule, and pagination.
|
||||
- [ ] Add the mobile category drawer with backdrop, close button, escape handling, and body-scroll-safe layout.
|
||||
- [ ] Run the page test and full website test suite.
|
||||
|
||||
### Task 7: Documentation and Structural References
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/references/structs.md`
|
||||
- Create: `docs/references/product-center.md`
|
||||
- Create: `skills/inkreach-official-website/SKILL.md`
|
||||
|
||||
- [ ] Document the product center route, real-data filters, default state, responsive behavior, and component responsibilities.
|
||||
- [ ] Update the structure reference with all created and modified website modules.
|
||||
- [ ] Add the agent-facing usage workflow and verification commands.
|
||||
|
||||
### Task 8: Verification and Visual Comparison
|
||||
|
||||
**Files:**
|
||||
- Modify only files required by issues found during verification.
|
||||
|
||||
- [ ] Run `pnpm --filter @inkreach/website test --run` and confirm zero failures.
|
||||
- [ ] Run `pnpm --filter @inkreach/website build` and confirm a successful Nuxt production build.
|
||||
- [ ] Start the website dev server on an available port.
|
||||
- [ ] Capture `/product-center` at 1440px desktop, tablet, and mobile widths with browser automation.
|
||||
- [ ] Compare desktop geometry against Figma node `1124:1633`; correct visible spacing, sizing, overflow, and overlap defects.
|
||||
- [ ] Re-run tests and build after visual corrections.
|
||||
|
||||
# Product Center Figma Restoration Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Restore Figma node `1124:1633` in the Nuxt product center while preserving real API-backed filtering, search, and pagination.
|
||||
|
||||
**Architecture:** Keep `useProductCenter` as the query/state boundary and restyle the existing focused Vue components. Add a small default-filter resolver so Figma's initial state is derived from backend names rather than IDs, then compose desktop and mobile layouts in the existing page.
|
||||
|
||||
**Tech Stack:** Nuxt 4, Vue 3 Composition API, TypeScript, CSS, Vitest, Vue Test Utils, Playwright/browser QA.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add Website Test Infrastructure
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/website/package.json`
|
||||
- Create: `apps/website/vitest.config.ts`
|
||||
- Create: `apps/website/test/setup.ts`
|
||||
|
||||
- [ ] Add Vitest, Vue Test Utils, happy-dom, and a scoped `test` script.
|
||||
- [ ] Configure Nuxt aliases and the Vue plugin for component tests.
|
||||
- [ ] Run `pnpm --filter @inkreach/website test --run` and confirm the empty suite exits successfully.
|
||||
|
||||
### Task 2: Resolve the Figma Default Filter State
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/website/app/composables/useProductCenter.ts`
|
||||
- Create: `apps/website/test/useProductCenter.test.ts`
|
||||
|
||||
- [ ] Write failing tests for resolving `女式服装`, `T恤`, `包邮`, and `烫画` by display name while tolerating missing entries.
|
||||
- [ ] Run the focused test and confirm it fails because the resolver does not exist.
|
||||
- [ ] Implement and export a pure name-based resolver, then apply its result once after categories and tags load.
|
||||
- [ ] Run the focused test and confirm it passes without hard-coded backend IDs.
|
||||
|
||||
### Task 3: Restore Sidebar and Filter Controls
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/website/app/components/product/ProductSidebar.vue`
|
||||
- Modify: `apps/website/app/components/product/ProductCountryFilter.vue`
|
||||
- Modify: `apps/website/app/components/product/ProductTagFilter.vue`
|
||||
- Modify: `apps/website/app/components/product/ProductFilterBar.vue`
|
||||
- Create: `apps/website/test/product-filters.test.ts`
|
||||
|
||||
- [ ] Write failing component tests for category selection, country selection, dropdown selection, tag removal, and enter/button search submission.
|
||||
- [ ] Run the focused tests and verify the missing Figma interactions or markup cause failure.
|
||||
- [ ] Implement the 240px category tree, country pills, filter pills, selected chips, and 220px search control using semantic buttons and inputs.
|
||||
- [ ] Add mobile drawer-compatible props and events without duplicating sidebar content.
|
||||
- [ ] Run focused tests and confirm all filter events pass.
|
||||
|
||||
### Task 4: Restore Product Cards and Grid
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/website/app/components/product/ProductCard.vue`
|
||||
- Modify: `apps/website/app/components/product/ProductCardSkeleton.vue`
|
||||
- Modify: `apps/website/app/components/product/ProductGrid.vue`
|
||||
- Create: `apps/website/test/product-grid.test.ts`
|
||||
|
||||
- [ ] Write failing tests for image/name/tag/country/price rendering and loading/empty states.
|
||||
- [ ] Run the focused tests and confirm Figma card structure expectations fail.
|
||||
- [ ] Implement 270 x 382 desktop cards with 270px media, 18px title, grouped tag colors, and Figma price typography.
|
||||
- [ ] Implement the four/three/two/one-column responsive grid and stable skeleton/empty layouts.
|
||||
- [ ] Run focused tests and confirm all card and grid states pass.
|
||||
|
||||
### Task 5: Restore Pagination
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/website/app/components/product/ProductPagination.vue`
|
||||
- Create: `apps/website/test/product-pagination.test.ts`
|
||||
|
||||
- [ ] Write failing tests for total text, visible page tokens, previous/next, page-size selection, and page jump.
|
||||
- [ ] Run the focused test and verify the expected Figma pagination controls fail.
|
||||
- [ ] Implement the compact 32px pagination controls with responsive wrapping and real emitted values.
|
||||
- [ ] Run focused tests and confirm pagination behavior passes.
|
||||
|
||||
### Task 6: Compose the Restored Page
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/website/app/pages/product-center.vue`
|
||||
- Modify: `apps/website/app/components/AppHeader.vue`
|
||||
- Create: `apps/website/test/product-center-page.test.ts`
|
||||
|
||||
- [ ] Write a failing page-level test for Figma section order, default-state initialization, sidebar drawer trigger, and clearing filters.
|
||||
- [ ] Run the focused test and confirm the current layout fails.
|
||||
- [ ] Compose the 80px header, fixed-width desktop sidebar, 1200px content region, filter rows, four-column grid, ad capsule, and pagination.
|
||||
- [ ] Add the mobile category drawer with backdrop, close button, escape handling, and body-scroll-safe layout.
|
||||
- [ ] Run the page test and full website test suite.
|
||||
|
||||
### Task 7: Documentation and Structural References
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/references/structs.md`
|
||||
- Create: `docs/references/product-center.md`
|
||||
- Create: `skills/inkreach-official-website/SKILL.md`
|
||||
|
||||
- [ ] Document the product center route, real-data filters, default state, responsive behavior, and component responsibilities.
|
||||
- [ ] Update the structure reference with all created and modified website modules.
|
||||
- [ ] Add the agent-facing usage workflow and verification commands.
|
||||
|
||||
### Task 8: Verification and Visual Comparison
|
||||
|
||||
**Files:**
|
||||
- Modify only files required by issues found during verification.
|
||||
|
||||
- [ ] Run `pnpm --filter @inkreach/website test --run` and confirm zero failures.
|
||||
- [ ] Run `pnpm --filter @inkreach/website build` and confirm a successful Nuxt production build.
|
||||
- [ ] Start the website dev server on an available port.
|
||||
- [ ] Capture `/product-center` at 1440px desktop, tablet, and mobile widths with browser automation.
|
||||
- [ ] Compare desktop geometry against Figma node `1124:1633`; correct visible spacing, sizing, overflow, and overlap defects.
|
||||
- [ ] Re-run tests and build after visual corrections.
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Product Center Navigation Feature Plan
|
||||
|
||||
**Goal:** Improve product filter interactions and connect homepage/product cards to their target routes.
|
||||
|
||||
**Architecture:** Keep backend IDs as the shared routing contract. Product filter state remains in `useProductCenter`; presentational components emit typed events and own only dropdown visibility.
|
||||
|
||||
- [x] Add failing tests for grouped selected tags, dropdown closing, product detail URLs, and country query validation.
|
||||
- [x] Close filter menus after selection and outside clicks; render selected tags under their owning groups.
|
||||
- [x] Link product cards to Inkpod detail pages.
|
||||
- [x] Link all homepage free-trial actions to Inkpod login.
|
||||
- [x] Render all backend-configured countries in a balanced two-row grid; switch countries in place and link category cards to combined country/category filters.
|
||||
- [x] Run the full website test suite, production build, and browser interaction verification.
|
||||
# Product Center Navigation Feature Plan
|
||||
|
||||
**Goal:** Improve product filter interactions and connect homepage/product cards to their target routes.
|
||||
|
||||
**Architecture:** Keep backend IDs as the shared routing contract. Product filter state remains in `useProductCenter`; presentational components emit typed events and own only dropdown visibility.
|
||||
|
||||
- [x] Add failing tests for grouped selected tags, dropdown closing, product detail URLs, and country query validation.
|
||||
- [x] Close filter menus after selection and outside clicks; render selected tags under their owning groups.
|
||||
- [x] Link product cards to Inkpod detail pages.
|
||||
- [x] Link all homepage free-trial actions to Inkpod login.
|
||||
- [x] Render all backend-configured countries in a balanced two-row grid; switch countries in place and link category cards to combined country/category filters.
|
||||
- [x] Run the full website test suite, production build, and browser interaction verification.
|
||||
|
||||
+364
-364
@@ -1,364 +1,364 @@
|
||||
# 标签组 (Tag Group) 功能
|
||||
|
||||
## 目标
|
||||
|
||||
为 `Tag` 引入分组(TagGroup)概念,覆盖三个分组:**物流渠道 / 印刷位置 / 印刷工艺**。
|
||||
分组可在后台通过标签筛选下拉的树形结构直接管理(增/删/重命名/拖动排序),同时在官网商品卡片上按组排序展示 tag chip,国家 chip 放在最后。
|
||||
|
||||
## 用户故事
|
||||
|
||||
1. 运营在「官网后台 → 商品配置 → 标签筛选」打开下拉,看到一个两层树:分组 → 标签,自动展开
|
||||
2. 运营可以拖动分组调整顺序,也可以拖动组内 tag 调整顺序(支持跨组拖动)
|
||||
3. 运营双击分组名直接 inline 编辑,按 Enter 提交
|
||||
4. 运营点击分组底部「+ 在此组下新建标签」在该组下创建新 tag
|
||||
5. 运营点击分组右上角删除按钮删除分组(组内 tag 自动归入「未分组」)
|
||||
6. 顾客在前端产品中心看到商品卡片的 tag chip 按 `物流渠道 → 印刷位置 → 印刷工艺` 顺序排列,国家 chip 排在最后
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 新表 `tag_groups`
|
||||
|
||||
```prisma
|
||||
model TagGroup {
|
||||
id BigInt @id @default(autoincrement()) @map("tag_group_id")
|
||||
groupName String @unique @map("group_name")
|
||||
groupIcon String? @map("group_icon")
|
||||
groupColor String? @map("group_color")
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||
|
||||
tags Tag[]
|
||||
|
||||
@@map("tag_groups")
|
||||
}
|
||||
```
|
||||
|
||||
### `Tag` 表加字段
|
||||
|
||||
```prisma
|
||||
model Tag {
|
||||
// ... 现有字段
|
||||
tagGroupId BigInt? @map("tag_group_id")
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
|
||||
tagGroup TagGroup? @relation(fields: [tagGroupId], references: [id], onDelete: SetNull, onUpdate: NoAction)
|
||||
|
||||
@@index([tagGroupId])
|
||||
@@index([tagGroupId, sortOrder])
|
||||
}
|
||||
```
|
||||
|
||||
### 种子数据
|
||||
|
||||
```sql
|
||||
INSERT INTO tag_groups (group_name, sort_order) VALUES
|
||||
('物流渠道', 1),
|
||||
('印刷位置', 2),
|
||||
('印刷工艺', 3);
|
||||
|
||||
UPDATE tags SET tag_group_id = (SELECT tag_group_id FROM tag_groups WHERE group_name = '物流渠道')
|
||||
WHERE tag_name IN ('包邮', '不包邮');
|
||||
UPDATE tags SET tag_group_id = (SELECT tag_group_id FROM tag_groups WHERE group_name = '印刷位置')
|
||||
WHERE tag_name IN ('单面印花', '双面印');
|
||||
UPDATE tags SET tag_group_id = (SELECT tag_group_id FROM tag_groups WHERE tag_group_name = '印刷工艺')
|
||||
WHERE tag_name IN ('烫画', '直喷', '不打印');
|
||||
|
||||
-- 同组内的 tag 按现有 id 升序分配 sortOrder
|
||||
WITH ordered AS (
|
||||
SELECT t.tag_id,
|
||||
ROW_NUMBER() OVER (PARTITION BY t.tag_group_id ORDER BY t.tag_id) AS rn
|
||||
FROM tags t
|
||||
WHERE t.tag_group_id IS NOT NULL
|
||||
)
|
||||
UPDATE tags SET sort_order = ordered.rn
|
||||
FROM ordered
|
||||
WHERE tags.tag_id = ordered.tag_id;
|
||||
```
|
||||
|
||||
## 后端 API
|
||||
|
||||
### TagGroup CRUD
|
||||
- `GET /tag-groups` — 列表(含 tags)
|
||||
- `POST /tag-groups` — 创建
|
||||
- `PATCH /tag-groups/:id` — 更新(重命名、颜色、图标)
|
||||
- `DELETE /tag-groups/:id` — 删除(级联 SetNull)
|
||||
- `PATCH /tag-groups/sort` — 批量更新 `sortOrder`
|
||||
|
||||
### Tag 扩展
|
||||
- `POST /tags` / `PATCH /tags/:id` — 支持 `tagGroupId` / `sortOrder`
|
||||
- `PATCH /tags/sort` — 批量更新 `sortOrder`(跨组时同时更新 `tagGroupId`)
|
||||
|
||||
### Public 公开接口
|
||||
- `GET /public/tag-groups` — 公开的分组列表
|
||||
- `GET /public/tags` 返回的 `PublicTagDto` 增加:
|
||||
```ts
|
||||
group: { id: string; name: string } | null
|
||||
```
|
||||
|
||||
## 后端文件改动
|
||||
|
||||
| 文件 | 操作 |
|
||||
|---|---|
|
||||
| `prisma/schema.prisma` | 新表 + Tag 字段 |
|
||||
| `prisma/migrations/<timestamp>_tag_groups/migration.sql` | 自动生成 + 种子 |
|
||||
| `src/tag-groups/tag-groups.module.ts` | 新建 |
|
||||
| `src/tag-groups/tag-groups.service.ts` | 新建 |
|
||||
| `src/tag-groups/tag-groups.controller.ts` | 新建 |
|
||||
| `src/tag-groups/dto/create-tag-group.dto.ts` | 新建 |
|
||||
| `src/tag-groups/dto/update-tag-group.dto.ts` | 新建 |
|
||||
| `src/tag-groups/dto/reorder-tag-groups.dto.ts` | 新建(批量排序) |
|
||||
| `src/tags/tags.service.ts` | 改:create/update 支持 `tagGroupId`/`sortOrder`;新增 `reorder` |
|
||||
| `src/tags/tags.controller.ts` | 改:加 `PATCH /tags/sort` |
|
||||
| `src/tags/dto/create-tag.dto.ts` | 改 |
|
||||
| `src/tags/dto/update-tag.dto.ts` | 改 |
|
||||
| `src/tags/dto/reorder-tags.dto.ts` | 新建 |
|
||||
| `src/public/dto/public-tag-group.dto.ts` | 新建 |
|
||||
| `src/public/dto/public-tag.dto.ts` | 改:加 `group` |
|
||||
| `src/public/dto/public-good.dto.ts` | 改:tag 类型加 group |
|
||||
| `src/public/public.service.ts` | 改:getTags include group、按 sortOrder 排序;新增 getTagGroups |
|
||||
| `src/public/public.controller.ts` | 改:加 `GET /public/tag-groups` |
|
||||
| `src/app.module.ts` | 改:注册 TagGroupsModule |
|
||||
| `src/tags/tags.service.spec.ts` | 改:加 tagGroupId 关联测试 |
|
||||
| `src/public/public.service.spec.ts` | 改:加 tag 排序测试 |
|
||||
|
||||
## Admin 前端
|
||||
|
||||
### 核心:`GoodsView.vue` 标签筛选改 X2
|
||||
|
||||
位置:第 661-668 行的 `<el-select multiple>` 标签筛选。
|
||||
|
||||
**新结构**:
|
||||
```vue
|
||||
<el-popover
|
||||
v-model:visible="tagTreeVisible"
|
||||
placement="bottom-start"
|
||||
:width="320"
|
||||
trigger="click"
|
||||
popper-class="tag-tree-popper"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button size="small" plain>
|
||||
<i class="fa-solid fa-tags" />
|
||||
标签筛选 ({{ selectedTagIds.length }})
|
||||
<i class="fa-solid fa-chevron-down" />
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<div class="tag-tree-panel">
|
||||
<div class="tag-tree-header">
|
||||
<span>标签分组</span>
|
||||
<el-button text size="small" :icon="Plus" @click="quickAddGroup">新建分组</el-button>
|
||||
</div>
|
||||
|
||||
<el-tree
|
||||
ref="tagTreeRef"
|
||||
:data="tagTreeData"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
draggable
|
||||
:allow-drop="allowDrop"
|
||||
:props="{ label: 'label', children: 'children' }"
|
||||
@node-drop="onNodeDrop"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div
|
||||
class="tree-node"
|
||||
@mouseenter="hoveredId = data.id"
|
||||
@mouseleave="hoveredId = null"
|
||||
>
|
||||
<!-- 分组节点 -->
|
||||
<template v-if="data.type === 'group'">
|
||||
<i class="fa-solid fa-folder node-icon" />
|
||||
<input
|
||||
v-if="editingId === data.id"
|
||||
ref="editInputRef"
|
||||
v-model="editValue"
|
||||
class="node-edit-input"
|
||||
@blur="commitEdit(data)"
|
||||
@keydown.enter="commitEdit(data)"
|
||||
@keydown.esc="cancelEdit"
|
||||
/>
|
||||
<span v-else class="node-label" @dblclick="startEdit(data)">
|
||||
{{ data.label }}
|
||||
</span>
|
||||
<span v-show="hoveredId === data.id && data.id !== 'g-ungrouped'" class="node-actions">
|
||||
<el-button text size="small" :icon="Delete" @click.stop="removeGroup(data)" />
|
||||
</span>
|
||||
<!-- 分组底部:+ 新建标签 -->
|
||||
<div v-if="data.id !== 'g-ungrouped' && hoveredId === data.id" class="group-add-tag">
|
||||
<el-button text size="small" :icon="Plus" @click.stop="quickAddTag(data)">在此组下新建标签</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 标签节点 -->
|
||||
<template v-else>
|
||||
<span
|
||||
class="node-chip"
|
||||
:style="{ background: data.tagColor + '4d', borderColor: data.tagColor + '99', color: data.fontColor || data.tagColor }"
|
||||
>
|
||||
{{ data.label }}
|
||||
</span>
|
||||
<span v-show="hoveredId === data.id" class="node-actions">
|
||||
<el-button text size="small" :icon="Edit" @click.stop="openTagEdit(data)" />
|
||||
<el-button text size="small" :icon="Delete" @click.stop="removeTag(data)" />
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</el-popover>
|
||||
```
|
||||
|
||||
**关键逻辑**:
|
||||
- `tagTreeData` computed:从 `tagGroups` + `tags` 构造两层树,组内按 `sortOrder`,未分组追加末尾(`disabled: true`)
|
||||
- `allowDrop(draggingNode, dropNode, type)`:
|
||||
- 拖动到分组节点上 → `type === 'inner'`(加入该组)
|
||||
- 拖动到 tag 节点上 → `type === 'before'/'after'`(同组排序)
|
||||
- 未分组节点不允许 drop
|
||||
- `onNodeDrop`:根据拖动结果调 `PATCH /tags/sort`(组内排序)或 `PATCH /tag-groups/sort`(分组排序),或更新 `tagGroupId`(跨组移动)
|
||||
- `startEdit(data)`:双击触发,记录 `editingId`,nextTick 聚焦输入框
|
||||
- `commitEdit(data)`:调 `PATCH /tag-groups/:id` 或 `PATCH /tags/:id` 保存新名称
|
||||
- `quickAddGroup`:弹小输入框 → 调 `POST /tag-groups`
|
||||
- `quickAddTag(group)`:弹小输入框 → 调 `POST /tags`,传入 `tagGroupId`
|
||||
- `removeGroup(data)`:`ElMessageBox.confirm` → 调 `DELETE /tag-groups/:id`
|
||||
- `removeTag(data)`:调 `DELETE /tags/:id`
|
||||
|
||||
**`selectedTagIds` 与树的勾选**:
|
||||
- 树上的勾选用 `show-checkbox` 即可
|
||||
- 但用户更倾向"单选即勾选 / 取消勾选"——可以省略勾选框,用 `@node-click` 处理
|
||||
- 简化方案:分组节点不可点击勾选(disabled),tag 节点单击切换 `selectedTagIds` 中是否包含该 id
|
||||
|
||||
### `TagsView.vue` 同步
|
||||
- 表格加「所属分组」列
|
||||
- 创建/编辑弹窗加「所属分组」`<el-select>` 字段(数据来自 `/tag-groups`)
|
||||
|
||||
### 类型与 API
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `src/types/index.ts` | 加 `TagGroup`、`CreateTagGroupRequest`、`UpdateTagGroupRequest` |
|
||||
| `src/api/tag-groups.ts` | 新建(含 `getList/create/update/remove/reorder`) |
|
||||
| `src/api/tags.ts` | 扩展支持 `tagGroupId`/`sortOrder` |
|
||||
|
||||
## 官网前端
|
||||
|
||||
### 数据加载
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `server/api/backend/tag-groups.get.ts` | 新建代理 |
|
||||
| `composables/useProductCenter.ts` | `BackendTag` 加 `tagGroupId`、`tagGroup: { id, name } \| null`;`Tag` 接口加 `group`;新增 `loadTagGroups()` + `useState('pc-tag-groups')`;新增 `sortTagsByGroup()` 工具函数 |
|
||||
|
||||
### `ProductCard.vue` chip 排序
|
||||
|
||||
**国家 chip 移到 tag 之后**:
|
||||
```vue
|
||||
<div v-if="product.tags.length > 0 || showCountry" class="chips">
|
||||
<span
|
||||
v-for="tag in sortedTags"
|
||||
:key="tag.id"
|
||||
class="chip"
|
||||
:style="{ color: tag.fontColor || tag.color || '#ff6a00', background: (tag.color || '#ff6a00') + '4d', borderColor: (tag.color || '#ff6a00') + '99' }"
|
||||
>
|
||||
{{ tag.name }}
|
||||
</span>
|
||||
<span v-if="showCountry" class="chip country-chip">
|
||||
<img v-if="product.country.icon" :src="product.country.icon" :alt="product.country.name" class="country-flag" />
|
||||
{{ product.country.name }}
|
||||
</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
`sortedTags` computed:
|
||||
```ts
|
||||
const sortedTags = computed(() => {
|
||||
const groupOrder = new Map(tagGroups.value.map((g, i) => [g.id, i]));
|
||||
return [...props.product.tags]
|
||||
.sort((a, b) => {
|
||||
const ao = a.group ? (groupOrder.get(a.group.id) ?? 999) : 999;
|
||||
const bo = b.group ? (groupOrder.get(b.group.id) ?? 999) : 999;
|
||||
return ao - bo;
|
||||
})
|
||||
.slice(0, 4);
|
||||
});
|
||||
```
|
||||
|
||||
### `product-center.vue` 已选 chip 排序
|
||||
- `.selected-tags` 区域按相同规则排序
|
||||
- country chip 排在已选 tag 之后
|
||||
|
||||
## 测试与文档
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `src/tags/tags.service.spec.ts` | 加 tagGroupId 关联测试 |
|
||||
| `src/tags/tags.service.ts` create/update 单测 | 加 `tagGroupId` 字段 |
|
||||
| `src/public/public.service.spec.ts` | 加 `/public/tag-groups` + tag 排序测试 |
|
||||
| `docs/references/structs.md` | 更新项目结构 |
|
||||
| `docs/references/admin/tags.md`(如有) | 新建或更新标签管理文档 |
|
||||
| `skills/inkreach-official-admin/SKILL.md` | 加标签分组管理说明 |
|
||||
| `skills/inkreach-official-website/SKILL.md` | 加 tag 排序说明 |
|
||||
|
||||
## 任务列表
|
||||
|
||||
- [ ] **T1**:后端 schema + migration
|
||||
- 改 `prisma/schema.prisma` 新增 TagGroup + Tag 字段
|
||||
- 跑 `npx prisma migrate dev --name add_tag_groups` 生成 migration
|
||||
- 在 migration.sql 末尾追加种子数据(3 个分组 + 关联现有 tag + 分配 sortOrder)
|
||||
- 跑 `npx prisma generate` 重新生成 client
|
||||
- [ ] **T2**:后端 tag-groups module
|
||||
- 新建 `tag-groups.module.ts` / `service.ts` / `controller.ts` / dto
|
||||
- 注册到 `app.module.ts`
|
||||
- 实现 CRUD + 批量排序
|
||||
- [ ] **T3**:后端 tags 扩展
|
||||
- `create/update` DTO 加 `tagGroupId` / `sortOrder`
|
||||
- `tags.service.ts` 处理新字段
|
||||
- 加 `PATCH /tags/sort` 端点
|
||||
- [ ] **T4**:后端 public 接口
|
||||
- 新建 `public-tag-group.dto.ts`
|
||||
- `PublicTagDto` 加 `group` 字段
|
||||
- `public.service.ts` `getTags` 用 `include: { tagGroup: true }`,按 `tagGroup.sortOrder, sortOrder` 排序
|
||||
- `public.service.ts` 新增 `getTagGroups()`
|
||||
- `public.controller.ts` 加 `GET /public/tag-groups`
|
||||
- [ ] **T5**:后端测试更新
|
||||
- 更新 `tags.service.spec.ts` 现有测试,加 `tagGroupId` 关联
|
||||
- 更新 `public.service.spec.ts` 加 tag group 关联 + 排序测试
|
||||
- [ ] **T6**:Admin 类型 + API
|
||||
- `types/index.ts` 加 `TagGroup` 类型
|
||||
- 新建 `api/tag-groups.ts`
|
||||
- `api/tags.ts` 扩展支持新字段
|
||||
- [ ] **T7**:Admin GoodsView 标签筛选(X2)
|
||||
- `GoodsView.vue:661-668` 改为 `<el-popover>` + `<el-tree>`
|
||||
- 实现 `tagTreeData` computed
|
||||
- 实现拖动(`@node-drop` + `allow-drop`)
|
||||
- 实现 inline 编辑(双击 + 失焦/Enter 提交)
|
||||
- 实现 quick add group / quick add tag
|
||||
- 实现 remove group / remove tag
|
||||
- `selectedTagIds` 与树节点单击同步
|
||||
- "未分组" 虚拟节点追加在最后(`disabled: true`)
|
||||
- [ ] **T8**:Admin TagsView 同步
|
||||
- 表格加「所属分组」列
|
||||
- 创建/编辑弹窗加「所属分组」下拉
|
||||
- [ ] **T9**:官网数据加载
|
||||
- 新建 `server/api/backend/tag-groups.get.ts` 代理
|
||||
- `useProductCenter.ts` 加载 tagGroups
|
||||
- 给 `Tag` 接口加 `group` 字段
|
||||
- 新增 `sortTagsByGroup()` 工具函数
|
||||
- [ ] **T10**:官网 ProductCard 排序
|
||||
- `ProductCard.vue` chip 顺序:tag 们 → 国家
|
||||
- 用 `sortedTags` computed
|
||||
- [ ] **T11**:官网 product-center 已选 chip 排序
|
||||
- `.selected-tags` 区域按组排序
|
||||
- country chip 排在最后
|
||||
- [ ] **T12**:文档
|
||||
- 更新 `docs/references/structs.md`
|
||||
- 更新 `skills/inkreach-official-admin/SKILL.md`
|
||||
- 更新 `skills/inkreach-official-website/SKILL.md`
|
||||
|
||||
## 风险与权衡
|
||||
|
||||
1. **拖动跨组 vs 同组**:用户允许跨组,但 UI 上要明确提示(鼠标 cursor + 节点高亮)
|
||||
2. **删除分组的级联**:用户确认 SetNull,但要在 UI 上二次确认(避免误删)
|
||||
3. **"+ 在此组下新建标签"位置**:用户说"底部",意味着每个组节点下方有固定行(hover 时显示)
|
||||
4. **未分组虚拟节点**:不可拖动、不可编辑/删除,仅作展示
|
||||
5. **数据库 seed 在 production 的处理**:seed 数据只在 migration 中执行一次,production 已有数据时迁移需谨慎
|
||||
# 标签组 (Tag Group) 功能
|
||||
|
||||
## 目标
|
||||
|
||||
为 `Tag` 引入分组(TagGroup)概念,覆盖三个分组:**物流渠道 / 印刷位置 / 印刷工艺**。
|
||||
分组可在后台通过标签筛选下拉的树形结构直接管理(增/删/重命名/拖动排序),同时在官网商品卡片上按组排序展示 tag chip,国家 chip 放在最后。
|
||||
|
||||
## 用户故事
|
||||
|
||||
1. 运营在「官网后台 → 商品配置 → 标签筛选」打开下拉,看到一个两层树:分组 → 标签,自动展开
|
||||
2. 运营可以拖动分组调整顺序,也可以拖动组内 tag 调整顺序(支持跨组拖动)
|
||||
3. 运营双击分组名直接 inline 编辑,按 Enter 提交
|
||||
4. 运营点击分组底部「+ 在此组下新建标签」在该组下创建新 tag
|
||||
5. 运营点击分组右上角删除按钮删除分组(组内 tag 自动归入「未分组」)
|
||||
6. 顾客在前端产品中心看到商品卡片的 tag chip 按 `物流渠道 → 印刷位置 → 印刷工艺` 顺序排列,国家 chip 排在最后
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 新表 `tag_groups`
|
||||
|
||||
```prisma
|
||||
model TagGroup {
|
||||
id BigInt @id @default(autoincrement()) @map("tag_group_id")
|
||||
groupName String @unique @map("group_name")
|
||||
groupIcon String? @map("group_icon")
|
||||
groupColor String? @map("group_color")
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||
|
||||
tags Tag[]
|
||||
|
||||
@@map("tag_groups")
|
||||
}
|
||||
```
|
||||
|
||||
### `Tag` 表加字段
|
||||
|
||||
```prisma
|
||||
model Tag {
|
||||
// ... 现有字段
|
||||
tagGroupId BigInt? @map("tag_group_id")
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
|
||||
tagGroup TagGroup? @relation(fields: [tagGroupId], references: [id], onDelete: SetNull, onUpdate: NoAction)
|
||||
|
||||
@@index([tagGroupId])
|
||||
@@index([tagGroupId, sortOrder])
|
||||
}
|
||||
```
|
||||
|
||||
### 种子数据
|
||||
|
||||
```sql
|
||||
INSERT INTO tag_groups (group_name, sort_order) VALUES
|
||||
('物流渠道', 1),
|
||||
('印刷位置', 2),
|
||||
('印刷工艺', 3);
|
||||
|
||||
UPDATE tags SET tag_group_id = (SELECT tag_group_id FROM tag_groups WHERE group_name = '物流渠道')
|
||||
WHERE tag_name IN ('包邮', '不包邮');
|
||||
UPDATE tags SET tag_group_id = (SELECT tag_group_id FROM tag_groups WHERE group_name = '印刷位置')
|
||||
WHERE tag_name IN ('单面印花', '双面印');
|
||||
UPDATE tags SET tag_group_id = (SELECT tag_group_id FROM tag_groups WHERE tag_group_name = '印刷工艺')
|
||||
WHERE tag_name IN ('烫画', '直喷', '不打印');
|
||||
|
||||
-- 同组内的 tag 按现有 id 升序分配 sortOrder
|
||||
WITH ordered AS (
|
||||
SELECT t.tag_id,
|
||||
ROW_NUMBER() OVER (PARTITION BY t.tag_group_id ORDER BY t.tag_id) AS rn
|
||||
FROM tags t
|
||||
WHERE t.tag_group_id IS NOT NULL
|
||||
)
|
||||
UPDATE tags SET sort_order = ordered.rn
|
||||
FROM ordered
|
||||
WHERE tags.tag_id = ordered.tag_id;
|
||||
```
|
||||
|
||||
## 后端 API
|
||||
|
||||
### TagGroup CRUD
|
||||
- `GET /tag-groups` — 列表(含 tags)
|
||||
- `POST /tag-groups` — 创建
|
||||
- `PATCH /tag-groups/:id` — 更新(重命名、颜色、图标)
|
||||
- `DELETE /tag-groups/:id` — 删除(级联 SetNull)
|
||||
- `PATCH /tag-groups/sort` — 批量更新 `sortOrder`
|
||||
|
||||
### Tag 扩展
|
||||
- `POST /tags` / `PATCH /tags/:id` — 支持 `tagGroupId` / `sortOrder`
|
||||
- `PATCH /tags/sort` — 批量更新 `sortOrder`(跨组时同时更新 `tagGroupId`)
|
||||
|
||||
### Public 公开接口
|
||||
- `GET /public/tag-groups` — 公开的分组列表
|
||||
- `GET /public/tags` 返回的 `PublicTagDto` 增加:
|
||||
```ts
|
||||
group: { id: string; name: string } | null
|
||||
```
|
||||
|
||||
## 后端文件改动
|
||||
|
||||
| 文件 | 操作 |
|
||||
|---|---|
|
||||
| `prisma/schema.prisma` | 新表 + Tag 字段 |
|
||||
| `prisma/migrations/<timestamp>_tag_groups/migration.sql` | 自动生成 + 种子 |
|
||||
| `src/tag-groups/tag-groups.module.ts` | 新建 |
|
||||
| `src/tag-groups/tag-groups.service.ts` | 新建 |
|
||||
| `src/tag-groups/tag-groups.controller.ts` | 新建 |
|
||||
| `src/tag-groups/dto/create-tag-group.dto.ts` | 新建 |
|
||||
| `src/tag-groups/dto/update-tag-group.dto.ts` | 新建 |
|
||||
| `src/tag-groups/dto/reorder-tag-groups.dto.ts` | 新建(批量排序) |
|
||||
| `src/tags/tags.service.ts` | 改:create/update 支持 `tagGroupId`/`sortOrder`;新增 `reorder` |
|
||||
| `src/tags/tags.controller.ts` | 改:加 `PATCH /tags/sort` |
|
||||
| `src/tags/dto/create-tag.dto.ts` | 改 |
|
||||
| `src/tags/dto/update-tag.dto.ts` | 改 |
|
||||
| `src/tags/dto/reorder-tags.dto.ts` | 新建 |
|
||||
| `src/public/dto/public-tag-group.dto.ts` | 新建 |
|
||||
| `src/public/dto/public-tag.dto.ts` | 改:加 `group` |
|
||||
| `src/public/dto/public-good.dto.ts` | 改:tag 类型加 group |
|
||||
| `src/public/public.service.ts` | 改:getTags include group、按 sortOrder 排序;新增 getTagGroups |
|
||||
| `src/public/public.controller.ts` | 改:加 `GET /public/tag-groups` |
|
||||
| `src/app.module.ts` | 改:注册 TagGroupsModule |
|
||||
| `src/tags/tags.service.spec.ts` | 改:加 tagGroupId 关联测试 |
|
||||
| `src/public/public.service.spec.ts` | 改:加 tag 排序测试 |
|
||||
|
||||
## Admin 前端
|
||||
|
||||
### 核心:`GoodsView.vue` 标签筛选改 X2
|
||||
|
||||
位置:第 661-668 行的 `<el-select multiple>` 标签筛选。
|
||||
|
||||
**新结构**:
|
||||
```vue
|
||||
<el-popover
|
||||
v-model:visible="tagTreeVisible"
|
||||
placement="bottom-start"
|
||||
:width="320"
|
||||
trigger="click"
|
||||
popper-class="tag-tree-popper"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button size="small" plain>
|
||||
<i class="fa-solid fa-tags" />
|
||||
标签筛选 ({{ selectedTagIds.length }})
|
||||
<i class="fa-solid fa-chevron-down" />
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<div class="tag-tree-panel">
|
||||
<div class="tag-tree-header">
|
||||
<span>标签分组</span>
|
||||
<el-button text size="small" :icon="Plus" @click="quickAddGroup">新建分组</el-button>
|
||||
</div>
|
||||
|
||||
<el-tree
|
||||
ref="tagTreeRef"
|
||||
:data="tagTreeData"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
draggable
|
||||
:allow-drop="allowDrop"
|
||||
:props="{ label: 'label', children: 'children' }"
|
||||
@node-drop="onNodeDrop"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div
|
||||
class="tree-node"
|
||||
@mouseenter="hoveredId = data.id"
|
||||
@mouseleave="hoveredId = null"
|
||||
>
|
||||
<!-- 分组节点 -->
|
||||
<template v-if="data.type === 'group'">
|
||||
<i class="fa-solid fa-folder node-icon" />
|
||||
<input
|
||||
v-if="editingId === data.id"
|
||||
ref="editInputRef"
|
||||
v-model="editValue"
|
||||
class="node-edit-input"
|
||||
@blur="commitEdit(data)"
|
||||
@keydown.enter="commitEdit(data)"
|
||||
@keydown.esc="cancelEdit"
|
||||
/>
|
||||
<span v-else class="node-label" @dblclick="startEdit(data)">
|
||||
{{ data.label }}
|
||||
</span>
|
||||
<span v-show="hoveredId === data.id && data.id !== 'g-ungrouped'" class="node-actions">
|
||||
<el-button text size="small" :icon="Delete" @click.stop="removeGroup(data)" />
|
||||
</span>
|
||||
<!-- 分组底部:+ 新建标签 -->
|
||||
<div v-if="data.id !== 'g-ungrouped' && hoveredId === data.id" class="group-add-tag">
|
||||
<el-button text size="small" :icon="Plus" @click.stop="quickAddTag(data)">在此组下新建标签</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 标签节点 -->
|
||||
<template v-else>
|
||||
<span
|
||||
class="node-chip"
|
||||
:style="{ background: data.tagColor + '4d', borderColor: data.tagColor + '99', color: data.fontColor || data.tagColor }"
|
||||
>
|
||||
{{ data.label }}
|
||||
</span>
|
||||
<span v-show="hoveredId === data.id" class="node-actions">
|
||||
<el-button text size="small" :icon="Edit" @click.stop="openTagEdit(data)" />
|
||||
<el-button text size="small" :icon="Delete" @click.stop="removeTag(data)" />
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</el-popover>
|
||||
```
|
||||
|
||||
**关键逻辑**:
|
||||
- `tagTreeData` computed:从 `tagGroups` + `tags` 构造两层树,组内按 `sortOrder`,未分组追加末尾(`disabled: true`)
|
||||
- `allowDrop(draggingNode, dropNode, type)`:
|
||||
- 拖动到分组节点上 → `type === 'inner'`(加入该组)
|
||||
- 拖动到 tag 节点上 → `type === 'before'/'after'`(同组排序)
|
||||
- 未分组节点不允许 drop
|
||||
- `onNodeDrop`:根据拖动结果调 `PATCH /tags/sort`(组内排序)或 `PATCH /tag-groups/sort`(分组排序),或更新 `tagGroupId`(跨组移动)
|
||||
- `startEdit(data)`:双击触发,记录 `editingId`,nextTick 聚焦输入框
|
||||
- `commitEdit(data)`:调 `PATCH /tag-groups/:id` 或 `PATCH /tags/:id` 保存新名称
|
||||
- `quickAddGroup`:弹小输入框 → 调 `POST /tag-groups`
|
||||
- `quickAddTag(group)`:弹小输入框 → 调 `POST /tags`,传入 `tagGroupId`
|
||||
- `removeGroup(data)`:`ElMessageBox.confirm` → 调 `DELETE /tag-groups/:id`
|
||||
- `removeTag(data)`:调 `DELETE /tags/:id`
|
||||
|
||||
**`selectedTagIds` 与树的勾选**:
|
||||
- 树上的勾选用 `show-checkbox` 即可
|
||||
- 但用户更倾向"单选即勾选 / 取消勾选"——可以省略勾选框,用 `@node-click` 处理
|
||||
- 简化方案:分组节点不可点击勾选(disabled),tag 节点单击切换 `selectedTagIds` 中是否包含该 id
|
||||
|
||||
### `TagsView.vue` 同步
|
||||
- 表格加「所属分组」列
|
||||
- 创建/编辑弹窗加「所属分组」`<el-select>` 字段(数据来自 `/tag-groups`)
|
||||
|
||||
### 类型与 API
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `src/types/index.ts` | 加 `TagGroup`、`CreateTagGroupRequest`、`UpdateTagGroupRequest` |
|
||||
| `src/api/tag-groups.ts` | 新建(含 `getList/create/update/remove/reorder`) |
|
||||
| `src/api/tags.ts` | 扩展支持 `tagGroupId`/`sortOrder` |
|
||||
|
||||
## 官网前端
|
||||
|
||||
### 数据加载
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `server/api/backend/tag-groups.get.ts` | 新建代理 |
|
||||
| `composables/useProductCenter.ts` | `BackendTag` 加 `tagGroupId`、`tagGroup: { id, name } \| null`;`Tag` 接口加 `group`;新增 `loadTagGroups()` + `useState('pc-tag-groups')`;新增 `sortTagsByGroup()` 工具函数 |
|
||||
|
||||
### `ProductCard.vue` chip 排序
|
||||
|
||||
**国家 chip 移到 tag 之后**:
|
||||
```vue
|
||||
<div v-if="product.tags.length > 0 || showCountry" class="chips">
|
||||
<span
|
||||
v-for="tag in sortedTags"
|
||||
:key="tag.id"
|
||||
class="chip"
|
||||
:style="{ color: tag.fontColor || tag.color || '#ff6a00', background: (tag.color || '#ff6a00') + '4d', borderColor: (tag.color || '#ff6a00') + '99' }"
|
||||
>
|
||||
{{ tag.name }}
|
||||
</span>
|
||||
<span v-if="showCountry" class="chip country-chip">
|
||||
<img v-if="product.country.icon" :src="product.country.icon" :alt="product.country.name" class="country-flag" />
|
||||
{{ product.country.name }}
|
||||
</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
`sortedTags` computed:
|
||||
```ts
|
||||
const sortedTags = computed(() => {
|
||||
const groupOrder = new Map(tagGroups.value.map((g, i) => [g.id, i]));
|
||||
return [...props.product.tags]
|
||||
.sort((a, b) => {
|
||||
const ao = a.group ? (groupOrder.get(a.group.id) ?? 999) : 999;
|
||||
const bo = b.group ? (groupOrder.get(b.group.id) ?? 999) : 999;
|
||||
return ao - bo;
|
||||
})
|
||||
.slice(0, 4);
|
||||
});
|
||||
```
|
||||
|
||||
### `product-center.vue` 已选 chip 排序
|
||||
- `.selected-tags` 区域按相同规则排序
|
||||
- country chip 排在已选 tag 之后
|
||||
|
||||
## 测试与文档
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `src/tags/tags.service.spec.ts` | 加 tagGroupId 关联测试 |
|
||||
| `src/tags/tags.service.ts` create/update 单测 | 加 `tagGroupId` 字段 |
|
||||
| `src/public/public.service.spec.ts` | 加 `/public/tag-groups` + tag 排序测试 |
|
||||
| `docs/references/structs.md` | 更新项目结构 |
|
||||
| `docs/references/admin/tags.md`(如有) | 新建或更新标签管理文档 |
|
||||
| `skills/inkreach-official-admin/SKILL.md` | 加标签分组管理说明 |
|
||||
| `skills/inkreach-official-website/SKILL.md` | 加 tag 排序说明 |
|
||||
|
||||
## 任务列表
|
||||
|
||||
- [ ] **T1**:后端 schema + migration
|
||||
- 改 `prisma/schema.prisma` 新增 TagGroup + Tag 字段
|
||||
- 跑 `npx prisma migrate dev --name add_tag_groups` 生成 migration
|
||||
- 在 migration.sql 末尾追加种子数据(3 个分组 + 关联现有 tag + 分配 sortOrder)
|
||||
- 跑 `npx prisma generate` 重新生成 client
|
||||
- [ ] **T2**:后端 tag-groups module
|
||||
- 新建 `tag-groups.module.ts` / `service.ts` / `controller.ts` / dto
|
||||
- 注册到 `app.module.ts`
|
||||
- 实现 CRUD + 批量排序
|
||||
- [ ] **T3**:后端 tags 扩展
|
||||
- `create/update` DTO 加 `tagGroupId` / `sortOrder`
|
||||
- `tags.service.ts` 处理新字段
|
||||
- 加 `PATCH /tags/sort` 端点
|
||||
- [ ] **T4**:后端 public 接口
|
||||
- 新建 `public-tag-group.dto.ts`
|
||||
- `PublicTagDto` 加 `group` 字段
|
||||
- `public.service.ts` `getTags` 用 `include: { tagGroup: true }`,按 `tagGroup.sortOrder, sortOrder` 排序
|
||||
- `public.service.ts` 新增 `getTagGroups()`
|
||||
- `public.controller.ts` 加 `GET /public/tag-groups`
|
||||
- [ ] **T5**:后端测试更新
|
||||
- 更新 `tags.service.spec.ts` 现有测试,加 `tagGroupId` 关联
|
||||
- 更新 `public.service.spec.ts` 加 tag group 关联 + 排序测试
|
||||
- [ ] **T6**:Admin 类型 + API
|
||||
- `types/index.ts` 加 `TagGroup` 类型
|
||||
- 新建 `api/tag-groups.ts`
|
||||
- `api/tags.ts` 扩展支持新字段
|
||||
- [ ] **T7**:Admin GoodsView 标签筛选(X2)
|
||||
- `GoodsView.vue:661-668` 改为 `<el-popover>` + `<el-tree>`
|
||||
- 实现 `tagTreeData` computed
|
||||
- 实现拖动(`@node-drop` + `allow-drop`)
|
||||
- 实现 inline 编辑(双击 + 失焦/Enter 提交)
|
||||
- 实现 quick add group / quick add tag
|
||||
- 实现 remove group / remove tag
|
||||
- `selectedTagIds` 与树节点单击同步
|
||||
- "未分组" 虚拟节点追加在最后(`disabled: true`)
|
||||
- [ ] **T8**:Admin TagsView 同步
|
||||
- 表格加「所属分组」列
|
||||
- 创建/编辑弹窗加「所属分组」下拉
|
||||
- [ ] **T9**:官网数据加载
|
||||
- 新建 `server/api/backend/tag-groups.get.ts` 代理
|
||||
- `useProductCenter.ts` 加载 tagGroups
|
||||
- 给 `Tag` 接口加 `group` 字段
|
||||
- 新增 `sortTagsByGroup()` 工具函数
|
||||
- [ ] **T10**:官网 ProductCard 排序
|
||||
- `ProductCard.vue` chip 顺序:tag 们 → 国家
|
||||
- 用 `sortedTags` computed
|
||||
- [ ] **T11**:官网 product-center 已选 chip 排序
|
||||
- `.selected-tags` 区域按组排序
|
||||
- country chip 排在最后
|
||||
- [ ] **T12**:文档
|
||||
- 更新 `docs/references/structs.md`
|
||||
- 更新 `skills/inkreach-official-admin/SKILL.md`
|
||||
- 更新 `skills/inkreach-official-website/SKILL.md`
|
||||
|
||||
## 风险与权衡
|
||||
|
||||
1. **拖动跨组 vs 同组**:用户允许跨组,但 UI 上要明确提示(鼠标 cursor + 节点高亮)
|
||||
2. **删除分组的级联**:用户确认 SetNull,但要在 UI 上二次确认(避免误删)
|
||||
3. **"+ 在此组下新建标签"位置**:用户说"底部",意味着每个组节点下方有固定行(hover 时显示)
|
||||
4. **未分组虚拟节点**:不可拖动、不可编辑/删除,仅作展示
|
||||
5. **数据库 seed 在 production 的处理**:seed 数据只在 migration 中执行一次,production 已有数据时迁移需谨慎
|
||||
|
||||
Reference in New Issue
Block a user