Files
inkreach-official-website/apps/website/plans/feature/product-center-feature.md
T

29 KiB
Raw Blame History

产品中心 (Product Center) Feature Plan

需求描述

为 InkReach 官网新增"产品中心"二级页面,串联三个子系统:

  1. NestJS 后端 (inkreach-official-nestjs/)Prisma + PostgreSQL,提供 Admin RESTful API、Website Public API、SDS 定时同步
  2. Vue 3 Admin (inkreach-official-admin/)Element Plus,管理商品/品类/国家/标签/位置/同步
  3. Nuxt 4 官网页 (inkreach-official-website/):在已有首页基础上新增 /product-center 二级页

实施顺序:后端 → Admin → 官网。所有任务按 TDD 流程开发:先写测试,再写实现,再重构。

架构与端口

项目 端口 直连方式
NestJS Backend 3001 Admin 直连(Vite proxy),Website 通过 Nitro 代理
Admin (Vite dev) 5173 -
Website (Nuxt dev) 3000 -

跨项目环境变量

文件 变量
inkreach-official-nestjs/.env DATABASE_URL postgresql://postgres:yoyoki219765.@localhost:5432/inkreach-official
inkreach-official-nestjs/.env JWT_SECRET 随机字符串(运行前生成)
inkreach-official-nestjs/.env SDS_API_BASE https://mapi.sdspod.com
inkreach-official-nestjs/.env PORT 3001
inkreach-official-admin/.env VITE_API_BASE http://localhost:3001
inkreach-official-website/.env NUXT_PUBLIC_BACKEND_URL http://localhost:3001

任务列表

阶段一:NestJS 后端 (inkreach-official-nestjs/)

  • B1: 初始化 NestJS + TypeScript 项目骨架
  • B2: 配置 Prisma + 数据库连接
  • B3: 编写 Prisma Schema 并执行 initial migration
  • B4: 实现 PrismaService 和全局 DatabaseModule
  • B5: 实现 Auth 模块(JWT + Guard + 注册/登录)
  • B6: 实现 Countries 模块(CRUD + 测试)
  • B7: 实现 Categories 模块(树形 CRUD + 测试)
  • B8: 实现 Tags 模块(CRUD + 测试)
  • B9: 实现 Positions 模块(CRUD + 测试)
  • B10: 实现 OriginGoods 模块(只读 + 测试)
  • B11: 实现 Goods 模块(CRUD + 批量 + 筛选 + 测试)
  • B12: 实现 SDS 同步模块(cron + 手动触发 + 测试)
  • B13: 实现 Public API 模块(无需鉴权 + 测试)
  • B14: 全局异常过滤器、响应拦截器、CORS
  • B15: 启动验证 + Swagger 文档生成

阶段二:Admin 后台 (inkreach-official-admin/)

  • A1: 初始化 Vite + Vue 3 + TS + Element Plus
  • A2: 配置 Vite proxy、环境变量、Tailwind(可选)
  • A3: 建立 Axios 实例与 API 模块层
  • A4: 建立 Pinia storesauth/app
  • A5: 建立路由 + 鉴权守卫
  • A6: 实现 LoginView
  • A7: 实现 DefaultLayout(侧边栏 + 头部)
  • A8: 实现 GoodsView(列表 + 筛选 + 增删改 + 批量添加 + 测试)
  • A9: 实现 CategoriesView(树形表格 + 测试)
  • A10: 实现 CountriesView(含图标上传/URL + 测试)
  • A11: 实现 TagsView(含颜色选择 + 测试)
  • A12: 实现 PositionsView(含国家/品类联动 + 测试)
  • A13: 实现 SyncView(同步状态 + 手动触发 + 测试)

阶段三:官网产品中心页 (inkreach-official-website/)

  • W1: 配置 runtimeConfig 与 .env
  • W2: 新增 Nitro 后端代理 routes
  • W3: 创建 useProductCenter composable
  • W4: 创建 ProductSidebar.vue 组件
  • W5: 创建 ProductCountryFilter.vue 组件
  • W6: 创建 ProductFilterBar.vue 组件
  • W7: 创建 ProductCard.vueProductCardSkeleton.vue
  • W8: 创建 ProductGrid.vue 组件
  • W9: 创建 ProductPagination.vue 组件
  • W10: 创建 app/pages/product-center.vue 页面 + 启用 Nuxt 路由
  • W11: 更新 AppHeader.vuePodProducts.vue 内部跳转链接
  • W12: 三断点视觉验证

