feat: start ecommerce product center

This commit is contained in:
QiuSW
2026-07-09 09:10:22 +08:00
parent b8323550da
commit bda9c57843
11 changed files with 448 additions and 8 deletions
+4 -1
View File
@@ -4,11 +4,13 @@
模板内置方向包括:应用壳、设置、登录、主业务模块、操作日志、数据统计和常用业务组件。第一批业务样例以“电商商品图片 AI 优化”为主,用它验证模板是否能支撑真实业务闭环。
`ec` 分支开始面向电商运营应用开发,当前新增商品中心,后续会把商品、SKU、渠道、库存和商品图片优化串成业务闭环。
## 技术栈
- 后端:Go 1.23.0、Wails v3.0.0-alpha.9、SQLite
- 前端:Vue 3、TypeScript、Vite、Tailwind CSS、Ant Design Vue
- 当前功能:多语言、主题切换、统一设置 schema、主窗口状态恢复、本地配置、快捷键、本地登录 provider、商品图片 AI 优化页面、本地模拟图片优化任务、审计日志、运行日志、P0 数据统计服务、常用组件示例页、OCR 示例
- 当前功能:多语言、主题切换、统一设置 schema、主窗口状态恢复、本地配置、快捷键、本地登录 provider、商品中心、商品图片 AI 优化页面、本地模拟图片优化任务、审计日志、运行日志、P0 数据统计服务、常用组件示例页、OCR 示例
- 目标模板模块:登录、设置、主业务、审计日志、运行日志、数据统计、常用组件示例
## 环境准备
@@ -110,6 +112,7 @@ wails3 generate bindings -clean=true
- [`docs/06-tasks.md`](docs/06-tasks.md):任务看板。
- [`docs/windows-packaging.md`](docs/windows-packaging.md):Windows 打包验证流程。
- [`docs/smoke-checklist.md`](docs/smoke-checklist.md):模板 smoke 验证清单。
- [`docs/ecommerce-scenario.md`](docs/ecommerce-scenario.md):`ec` 分支电商场景开发说明。
- [`progress.md`](progress.md):执行流水和验证证据。
统一验证入口:
+10
View File
@@ -52,6 +52,16 @@
| T-033 | 完成一次模板 smoke 清单 | T-023 | 登录、设置、业务样例、日志、统计、组件示例、托盘、热键都按清单验证并记录结果 | DONE |
| T-034 | 添加 Windows Wails 快捷脚本 | T-001 | `dev.bat` 一行切到项目根目录、补 Go bin PATH、自动选择空闲端口并运行 `wails3 dev`;`build.bat` 一行切到项目根目录、补 Go bin PATH 并运行 `wails3 build`;README、技术栈、当前状态和执行流水已同步 | DONE |
## EC 分支 · 电商场景
| ID | 任务 | 依赖 | 验收要点 | 状态 |
| --- | --- | --- | --- | --- |
| EC-001 | 建立商品中心页面 | T-023 | `/#/products` 可访问;支持商品草稿、筛选、列表、状态标签和图片优化入口 | DONE |
| EC-002 | 建立商品后端模型 | EC-001 | 新增 `products` 表、ProductService、基础测试和 Wails bindings | TODO |
| EC-003 | 商品中心接入后端 | EC-002 | 商品中心不再使用前端本地示例数据,支持创建和查询商品 | TODO |
| EC-004 | 图片优化关联商品 | EC-002 | 图片优化任务可关联 SKU 或 product_id,商品中心能看到图片状态 | TODO |
| EC-005 | 电商统计指标 | EC-003 | 展示商品总数、待优化图片、在售商品、失败图片任务和处理趋势 | TODO |
## Backlog
- 评估是否需要 Playwright / Vitest 覆盖前端关键页面。
+15 -4
View File
@@ -4,13 +4,14 @@
## 当前阶段
项目处于可复用桌面应用模板的 Phase 2 业务样例与日志统计收尾阶段。目标已经从迁移后的基础工程调整为“Windows-first、可扩展到其他平台的桌面端应用模板”。
项目处于 `ec` 分支的电商场景开发起步阶段。主线模板能力已经具备,当前分支开始把模板能力收敛成面向电商运营的桌面端应用。
当前代码已从迁移后的 Wails/Vue/SQLite 基线推进到主应用路由骨架、登录 provider 边界、统一设置 schema、主窗口状态恢复、商品图片 AI 优化页面、日志服务边界、P0 数据统计和常用组件示例阶段,已具备设置、多语言、主题、本地配置、热键、托盘、OCR 示例、模板级模块入口、本地登录能力、本地模拟图片优化任务能力、业务样例页面、组件示例页、审计日志、运行日志和 P0 统计查询能力。统计页面仍是骨架,尚未展示真实统计数据。
当前代码已从迁移后的 Wails/Vue/SQLite 基线推进到主应用路由骨架、登录 provider 边界、统一设置 schema、主窗口状态恢复、商品中心、商品图片 AI 优化页面、日志服务边界、P0 数据统计和常用组件示例阶段,已具备设置、多语言、主题、本地配置、热键、托盘、OCR 示例、本地登录能力、商品运营入口、本地模拟图片优化任务能力、审计日志、运行日志和 P0 统计查询能力。统计页面仍是骨架,尚未展示真实统计数据。
## 已确认事实
- 项目类型:可复用桌面应用模板。
- 当前业务分支:`ec`,面向电商运营应用开发。
- 主要运行场景:Windows 10 以及以后的 Windows 操作系统。
- 跨平台策略:Windows-first,保留 macOS / Linux 适配边界,不在 MVP 承诺完整体验。
- 第一批业务样例:电商商品图片 AI 优化。
@@ -29,7 +30,7 @@
- 本地数据库:SQLite,用户配置目录下 `cmbone/cmbone.db`
- 当前本地演示账号:`admin` / `admin123`,密码由后端初始化为 salt + SHA-256 hash,不写死在前端。
- 当前主窗口路由:`/#/` 重定向到 `/#/dashboard`。
- 当前主应用模块路由:`/#/login`、`/#/dashboard`、`/#/image-optimizer`、`/#/logs`、`/#/statistics`、`/#/settings`、`/#/components`。
- 当前主应用模块路由:`/#/login`、`/#/dashboard`、`/#/products`、`/#/image-optimizer`、`/#/logs`、`/#/statistics`、`/#/settings`、`/#/components`。
- 当前兼容重定向路由:`/#/business/image-optimizer`、`/#/logs/audit`、`/#/logs/app`。
- 当前第二窗口路由:`/#/second`
- 已实现主窗口策略:默认不最大化,初始 `1280x800`,最小 `1024x720`,居中,可调整大小;启动后可按 `window.*` 配置恢复尺寸和最大化状态。
@@ -41,6 +42,7 @@
- 已实现统一设置 schema:主题、语言、窗口行为、快捷键展示、数据目录、日志策略和登录 provider 配置都有默认值、说明和基础校验。
- 已实现设置页 schema 展示:`/#/settings` 可读取后端 `GetSettingsSchema` 并展示分组、当前值、默认值、说明和只读标记。
- 已实现主窗口状态持久化:主窗口普通尺寸和最大化状态会写回 `appconfig`,下次启动恢复;第二窗口不参与该逻辑。
- 已实现商品中心页面:支持商品草稿、SKU、类目、渠道、售价、库存、上架状态、图片状态、筛选和分页展示。
- 已实现 ImageOptimizationService:`CreateTask`、`ListTasks`、`GetTask`、`CancelTask`,当前 provider 为本地模拟实现。
- 已实现 `image_tasks` 和 `image_task_results` 表,模拟任务可写入成功状态、耗时、输出路径和前后文件大小。
- 已实现商品图片 AI 优化页面:支持文件选择、手动图片路径、创建任务、筛选任务、任务列表、状态标签、结果预览和失败提示。
@@ -239,9 +241,18 @@ wails3 build
- 自动发送默认 `Alt+P` 后可见窗口包含 `second, main`,热键打开第二窗口通过。
- 登录、设置、业务样例、日志、统计、组件示例、托盘、热键均已按清单记录验证方式和结果。
同日创建 `ec` 分支并启动电商场景:
- `ec` 分支已推送到 `origin/ec`。
- 新增 `/#/products` 商品中心路由。
- 新增商品中心页面,当前使用前端本地示例数据。
- 商品中心支持商品草稿、SKU、类目、渠道、售价、库存、上架状态、图片状态、筛选、分页和图片优化入口。
- 新增 `docs/ecommerce-scenario.md`。
- `go test ./...`、`cd frontend && npm run build`、`go build -o bin\cmbone.exe .` 和 `cmd /c build.bat` 通过。
## 下一步
当前任务看板中剩余任务均已完成。下一步建议先人工复核托盘菜单点击和 NSIS 打包环境,再决定是否新增真实 AI provider、统计页面展示或日志页面列表任务。
从 [`06-tasks.md`](06-tasks.md) 领取 `EC-002`:建立商品后端模型,新增 `products` 表、ProductService、基础测试和 Wails bindings。
## 当前 blocker
+34
View File
@@ -0,0 +1,34 @@
# 电商场景开发说明
当前分支:`ec`
目标:在可复用桌面应用模板基础上,发展一个面向电商运营的 Windows 桌面端应用。
## 场景边界
第一阶段聚焦商品运营,不做完整 ERP:
- 商品中心:维护 SKU、商品名称、类目、渠道、售价、库存、上架状态和图片状态。
- 商品图片 AI 优化:复用现有 `ImageOptimizationService`,服务于商品主图和渠道图。
- 操作日志:记录登录、设置变更、任务创建和后续商品关键操作。
- 数据统计:围绕商品图片处理量、成功率、失败任务和运营审计做基础统计。
## 当前已落地
- 新增 `/#/products` 商品中心页面。
- 主导航新增“商品中心”。
- 商品中心当前使用前端本地示例数据,支持:
- 新增商品草稿。
- 商品、SKU、渠道关键字筛选。
- 类目、上架状态、图片状态筛选。
- 商品列表、价格、库存、上架状态、图片状态展示。
- 跳转到图片优化和统计模块。
## 下一步建议
1. 后端新增商品模型和 SQLite 表:`products`。
2. 新增 `ProductService`:`CreateProduct`、`ListProducts`、`UpdateProductStatus`、`UpdateImageStatus`。
3. 商品中心改为调用 Wails bindings,不再使用前端本地示例数据。
4. 商品图片 AI 优化任务增加 `product_id` 或 `sku` 关联。
5. 统计模块增加商品维度:商品总数、待优化图片、在售商品、失败图片任务。
6. 操作日志接入商品草稿创建、上架、下架、图片优化关联等审计事件。
+2
View File
@@ -93,6 +93,7 @@
| `frontend/src/components/template/ModulePageShell.vue` | 模块页共享壳 | 模块标题、状态和摘要卡片 |
| `frontend/src/modules/auth/LoginPage.vue` | 登录页 | 调用 AuthService 登录并处理错误 |
| `frontend/src/modules/dashboard/DashboardPage.vue` | 工作台骨架 | P0 指标和近期任务入口 |
| `frontend/src/modules/products/ProductCenterPage.vue` | 商品中心 | 电商商品草稿、筛选、列表和状态入口 |
| `frontend/src/modules/image-optimizer/ImageOptimizerPage.vue` | 主业务页面 | 商品图片 AI 优化任务创建、列表、状态和结果预览 |
| `frontend/src/modules/logs/LogsPage.vue` | 日志页骨架 | 审计日志和运行日志入口 |
| `frontend/src/modules/statistics/StatisticsPage.vue` | 统计页骨架 | P0 指标和 7 天趋势入口 |
@@ -109,6 +110,7 @@
| `frontend/src/layouts/` | 登录布局和主应用布局 |
| `frontend/src/modules/auth/` | 登录和会话 |
| `frontend/src/modules/settings/` | 设置中心 |
| `frontend/src/modules/products/` | 电商商品中心 |
| `frontend/src/modules/image-optimizer/` | 商品图片 AI 优化业务样例 |
| `frontend/src/modules/logs/` | 审计日志和运行日志 |
| `frontend/src/modules/statistics/` | 数据统计 |
+2
View File
@@ -11,6 +11,7 @@
| `#/` | `frontend/src/layouts/MainLayout.vue` | 主窗口 | 重定向到 `#/dashboard` |
| `#/login` | `frontend/src/modules/auth/LoginPage.vue` | 主窗口 | 登录入口,已接入本地固定账号 provider,真实后端 provider 保留边界 |
| `#/dashboard` | `frontend/src/modules/dashboard/DashboardPage.vue` | 主窗口 | 工作台,展示 P0 指标和近期任务入口 |
| `#/products` | `frontend/src/modules/products/ProductCenterPage.vue` | 主窗口 | 电商商品中心,支持商品草稿、筛选、列表和图片优化入口 |
| `#/image-optimizer` | `frontend/src/modules/image-optimizer/ImageOptimizerPage.vue` | 主窗口 | 商品图片 AI 优化业务样例,已接入任务创建、列表和结果预览 |
| `#/logs` | `frontend/src/modules/logs/LogsPage.vue` | 主窗口 | 审计日志和运行日志统一入口骨架 |
| `#/statistics` | `frontend/src/modules/statistics/StatisticsPage.vue` | 主窗口 | 第一批统计指标和趋势入口;后端 `MetricsService` 已可查询 |
@@ -26,6 +27,7 @@
| 路由 | 页面 | 职责 |
| --- | --- | --- |
| `#/login` | 登录页 | 已接入本地固定账号登录;真实后端登录待后续接入 |
| `#/products` | 商品中心 | `ec` 分支起点,当前为前端本地示例数据;后续接入 ProductService |
| `#/image-optimizer` | 商品图片 AI 优化 | 已接入页面任务列表、状态、结果和失败原因 |
| `#/logs` | 操作日志 | 后端审计日志和运行日志服务已接入;页面列表待后续扩展 |
| `#/statistics` | 数据统计 | 后端 `MetricsService` 已接入趋势、成功率、耗时和失败任务;页面展示待后续扩展 |
+1
View File
@@ -36,6 +36,7 @@ const sessionProvider = ref('Auth Provider')
const navItems: NavItem[] = [
{ path: '/dashboard', label: '工作台', detail: '指标与近期任务', icon: DashboardOutlined },
{ path: '/products', label: '商品中心', detail: 'SKU 与渠道运营', icon: TagsOutlined },
{ path: '/image-optimizer', label: '图片优化', detail: '电商商品图样例', icon: TagsOutlined },
{ path: '/logs', label: '操作日志', detail: '审计与排错', icon: ScheduleOutlined },
{ path: '/statistics', label: '数据统计', detail: '趋势和成功率', icon: FileDoneOutlined },
@@ -9,9 +9,9 @@ const metrics = [
]
const recentItems = [
{ title: '商品图片 AI 优化', status: '目标业务样例', time: 'T-020' },
{ title: '审计日志', status: '目标模块', time: 'T-021' },
{ title: 'P0 数据统计', status: '目标模块', time: 'T-022' },
{ title: '商品中心', status: 'EC 分支起点', time: 'EC-001' },
{ title: '商品图片 AI 优化', status: '已接入', time: 'T-023' },
{ title: 'P0 数据统计', status: '后端已接入', time: 'T-022' },
]
</script>
@@ -0,0 +1,354 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { RouterLink } from 'vue-router'
import {
PlusOutlined,
ReloadOutlined,
SearchOutlined,
} from '@ant-design/icons-vue'
import ModulePageShell from '../../components/template/ModulePageShell.vue'
type PublishStatus = 'draft' | 'reviewing' | 'online' | 'offline'
type ImageStatus = 'pending' | 'optimized' | 'failed'
type ProductRow = {
id: number
sku: string
name: string
category: string
channel: string
price: number
stock: number
publishStatus: PublishStatus
imageStatus: ImageStatus
updatedAt: string
}
const categoryOptions = ['服饰', '美妆', '家居', '数码', '食品']
const channelOptions = ['淘宝', '京东', '抖音', '小红书', '独立站']
const draftName = ref('夏季防晒衬衫')
const draftSku = ref('EC-SHIRT-001')
const draftCategory = ref('服饰')
const draftChannel = ref('淘宝')
const draftPrice = ref(129)
const draftStock = ref(80)
const keyword = ref('')
const categoryFilter = ref('')
const imageFilter = ref('')
const publishFilter = ref('')
const currentPage = ref(1)
const pageSize = 6
const products = ref<ProductRow[]>([
{ id: 1, sku: 'EC-SHIRT-001', name: '夏季防晒衬衫', category: '服饰', channel: '淘宝', price: 129, stock: 80, publishStatus: 'online', imageStatus: 'optimized', updatedAt: '2026-07-09' },
{ id: 2, sku: 'EC-LIP-021', name: '丝绒口红套装', category: '美妆', channel: '小红书', price: 199, stock: 42, publishStatus: 'reviewing', imageStatus: 'pending', updatedAt: '2026-07-09' },
{ id: 3, sku: 'EC-LAMP-018', name: '床头阅读灯', category: '家居', channel: '京东', price: 89, stock: 120, publishStatus: 'online', imageStatus: 'optimized', updatedAt: '2026-07-08' },
{ id: 4, sku: 'EC-CAM-086', name: '智能云台摄像头', category: '数码', channel: '独立站', price: 269, stock: 18, publishStatus: 'draft', imageStatus: 'pending', updatedAt: '2026-07-08' },
{ id: 5, sku: 'EC-TEA-033', name: '冷泡茶组合', category: '食品', channel: '抖音', price: 59, stock: 240, publishStatus: 'online', imageStatus: 'failed', updatedAt: '2026-07-07' },
{ id: 6, sku: 'EC-BAG-012', name: '通勤托特包', category: '服饰', channel: '小红书', price: 159, stock: 35, publishStatus: 'offline', imageStatus: 'optimized', updatedAt: '2026-07-07' },
{ id: 7, sku: 'EC-CREAM-056', name: '修护面霜', category: '美妆', channel: '淘宝', price: 239, stock: 64, publishStatus: 'reviewing', imageStatus: 'pending', updatedAt: '2026-07-06' },
])
watch([keyword, categoryFilter, imageFilter, publishFilter], () => {
currentPage.value = 1
})
const filteredProducts = computed(() => {
const term = keyword.value.trim().toLowerCase()
return products.value.filter((product) => {
const matchesKeyword = !term
|| product.name.toLowerCase().includes(term)
|| product.sku.toLowerCase().includes(term)
|| product.channel.toLowerCase().includes(term)
const matchesCategory = !categoryFilter.value || product.category === categoryFilter.value
const matchesImage = !imageFilter.value || product.imageStatus === imageFilter.value
const matchesPublish = !publishFilter.value || product.publishStatus === publishFilter.value
return matchesKeyword && matchesCategory && matchesImage && matchesPublish
})
})
const totalPages = computed(() => Math.max(1, Math.ceil(filteredProducts.value.length / pageSize)))
const visiblePage = computed(() => Math.min(currentPage.value, totalPages.value))
const pagedProducts = computed(() => {
const start = (visiblePage.value - 1) * pageSize
return filteredProducts.value.slice(start, start + pageSize)
})
const summary = computed(() => [
{ label: '商品总数', value: String(products.value.length), tone: 'blue' as const },
{ label: '待优化图片', value: String(products.value.filter((item) => item.imageStatus === 'pending').length), tone: 'amber' as const },
{ label: '在售商品', value: String(products.value.filter((item) => item.publishStatus === 'online').length), tone: 'green' as const },
{ label: '图片失败', value: String(products.value.filter((item) => item.imageStatus === 'failed').length), tone: 'red' as const },
])
function createDraft() {
const nextID = Math.max(...products.value.map((item) => item.id), 0) + 1
products.value.unshift({
id: nextID,
sku: draftSku.value.trim() || `EC-SKU-${String(nextID).padStart(3, '0')}`,
name: draftName.value.trim() || '未命名商品',
category: draftCategory.value,
channel: draftChannel.value,
price: Number(draftPrice.value) || 0,
stock: Number(draftStock.value) || 0,
publishStatus: 'draft',
imageStatus: 'pending',
updatedAt: new Date().toISOString().slice(0, 10),
})
currentPage.value = 1
}
function resetFilters() {
keyword.value = ''
categoryFilter.value = ''
imageFilter.value = ''
publishFilter.value = ''
currentPage.value = 1
}
function applyFilters() {
currentPage.value = 1
}
function previousPage() {
currentPage.value = Math.max(1, currentPage.value - 1)
}
function nextPage() {
currentPage.value = Math.min(totalPages.value, currentPage.value + 1)
}
function formatPrice(value: number) {
return `¥${value.toFixed(2)}`
}
function publishLabel(status: PublishStatus) {
const labels: Record<PublishStatus, string> = {
draft: '草稿',
reviewing: '审核中',
online: '在售',
offline: '下架',
}
return labels[status]
}
function imageLabel(status: ImageStatus) {
const labels: Record<ImageStatus, string> = {
pending: '待优化',
optimized: '已优化',
failed: '失败',
}
return labels[status]
}
function statusClass(status: PublishStatus | ImageStatus) {
if (status === 'online' || status === 'optimized') {
return 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-200'
}
if (status === 'failed' || status === 'offline') {
return 'border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-900/60 dark:bg-rose-950/40 dark:text-rose-200'
}
if (status === 'reviewing') {
return 'border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-900/60 dark:bg-blue-950/40 dark:text-blue-200'
}
return 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/60 dark:bg-amber-950/40 dark:text-amber-200'
}
</script>
<template>
<ModulePageShell
eyebrow="E-Commerce"
title="商品中心"
description="电商场景的商品运营入口,先沉淀 SKU、渠道、库存、上架状态和图片优化状态。"
status="EC 分支起点"
:items="summary"
>
<div class="grid grid-cols-1 gap-4 xl:grid-cols-[360px_minmax(0,1fr)]">
<aside class="space-y-4">
<section class="rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
<h2 class="mb-4 text-base font-semibold text-slate-950 dark:text-white">
商品草稿
</h2>
<div class="space-y-4">
<label class="block">
<span class="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-200">商品名称</span>
<input v-model="draftName" class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950">
</label>
<label class="block">
<span class="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-200">SKU</span>
<input v-model="draftSku" class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950">
</label>
<div class="grid grid-cols-2 gap-3">
<label class="block">
<span class="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-200">类目</span>
<select v-model="draftCategory" class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950">
<option v-for="item in categoryOptions" :key="item" :value="item">
{{ item }}
</option>
</select>
</label>
<label class="block">
<span class="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-200">渠道</span>
<select v-model="draftChannel" class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950">
<option v-for="item in channelOptions" :key="item" :value="item">
{{ item }}
</option>
</select>
</label>
</div>
<div class="grid grid-cols-2 gap-3">
<label class="block">
<span class="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-200">售价</span>
<input v-model.number="draftPrice" min="0" step="1" type="number" class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950">
</label>
<label class="block">
<span class="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-200">库存</span>
<input v-model.number="draftStock" min="0" step="1" type="number" class="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950">
</label>
</div>
<button
class="inline-flex h-10 w-full items-center justify-center gap-2 rounded-md bg-slate-950 px-4 text-sm font-medium text-white dark:bg-white dark:text-slate-950"
type="button"
@click="createDraft"
>
<PlusOutlined />
<span>新增草稿</span>
</button>
</div>
</section>
<section class="rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
<h2 class="mb-3 text-base font-semibold text-slate-950 dark:text-white">
运营动作
</h2>
<div class="space-y-3 text-sm">
<RouterLink class="flex items-center justify-between rounded-md border border-slate-200 px-3 py-2 text-slate-700 hover:bg-slate-50 dark:border-slate-800 dark:text-slate-200 dark:hover:bg-slate-950" to="/image-optimizer">
<span>商品图片 AI 优化</span>
<span class="text-xs text-slate-400">进入</span>
</RouterLink>
<RouterLink class="flex items-center justify-between rounded-md border border-slate-200 px-3 py-2 text-slate-700 hover:bg-slate-50 dark:border-slate-800 dark:text-slate-200 dark:hover:bg-slate-950" to="/statistics">
<span>运营数据统计</span>
<span class="text-xs text-slate-400">进入</span>
</RouterLink>
</div>
</section>
</aside>
<section class="min-w-0 rounded-lg border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
<div class="grid grid-cols-1 gap-3 border-b border-slate-200 p-4 dark:border-slate-800 xl:grid-cols-[minmax(180px,1fr)_130px_130px_130px_auto_auto]">
<input
v-model="keyword"
class="h-10 rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950"
placeholder="搜索商品、SKU、渠道"
@keyup.enter="applyFilters"
>
<select v-model="categoryFilter" class="h-10 rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950" @change="applyFilters">
<option value="">全部类目</option>
<option v-for="item in categoryOptions" :key="item" :value="item">
{{ item }}
</option>
</select>
<select v-model="publishFilter" class="h-10 rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950" @change="applyFilters">
<option value="">上架状态</option>
<option value="draft">草稿</option>
<option value="reviewing">审核中</option>
<option value="online">在售</option>
<option value="offline">下架</option>
</select>
<select v-model="imageFilter" class="h-10 rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950" @change="applyFilters">
<option value="">图片状态</option>
<option value="pending">待优化</option>
<option value="optimized">已优化</option>
<option value="failed">失败</option>
</select>
<button class="inline-flex h-10 items-center justify-center gap-2 rounded-md bg-slate-950 px-4 text-sm font-medium text-white dark:bg-white dark:text-slate-950" type="button" @click="applyFilters">
<SearchOutlined />
<span>筛选</span>
</button>
<button class="inline-flex h-10 items-center justify-center gap-2 rounded-md border border-slate-300 bg-white px-4 text-sm font-medium text-slate-700 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-200 dark:hover:bg-slate-900" type="button" @click="resetFilters">
<ReloadOutlined />
<span>重置</span>
</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full text-left text-sm">
<thead class="bg-slate-50 text-xs uppercase text-slate-500 dark:bg-slate-950 dark:text-slate-400">
<tr>
<th class="min-w-[220px] px-4 py-3 font-medium">商品</th>
<th class="w-[110px] px-4 py-3 font-medium">类目</th>
<th class="w-[110px] px-4 py-3 font-medium">渠道</th>
<th class="w-[110px] px-4 py-3 font-medium">售价</th>
<th class="w-[100px] px-4 py-3 font-medium">库存</th>
<th class="w-[120px] px-4 py-3 font-medium">上架</th>
<th class="w-[120px] px-4 py-3 font-medium">图片</th>
<th class="w-[120px] px-4 py-3 font-medium">更新</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200 dark:divide-slate-800">
<tr v-if="pagedProducts.length === 0">
<td class="px-4 py-8 text-center text-slate-500 dark:text-slate-400" colspan="8">
暂无商品
</td>
</tr>
<tr v-for="product in pagedProducts" :key="product.id" class="hover:bg-slate-50 dark:hover:bg-slate-950">
<td class="px-4 py-3">
<div class="font-medium text-slate-900 dark:text-white">
{{ product.name }}
</div>
<div class="text-xs text-slate-500 dark:text-slate-400">
{{ product.sku }}
</div>
</td>
<td class="px-4 py-3 text-slate-700 dark:text-slate-200">
{{ product.category }}
</td>
<td class="px-4 py-3 text-slate-700 dark:text-slate-200">
{{ product.channel }}
</td>
<td class="px-4 py-3 text-slate-700 dark:text-slate-200">
{{ formatPrice(product.price) }}
</td>
<td class="px-4 py-3 text-slate-700 dark:text-slate-200">
{{ product.stock }}
</td>
<td class="px-4 py-3">
<span class="inline-flex rounded-md border px-2 py-1 text-xs font-medium" :class="statusClass(product.publishStatus)">
{{ publishLabel(product.publishStatus) }}
</span>
</td>
<td class="px-4 py-3">
<span class="inline-flex rounded-md border px-2 py-1 text-xs font-medium" :class="statusClass(product.imageStatus)">
{{ imageLabel(product.imageStatus) }}
</span>
</td>
<td class="px-4 py-3 text-slate-500 dark:text-slate-400">
{{ product.updatedAt }}
</td>
</tr>
</tbody>
</table>
</div>
<div class="flex flex-col gap-3 border-t border-slate-200 p-4 dark:border-slate-800 md:flex-row md:items-center md:justify-between">
<div class="text-sm text-slate-500 dark:text-slate-400">
{{ filteredProducts.length }} 个商品
</div>
<div class="flex items-center gap-3 text-sm">
<button class="h-9 rounded-md border border-slate-300 px-3 disabled:cursor-not-allowed disabled:opacity-40 dark:border-slate-700" type="button" :disabled="visiblePage === 1" @click="previousPage">
上一页
</button>
<span class="min-w-[72px] text-center text-slate-700 dark:text-slate-200">
{{ visiblePage }} / {{ totalPages }}
</span>
<button class="h-9 rounded-md border border-slate-300 px-3 disabled:cursor-not-allowed disabled:opacity-40 dark:border-slate-700" type="button" :disabled="visiblePage === totalPages" @click="nextPage">
下一页
</button>
</div>
</div>
</section>
</div>
</ModulePageShell>
</template>
+5
View File
@@ -26,6 +26,11 @@ const routes: RouteRecordRaw[] = [
name: 'image-optimizer',
component: () => import('../modules/image-optimizer/ImageOptimizerPage.vue'),
},
{
path: 'products',
name: 'products',
component: () => import('../modules/products/ProductCenterPage.vue'),
},
{
path: 'business/image-optimizer',
redirect: '/image-optimizer',
+18
View File
@@ -388,3 +388,21 @@
- 前端构建仍提示 Browserslist 数据过期和 chunk 大小警告。
- `cmd /c build.bat` 仍提示 `wails3 build` 是 `wails3 task` 的 alias。
- 本机缺少 NSIS `makensis`,未执行安装包打包 smoke。
### 创建 ec 分支并启动电商场景
- 任务:新建 `ec` 分支,同步到 Git 服务,切换到 `ec` 分支,开始电商场景应用开发。
- 结果:
- 已从当前代码创建 `ec` 分支并推送到 `origin/ec`。
- 新增 `frontend/src/modules/products/ProductCenterPage.vue`。
- 新增 `/#/products` 路由和主导航“商品中心”。
- 商品中心支持商品草稿、SKU、类目、渠道、售价、库存、上架状态、图片状态、筛选、分页和图片优化入口。
- 新增 `docs/ecommerce-scenario.md`,记录电商场景边界和后续任务。
- 验证:
- `cd frontend && npm run build` 通过。
- `go test ./...` 通过。
- `go build -o bin\cmbone.exe .` 通过。
- `cmd /c build.bat` 通过。
- 非阻塞警告:
- `cmd /c build.bat` 仍提示 `wails3 build` 是 `wails3 task` 的 alias。
- 前端构建仍提示 Browserslist 数据过期和 chunk 大小警告。