From a903dd490340c1b2a68f1004df359612251f9cdb Mon Sep 17 00:00:00 2001 From: yeuimu <2197651308@qq.com> Date: Mon, 22 Jun 2026 01:47:20 +0800 Subject: [PATCH] feat(admin): redesign GoodsView with dual-tree UX, filter bar, inline CRUD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Two-line good nodes with country + tag chips, hover tooltip - Resizable dual-tree with fixed-height panels (no node overlap) - Filter bar: search, country filter, tag filter (multi-select with rename) - Mode switch (品类/国家) pushed to right via spacer - Inline create country/tag in edit & config modals via quick-create buttons - Right tree node height auto-fix for locate highlight - Filter dropdown styles in global scope for teleported popper - Backend: multi-tag (GoodTag junction), goodImage column, origin goods tree API - Admin response interceptor unwraps {data, success} envelope - Simplified sidebar to single 商品管理 entry with tabs - SyncView simplified to product sync only --- .../specs/2026-06-21-goods-redesign.md | 92 + inkreach-official-admin/src/api/categories.ts | 2 +- .../src/api/origin-goods.ts | 16 +- inkreach-official-admin/src/api/request.ts | 8 +- inkreach-official-admin/src/api/sync.ts | 20 +- inkreach-official-admin/src/components.d.ts | 13 + .../src/layouts/DefaultLayout.vue | 41 +- inkreach-official-admin/src/router/index.ts | 40 +- inkreach-official-admin/src/stores/auth.ts | 8 +- inkreach-official-admin/src/types/index.ts | 164 +- .../src/views/categories/CategoriesView.vue | 84 +- .../src/views/countries/CountriesView.vue | 80 +- .../src/views/goods/GoodsView.vue | 1627 +++++++++++------ .../src/views/login/LoginView.vue | 26 +- .../src/views/positions/PositionsView.vue | 72 +- .../ProductManagementView.vue | 55 + .../src/views/sync/SyncView.vue | 240 +-- .../src/views/tags/TagsView.vue | 94 +- inkreach-official-nestjs/prisma/schema.prisma | 19 +- .../src/categories/dto/category-node.dto.ts | 4 + .../src/goods/dto/batch-create-good.dto.ts | 11 +- .../src/goods/dto/create-good.dto.ts | 19 +- .../src/goods/dto/good.dto.ts | 17 +- .../src/goods/dto/update-good.dto.ts | 17 +- .../src/goods/goods.service.spec.ts | 8 +- .../src/goods/goods.service.ts | 151 +- .../origin-goods/origin-goods.controller.ts | 8 + .../src/origin-goods/origin-goods.service.ts | 198 ++ .../src/public/dto/public-good.dto.ts | 5 +- .../src/public/public.service.ts | 40 +- 30 files changed, 1970 insertions(+), 1209 deletions(-) create mode 100644 docs/superpowers/specs/2026-06-21-goods-redesign.md create mode 100644 inkreach-official-admin/src/views/product-management/ProductManagementView.vue diff --git a/docs/superpowers/specs/2026-06-21-goods-redesign.md b/docs/superpowers/specs/2026-06-21-goods-redesign.md new file mode 100644 index 0000000..b92f8fb --- /dev/null +++ b/docs/superpowers/specs/2026-06-21-goods-redesign.md @@ -0,0 +1,92 @@ +# Goods Management Redesign + +## Date: 2026-06-21 + +## Problem + +1. **Origin Good name not displayed**: GoodsView doesn't show the origin product name/image/price +2. **Single tag per good**: Current schema only allows one tag per good via `goods.tag_id` FK +3. **Easy to miss unconfigured origin products**: Flat paginated table with no visibility into which origin products have been configured vs not + +## Solution + +### 1. Multi-Tag Support (GoodTag Junction Table) + +**Schema change:** +- New `good_tags` junction table: `(good_id, tag_id)` composite PK +- Migrate existing `goods.tag_id` data into `good_tags` +- Set `goods.tag_id` to nullable (will be dropped in a future migration) + +**Backend:** +- `CreateGoodDto`: `tagIds: number[]` (replaces `tagId`) +- `UpdateGoodDto`: `tagIds?: number[]` (replaces `tagId`) +- `BatchCreateGoodDto`: `tagIds?: number[]` (replaces `tagId`) +- `QueryGoodDto`: `tagId` filter queries `tags.some` instead of direct FK +- `GoodsService.create/update`: manage `good_tags` records in transaction +- `GOOD_INCLUDE`: include `tags: { include: { tag: true } }` + +### 2. Origin Goods Tree API + +New endpoint: `GET /origin-goods/tree` + +Returns origin goods grouped by SDS category, with configuration status: + +```json +[ + { + "sdsCategoryId": "4185", + "categoryName": "(包邮)180g纯棉T恤-单面印花", + "originGoods": [ + { + "id": "123", + "goodName": "产品A", + "goodImage": "https://...", + "goodPrice": "12.50", + "sdsGoodId": "p-123", + "configuredCount": 2, + "configuredCountries": ["美国", "英国"] + } + ] + } +] +``` + +Origin goods without `sdsCategoryId` are grouped under "未分类". + +### 3. Admin UI: Dual Tree Layout + +``` +┌──────────────────────────────────────────────────────────┐ +│ [搜索原产品] [210/439 已配置] │ +├───────────────────┬─────────────────────────────────────┤ +│ 首页分类树 │ 原产品分类树 │ +│ │ │ +│ ▼ 美国 │ ▼ (包邮)180g纯棉T恤 │ +│ ▼ T恤 │ ✓ 产品A [美国,英国] │ +│ ▼ 卫衣 │ ✗ 产品B [未配置] │ +│ ▼ 英国 │ ▼ 未分类 │ +│ ▼ 韩国 │ ✗ 产品C [未配置] │ +├───────────────────┴─────────────────────────────────────┤ +│ 详情/操作区域 │ +│ - 选左侧分类: 该分类下所有 goods 表格 │ +│ - 选右侧原产品: 该原产品的配置详情 + 快捷配置按钮 │ +│ - 多标签选择 (el-select multiple) │ +└──────────────────────────────────────────────────────────┘ +``` + +**Key interactions:** +- Left tree click: filter goods by selected category +- Right tree click: show origin good detail + all its goods configs +- Right tree badges: ✓ configured / ✗ unconfigured +- Quick configure: select country+category+tags, click origin goods to batch create +- Statistics bar: configured/total count + +### 4. Data Migration + +```sql +-- Step 1: Create good_tags table +-- Step 2: Copy existing tag_id data +INSERT INTO good_tags (good_id, tag_id, created_at) + SELECT good_id, tag_id, NOW() FROM goods WHERE tag_id IS NOT NULL; +-- Step 3: Good.tag_id becomes nullable (already nullable) +``` diff --git a/inkreach-official-admin/src/api/categories.ts b/inkreach-official-admin/src/api/categories.ts index 2dbc06e..cfd70db 100644 --- a/inkreach-official-admin/src/api/categories.ts +++ b/inkreach-official-admin/src/api/categories.ts @@ -16,7 +16,7 @@ export const categoriesApi = { // Get category tree getCategoryTree: () => { - return request.get('/categories/tree') + return request.get('/categories') }, // Get category by id diff --git a/inkreach-official-admin/src/api/origin-goods.ts b/inkreach-official-admin/src/api/origin-goods.ts index 11a90db..0c3d0e0 100644 --- a/inkreach-official-admin/src/api/origin-goods.ts +++ b/inkreach-official-admin/src/api/origin-goods.ts @@ -1,16 +1,12 @@ import request from './request' -import type { OriginGood } from '@/types' +import type { OriginGood, OriginGoodsTreeResponse, PaginatedResult } from '@/types' export const originGoodsApi = { - // Search origin goods - searchOriginGoods: (keyword: string) => { - return request.get('/origin-goods/search', { params: { keyword } }) + getTree: () => { + return request.get('/origin-goods/tree') }, - // Get origin goods list - getOriginGoodsList: (page = 1, pageSize = 20) => { - return request.get('/origin-goods', { - params: { page, pageSize }, - }) + getOriginGoodsList: (params: { page?: number; pageSize?: number; keyword?: string }) => { + return request.get>('/origin-goods', { params }) }, -} \ No newline at end of file +} diff --git a/inkreach-official-admin/src/api/request.ts b/inkreach-official-admin/src/api/request.ts index 30ff9f9..c71db2e 100644 --- a/inkreach-official-admin/src/api/request.ts +++ b/inkreach-official-admin/src/api/request.ts @@ -28,8 +28,12 @@ request.interceptors.request.use( // Response interceptor request.interceptors.response.use( (response: AxiosResponse) => { - // Unwrap data.data - return response.data + // Backend wraps everything in { data, success } — unwrap to data + const body = response.data + if (body && typeof body === 'object' && 'success' in body && 'data' in body) { + return body.data + } + return body }, (error: AxiosError) => { if (error.response) { diff --git a/inkreach-official-admin/src/api/sync.ts b/inkreach-official-admin/src/api/sync.ts index 084ad22..d36bde1 100644 --- a/inkreach-official-admin/src/api/sync.ts +++ b/inkreach-official-admin/src/api/sync.ts @@ -1,24 +1,14 @@ import request from './request' -import type { SyncLog, SyncStats, PaginatedResult } from '@/types' +import type { SyncLog } from '@/types' export const syncApi = { - // Trigger category sync - syncCategories: () => { - return request.post('/sync/categories') - }, - - // Trigger product sync syncProducts: () => { return request.post('/sync/products') }, - // Get sync logs - getSyncLogs: (params: { page?: number; pageSize?: number; type?: 'CATEGORY' | 'PRODUCT' }) => { - return request.get>('/sync/logs', { params }) - }, - - // Get sync stats - getSyncStats: () => { - return request.get('/sync/stats') + getSyncStatus: (limit?: number) => { + return request.get('/sync/status', { + params: limit ? { limit } : undefined, + }) }, } diff --git a/inkreach-official-admin/src/components.d.ts b/inkreach-official-admin/src/components.d.ts index 816993b..8dc3411 100644 --- a/inkreach-official-admin/src/components.d.ts +++ b/inkreach-official-admin/src/components.d.ts @@ -12,13 +12,18 @@ export {} declare module 'vue' { export interface GlobalComponents { ElAside: typeof import('element-plus/es')['ElAside'] + ElBadge: typeof import('element-plus/es')['ElBadge'] ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb'] ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'] ElButton: typeof import('element-plus/es')['ElButton'] ElCard: typeof import('element-plus/es')['ElCard'] ElCascader: typeof import('element-plus/es')['ElCascader'] + ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] + ElCol: typeof import('element-plus/es')['ElCol'] ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] ElContainer: typeof import('element-plus/es')['ElContainer'] + ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] + ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] @@ -36,10 +41,18 @@ declare module 'vue' { ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElOption: typeof import('element-plus/es')['ElOption'] ElPagination: typeof import('element-plus/es')['ElPagination'] + ElPopover: typeof import('element-plus/es')['ElPopover'] + ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] + ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] + ElRow: typeof import('element-plus/es')['ElRow'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElTable: typeof import('element-plus/es')['ElTable'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] + ElTabPane: typeof import('element-plus/es')['ElTabPane'] + ElTabs: typeof import('element-plus/es')['ElTabs'] ElTag: typeof import('element-plus/es')['ElTag'] + ElTooltip: typeof import('element-plus/es')['ElTooltip'] + ElTree: typeof import('element-plus/es')['ElTree'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] } diff --git a/inkreach-official-admin/src/layouts/DefaultLayout.vue b/inkreach-official-admin/src/layouts/DefaultLayout.vue index 82d8149..0f3f602 100644 --- a/inkreach-official-admin/src/layouts/DefaultLayout.vue +++ b/inkreach-official-admin/src/layouts/DefaultLayout.vue @@ -4,11 +4,6 @@ import { useRouter, useRoute } from 'vue-router' import { ElMessageBox } from 'element-plus' import { Goods, - Menu, - Location, - CollectionTag, - Sort, - Refresh, Expand, Fold, ArrowDown, @@ -30,21 +25,11 @@ interface MenuItem { } const menuItems = ref([ - { index: '/goods', title: 'Goods', icon: 'Goods' }, - { index: '/categories', title: 'Categories', icon: 'Menu' }, - { index: '/countries', title: 'Countries', icon: 'Location' }, - { index: '/tags', title: 'Tags', icon: 'CollectionTag' }, - { index: '/positions', title: 'Positions', icon: 'Sort' }, - { index: '/sync', title: 'Sync', icon: 'Refresh' }, + { index: '/goods', title: '商品管理', icon: 'Goods' }, ]) const iconMap: Record = { Goods, - Menu, - Location, - CollectionTag, - Sort, - Refresh, } const activeMenu = computed(() => route.path) @@ -53,7 +38,7 @@ const username = computed(() => authStore.user?.username || 'Admin') const breadcrumb = computed(() => { const meta = route.meta as { title?: string } | undefined - return meta?.title || 'Dashboard' + return meta?.title || '商品管理' }) function toggleSidebar() { @@ -62,9 +47,9 @@ function toggleSidebar() { async function handleLogout() { try { - await ElMessageBox.confirm('Are you sure to sign out?', 'Sign out', { - confirmButtonText: 'Sign out', - cancelButtonText: 'Cancel', + await ElMessageBox.confirm('确定退出登录吗?', '退出登录', { + confirmButtonText: '退出登录', + cancelButtonText: '取消', type: 'warning', }) } catch { @@ -86,11 +71,11 @@ function handleCommand(command: string) {
-
IR
-
-
InkReach
-
Product Center
-
+
IR
+
+
Inkreach
+
官网后台
+
@@ -127,7 +112,7 @@ function handleCommand(command: string) { - Home + 首页 {{ breadcrumb }} @@ -143,7 +128,7 @@ function handleCommand(command: string) { - Sign out + 退出登录 diff --git a/inkreach-official-admin/src/router/index.ts b/inkreach-official-admin/src/router/index.ts index 0b48938..972064e 100644 --- a/inkreach-official-admin/src/router/index.ts +++ b/inkreach-official-admin/src/router/index.ts @@ -6,7 +6,7 @@ const routes: RouteRecordRaw[] = [ path: '/login', name: 'Login', component: () => import('@/views/login/LoginView.vue'), - meta: { title: 'Login', public: true }, + meta: { title: '登录', public: true }, }, { path: '/', @@ -16,38 +16,8 @@ const routes: RouteRecordRaw[] = [ { path: 'goods', name: 'Goods', - component: () => import('@/views/goods/GoodsView.vue'), - meta: { title: 'Goods', icon: 'Goods' }, - }, - { - path: 'categories', - name: 'Categories', - component: () => import('@/views/categories/CategoriesView.vue'), - meta: { title: 'Categories', icon: 'Menu' }, - }, - { - path: 'countries', - name: 'Countries', - component: () => import('@/views/countries/CountriesView.vue'), - meta: { title: 'Countries', icon: 'Location' }, - }, - { - path: 'tags', - name: 'Tags', - component: () => import('@/views/tags/TagsView.vue'), - meta: { title: 'Tags', icon: 'CollectionTag' }, - }, - { - path: 'positions', - name: 'Positions', - component: () => import('@/views/positions/PositionsView.vue'), - meta: { title: 'Positions', icon: 'Sort' }, - }, - { - path: 'sync', - name: 'Sync', - component: () => import('@/views/sync/SyncView.vue'), - meta: { title: 'Sync', icon: 'Refresh' }, + component: () => import('@/views/product-management/ProductManagementView.vue'), + meta: { title: '商品管理' }, }, ], }, @@ -78,8 +48,8 @@ router.beforeEach((to) => { }) router.afterEach((to) => { - const title = (to.meta?.title as string) || 'InkReach Admin' - document.title = `${title} | InkReach Admin` + const title = (to.meta?.title as string) || 'Inkreach 官网后台' + document.title = `${title} | Inkreach 官网后台` }) export default router diff --git a/inkreach-official-admin/src/stores/auth.ts b/inkreach-official-admin/src/stores/auth.ts index f4e23ee..0e7046d 100644 --- a/inkreach-official-admin/src/stores/auth.ts +++ b/inkreach-official-admin/src/stores/auth.ts @@ -42,10 +42,10 @@ export const useAuthStore = defineStore('auth', () => { async function login(payload: LoginRequest) { const res = await authApi.login(payload) as any; - const accessToken = res.data?.accessToken ?? res.accessToken ?? res.token; - const userData = res.data?.user ?? res.user; - setToken(accessToken); - setUser(userData); + const accessToken: string = res.accessToken ?? res.data?.accessToken ?? ''; + const userData: User | null = res.user ?? res.data?.user ?? null; + if (accessToken) setToken(accessToken); + if (userData) setUser(userData); return res; } diff --git a/inkreach-official-admin/src/types/index.ts b/inkreach-official-admin/src/types/index.ts index 77ae416..6973167 100644 --- a/inkreach-official-admin/src/types/index.ts +++ b/inkreach-official-admin/src/types/index.ts @@ -1,6 +1,6 @@ // Common types export interface PaginatedResult { - data: T[] + items: T[] total: number page: number pageSize: number @@ -13,7 +13,7 @@ export interface LoginRequest { } export interface LoginResponse { - token: string + accessToken: string user: User } @@ -26,49 +26,58 @@ export interface User { // Good types export interface Good { id: string - name: string + goodName: string + goodImage: string | null + goodPriority: number originGoodId: string originGood?: OriginGood countryId: string country?: Country categoryId: string category?: Category - tagId: string + tagId?: string tag?: Tag + tags: Tag[] positionId?: string position?: Position - priority: number createdAt: string updatedAt: string } export interface CreateGoodRequest { - name: string - originGoodId: string - countryId: string - categoryId: string - tagId: string - positionId?: string - priority: number + goodName: string + goodImage?: string + originGoodId: number + countryId: number + categoryId: number + tagIds?: number[] + positionId?: number + goodPriority?: number } export interface UpdateGoodRequest { - name?: string - originGoodId?: string - countryId?: string - categoryId?: string - tagId?: string - positionId?: string + goodName?: string + goodImage?: string | null + originGoodId?: number + countryId?: number + categoryId?: number + tagIds?: number[] + positionId?: number | null + goodPriority?: number +} + +export interface BatchCreateItem { + originGoodId: number priority?: number } export interface BatchCreateGoodsRequest { - originGoodIds: string[] - countryId: string - categoryId: string - tagId: string - positionId?: string - priority: number + items: BatchCreateItem[] + countryId: number + categoryId: number + tagIds?: number[] + positionId?: number + defaultPriority?: number } export interface UpdatePriorityRequest { @@ -79,28 +88,29 @@ export interface UpdatePriorityRequest { // Country types export interface Country { id: string - name: string - icon?: string + countryName: string + countryIcon?: string | null createdAt: string updatedAt: string } export interface CreateCountryRequest { - name: string - icon?: string + countryName: string + countryIcon?: string } export interface UpdateCountryRequest { - name?: string - icon?: string + countryName?: string + countryIcon?: string | null } // Category types export interface Category { id: string - name: string - icon?: string - parentId?: string + categoryName: string + categoryIcon?: string | null + sdsCategoryId?: string | null + parentCategoryId?: string | null parent?: Category children?: Category[] _count?: { @@ -111,15 +121,15 @@ export interface Category { } export interface CreateCategoryRequest { - name: string - icon?: string - parentId?: string + categoryName: string + categoryIcon?: string + parentCategoryId?: number } export interface UpdateCategoryRequest { - name?: string - icon?: string - parentId?: string + categoryName?: string + categoryIcon?: string | null + parentCategoryId?: number | null } export interface CategoryTree extends Category { @@ -129,23 +139,23 @@ export interface CategoryTree extends Category { // Tag types export interface Tag { id: string - name: string - color?: string - timing?: string + tagName: string + tagColor?: string | null + timing?: string | null createdAt: string updatedAt: string } export interface CreateTagRequest { - name: string - color?: string + tagName: string + tagColor?: string timing?: string } export interface UpdateTagRequest { - name?: string - color?: string - timing?: string + tagName?: string + tagColor?: string | null + timing?: string | null } // Position types @@ -162,26 +172,56 @@ export interface Position { export interface CreatePositionRequest { indexVal: number - countryId?: string - categoryId?: string + countryId?: number + categoryId?: number } export interface UpdatePositionRequest { indexVal?: number - countryId?: string - categoryId?: string + countryId?: number | null + categoryId?: number | null } // Origin Good types export interface OriginGood { id: string - name: string - productId: string - categoryId?: string + goodName: string | null + goodImage: string | null + goodPrice: string | null + sdsGoodId: string + sdsCategoryId: string | null createdAt: string updatedAt: string } +// Origin Goods Tree types +export interface OriginGoodsTreeNode { + id: string + goodName: string + goodImage: string | null + goodPrice: string | null + sdsGoodId: string + configuredCount: number + configuredCountries: string[] + configuredTags: { tagName: string; tagColor: string | null }[] +} + +export interface OriginGoodsTreeCategoryNode { + categoryId: string + categoryName: string + sdsCategoryId: string | null + configuredCount: number + totalCount: number + children: OriginGoodsTreeCategoryNode[] + originGoods: OriginGoodsTreeNode[] +} + +export interface OriginGoodsTreeResponse { + tree: OriginGoodsTreeCategoryNode[] + totalOriginGoods: number + configuredCount: number +} + // Sync types export interface SyncLog { id: string @@ -194,12 +234,6 @@ export interface SyncLog { createdAt: string } -export interface SyncStats { - lastSyncTime?: string - status?: 'IDLE' | 'SYNCING' - errorCount?: number -} - // Filter types export interface GoodsFilter { countryId?: string @@ -211,20 +245,20 @@ export interface GoodsFilter { } export interface CategoryFilter { - parentId?: string - name?: string + parentCategoryId?: string + categoryName?: string page?: number pageSize?: number } export interface CountryFilter { - name?: string + countryName?: string page?: number pageSize?: number } export interface TagFilter { - name?: string + tagName?: string page?: number pageSize?: number } @@ -234,4 +268,4 @@ export interface PositionFilter { categoryId?: string page?: number pageSize?: number -} \ No newline at end of file +} diff --git a/inkreach-official-admin/src/views/categories/CategoriesView.vue b/inkreach-official-admin/src/views/categories/CategoriesView.vue index 5299a24..787f35d 100644 --- a/inkreach-official-admin/src/views/categories/CategoriesView.vue +++ b/inkreach-official-admin/src/views/categories/CategoriesView.vue @@ -34,7 +34,7 @@ const cascaderOptions = ref([]) function buildCascader(nodes: CategoryTree[]): CascaderNode[] { return nodes.map((n) => ({ value: n.id, - label: n.name, + label: n.categoryName, children: n.children?.length ? buildCascader(n.children) : undefined, })) } @@ -50,18 +50,18 @@ const dialogMode = ref<'create' | 'edit'>('create') const dialogLoading = ref(false) const dialogForm = reactive({ - name: '', - icon: '', - parentId: '', + categoryName: '', + categoryIcon: '', + parentCategoryId: '', }) const dialogRules = { - name: [{ required: true, message: 'Name is required', trigger: 'blur' }], + categoryName: [{ required: true, message: '名称是必填项', trigger: 'blur' }], } async function openAddDialog() { dialogMode.value = 'create' - Object.assign(dialogForm, { id: undefined, name: '', icon: '', parentId: '' }) + Object.assign(dialogForm, { id: undefined, categoryName: '', categoryIcon: '', parentCategoryId: '' }) rebuildCascader() dialogVisible.value = true } @@ -70,9 +70,9 @@ async function openEditDialog(c: Category) { dialogMode.value = 'edit' Object.assign(dialogForm, { id: c.id, - name: c.name, - icon: c.icon || '', - parentId: c.parentId || '', + categoryName: c.categoryName, + categoryIcon: c.categoryIcon || '', + parentCategoryId: c.parentCategoryId || '', }) rebuildCascader() dialogVisible.value = true @@ -85,16 +85,16 @@ async function handleSubmit() { dialogLoading.value = true try { const payload: CreateCategoryRequest = { - name: dialogForm.name, - icon: dialogForm.icon || undefined, - parentId: dialogForm.parentId || undefined, + categoryName: dialogForm.categoryName, + categoryIcon: dialogForm.categoryIcon || undefined, + parentCategoryId: dialogForm.parentCategoryId || undefined, } if (dialogMode.value === 'create') { await categoriesApi.createCategory(payload) - ElMessage.success('Category created') + ElMessage.success('分类创建成功') } else { await categoriesApi.updateCategory(dialogForm.id!, payload as UpdateCategoryRequest) - ElMessage.success('Category updated') + ElMessage.success('分类更新成功') } dialogVisible.value = false fetchTree() @@ -106,16 +106,16 @@ async function handleSubmit() { async function handleDelete(c: Category) { try { - await ElMessageBox.confirm(`Delete "${c.name}"?`, 'Confirm', { + await ElMessageBox.confirm(`确定删除「${c.categoryName}」吗?`, '确认', { type: 'warning', - confirmButtonText: 'Delete', - cancelButtonText: 'Cancel', + confirmButtonText: '删除', + cancelButtonText: '取消', }) } catch { return } await categoriesApi.deleteCategory(c.id) - ElMessage.success('Deleted') + ElMessage.success('删除成功') fetchTree() } @@ -128,11 +128,11 @@ onMounted(fetchTree)
- Add Category + 新增分类 - Refresh + 刷新
@@ -144,52 +144,52 @@ onMounted(fetchTree) :tree-props="{ children: 'children' }" default-expand-all > - - + + - + - + - + @@ -199,12 +199,12 @@ onMounted(fetchTree) :rules="dialogRules" label-width="100px" > - - + + - + - - + + diff --git a/inkreach-official-admin/src/views/countries/CountriesView.vue b/inkreach-official-admin/src/views/countries/CountriesView.vue index 641bddd..8b30d0d 100644 --- a/inkreach-official-admin/src/views/countries/CountriesView.vue +++ b/inkreach-official-admin/src/views/countries/CountriesView.vue @@ -15,7 +15,7 @@ const list = ref([]) const total = ref(0) const filter = reactive>({ - name: '', + countryName: '', page: 1, pageSize: 10, }) @@ -24,8 +24,10 @@ async function fetchList() { loading.value = true try { const res = await countriesApi.getCountriesList(filter) - list.value = res.data - total.value = res.total + const data = await countriesApi.getCountriesList(filter) as any + const arr = Array.isArray(data) ? data : (data.items ?? []) + list.value = arr + total.value = Array.isArray(data) ? data.length : (data.total ?? 0) } finally { loading.value = false } @@ -37,7 +39,7 @@ function handleSearch() { } function handleReset() { - filter.name = '' + filter.countryName = '' filter.page = 1 fetchList() } @@ -48,23 +50,23 @@ const dialogMode = ref<'create' | 'edit'>('create') const dialogLoading = ref(false) const dialogForm = reactive({ - name: '', - icon: '', + countryName: '', + countryIcon: '', }) const dialogRules = { - name: [{ required: true, message: 'Name is required', trigger: 'blur' }], + countryName: [{ required: true, message: '名称是必填项', trigger: 'blur' }], } function openAddDialog() { dialogMode.value = 'create' - Object.assign(dialogForm, { id: undefined, name: '', icon: '' }) + Object.assign(dialogForm, { id: undefined, countryName: '', countryIcon: '' }) dialogVisible.value = true } function openEditDialog(c: Country) { dialogMode.value = 'edit' - Object.assign(dialogForm, { id: c.id, name: c.name, icon: c.icon || '' }) + Object.assign(dialogForm, { id: c.id, countryName: c.countryName, countryIcon: c.countryIcon || '' }) dialogVisible.value = true } @@ -75,15 +77,15 @@ async function handleSubmit() { dialogLoading.value = true try { const payload: CreateCountryRequest = { - name: dialogForm.name, - icon: dialogForm.icon || undefined, + countryName: dialogForm.countryName, + countryIcon: dialogForm.countryIcon || undefined, } if (dialogMode.value === 'create') { await countriesApi.createCountry(payload) - ElMessage.success('Country created') + ElMessage.success('国家创建成功') } else { await countriesApi.updateCountry(dialogForm.id!, payload as UpdateCountryRequest) - ElMessage.success('Country updated') + ElMessage.success('国家更新成功') } dialogVisible.value = false fetchList() @@ -95,16 +97,16 @@ async function handleSubmit() { async function handleDelete(c: Country) { try { - await ElMessageBox.confirm(`Delete "${c.name}"?`, 'Confirm', { + await ElMessageBox.confirm(`确定删除「${c.countryName}」吗?`, '确认', { type: 'warning', - confirmButtonText: 'Delete', - cancelButtonText: 'Cancel', + confirmButtonText: '删除', + cancelButtonText: '取消', }) } catch { return } await countriesApi.deleteCountry(c.id) - ElMessage.success('Deleted') + ElMessage.success('删除成功') fetchList() } @@ -116,8 +118,8 @@ onMounted(fetchList)
- Search + 搜索 - Reset + 重置
- Add Country + 新增国家
- + - - + + - + @@ -195,22 +197,22 @@ onMounted(fetchList) - - + + - - + + diff --git a/inkreach-official-admin/src/views/goods/GoodsView.vue b/inkreach-official-admin/src/views/goods/GoodsView.vue index 664ca58..ab250fb 100644 --- a/inkreach-official-admin/src/views/goods/GoodsView.vue +++ b/inkreach-official-admin/src/views/goods/GoodsView.vue @@ -1,19 +1,13 @@ + + diff --git a/inkreach-official-admin/src/views/login/LoginView.vue b/inkreach-official-admin/src/views/login/LoginView.vue index ac9ccdd..868f9af 100644 --- a/inkreach-official-admin/src/views/login/LoginView.vue +++ b/inkreach-official-admin/src/views/login/LoginView.vue @@ -17,12 +17,12 @@ const form = reactive({ const rules: FormRules = { username: [ - { required: true, message: 'Please enter your username', trigger: 'blur' }, - { min: 2, max: 64, message: 'Length 2-64', trigger: 'blur' }, + { required: true, message: '请输入用户名', trigger: 'blur' }, + { min: 2, max: 64, message: '长度 2-64', trigger: 'blur' }, ], password: [ - { required: true, message: 'Please enter your password', trigger: 'blur' }, - { min: 4, max: 64, message: 'Length 4-64', trigger: 'blur' }, + { required: true, message: '请输入密码', trigger: 'blur' }, + { min: 4, max: 64, message: '长度 4-64', trigger: 'blur' }, ], } @@ -33,7 +33,7 @@ async function handleSubmit() { loading.value = true try { await authStore.login({ username: form.username, password: form.password }) - ElMessage.success('Login successful') + ElMessage.success('登录成功') router.push('/') } catch (err) { // Error toast is shown by axios response interceptor @@ -50,8 +50,8 @@ async function handleSubmit() { diff --git a/inkreach-official-admin/src/views/positions/PositionsView.vue b/inkreach-official-admin/src/views/positions/PositionsView.vue index 0381d1b..46fb752 100644 --- a/inkreach-official-admin/src/views/positions/PositionsView.vue +++ b/inkreach-official-admin/src/views/positions/PositionsView.vue @@ -34,7 +34,7 @@ async function loadLookups() { countriesApi.getCountriesList({ page: 1, pageSize: 500 }), categoriesApi.getCategoryTree(), ]) - countries.value = c.data + countries.value = c.items categoriesTree.value = ct } @@ -42,8 +42,10 @@ async function fetchList() { loading.value = true try { const res = await positionsApi.getPositionsList(filter) - list.value = res.data - total.value = res.total + const data = await positionsApi.getPositionsList(filter) as any + const arr = Array.isArray(data) ? data : (data.items ?? []) + list.value = arr + total.value = Array.isArray(data) ? data.length : (data.total ?? 0) } finally { loading.value = false } @@ -73,7 +75,7 @@ const cascaderOptions = ref([]) function buildCascader(nodes: CategoryTree[]): CascaderNode[] { return nodes.map((n) => ({ value: n.id, - label: n.name, + label: n.categoryName, children: n.children?.length ? buildCascader(n.children) : undefined, })) } @@ -91,7 +93,7 @@ const dialogForm = reactive({ }) const dialogRules = { - indexVal: [{ required: true, message: 'Index is required', trigger: 'blur' }], + indexVal: [{ required: true, message: '排序值是必填项', trigger: 'blur' }], } function rebuildCascader() { @@ -130,10 +132,10 @@ async function handleSubmit() { } if (dialogMode.value === 'create') { await positionsApi.createPosition(payload) - ElMessage.success('Position created') + ElMessage.success('位置创建成功') } else { await positionsApi.updatePosition(dialogForm.id!, payload as UpdatePositionRequest) - ElMessage.success('Position updated') + ElMessage.success('位置更新成功') } dialogVisible.value = false fetchList() @@ -145,22 +147,22 @@ async function handleSubmit() { async function handleDelete(p: Position) { try { - await ElMessageBox.confirm(`Delete position #${p.indexVal}?`, 'Confirm', { + await ElMessageBox.confirm(`确定删除位置 #${p.indexVal} 吗?`, '确认', { type: 'warning', - confirmButtonText: 'Delete', - cancelButtonText: 'Cancel', + confirmButtonText: '删除', + cancelButtonText: '取消', }) } catch { return } await positionsApi.deletePosition(p.id) - ElMessage.success('Deleted') + ElMessage.success('删除成功') fetchList() } function getCategoryName(p: Position): string { if (!p.category) return p.categoryId || '-' - return p.category.parent ? `${p.category.parent.name} / ${p.category.name}` : p.category.name + return p.category.parent ? `${p.category.parent.categoryName} / ${p.category.categoryName}` : p.category.categoryName } onMounted(async () => { @@ -175,14 +177,14 @@ onMounted(async () => {
@@ -191,56 +193,56 @@ onMounted(async () => { v-model="filter.categoryId" :options="cascaderOptions" :props="{ checkStrictly: true, value: 'value', label: 'label', children: 'children', emitPath: false }" - placeholder="Category" + placeholder="请选择分类" clearable @change="handleSearch" /> - Search + 搜索 - Reset + 重置
- Add Position + 新增位置
- - + + - + - + @@ -258,25 +260,25 @@ onMounted(async () => { - + - - + + - + { children: 'children', emitPath: false, }" - placeholder="Optional" + placeholder="可选" clearable style="width: 100%" /> diff --git a/inkreach-official-admin/src/views/product-management/ProductManagementView.vue b/inkreach-official-admin/src/views/product-management/ProductManagementView.vue new file mode 100644 index 0000000..3f74146 --- /dev/null +++ b/inkreach-official-admin/src/views/product-management/ProductManagementView.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/inkreach-official-admin/src/views/sync/SyncView.vue b/inkreach-official-admin/src/views/sync/SyncView.vue index 2b5730a..c17fc1d 100644 --- a/inkreach-official-admin/src/views/sync/SyncView.vue +++ b/inkreach-official-admin/src/views/sync/SyncView.vue @@ -1,99 +1,46 @@