阶段四:收尾

  • F1: 更新根级 docs/references/structs.md(新增 3 子项目结构)
  • F2: 更新各子项目 README.mddocs/references/* 使用文档
  • F3: 更新 skills/[项目名]/SKILL.md
  • F4: 全链路联调(后端 + Admin + 官网三端跑通)

任务详情

B1: 初始化 NestJS + TypeScript 项目骨架

inkreach-official-nestjs/ 目录初始化 NestJS 工程。

操作步骤

  1. npm init 创建 package.json,包名 inkreach-official-nestjs
  2. 安装核心依赖:@nestjs/core @nestjs/common @nestjs/platform-express @nestjs/config reflect-metadata rxjs
  3. 安装开发依赖:@nestjs/cli @nestjs/testing typescript ts-node tsconfig-paths @types/node jest ts-jest @types/jest supertest @types/supertest
  4. 创建配置文件:nest-cli.jsontsconfig.jsontsconfig.build.json.eslintrc.js.prettierrc
  5. 创建入口文件:src/main.tssrc/app.module.ts,监听 process.env.PORT ?? 3001
  6. package.json 中加入脚本:start/start:dev/start:debug/build/test/test:e2e/test:cov

验收npm run start:dev 能启动并监听 3001 端口。

B2: 配置 Prisma + 数据库连接

操作步骤

  1. 安装:prisma @prisma/clientdev: prisma
  2. npx prisma init 初始化 prisma/.env
  3. .env 写入:
    DATABASE_URL=postgresql://postgres:yoyoki219765.@localhost:5432/inkreach-official
    JWT_SECRET=<openssl rand -hex 32 生成>
    SDS_API_BASE=https://mapi.sdspod.com
    PORT=3001
    
  4. .gitignore 加入 .envnode_modulesdist
  5. app.module.ts 注册 ConfigModule.forRoot({ isGlobal: true })

验收npx prisma db pullprisma validate 通过。

B3: 编写 Prisma Schema 并执行 initial migration

docs/dev/database-table-design.md 的 DDL 翻译为 prisma/schema.prisma严格保持 6 张表(origin_goods / countries / categories / tags / positions / goods)的字段、外键、删除策略

关系约束(必须)

  • Good.originGoodsOriginGoods (RESTRICT)
  • Good.countryCountry (RESTRICT, NOT NULL)
  • Good.categoryCategory (RESTRICT, NOT NULL)
  • Good.tagTag (SET NULL, optional)
  • Good.positionPosition (SET NULL, optional)
  • Category.parentCategory (RESTRICT, self-relation)
  • Position.countryCountry (CASCADE)
  • Position.categoryCategory (CASCADE)

字段映射:所有 BIGINTBigIntTIMESTAMPTZDateTime @db.Timestamptz(6)TEXTString。表名通过 @@map("xxx") 映射到 snake_case,字段名同理用 @map

额外

  • 创建 User 表用于 Admin 登录(id / username UNIQUE / passwordHash / 时间戳)
  • 创建 SyncLog 表记录同步历史(id / type enum / status enum / message / startedAt / finishedAt
  • 索引按 DDL 中 CREATE INDEX 创建对应 @@index

操作

  1. 编写 schema
  2. npx prisma migrate dev --name init 生成首个 migration
  3. npx prisma generate 生成 Client

验收:数据库内 8 张表(6 业务表 + users + sync_logs)正确生成。

B4: 实现 PrismaService 和全局 DatabaseModule

文件

  • src/prisma/prisma.service.ts:继承 PrismaClient,在 onModuleInit$connect(),在 onModuleDestroy$disconnect()
  • src/prisma/prisma.module.ts@Global() + 导出 PrismaService
  • app.module.ts 注册 PrismaModule

测试src/prisma/prisma.service.spec.ts,验证可注入并连接数据库。

B5: 实现 Auth 模块

文件

  • src/auth/auth.module.tsauth.controller.tsauth.service.ts
  • src/auth/strategies/jwt.strategy.tssrc/auth/guards/jwt-auth.guard.ts
  • src/auth/dto/login.dto.tsregister.dto.ts

依赖@nestjs/jwt @nestjs/passport passport passport-jwt bcrypt class-validator class-transformer

接口

方法 路径 描述
POST /auth/register 注册(仅初始化用,可加环境变量门禁)
POST /auth/login 登录返回 { accessToken, user }

实现要点

  • 密码用 bcrypt hashrounds=10
  • JWT payload: { sub: userId, username },过期 7 天
  • JwtAuthGuard 默认作用于 Admin 路由

测试 (src/auth/auth.service.spec.ts)

  1. 注册新用户 → 数据库存在记录,密码已 hash
  2. 重复用户名 → 抛 ConflictException
  3. 登录正确密码 → 返回 token
  4. 登录错误密码 → 抛 UnauthorizedException

B6: Countries 模块

文件src/countries/{countries.module.ts, countries.controller.ts, countries.service.ts, dto/create-country.dto.ts, dto/update-country.dto.ts}

接口

方法 路径 描述
GET /countries 列表
POST /countries 创建 ({ countryName, countryIcon? })
PATCH /countries/:id 更新
DELETE /countries/:id 删除(被引用时抛错)

所有路由加 @UseGuards(JwtAuthGuard)DELETE 在数据库 RESTRICT 抛错时返回 409 Conflict + 友好信息。

测试 (countries.service.spec.ts)

  1. CRUD 正常路径
  2. 重名 → ConflictException
  3. goods 引用时删除 → 抛 BadRequestException

B7: Categories 模块(树形)

文件src/categories/{categories.module.ts, ...}

接口

方法 路径 描述
GET /categories 返回树形结构(按 parentCategoryId 递归拼接)
GET /categories/flat 扁平列表(用于下拉选择)
GET /categories/:id 详情
POST /categories 创建(parentCategoryId 可空)
PATCH /categories/:id 更新
DELETE /categories/:id 删除(含子节点 → 抛错)

树形构建:一次查询所有记录 → 在内存中按 parentCategoryId 分组拼装。

测试:CRUD + 树结构组装 + 删除存在子项时报错。

B8: Tags 模块

文件src/tags/{tags.module.ts, ...}

接口GET / POST / PATCH /:id / DELETE /:id(同 Countries

字段tagName(唯一)、tagColorhex 字符串验证 /^#[0-9A-Fa-f]{6}$/)、timing

测试CRUD + 颜色格式验证。

B9: Positions 模块

文件src/positions/{positions.module.ts, ...}

接口GET / POST / PATCH /:id / DELETE /:id

字段indexValcountryId (可空)、categoryId (可空)

查询参数GET /positions?countryId=&categoryId=

测试CRUD + 联表返回 country/category 名称。

B10: OriginGoods 模块(只读)

文件src/origin-goods/{origin-goods.module.ts, ...}

接口

方法 路径 描述
GET /origin-goods 分页 + 关键词搜索 (page/pageSize/keyword)

只读模块(数据由 SDS 同步生成),无 POST/PATCH/DELETE。

测试:分页 + 关键词搜索。

B11: Goods 模块

文件src/goods/{goods.module.ts, goods.controller.ts, goods.service.ts, dto/...}

接口

方法 路径 描述
GET /goods 分页 + 多维筛选 (countryId/categoryId/tagId/positionId/keyword/page/pageSize)
GET /goods/:id 详情(含关联 country/category/tag/position/originGoods
POST /goods 创建
PATCH /goods/:id 更新
DELETE /goods/:id 删除
PATCH /goods/batch-priority 批量更新优先级 ([{ id, priority }])
POST /goods/batch origin_goods 批量创建(统一国家/品类/标签/位置/起始优先级)

关键实现

  • 筛选 categoryId 时,递归查出所有子品类 ID,用 IN 查询
  • 关键词搜索 goodNamecontains mode insensitive
  • 排序:goodPriority DESC + createdAt DESC
  • 关联返回用 Prisma include

测试 (goods.service.spec.ts)

  1. 单个 CRUD
  2. 筛选(每种参数)
  3. categoryId 包含子品类
  4. 批量优先级更新(事务)
  5. 批量创建(事务,全成或全败)

B12: SDS 同步模块

文件src/sync/{sync.module.ts, sync.controller.ts, sync.service.ts, sds-client.service.ts}

依赖@nestjs/schedule @nestjs/axios axios

SDS Client:复用现网逻辑(参考 inkreach-official-website/server/utils/pod-api.ts):

const POD_API_BASE = 'https://mapi.sdspod.com';
const POD_HEADERS = {
  'Content-Type': 'application/json;charset=UTF-8',
  'Origin': 'https://inkpod.vip',
  'Referer': 'https://inkpod.vip/',
};

接口

方法 路径 描述
POST /sync/categories 手动触发品类同步
POST /sync/products 手动触发产品同步
GET /sync/status 最近 N 条 sync_logs

同步逻辑

  1. 品类同步 syncCategories():调用 POST /category/tree/3 body { withActivityArea:true, withPrivate:true, onlyHaveProduct:true },递归扁平化结果,按 sdsId 去重写入/更新 categories 表(需扩展 schema 增 sdsCategoryId TEXT UNIQUE 字段,B3 步骤已含)
  2. 产品同步 syncProducts():遍历叶子品类,调用 GET /products/page?categoryId=&page=&size=50,去重写入 origin_goods 表(按 sdsGoodId
  3. Cron@Cron('0 * * * *') 每小时执行一次完整同步
  4. 日志:每次同步开始写入 sync_logsstatus=RUNNING),完成后更新为 SUCCESS/FAILED

测试mock axios,验证 mapper 与去重逻辑(不真实调用 SDS)。

B13: Public API 模块

文件src/public/{public.module.ts, public.controller.ts, public.service.ts}

接口(无需鉴权)

方法 路径 描述
GET /public/categories 品类树,仅含有商品的品类
GET /public/countries 国家列表,仅含有商品的国家
GET /public/goods 分页商品 (countryId/categoryId/tagId/keyword/page/pageSize)
GET /public/goods/:id 商品详情

实现要点

  • 排序:goodPriority DESC → position.indexVal ASC → createdAt DESC
  • 仅返回 Public 字段(排除内部 originGoodId 等),DTO 转换在 service 完成
  • 接受 countryIdcategoryId 多维过滤
  • GET /public/categoriesEXISTS 子查询过滤

测试:覆盖所有筛选维度 + 排序正确性。

B14: 全局异常过滤器、响应拦截器、CORS

文件

  • src/common/filters/http-exception.filter.ts:统一错误格式 { statusCode, message, error, timestamp, path }
  • src/common/interceptors/transform.interceptor.ts:统一响应 { data, success: true }
  • src/common/pipes/validation.pipe.ts:全局 ValidationPipe({ whitelist: true, transform: true })

main.ts

app.enableCors({ origin: ['http://localhost:5173', 'http://localhost:3000'], credentials: true });
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new TransformInterceptor());

B15: 启动验证 + Swagger 文档

操作

  1. 安装 @nestjs/swagger
  2. main.ts 中配置 SwaggerModule.setup('api/docs', app, document)
  3. 每个 controller / DTO 加 @ApiTags@ApiOperation@ApiProperty
  4. npm run start:dev 启动后访问 http://localhost:3001/api/docs 确认所有接口可见

验收:所有阶段一测试 npm run test 全绿,启动后 Swagger 完整。


A1: 初始化 Vite + Vue 3 + TS + Element Plus

inkreach-official-admin/ 执行:

  1. npm create vite@latest . -- --template vue-ts(注意当前目录)
  2. 安装:element-plus @element-plus/icons-vue pinia vue-router@4 axios @vueuse/core dayjs
  3. 开发依赖:@types/node sass unplugin-auto-import unplugin-vue-components
  4. vite.config.ts 配置 AutoImport + Components 插件按需引入 Element Plus
  5. main.ts 注册 Pinia、Router、Element Plus 中文 locale

验收npm run dev 启动 5173 端口,首页可见。

A2: Vite proxy + 环境变量

.env.development

VITE_API_BASE=/api

vite.config.ts

server: {
  port: 5173,
  proxy: {
    '/api': {
      target: 'http://localhost:3001',
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/api/, ''),
    },
  },
},

A3: Axios 实例与 API 模块层

文件

  • src/api/request.ts:创建 axios 实例,请求拦截器加 Authorization: Bearer <token>,响应拦截器统一脱壳 data.data401 跳登录
  • src/api/auth.tslogin(username, password) / register(...)
  • src/api/goods.tscategories.tscountries.tstags.tspositions.tsorigin-goods.tssync.ts:对应后端接口,TS 类型明确

类型src/types/index.ts 定义 GoodCountryCategoryTagPositionOriginGoodsPaginatedResult<T> 等。

A4: Pinia stores

文件

  • src/stores/auth.tstokenuserlogin()logout()isLoggedIntoken 持久化到 localStorage
  • src/stores/app.ts:侧边栏折叠状态、面包屑

A5: 路由 + 鉴权守卫

文件src/router/index.ts

/login                  → LoginView
/                       → DefaultLayout(重定向到 /goods
  ├── /goods            → GoodsView
  ├── /categories       → CategoriesView
  ├── /countries        → CountriesView
  ├── /tags             → TagsView
  ├── /positions        → PositionsView
  ├── /origin-goods     → OriginGoodsView(只读列表)
  └── /sync             → SyncView

全局守卫:未登录访问受保护路由 → 跳 /login;已登录访问 /login → 跳 /goods

A6: LoginView

文件src/views/login/LoginView.vue

  • Element Plus <el-form> 实现:用户名 + 密码 + 登录按钮
  • 居中卡片布局,背景渐变 / 品牌色
  • 调用 authStore.login() 成功后跳 /

A7: DefaultLayout

文件src/layouts/DefaultLayout.vue

布局:

  • 左侧 <el-menu> 侧边栏(240px 宽),可折叠
  • 顶部 header:面包屑 + 用户头像下拉(含登出)
  • 中间 <router-view> 内容区

菜单项与路由一一对应,使用 @element-plus/icons-vue 图标。

A8: GoodsView

文件src/views/goods/{GoodsView.vue, components/GoodFormDialog.vue, components/BatchAddDialog.vue}

功能

  • 顶部筛选栏:国家下拉 + 品类级联 + 标签下拉 + 关键词输入 + 搜索/重置按钮
  • 表格列:商品名、国家、品类、标签、位置、优先级、创建时间、操作(编辑/删除)
  • 分页器(Element Plus <el-pagination>
  • 工具栏:新增按钮 + 批量添加按钮 + 批量调优先级按钮
  • 新增/编辑对话框:商品名、关联 originGood(远程搜索)、国家、品类(级联)、标签、位置、优先级
  • 批量添加对话框:左侧 originGoods 多选表格,右侧统一配置(国家/品类/标签/位置/优先级),保存调 POST /goods/batch

测试 (GoodsView.spec.ts)

  • mount 后展示列表
  • 筛选触发重新请求
  • 删除二次确认后调接口

A9: CategoriesView

文件src/views/categories/{CategoriesView.vue, components/CategoryFormDialog.vue}

功能

  • Element Plus <el-table> :tree-props 树形展示
  • 列:品类名、图标预览、子品类数、操作
  • 新增/编辑:品类名、父品类(级联,可空)、图标 URL
  • 删除:如有子品类二次确认

测试:树形展开、新增子品类后刷新。

A10: CountriesView

文件src/views/countries/{CountriesView.vue, components/CountryFormDialog.vue}

  • 表格:国家名、图标预览、操作
  • 表单:国家名(唯一校验)、图标 URL(可选)

测试CRUD 流程。

A11: TagsView

文件src/views/tags/{TagsView.vue, components/TagFormDialog.vue}

  • 表格:标签名、颜色色块、timing、操作
  • 表单:标签名(唯一)、颜色(<el-color-picker>)、timing

测试:颜色验证、唯一冲突提示。

A12: PositionsView

文件src/views/positions/{PositionsView.vue, components/PositionFormDialog.vue}

  • 表格:index_val、关联国家、关联品类、操作
  • 表单:index_val(整数)、国家下拉(可空)、品类级联(可空)
  • 筛选:按国家/品类筛选

测试:国家/品类联动加载。

A13: SyncView

文件src/views/sync/SyncView.vue

  • 卡片 1:品类同步 — 按钮 + 最近同步时间/状态
  • 卡片 2:产品同步 — 按钮 + 最近同步时间/状态
  • 表格:最近 20 条 sync_logs
  • 触发后调 POST /sync/categoriesPOST /sync/products,加 loading + 成功/失败提示
  • 自动每 30s 拉取一次状态(useIntervalFn from VueUse

测试:触发同步 + 状态刷新。


W1: Nuxt runtimeConfig 与 .env

文件inkreach-official-website/.env(追加)

NUXT_PUBLIC_BACKEND_URL=http://localhost:3001

nuxt.config.ts 追加:

runtimeConfig: {
  public: {
    backendUrl: 'http://localhost:3001',
  },
},

W2: Nitro 后端代理 routes

新建文件:

路径 转发到
server/api/backend/categories.get.ts GET :3001/public/categories
server/api/backend/countries.get.ts GET :3001/public/countries
server/api/backend/goods.get.ts GET :3001/public/goods?...
server/api/backend/goods/[id].get.ts GET :3001/public/goods/:id

所有 handler 用 defineCachedEventHandler 缓存 60s,透传 query 参数。参考现有 server/api/pod/categories.post.ts 写法。

示例 server/api/backend/goods.get.ts

export default defineCachedEventHandler(async (event) => {
  const config = useRuntimeConfig();
  const query = getQuery(event);
  return $fetch(`${config.public.backendUrl}/public/goods`, { query });
}, {
  maxAge: 60,
  swr: true,
  name: 'backend-goods',
  getKey: (event) => JSON.stringify(getQuery(event)),
});

W3: useProductCenter composable

文件app/composables/useProductCenter.ts

类型

export interface Country { id: number; name: string; icon?: string; }
export interface Category {
  id: number;
  name: string;
  icon?: string;
  parentId: number | null;
  children: Category[];
}
export interface Tag { id: number; name: string; color?: string; }
export interface Product {
  id: number;
  name: string;
  image: string;
  price: number;
  country: Country;
  category: Category;
  tag?: Tag;
}
export interface ProductQuery {
  countryId?: number;
  categoryId?: number;
  tagId?: number;
  keyword?: string;
  page: number;
  pageSize: number;
}

API

  • categories / countries(用 useFetch SSR 拉取)
  • products / total / loading
  • queryreactive 筛选条件)
  • selectedCategoryselectedCountryselectedTags
  • setCategory(id)setCountry(id)setKeyword(v)setPage(n)setPageSize(n)
  • fetchProducts() watch query 变化自动触发

W4: ProductSidebar.vue

文件app/components/product/ProductSidebar.vue

按设计稿 product-center-1.png 还原:

  • 顶级品类带 emoji/图标 + 名称 + 右侧折叠箭头
  • 点击顶级展开/收起子品类
  • 子品类列表带缩进;选中项左侧 4px 橙色竖条 + bg-inkreach-homepage-3 + 橙色文字
  • 未选中:灰色文字 + hover 浅灰背景
  • 宽度 240px,背景白色,圆角 12px

PropscategoriesactiveCategoryIdemit update:activeCategoryId

W5: ProductCountryFilter.vue

文件app/components/product/ProductCountryFilter.vue

  • 横向 pill 按钮组,每个 pill:国旗图标 + 国家名
  • "全部"为默认项,selected 时橙色边框 + 橙色文字 + 白底
  • 未选中:灰色边框 + 灰色文字
  • 横向溢出可滚动

PropscountriesactiveCountryId (null 表示"全部")

W6: ProductFilterBar.vue

文件app/components/product/ProductFilterBar.vue

按设计稿 product-center-2.png

  • 左侧:筛选下拉(如"价格区间"等占位)+ 已选标签 chips(可点击 × 清除)
  • 右侧:分体式搜索框 — 灰色输入框 + 橙色"搜索"按钮(圆角矩形)

Props/Emitskeyword v-model、selectedTagsonSearch

W7: ProductCard + ProductCardSkeleton

ProductCard.vue

  • 整卡白底圆角 12px,hover 浮起阴影
  • 顶部 1:1 商品图(背景 bg-inkreach-homepage-1
  • 商品名(单行 truncate
  • 标签行:横向 pill 标签,颜色按 tag.color 渲染;约定:绿=包邮,橙=工艺,灰=其他
  • 价格:"¥XX.XX 起",粗体橙色

ProductCardSkeleton.vue

  • 与 ProductCard 同尺寸
  • 图片区灰色 + animate-pulse
  • 标题/价格行用灰色矩形条 + animate-pulse

W8: ProductGrid.vue

文件app/components/product/ProductGrid.vue

  • 4 列(lg/ 3 列(md)/ 2 列(sm)网格
  • loading=true 时渲染 12 个 ProductCardSkeleton
  • products 为空时渲染空状态插画 + "暂无商品"提示

Propsproductsloading

W9: ProductPagination.vue

文件app/components/product/ProductPagination.vue

按设计稿 product-center-3.png

  • 左侧:"总计 N 个产品"
  • 中间:上一页 / 页码方形按钮(当前页橙色填充) / 下一页
  • 右侧:<select> 每页条数(12/24/48 + 跳转到 input + Go 按钮

Props/EmitstotalpagepageSizeemit update:pageupdate:pageSize

W10: app/pages/product-center.vue

文件app/pages/product-center.vue

布局:

<AppHeader />
<main class="bg-inkreach-homepage-2 min-h-screen pt-6 pb-12">
  <div class="max-w-7xl mx-auto px-4 lg:px-6 flex gap-6">
    <ProductSidebar :categories="..." v-model:activeCategoryId="..." />
    <div class="flex-1 flex flex-col gap-4">
      <ProductCountryFilter ... />
      <ProductFilterBar ... />
      <ProductGrid ... />
      <ProductPagination v-if="!loading" ... />
    </div>
  </div>
</main>
<AppFooter />

由于 Nuxt 4 项目目前用 app.vue 单入口,需在 app.vue 替换为 <NuxtPage /> 并把首页改成 app/pages/index.vue(迁移现有 app.vue 内容到 pages/index.vue),同时新增 pages/product-center.vue

响应式:移动端侧边栏改为顶部折叠 <details>,国家筛选可横向滚动。

SEOuseSeoMeta({ title: '产品中心 | InkReach', description: '...' })

W11: AppHeader + PodProducts 链接更新

改动

  • app/components/AppHeader.vue:将"产品中心"的 <a :href="PORTAL_URL"> 替换为 <NuxtLink to="/product-center">,桌面端 + 移动端两处
  • app/components/PodProducts.vue:把底部"更多产品"按钮改为 <NuxtLink to="/product-center">,并补样式(橙色边框/填充等品牌按钮样式)

W12: 三断点视觉验证

依次在 375 / 768 / 1440 宽度验证:

  • 侧边栏在移动端是否正常折叠
  • 国家筛选横向滚动是否流畅
  • 商品网格列数响应正确(2/3/4
  • 分页器在移动端单行显示

F1: 更新 docs/references/structs.md

更新根级 docs/references/structs.md(若无则新建),按以下结构记录:

inkreach-official/
├── inkreach-official-nestjs/    # NestJS 后端,:3001
│   └── src/
│       ├── auth/                # JWT 认证
│       ├── countries/           # 国家 CRUD
│       ├── categories/          # 品类树 CRUD
│       ├── ...
│       ├── sync/                # SDS 定时同步
│       └── public/              # 官网公开 API
├── inkreach-official-admin/     # Vue 3 Admin, :5173
│   └── src/
│       ├── views/               # 各管理页
│       ├── api/                 # API 调用
│       └── stores/              # Pinia
└── inkreach-official-website/   # Nuxt 4 官网, :3000
    └── app/
        ├── pages/
        │   ├── index.vue
        │   └── product-center.vue  # ← 新增
        └── components/
            └── product/             # ← 新增

每个子项目下也维护自己的 docs/references/structs.md

F2: 更新 README 与 docs/references

  • 根目录 README.md:补充三子项目启动方式与端口
  • 每个子项目的 README.md:补充本子项目快速启动
  • 各子项目 docs/references/index.md:详细使用说明(接口、命令、配置)

F3: 更新 SKILL.md

  • skills/inkreach-official-nestjs/SKILL.md:后端开发约定(如何加模块、TDD 流程)
  • skills/inkreach-official-admin/SKILL.md:Admin 开发约定(如何加页面、API 层)
  • skills/inkreach-official-website/SKILL.md:补充产品中心页面与代理路由相关说明

F4: 全链路联调

  1. 启动 Postgres + 后端:cd inkreach-official-nestjs && npm run start:dev
  2. 手动调用 POST /auth/register 创建管理员
  3. 手动调用 POST /sync/categories/sync/products 拉取 SDS 数据到 origin_goods
  4. 启动 Admincd inkreach-official-admin && npm run dev,登录后在各页配置国家/品类/标签/位置,并批量从 origin_goods 创建 goods
  5. 启动 Websitecd inkreach-official-website && npm run dev,访问 /product-center 验证侧边栏、筛选、分页、跳转
  6. 三端同时运行无 console 报错,所有筛选条件返回正确数据

完成标准

  • 三个子项目均能 npm run test 全绿
  • 三端联调跑通完整业务流
  • 文档与结构图已更新