- 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
30 KiB
产品中心 (Product Center) Feature Plan
需求描述
为 InkReach 官网新增"产品中心"二级页面,串联三个子系统:
- NestJS 后端 (
inkreach-official-nestjs/):Prisma + PostgreSQL,提供 Admin RESTful API、Website Public API、SDS 定时同步 - Vue 3 Admin (
inkreach-official-admin/):Element Plus,管理商品/品类/国家/标签/位置/同步 - 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 stores(auth/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: 创建
useProductCentercomposable - W4: 创建
ProductSidebar.vue组件 - W5: 创建
ProductCountryFilter.vue组件 - W6: 创建
ProductFilterBar.vue组件 - W7: 创建
ProductCard.vue与ProductCardSkeleton.vue - W8: 创建
ProductGrid.vue组件 - W9: 创建
ProductPagination.vue组件 - W10: 创建
app/pages/product-center.vue页面 + 启用 Nuxt 路由 - W11: 更新
AppHeader.vue与PodProducts.vue内部跳转链接 - W12: 三断点视觉验证
阶段四:收尾
- F1: 更新根级
docs/references/structs.md(新增 3 子项目结构) - F2: 更新各子项目
README.md与docs/references/*使用文档 - F3: 更新
skills/[项目名]/SKILL.md - F4: 全链路联调(后端 + Admin + 官网三端跑通)
任务详情
B1: 初始化 NestJS + TypeScript 项目骨架
在 inkreach-official-nestjs/ 目录初始化 NestJS 工程。
操作步骤:
npm init创建package.json,包名inkreach-official-nestjs- 安装核心依赖:
@nestjs/core @nestjs/common @nestjs/platform-express @nestjs/config reflect-metadata rxjs - 安装开发依赖:
@nestjs/cli @nestjs/testing typescript ts-node tsconfig-paths @types/node jest ts-jest @types/jest supertest @types/supertest - 创建配置文件:
nest-cli.json、tsconfig.json、tsconfig.build.json、.eslintrc.js、.prettierrc - 创建入口文件:
src/main.ts与src/app.module.ts,监听process.env.PORT ?? 3001 package.json中加入脚本:start/start:dev/start:debug/build/test/test:e2e/test:cov
验收:npm run start:dev 能启动并监听 3001 端口。
B2: 配置 Prisma + 数据库连接
操作步骤:
- 安装:
prisma @prisma/client(dev:prisma) npx prisma init初始化prisma/与.env- 在
.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 .gitignore加入.env、node_modules、dist- 在
app.module.ts注册ConfigModule.forRoot({ isGlobal: true })
验收:npx prisma db pull 或 prisma 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.originGoods→OriginGoods(RESTRICT)Good.country→Country(RESTRICT, NOT NULL)Good.category→Category(RESTRICT, NOT NULL)Good.tag→Tag(SET NULL, optional)Good.position→Position(SET NULL, optional)Category.parent→Category(RESTRICT, self-relation)Position.country→Country(CASCADE)Position.category→Category(CASCADE)
字段映射:所有 BIGINT 用 BigInt,TIMESTAMPTZ 用 DateTime @db.Timestamptz(6),TEXT 用 String。表名通过 @@map("xxx") 映射到 snake_case,字段名同理用 @map。
额外:
- 创建
User表用于 Admin 登录(id/usernameUNIQUE /passwordHash/ 时间戳) - 创建
SyncLog表记录同步历史(id/typeenum /statusenum /message/startedAt/finishedAt) - 索引按 DDL 中
CREATE INDEX创建对应@@index
操作:
- 编写 schema
npx prisma migrate dev --name init生成首个 migrationnpx 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.ts、auth.controller.ts、auth.service.tssrc/auth/strategies/jwt.strategy.ts、src/auth/guards/jwt-auth.guard.tssrc/auth/dto/login.dto.ts、register.dto.ts
依赖:@nestjs/jwt @nestjs/passport passport passport-jwt bcrypt class-validator class-transformer
接口:
| 方法 | 路径 | 描述 |
|---|---|---|
| POST | /auth/register |
注册(仅初始化用,可加环境变量门禁) |
| POST | /auth/login |
登录返回 { accessToken, user } |
实现要点:
- 密码用
bcrypthash(rounds=10) - JWT payload:
{ sub: userId, username },过期 7 天 JwtAuthGuard默认作用于 Admin 路由
测试 (src/auth/auth.service.spec.ts):
- 注册新用户 → 数据库存在记录,密码已 hash
- 重复用户名 → 抛
ConflictException - 登录正确密码 → 返回 token
- 登录错误密码 → 抛
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):
- CRUD 正常路径
- 重名 →
ConflictException - 被
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(唯一)、tagColor(hex 字符串验证 /^#[0-9A-Fa-f]{6}$/)、timing
测试:CRUD + 颜色格式验证。
B9: Positions 模块
文件:src/positions/{positions.module.ts, ...}
接口:GET / POST / PATCH /:id / DELETE /:id
字段:indexVal、countryId (可空)、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查询 - 关键词搜索
goodName用containsmode insensitive - 排序:
goodPriority DESC+createdAt DESC - 关联返回用 Prisma
include
测试 (goods.service.spec.ts):
- 单个 CRUD
- 筛选(每种参数)
- categoryId 包含子品类
- 批量优先级更新(事务)
- 批量创建(事务,全成或全败)
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 |
同步逻辑:
- 品类同步
syncCategories():调用POST /category/tree/3body{ withActivityArea:true, withPrivate:true, onlyHaveProduct:true },递归扁平化结果,按sdsId去重写入/更新categories表(需扩展 schema 增sdsCategoryId TEXT UNIQUE字段,B3 步骤已含) - 产品同步
syncProducts():遍历叶子品类,调用GET /products/page?categoryId=&page=&size=50,去重写入origin_goods表(按sdsGoodId) - Cron:
@Cron('0 * * * *')每小时执行一次完整同步 - 日志:每次同步开始写入
sync_logs(status=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 完成 - 接受
countryId与categoryId多维过滤 GET /public/categories用EXISTS子查询过滤
测试:覆盖所有筛选维度 + 排序正确性。
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 文档
操作:
- 安装
@nestjs/swagger main.ts中配置SwaggerModule.setup('api/docs', app, document)- 每个 controller / DTO 加
@ApiTags、@ApiOperation、@ApiProperty npm run start:dev启动后访问http://localhost:3001/api/docs确认所有接口可见
验收:所有阶段一测试 npm run test 全绿,启动后 Swagger 完整。
A1: 初始化 Vite + Vue 3 + TS + Element Plus
在 inkreach-official-admin/ 执行:
npm create vite@latest . -- --template vue-ts(注意当前目录)- 安装:
element-plus @element-plus/icons-vue pinia vue-router@4 axios @vueuse/core dayjs - 开发依赖:
@types/node sass unplugin-auto-import unplugin-vue-components - 在
vite.config.ts配置AutoImport+Components插件按需引入 Element Plus 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.data,401 跳登录src/api/auth.ts:login(username, password)/register(...)src/api/goods.ts、categories.ts、countries.ts、tags.ts、positions.ts、origin-goods.ts、sync.ts:对应后端接口,TS 类型明确
类型:src/types/index.ts 定义 Good、Country、Category、Tag、Position、OriginGoods、PaginatedResult<T> 等。
A4: Pinia stores
文件:
src/stores/auth.ts:token、user、login()、logout()、isLoggedIn,token 持久化到localStoragesrc/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/categories或POST /sync/products,加 loading + 成功/失败提示 - 自动每 30s 拉取一次状态(
useIntervalFnfrom 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(用useFetchSSR 拉取)products/total/loadingquery(reactive 筛选条件)selectedCategory、selectedCountry、selectedTagssetCategory(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
Props:categories、activeCategoryId,emit update:activeCategoryId
W5: ProductCountryFilter.vue
文件:app/components/product/ProductCountryFilter.vue
- 横向 pill 按钮组,每个 pill:国旗图标 + 国家名
- "全部"为默认项,selected 时橙色边框 + 橙色文字 + 白底
- 未选中:灰色边框 + 灰色文字
- 横向溢出可滚动
Props:countries、activeCountryId (null 表示"全部")
W6: ProductFilterBar.vue
文件:app/components/product/ProductFilterBar.vue
按设计稿 product-center-2.png:
- 左侧:筛选下拉(如"价格区间"等占位)+ 已选标签 chips(可点击 × 清除)
- 右侧:分体式搜索框 — 灰色输入框 + 橙色"搜索"按钮(圆角矩形)
Props/Emits:keyword v-model、selectedTags、onSearch
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 个ProductCardSkeletonproducts为空时渲染空状态插画 + "暂无商品"提示
Props:products、loading
W9: ProductPagination.vue
文件:app/components/product/ProductPagination.vue
按设计稿 product-center-3.png:
- 左侧:"总计 N 个产品"
- 中间:上一页 / 页码方形按钮(当前页橙色填充) / 下一页
- 右侧:
<select>每页条数(12/24/48) + 跳转到 input + Go 按钮
Props/Emits:total、page、pageSize,emit update:page、update: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>,国家筛选可横向滚动。
SEO:useSeoMeta({ 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: 全链路联调
- 启动 Postgres + 后端:
cd inkreach-official-nestjs && npm run start:dev - 手动调用
POST /auth/register创建管理员 - 手动调用
POST /sync/categories和/sync/products拉取 SDS 数据到origin_goods - 启动 Admin:
cd inkreach-official-admin && npm run dev,登录后在各页配置国家/品类/标签/位置,并批量从 origin_goods 创建 goods - 启动 Website:
cd inkreach-official-website && npm run dev,访问/product-center验证侧边栏、筛选、分页、跳转 - 三端同时运行无 console 报错,所有筛选条件返回正确数据
完成标准:
- 三个子项目均能
npm run test全绿 - 三端联调跑通完整业务流
- 文档与结构图已更新