feat: harden phase one workflows

This commit is contained in:
王鹏
2026-07-14 16:04:51 +08:00
parent 47f4a24760
commit 767534df48
103 changed files with 4849 additions and 388 deletions

View File

@@ -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 {

View File

@@ -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')
}

View File

@@ -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
})
})
})

View File

@@ -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`)
}

View 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
})
})
})

View File

@@ -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>) {

View File

@@ -140,3 +140,9 @@ body {
display: inline-flex;
gap: 8px;
}
.pagination-bar {
display: flex;
justify-content: flex-end;
padding-top: 14px;
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>