feat: 前端基础设施更新 - API模块、路由、状态管理和工具类
- 新增 address/admin/favorite/review API 模块 - 更新已有 API 模块适配后端接口变更 - 新增 admin 类型定义和工具函数 - 添加静态资源文件 - 更新路由配置和守卫逻辑 - 更新 Vite 配置和依赖锁文件
This commit is contained in:
287
flash-sale-frontend/src/utils/normalizers.ts
Normal file
287
flash-sale-frontend/src/utils/normalizers.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
import type {
|
||||
CartItem,
|
||||
FlashSale,
|
||||
Order,
|
||||
OrderAddress,
|
||||
PageResponse,
|
||||
Product,
|
||||
User,
|
||||
} from '@/types/api'
|
||||
import type {
|
||||
AdminHotProductRow,
|
||||
AdminOrderRow,
|
||||
AdminProductRow,
|
||||
AdminRecentOrderRow,
|
||||
AdminUserRow,
|
||||
} from '@/types/admin'
|
||||
import { DEFAULT_PRODUCT_IMAGE, resolveImageUrl } from '@/utils/image'
|
||||
|
||||
const toNumber = (value: unknown, fallback = 0) => {
|
||||
const result = Number(value)
|
||||
return Number.isFinite(result) ? result : fallback
|
||||
}
|
||||
|
||||
const toString = (value: unknown, fallback = '') => {
|
||||
if (value === null || value === undefined) {
|
||||
return fallback
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
const toIsoLikeString = (value: unknown) => {
|
||||
const raw = toString(value)
|
||||
return raw || new Date().toISOString()
|
||||
}
|
||||
|
||||
export const buildOrderNo = (id: number | string) => {
|
||||
const numericId = toString(id).padStart(6, '0')
|
||||
return `FS${numericId}`
|
||||
}
|
||||
|
||||
export const mapUserStatusText = (status: number) => {
|
||||
return status === 1 ? '正常' : '禁用'
|
||||
}
|
||||
|
||||
export const mapOrderStatus = (status: number | string): Order['status'] => {
|
||||
const value = typeof status === 'string' ? status : toNumber(status)
|
||||
if (value === 'PENDING' || value === 1) return 'PENDING'
|
||||
if (value === 'PAID' || value === 2) return 'PAID'
|
||||
if (value === 'SHIPPED' || value === 3) return 'SHIPPED'
|
||||
if (value === 'COMPLETED' || value === 4) return 'COMPLETED'
|
||||
if (value === 'CANCELLED' || value === 5) return 'CANCELLED'
|
||||
return 'PENDING'
|
||||
}
|
||||
|
||||
export const mapFlashSaleStatus = (status: number | string): FlashSale['status'] => {
|
||||
const value = typeof status === 'string' ? status : toNumber(status)
|
||||
if (value === 'UPCOMING' || value === 1) return 'UPCOMING'
|
||||
if (value === 'ACTIVE' || value === 2) return 'ACTIVE'
|
||||
if (value === 'ENDED' || value === 3) return 'ENDED'
|
||||
return 'UPCOMING'
|
||||
}
|
||||
|
||||
export const mapProductStatus = (status: number | string, stock = 0): Product['status'] => {
|
||||
const value = typeof status === 'string' ? status : toNumber(status)
|
||||
if (stock <= 0) return 'SOLD_OUT'
|
||||
if (value === 'OFF_SALE' || value === 0) return 'OFF_SALE'
|
||||
return 'ON_SALE'
|
||||
}
|
||||
|
||||
export const normalizeUser = (user: Record<string, any>): User => {
|
||||
const username = toString(user.username)
|
||||
return {
|
||||
id: toNumber(user.id),
|
||||
username,
|
||||
email: toString(user.email),
|
||||
phone: toString(user.phone),
|
||||
avatar: resolveImageUrl(toString(user.avatar, '')),
|
||||
role: toString(user.role).toUpperCase() === 'ADMIN' ? 'ADMIN' : username === 'admin' ? 'ADMIN' : 'USER',
|
||||
status: toNumber(user.status, 1) === 1 ? 'ACTIVE' : 'BANNED',
|
||||
createdAt: toIsoLikeString(user.createdAt),
|
||||
updatedAt: toIsoLikeString(user.updatedAt || user.createdAt),
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeProduct = (product: Record<string, any>): Product => {
|
||||
const stock = toNumber(product.stock)
|
||||
const imageUrl = resolveImageUrl(toString(product.imageUrl, ''))
|
||||
return {
|
||||
id: toNumber(product.id),
|
||||
name: toString(product.name),
|
||||
description: toString(product.description),
|
||||
price: toNumber(product.price),
|
||||
stock,
|
||||
imageUrl,
|
||||
images: imageUrl ? [imageUrl] : [DEFAULT_PRODUCT_IMAGE],
|
||||
category: toString(product.category, '默认分类'),
|
||||
status: mapProductStatus(product.status, stock),
|
||||
sales: toNumber(product.sales),
|
||||
views: toNumber(product.viewCount ?? product.views),
|
||||
createdAt: toIsoLikeString(product.createdAt),
|
||||
updatedAt: toIsoLikeString(product.updatedAt || product.createdAt),
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeFlashSale = (flashSale: Record<string, any>): FlashSale => {
|
||||
const flashStock = toNumber(flashSale.flashStock)
|
||||
const remainingStock = toNumber(flashSale.remainingStock, flashStock)
|
||||
return {
|
||||
id: toNumber(flashSale.id),
|
||||
productId: toNumber(flashSale.productId),
|
||||
productName: toString(flashSale.productName),
|
||||
productImageUrl: resolveImageUrl(toString(flashSale.productImageUrl, '')),
|
||||
originalPrice: toNumber(flashSale.originalPrice),
|
||||
flashPrice: toNumber(flashSale.flashPrice),
|
||||
flashStock,
|
||||
remainingStock,
|
||||
startTime: toIsoLikeString(flashSale.startTime),
|
||||
endTime: toIsoLikeString(flashSale.endTime),
|
||||
status: mapFlashSaleStatus(flashSale.status),
|
||||
limitPerUser: toNumber(flashSale.limitPerUser, 1),
|
||||
description: toString(flashSale.description || flashSale.statusDescription),
|
||||
createdAt: toIsoLikeString(flashSale.createdAt),
|
||||
updatedAt: toIsoLikeString(flashSale.updatedAt || flashSale.createdAt),
|
||||
}
|
||||
}
|
||||
|
||||
const buildOrderAddress = (order: Record<string, any>): OrderAddress | undefined => {
|
||||
const name = toString(order.receiverName)
|
||||
const phone = toString(order.receiverPhone)
|
||||
const address = toString(order.receiverAddress)
|
||||
if (!name && !phone && !address) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
name,
|
||||
phone,
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
address,
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeOrder = (order: Record<string, any>): Order => {
|
||||
const totalAmount = toNumber(order.totalAmount ?? order.totalPrice)
|
||||
const quantity = toNumber(order.quantity, 1)
|
||||
const status = mapOrderStatus(order.status)
|
||||
const createdAt = toIsoLikeString(order.createdAt)
|
||||
const updatedAt = toIsoLikeString(order.updatedAt || order.createdAt)
|
||||
const productImage = resolveImageUrl(toString(order.productImageUrl, ''))
|
||||
|
||||
const fallbackItem = {
|
||||
id: toNumber(order.productId || order.id),
|
||||
productId: toNumber(order.productId),
|
||||
productName: toString(order.productName, '未知商品'),
|
||||
productImage,
|
||||
price: quantity > 0 ? Number((totalAmount / quantity).toFixed(2)) : totalAmount,
|
||||
quantity,
|
||||
subtotal: totalAmount,
|
||||
}
|
||||
|
||||
const items = Array.isArray(order.items) && order.items.length > 0
|
||||
? order.items.map((item: Record<string, any>) => ({
|
||||
id: toNumber(item.id || item.productId),
|
||||
productId: toNumber(item.productId),
|
||||
productName: toString(item.productName, '未知商品'),
|
||||
productImage: resolveImageUrl(toString(item.productImageUrl || item.productImage, '')),
|
||||
price: toNumber(item.price),
|
||||
quantity: toNumber(item.quantity, 1),
|
||||
subtotal: toNumber(item.subtotal ?? item.price),
|
||||
}))
|
||||
: [fallbackItem]
|
||||
|
||||
return {
|
||||
id: toNumber(order.id),
|
||||
orderNo: toString(order.orderNo, buildOrderNo(order.id)),
|
||||
userId: toNumber(order.userId),
|
||||
username: toString(order.username),
|
||||
totalAmount,
|
||||
paymentAmount: totalAmount,
|
||||
paymentMethod: toString(order.paymentMethod) || (status === 'PENDING' ? undefined : 'ONLINE'),
|
||||
status,
|
||||
items,
|
||||
address: buildOrderAddress(order),
|
||||
remark: toString(order.remark),
|
||||
createdAt,
|
||||
updatedAt,
|
||||
paidAt: order.paidAt ? toIsoLikeString(order.paidAt) : (status === 'PAID' || status === 'SHIPPED' || status === 'COMPLETED' ? updatedAt : undefined),
|
||||
shippedAt: order.shippedAt ? toIsoLikeString(order.shippedAt) : (status === 'SHIPPED' || status === 'COMPLETED' ? updatedAt : undefined),
|
||||
completedAt: order.completedAt ? toIsoLikeString(order.completedAt) : (status === 'COMPLETED' ? updatedAt : undefined),
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeCartItems = (cart: Record<string, any> | undefined): CartItem[] => {
|
||||
const items = Array.isArray(cart?.items) ? cart.items : []
|
||||
return items.map((item: Record<string, any>) => ({
|
||||
id: toString(item.productId),
|
||||
productId: toNumber(item.productId),
|
||||
productName: toString(item.productName),
|
||||
productImage: resolveImageUrl(toString(item.productImageUrl || item.productImage, '')),
|
||||
price: toNumber(item.productPrice),
|
||||
quantity: toNumber(item.quantity, 1),
|
||||
stock: toNumber(item.stock),
|
||||
selected: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
}))
|
||||
}
|
||||
|
||||
export const normalizePage = <T>(payload: Record<string, any>, mapper: (item: Record<string, any>) => T): PageResponse<T> => {
|
||||
const content = Array.isArray(payload.content) ? payload.content.map((item: Record<string, any>) => mapper(item)) : []
|
||||
const size = toNumber(payload.size, content.length || 10)
|
||||
const pageNumber = toNumber(payload.currentPage ?? payload.number)
|
||||
const totalElements = toNumber(payload.totalElements, content.length)
|
||||
const totalPages = toNumber(payload.totalPages, size > 0 ? Math.ceil(totalElements / size) : 1)
|
||||
return {
|
||||
content,
|
||||
totalElements,
|
||||
totalPages,
|
||||
size,
|
||||
number: pageNumber,
|
||||
first: pageNumber <= 0,
|
||||
last: totalPages === 0 ? true : pageNumber >= totalPages - 1,
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeAdminRecentOrder = (order: Record<string, any>): AdminRecentOrderRow => ({
|
||||
id: toNumber(order.id),
|
||||
orderNo: buildOrderNo(order.id),
|
||||
username: toString(order.username),
|
||||
productName: toString(order.productName),
|
||||
quantity: toNumber(order.quantity, 1),
|
||||
totalAmount: toNumber(order.totalAmount ?? order.totalPrice),
|
||||
status: mapOrderStatus(order.status),
|
||||
createdAt: toIsoLikeString(order.createdAt),
|
||||
isFlashSale: Boolean(order.isFlashSale),
|
||||
})
|
||||
|
||||
export const normalizeAdminHotProduct = (product: Record<string, any>): AdminHotProductRow => ({
|
||||
id: toNumber(product.id),
|
||||
name: toString(product.name),
|
||||
price: toNumber(product.price),
|
||||
stock: toNumber(product.stock),
|
||||
sales: toNumber(product.sales),
|
||||
})
|
||||
|
||||
export const normalizeAdminUser = (user: Record<string, any>): AdminUserRow => ({
|
||||
id: toNumber(user.id),
|
||||
username: toString(user.username),
|
||||
email: toString(user.email),
|
||||
phone: toString(user.phone),
|
||||
status: toNumber(user.status, 1),
|
||||
statusText: mapUserStatusText(toNumber(user.status, 1)),
|
||||
role: toString(user.role).toUpperCase() === 'ADMIN' || toString(user.username) === 'admin' ? 'ADMIN' : 'USER',
|
||||
isOnline: Boolean(user.isOnline),
|
||||
createdAt: toIsoLikeString(user.createdAt),
|
||||
lastLogin: user.lastLogin ? toIsoLikeString(user.lastLogin) : undefined,
|
||||
})
|
||||
|
||||
export const normalizeAdminOrder = (order: Record<string, any>): AdminOrderRow => ({
|
||||
id: toNumber(order.id),
|
||||
orderNo: buildOrderNo(order.id),
|
||||
username: toString(order.username),
|
||||
productName: toString(order.productName),
|
||||
productId: toNumber(order.productId),
|
||||
quantity: toNumber(order.quantity, 1),
|
||||
totalAmount: toNumber(order.totalAmount),
|
||||
status: mapOrderStatus(order.status),
|
||||
createdAt: toIsoLikeString(order.createdAt),
|
||||
isFlashSale: Boolean(order.isFlashSale),
|
||||
})
|
||||
|
||||
export const normalizeAdminProduct = (product: Record<string, any>): AdminProductRow => ({
|
||||
id: toNumber(product.id),
|
||||
name: toString(product.name),
|
||||
description: toString(product.description),
|
||||
category: toString(product.category, '默认分类'),
|
||||
price: toNumber(product.price),
|
||||
stock: toNumber(product.stock),
|
||||
status: toNumber(product.status, 1),
|
||||
imageUrl: resolveImageUrl(toString(product.imageUrl, '')),
|
||||
createdAt: toIsoLikeString(product.createdAt),
|
||||
updatedAt: product.updatedAt ? toIsoLikeString(product.updatedAt) : undefined,
|
||||
totalSales: toNumber(product.totalSales),
|
||||
totalRevenue: toNumber(product.totalRevenue),
|
||||
viewCount: toNumber(product.viewCount),
|
||||
rating: toNumber(product.rating),
|
||||
})
|
||||
Reference in New Issue
Block a user