feat: harden phase one workflows
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
buildGroupBuyOrderActionPath,
|
||||
buildGroupBuyOrderQuery,
|
||||
filterPendingDeliveryGroupBuyOrders,
|
||||
groupBuyOrderDispatchLabel
|
||||
} from './groupBuyApi'
|
||||
@@ -30,6 +31,18 @@ describe('group buy dispatch helpers', () => {
|
||||
expect(buildGroupBuyOrderActionPath(1, 'confirm')).toBe('/api/admin/group-buy-orders/1/confirm')
|
||||
expect(buildGroupBuyOrderActionPath(1, 'ready')).toBe('/api/admin/group-buy-orders/1/ready')
|
||||
})
|
||||
|
||||
it('builds group buy order page query params', () => {
|
||||
expect(buildGroupBuyOrderQuery({
|
||||
status: 'PENDING_CONFIRM',
|
||||
pageNo: 2,
|
||||
pageSize: 10
|
||||
})).toEqual({
|
||||
status: 'PENDING_CONFIRM',
|
||||
pageNo: 2,
|
||||
pageSize: 10
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function groupBuyOrder(overrides: Partial<GroupBuyOrder>): GroupBuyOrder {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { GroupBuy, GroupBuyOrder } from '../types'
|
||||
import type { GroupBuy, GroupBuyOrder, PageResponse } from '../types'
|
||||
import { apiGet, apiPost, apiPut } from './http'
|
||||
|
||||
export function listGroupBuys() {
|
||||
@@ -9,6 +9,24 @@ export function listGroupBuyOrders(groupBuyId: number) {
|
||||
return apiGet<GroupBuyOrder[]>(`/api/admin/group-buys/${groupBuyId}/orders`)
|
||||
}
|
||||
|
||||
export interface GroupBuyOrderQuery {
|
||||
status?: string
|
||||
pageNo?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export function buildGroupBuyOrderQuery(query: GroupBuyOrderQuery = {}) {
|
||||
const params: Record<string, string | number> = {}
|
||||
if (query.status) params.status = query.status
|
||||
if (query.pageNo) params.pageNo = query.pageNo
|
||||
if (query.pageSize) params.pageSize = query.pageSize
|
||||
return params
|
||||
}
|
||||
|
||||
export function pageGroupBuyOrders(groupBuyId: number, query: GroupBuyOrderQuery = {}) {
|
||||
return apiGet<PageResponse<GroupBuyOrder>>(`/api/admin/group-buys/${groupBuyId}/orders/page`, buildGroupBuyOrderQuery(query))
|
||||
}
|
||||
|
||||
export function filterPendingDeliveryGroupBuyOrders(orders: GroupBuyOrder[]) {
|
||||
return orders.filter((order) => order.status === 'PENDING_DELIVERY')
|
||||
}
|
||||
|
||||
@@ -6,11 +6,15 @@ describe('buildGoodsOrderQuery', () => {
|
||||
expect(buildGoodsOrderQuery({
|
||||
status: 'PENDING_DELIVERY',
|
||||
startDate: '2026-07-01',
|
||||
endDate: '2026-07-07'
|
||||
endDate: '2026-07-07',
|
||||
pageNo: 2,
|
||||
pageSize: 20
|
||||
})).toEqual({
|
||||
status: 'PENDING_DELIVERY',
|
||||
startDate: '2026-07-01',
|
||||
endDate: '2026-07-07'
|
||||
endDate: '2026-07-07',
|
||||
pageNo: 2,
|
||||
pageSize: 20
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import type { ExpressOrder, GoodsOrder } from '../types'
|
||||
import type { ExpressOrder, GoodsOrder, PageResponse } from '../types'
|
||||
import { apiGet, apiPut } from './http'
|
||||
|
||||
export interface GoodsOrderQuery {
|
||||
status?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
pageNo?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export function buildGoodsOrderQuery(query: GoodsOrderQuery) {
|
||||
const params: Record<string, string> = {}
|
||||
const params: Record<string, string | number> = {}
|
||||
if (query.status) params.status = query.status
|
||||
if (query.startDate) params.startDate = query.startDate
|
||||
if (query.endDate) params.endDate = query.endDate
|
||||
if (query.pageNo) params.pageNo = query.pageNo
|
||||
if (query.pageSize) params.pageSize = query.pageSize
|
||||
return params
|
||||
}
|
||||
|
||||
@@ -19,6 +23,10 @@ export function listGoodsOrders(query: GoodsOrderQuery = {}) {
|
||||
return apiGet<GoodsOrder[]>('/api/admin/goods-orders', buildGoodsOrderQuery(query))
|
||||
}
|
||||
|
||||
export function pageGoodsOrders(query: GoodsOrderQuery = {}) {
|
||||
return apiGet<PageResponse<GoodsOrder>>('/api/admin/goods-orders/page', buildGoodsOrderQuery(query))
|
||||
}
|
||||
|
||||
export function confirmGoodsOrder(id: number) {
|
||||
return apiPut<GoodsOrder>(`/api/admin/goods-orders/${id}/confirm`)
|
||||
}
|
||||
@@ -28,17 +36,35 @@ export function markGoodsOrderPrepared(id: number) {
|
||||
}
|
||||
|
||||
export function cancelGoodsOrder(id: number) {
|
||||
return apiPut<GoodsOrder>(`/api/mini/goods-orders/${id}/cancel`)
|
||||
return apiPut<GoodsOrder>(`/api/admin/goods-orders/${id}/cancel`)
|
||||
}
|
||||
|
||||
export function markGoodsOrderPaid(id: number) {
|
||||
return apiPut<GoodsOrder>(`/api/admin/goods-orders/${id}/paid`)
|
||||
}
|
||||
|
||||
export interface ExpressOrderQuery {
|
||||
status?: string
|
||||
pageNo?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export function buildExpressOrderQuery(query: ExpressOrderQuery = {}) {
|
||||
const params: Record<string, string | number> = {}
|
||||
if (query.status) params.status = query.status
|
||||
if (query.pageNo) params.pageNo = query.pageNo
|
||||
if (query.pageSize) params.pageSize = query.pageSize
|
||||
return params
|
||||
}
|
||||
|
||||
export function listExpressOrders(status?: string) {
|
||||
return apiGet<ExpressOrder[]>('/api/admin/express-orders', status ? { status } : undefined)
|
||||
}
|
||||
|
||||
export function pageExpressOrders(query: ExpressOrderQuery = {}) {
|
||||
return apiGet<PageResponse<ExpressOrder>>('/api/admin/express-orders/page', buildExpressOrderQuery(query))
|
||||
}
|
||||
|
||||
export function confirmExpressOrder(id: number) {
|
||||
return apiPut<ExpressOrder>(`/api/admin/express-orders/${id}/confirm`)
|
||||
}
|
||||
|
||||
20
admin-web/src/api/productApi.test.ts
Normal file
20
admin-web/src/api/productApi.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildProductQuery } from './productApi'
|
||||
|
||||
describe('buildProductQuery', () => {
|
||||
it('keeps pagination and optional product filters stable', () => {
|
||||
expect(buildProductQuery({
|
||||
keyword: '苹果',
|
||||
categoryId: 2,
|
||||
merchantId: 1,
|
||||
pageNo: 3,
|
||||
pageSize: 10
|
||||
})).toEqual({
|
||||
keyword: '苹果',
|
||||
categoryId: 2,
|
||||
merchantId: 1,
|
||||
pageNo: 3,
|
||||
pageSize: 10
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,28 @@
|
||||
import type { Product, ProductCategory, ProductSku } from '../types'
|
||||
import type { PageResponse, Product, ProductCategory, ProductSku } from '../types'
|
||||
import { apiGet, apiPost, apiPut } from './http'
|
||||
|
||||
export function listProducts() {
|
||||
return apiGet<Product[]>('/api/admin/products')
|
||||
export interface ProductQuery {
|
||||
keyword?: string
|
||||
categoryId?: number
|
||||
merchantId?: number
|
||||
pageNo?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export function buildProductQuery(query: ProductQuery = {}) {
|
||||
const params: Record<string, string | number> = {
|
||||
pageNo: query.pageNo || 1,
|
||||
pageSize: query.pageSize || 20
|
||||
}
|
||||
if (query.keyword) params.keyword = query.keyword
|
||||
if (query.categoryId) params.categoryId = query.categoryId
|
||||
if (query.merchantId) params.merchantId = query.merchantId
|
||||
return params
|
||||
}
|
||||
|
||||
export async function listProducts(query: ProductQuery = {}) {
|
||||
const page = await apiGet<PageResponse<Product>>('/api/admin/products/page', buildProductQuery(query))
|
||||
return page.records
|
||||
}
|
||||
|
||||
export function createProduct(data: Partial<Product>) {
|
||||
|
||||
@@ -140,3 +140,9 @@ body {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pagination-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,13 @@ export interface CurrentUser {
|
||||
roleCode: string
|
||||
}
|
||||
|
||||
export interface PageResponse<T> {
|
||||
total: number
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
records: T[]
|
||||
}
|
||||
|
||||
export interface ProductSku {
|
||||
id: number
|
||||
productId: number
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
adjustExpressFee,
|
||||
cancelExpressOrder,
|
||||
confirmExpressOrder,
|
||||
listExpressOrders,
|
||||
pageExpressOrders,
|
||||
updateExpressPayStatus
|
||||
} from '../api/orderApi'
|
||||
import type { ExpressOrder } from '../types'
|
||||
@@ -15,11 +15,18 @@ import { statusText, yuan } from '../utils/format'
|
||||
const loading = ref(false)
|
||||
const rows = ref<ExpressOrder[]>([])
|
||||
const filters = reactive({ status: '' })
|
||||
const pagination = reactive({ pageNo: 1, pageSize: 10, total: 0 })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
rows.value = await listExpressOrders(filters.status || undefined)
|
||||
const page = await pageExpressOrders({
|
||||
status: filters.status || undefined,
|
||||
pageNo: pagination.pageNo,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
rows.value = page.records
|
||||
pagination.total = page.total
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '加载失败')
|
||||
} finally {
|
||||
@@ -27,6 +34,17 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
function search() {
|
||||
pagination.pageNo = 1
|
||||
load()
|
||||
}
|
||||
|
||||
function changeSize(size: number) {
|
||||
pagination.pageSize = size
|
||||
pagination.pageNo = 1
|
||||
load()
|
||||
}
|
||||
|
||||
async function run(action: () => Promise<ExpressOrder>) {
|
||||
try {
|
||||
await action()
|
||||
@@ -63,7 +81,7 @@ onMounted(load)
|
||||
<el-option label="配送中" value="DELIVERING" />
|
||||
<el-option label="已完成" value="COMPLETED" />
|
||||
</el-select>
|
||||
<el-button type="primary" :icon="Search" :loading="loading" @click="load">查询</el-button>
|
||||
<el-button type="primary" :icon="Search" :loading="loading" @click="search">查询</el-button>
|
||||
<el-button :icon="Refresh" @click="load">刷新</el-button>
|
||||
</div>
|
||||
<el-table :data="rows" border v-loading="loading">
|
||||
@@ -88,5 +106,16 @@ onMounted(load)
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.pageNo"
|
||||
:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="load"
|
||||
@size-change="changeSize"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -5,9 +5,9 @@ import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
cancelGoodsOrder,
|
||||
confirmGoodsOrder,
|
||||
listGoodsOrders,
|
||||
markGoodsOrderPaid,
|
||||
markGoodsOrderPrepared
|
||||
markGoodsOrderPrepared,
|
||||
pageGoodsOrders
|
||||
} from '../api/orderApi'
|
||||
import type { GoodsOrder } from '../types'
|
||||
import { statusText, yuan } from '../utils/format'
|
||||
@@ -15,11 +15,18 @@ import { statusText, yuan } from '../utils/format'
|
||||
const loading = ref(false)
|
||||
const rows = ref<GoodsOrder[]>([])
|
||||
const filters = reactive({ status: '' })
|
||||
const pagination = reactive({ pageNo: 1, pageSize: 10, total: 0 })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
rows.value = await listGoodsOrders({ status: filters.status || undefined })
|
||||
const page = await pageGoodsOrders({
|
||||
status: filters.status || undefined,
|
||||
pageNo: pagination.pageNo,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
rows.value = page.records
|
||||
pagination.total = page.total
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '加载失败')
|
||||
} finally {
|
||||
@@ -27,6 +34,17 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
function search() {
|
||||
pagination.pageNo = 1
|
||||
load()
|
||||
}
|
||||
|
||||
function changeSize(size: number) {
|
||||
pagination.pageSize = size
|
||||
pagination.pageNo = 1
|
||||
load()
|
||||
}
|
||||
|
||||
async function run(action: () => Promise<GoodsOrder>) {
|
||||
try {
|
||||
await action()
|
||||
@@ -51,7 +69,7 @@ onMounted(load)
|
||||
<el-option label="配送中" value="DELIVERING" />
|
||||
<el-option label="已完成" value="COMPLETED" />
|
||||
</el-select>
|
||||
<el-button type="primary" :icon="Search" :loading="loading" @click="load">查询</el-button>
|
||||
<el-button type="primary" :icon="Search" :loading="loading" @click="search">查询</el-button>
|
||||
<el-button :icon="Refresh" @click="load">刷新</el-button>
|
||||
</div>
|
||||
<el-table :data="rows" border v-loading="loading">
|
||||
@@ -79,5 +97,16 @@ onMounted(load)
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.pageNo"
|
||||
:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="load"
|
||||
@size-change="changeSize"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
completeGroupBuy,
|
||||
confirmGroupBuyOrder,
|
||||
createGroupBuy,
|
||||
listGroupBuyOrders,
|
||||
listGroupBuys,
|
||||
markGroupBuyOrderReady,
|
||||
pageGroupBuyOrders,
|
||||
startGroupBuy
|
||||
} from '../api/groupBuyApi'
|
||||
import type { GroupBuy, GroupBuyOrder } from '../types'
|
||||
@@ -22,6 +22,7 @@ const orders = ref<GroupBuyOrder[]>([])
|
||||
const activeGroupBuy = ref<GroupBuy | null>(null)
|
||||
const dialog = ref(false)
|
||||
const orderDialog = ref(false)
|
||||
const orderPagination = reactive({ pageNo: 1, pageSize: 10, total: 0 })
|
||||
const form = reactive({
|
||||
title: '',
|
||||
coverUrl: '',
|
||||
@@ -46,6 +47,7 @@ async function load() {
|
||||
|
||||
async function openOrders(row: GroupBuy) {
|
||||
activeGroupBuy.value = row
|
||||
orderPagination.pageNo = 1
|
||||
orderDialog.value = true
|
||||
await loadOrders(row.id)
|
||||
}
|
||||
@@ -54,7 +56,12 @@ async function loadOrders(groupBuyId = activeGroupBuy.value?.id) {
|
||||
if (!groupBuyId) return
|
||||
orderLoading.value = true
|
||||
try {
|
||||
orders.value = await listGroupBuyOrders(groupBuyId)
|
||||
const page = await pageGroupBuyOrders(groupBuyId, {
|
||||
pageNo: orderPagination.pageNo,
|
||||
pageSize: orderPagination.pageSize
|
||||
})
|
||||
orders.value = page.records
|
||||
orderPagination.total = page.total
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '加载失败')
|
||||
} finally {
|
||||
@@ -62,6 +69,12 @@ async function loadOrders(groupBuyId = activeGroupBuy.value?.id) {
|
||||
}
|
||||
}
|
||||
|
||||
function changeOrderSize(size: number) {
|
||||
orderPagination.pageSize = size
|
||||
orderPagination.pageNo = 1
|
||||
loadOrders()
|
||||
}
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
await createGroupBuy(form)
|
||||
@@ -153,6 +166,17 @@ onMounted(load)
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="orderPagination.pageNo"
|
||||
:page-size="orderPagination.pageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="orderPagination.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="loadOrders()"
|
||||
@size-change="changeOrderSize"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="dialog" title="新建团购" width="560px">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { Check, Edit, Plus, Refresh, Upload } from '@element-plus/icons-vue'
|
||||
import { Check, Edit, Plus, Refresh, Search, Upload } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
addSku,
|
||||
@@ -17,6 +17,7 @@ import { statusText, yuan } from '../utils/format'
|
||||
const loading = ref(false)
|
||||
const products = ref<Product[]>([])
|
||||
const categories = ref<ProductCategory[]>([])
|
||||
const keyword = ref('')
|
||||
const productDialog = ref(false)
|
||||
const categoryDialog = ref(false)
|
||||
const skuDialog = ref(false)
|
||||
@@ -28,7 +29,10 @@ const skuForm = reactive({ skuName: '', priceCent: 0, originPriceCent: undefined
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [productRows, categoryRows] = await Promise.all([listProducts(), listProductCategories()])
|
||||
const [productRows, categoryRows] = await Promise.all([
|
||||
listProducts({ keyword: keyword.value.trim() || undefined }),
|
||||
listProductCategories()
|
||||
])
|
||||
products.value = productRows
|
||||
categories.value = categoryRows
|
||||
if (!productForm.categoryId && categoryRows[0]) {
|
||||
@@ -99,6 +103,15 @@ onMounted(load)
|
||||
<h1 class="page-title">商品管理</h1>
|
||||
<div class="panel">
|
||||
<div class="toolbar">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
clearable
|
||||
placeholder="搜索商品"
|
||||
style="width: 220px"
|
||||
@keyup.enter="load"
|
||||
@clear="load"
|
||||
/>
|
||||
<el-button :icon="Search" :loading="loading" @click="load">搜索</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="productDialog = true">新建商品</el-button>
|
||||
<el-button :icon="Plus" @click="categoryDialog = true">新建分类</el-button>
|
||||
<el-button :icon="Refresh" :loading="loading" @click="load">刷新</el-button>
|
||||
|
||||
Reference in New Issue
Block a user