feat: 前端基础设施更新 - API模块、路由、状态管理和工具类

- 新增 address/admin/favorite/review API 模块
- 更新已有 API 模块适配后端接口变更
- 新增 admin 类型定义和工具函数
- 添加静态资源文件
- 更新路由配置和守卫逻辑
- 更新 Vite 配置和依赖锁文件
This commit is contained in:
2026-03-10 23:21:17 +08:00
parent 9f1c5f837e
commit abba469a20
19 changed files with 1202 additions and 77 deletions

View File

@@ -1464,6 +1464,7 @@
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/lodash": "*"
}
@@ -1474,6 +1475,7 @@
"integrity": "sha512-iAFpG6DokED3roLSP0K+ybeDdIX6Bc0Vd3mLW5uDqThPWtNos3E+EqOM11mPQHKzfWHqEBuLjIlsBQQ8CsISmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -1533,6 +1535,7 @@
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "6.21.0",
"@typescript-eslint/types": "6.21.0",
@@ -1877,6 +1880,7 @@
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.18.tgz",
"integrity": "sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.18",
"@vue/shared": "3.5.18"
@@ -1955,6 +1959,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2200,6 +2205,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001726",
"electron-to-chromium": "^1.5.173",
@@ -2551,6 +2557,7 @@
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": "2.3.0",
"zrender": "5.6.1"
@@ -2770,6 +2777,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -2826,6 +2834,7 @@
"integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -2870,6 +2879,7 @@
"integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"globals": "^13.24.0",
@@ -3744,13 +3754,15 @@
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash-unified": {
"version": "1.0.3",
@@ -4228,6 +4240,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -4374,6 +4387,7 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -4593,6 +4607,7 @@
"integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
@@ -5090,6 +5105,7 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -5159,6 +5175,7 @@
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@@ -5218,6 +5235,7 @@
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.18.tgz",
"integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.18",
"@vue/compiler-sfc": "3.5.18",

View File

@@ -0,0 +1,52 @@
import { request } from '../request'
import type { ApiResponse } from '@/types/api'
export interface AddressItem {
id: number
userId?: number
name: string
phone: string
province: string
city: string
district: string
address: string
isDefault: boolean
createdAt?: string
updatedAt?: string
}
export interface SaveAddressParams {
name: string
phone: string
province: string
city: string
district: string
address: string
isDefault?: boolean
}
export const addressApi = {
getList(): Promise<ApiResponse<AddressItem[]>> {
return request.get('/api/address')
},
getDefault(): Promise<ApiResponse<AddressItem>> {
return request.get('/api/address/default')
},
create(data: SaveAddressParams): Promise<ApiResponse<AddressItem>> {
return request.post('/api/address', data)
},
update(id: number, data: SaveAddressParams): Promise<ApiResponse<AddressItem>> {
return request.put(`/api/address/${id}`, data)
},
setDefault(id: number): Promise<ApiResponse<AddressItem>> {
return request.post(`/api/address/${id}/default`)
},
delete(id: number): Promise<ApiResponse> {
return request.delete(`/api/address/${id}`)
},
}

View File

@@ -0,0 +1,138 @@
import { request } from '../request'
import type { ApiResponse } from '@/types/api'
import type {
AdminDashboardStats,
AdminFavoriteRow,
AdminFavoriteStats,
AdminFlashSaleStats,
AdminHotProductRow,
AdminOrderRow,
AdminOrderStats,
AdminProductRow,
AdminProductStats,
AdminRecentOrderRow,
AdminReviewRow,
AdminReviewStats,
AdminUserRow,
AdminUserStats,
MonitorSystemStatus,
RedisNodeStatus,
} from '@/types/admin'
import {
normalizeAdminHotProduct,
normalizeAdminOrder,
normalizeAdminProduct,
normalizeAdminRecentOrder,
normalizeAdminUser,
} from '@/utils/normalizers'
export const adminApi = {
getDashboardStats(): Promise<ApiResponse<AdminDashboardStats>> {
return request.get('/api/admin/dashboard/stats')
},
getUserStats(): Promise<ApiResponse<AdminUserStats>> {
return request.get('/api/admin/users/stats')
},
getOrderStats(): Promise<ApiResponse<AdminOrderStats>> {
return request.get('/api/admin/orders/stats')
},
getProductStats(): Promise<ApiResponse<AdminProductStats>> {
return request.get('/api/admin/products/stats')
},
getFlashSaleStats(): Promise<ApiResponse<AdminFlashSaleStats>> {
return request.get('/api/admin/flashsales/stats')
},
getRecentOrders(limit = 10): Promise<ApiResponse<AdminRecentOrderRow[]>> {
return request.get<ApiResponse<any[]>>('/api/admin/orders/recent', { limit }).then((res) => ({
...res,
data: Array.isArray(res.data) ? res.data.map((item) => normalizeAdminRecentOrder(item)) : [],
}))
},
getHotProducts(limit = 5): Promise<ApiResponse<AdminHotProductRow[]>> {
return request.get<ApiResponse<any[]>>('/api/admin/products/hot', { limit }).then((res) => ({
...res,
data: Array.isArray(res.data) ? res.data.map((item) => normalizeAdminHotProduct(item)) : [],
}))
},
getUsers(params: { page: number; size: number; keyword?: string; status?: number | '' }): Promise<ApiResponse<{ users: AdminUserRow[]; total: number; totalPages: number; currentPage: number; size: number }>> {
const query = { page: params.page, size: params.size, keyword: params.keyword, status: params.status === '' ? undefined : params.status }
return request.get<ApiResponse<Record<string, any>>>('/api/admin/users', query).then((res) => ({
...res,
data: {
users: Array.isArray(res.data.users) ? res.data.users.map((item) => normalizeAdminUser(item)) : [],
total: Number(res.data.total || 0),
totalPages: Number(res.data.totalPages || 0),
currentPage: Number(res.data.currentPage || params.page),
size: Number(res.data.size || params.size),
},
}))
},
getOrders(params: { page: number; size: number; keyword?: string; status?: string | '' }): Promise<ApiResponse<{ orders: AdminOrderRow[]; total: number; totalPages: number; currentPage: number; size: number }>> {
const query = { page: params.page, size: params.size, keyword: params.keyword, status: params.status === '' ? undefined : params.status }
return request.get<ApiResponse<Record<string, any>>>('/api/admin/orders', query).then((res) => ({
...res,
data: {
orders: Array.isArray(res.data.orders) ? res.data.orders.map((item) => normalizeAdminOrder(item)) : [],
total: Number(res.data.total || 0),
totalPages: Number(res.data.totalPages || 0),
currentPage: Number(res.data.currentPage || params.page),
size: Number(res.data.size || params.size),
},
}))
},
getProducts(params: { page: number; size: number; keyword?: string; category?: string; status?: number | '' }): Promise<ApiResponse<{ products: AdminProductRow[]; total: number; totalPages: number; currentPage: number; size: number }>> {
const query = { page: params.page, size: params.size, keyword: params.keyword, category: params.category, status: params.status === '' ? undefined : params.status }
return request.get<ApiResponse<Record<string, any>>>('/api/admin/products', query).then((res) => ({
...res,
data: {
products: Array.isArray(res.data.products) ? res.data.products.map((item) => normalizeAdminProduct(item)) : [],
total: Number(res.data.total || 0),
totalPages: Number(res.data.totalPages || 0),
currentPage: Number(res.data.currentPage || params.page),
size: Number(res.data.size || params.size),
},
}))
},
getSystemStatus(): Promise<ApiResponse<MonitorSystemStatus>> { return request.get('/api/admin/monitor/system') },
getRedisStatus(): Promise<ApiResponse<RedisNodeStatus[]>> { return request.get('/api/admin/monitor/redis') },
getProduct(id: number): Promise<ApiResponse<AdminProductRow>> {
return request.get<ApiResponse<any>>(`/api/admin/products/${id}`).then((res) => ({ ...res, data: normalizeAdminProduct(res.data) }))
},
createProduct(data: Record<string, unknown>): Promise<ApiResponse<AdminProductRow>> {
return request.post<ApiResponse<any>>('/api/admin/products', data).then((res) => ({ ...res, data: normalizeAdminProduct(res.data) }))
},
updateProduct(id: number, data: Record<string, unknown>): Promise<ApiResponse<AdminProductRow>> {
return request.put<ApiResponse<any>>(`/api/admin/products/${id}`, data).then((res) => ({ ...res, data: normalizeAdminProduct(res.data) }))
},
deleteProduct(id: number): Promise<ApiResponse> { return request.delete(`/api/admin/products/${id}`) },
getReviewStats(): Promise<ApiResponse<AdminReviewStats>> { return request.get('/api/admin/reviews/stats') },
getFavoriteStats(): Promise<ApiResponse<AdminFavoriteStats>> { return request.get('/api/admin/favorites/stats') },
getReviews(params: { page: number; size: number; keyword?: string }): Promise<ApiResponse<{ reviews: AdminReviewRow[]; total: number; totalPages: number; currentPage: number; size: number }>> {
return request.get<ApiResponse<Record<string, any>>>('/api/admin/reviews', params).then((res) => ({
...res,
data: {
reviews: Array.isArray(res.data.reviews) ? (res.data.reviews as AdminReviewRow[]) : [],
total: Number(res.data.total || 0),
totalPages: Number(res.data.totalPages || 0),
currentPage: Number(res.data.currentPage || params.page),
size: Number(res.data.size || params.size),
},
}))
},
updateReview(id: number, data: { status?: number; adminReply?: string }): Promise<ApiResponse> { return request.put(`/api/admin/reviews/${id}`, data) },
deleteReview(id: number): Promise<ApiResponse> { return request.delete(`/api/admin/reviews/${id}`) },
getFavorites(params: { page: number; size: number; keyword?: string }): Promise<ApiResponse<{ favorites: AdminFavoriteRow[]; total: number; totalPages: number; currentPage: number; size: number }>> {
return request.get<ApiResponse<Record<string, any>>>('/api/admin/favorites', params).then((res) => ({
...res,
data: {
favorites: Array.isArray(res.data.favorites) ? (res.data.favorites as AdminFavoriteRow[]) : [],
total: Number(res.data.total || 0),
totalPages: Number(res.data.totalPages || 0),
currentPage: Number(res.data.currentPage || params.page),
size: Number(res.data.size || params.size),
},
}))
},
deleteFavorite(id: number): Promise<ApiResponse> { return request.delete(`/api/admin/favorites/${id}`) },
migrateLegacyOrderItems(): Promise<ApiResponse<{ totalOrders: number; migrated: number; skipped: number }>> { return request.post('/api/admin/orders/migrate-items') },
}

View File

@@ -1,10 +1,14 @@
import { request } from '../request'
import type { ApiResponse, CartItem } from '@/types/api'
import { normalizeCartItems, normalizeOrder } from '@/utils/normalizers'
export const cartApi = {
// 获取购物车
getCart(): Promise<ApiResponse<CartItem[]>> {
return request.get('/api/cart')
return request.get<ApiResponse<any>>('/api/cart').then((res) => ({
...res,
data: normalizeCartItems(res.data),
}))
},
// 添加到购物车
@@ -17,17 +21,17 @@ export const cartApi = {
// 更新数量
updateQuantity(itemId: string, quantity: number): Promise<ApiResponse> {
return request.put(`/api/cart/item/${itemId}`, { quantity })
return request.put('/api/cart/update', { productId: Number(itemId), quantity })
},
// 删除商品
removeItem(itemId: string): Promise<ApiResponse> {
return request.delete(`/api/cart/item/${itemId}`)
return request.delete('/api/cart/remove', { productId: Number(itemId) })
},
// 批量删除
batchRemove(ids: string[]): Promise<ApiResponse> {
return request.post('/api/cart/batch-remove', { ids })
return request.delete('/api/cart/batch-remove', { productIds: ids.map(Number) })
},
// 清空购物车
@@ -39,4 +43,13 @@ export const cartApi = {
getCount(): Promise<ApiResponse<{ count: number }>> {
return request.get('/api/cart/count')
},
}
checkout(ids?: string[]): Promise<ApiResponse<any>> {
return request.post<ApiResponse<any>>('/api/cart/checkout', {
productIds: ids?.map(Number),
}).then((res) => ({
...res,
data: normalizeOrder(res.data),
}))
},
}

View File

@@ -0,0 +1,31 @@
import { request } from '../request'
import type { ApiResponse } from '@/types/api'
export interface FavoriteItem {
id: number
userId: number
productId: number
productName: string
productImageUrl: string
productPrice: number
productCategory: string
createdAt: string
}
export const favoriteApi = {
getList(): Promise<ApiResponse<FavoriteItem[]>> {
return request.get('/api/favorite')
},
getCount(): Promise<ApiResponse<{ count: number }>> {
return request.get('/api/favorite/count')
},
check(productId: number): Promise<ApiResponse<{ favorited: boolean }>> {
return request.get('/api/favorite/check', { productId })
},
toggle(productId: number): Promise<ApiResponse<{ favorited: boolean }>> {
return request.post('/api/favorite/toggle', { productId })
},
}

View File

@@ -1,20 +1,51 @@
import { request } from '../request'
import type { ApiResponse, FlashSale, PageParams, PageResponse } from '@/types/api'
import { mapOrderStatus, normalizeFlashSale, normalizePage } from '@/utils/normalizers'
const flashSaleStatusToCode = (status?: string) => {
if (status === 'UPCOMING') return 1
if (status === 'ACTIVE') return 2
if (status === 'ENDED') return 3
return undefined
}
const flashSaleSortField = (sort?: string) => {
if (sort === 'flashPrice') return 'flashPrice'
if (sort === 'endTime') return 'endTime'
return 'startTime'
}
export const flashsaleApi = {
// 获取秒杀活动列表
getList(params?: PageParams & { status?: string }): Promise<ApiResponse<PageResponse<FlashSale>>> {
return request.get('/api/flashsale/list', params)
return request.post<ApiResponse<Record<string, any>>>('/api/flashsale/list', {
status: flashSaleStatusToCode(params?.status),
page: params?.page ?? 0,
size: params?.size ?? 10,
sortBy: flashSaleSortField(params?.sort),
sortDirection: params?.order || 'asc',
}).then((res) => ({
...res,
data: normalizePage(res.data, normalizeFlashSale),
}))
},
// 获取正在进行的秒杀活动
getActive(limit?: number): Promise<ApiResponse<FlashSale[]>> {
return request.get('/api/flashsale/active', { limit })
return request.get<ApiResponse<any[]>>('/api/flashsale/active').then((res) => ({
...res,
data: (Array.isArray(res.data) ? res.data : [])
.map((item) => normalizeFlashSale(item))
.slice(0, limit ?? Number.MAX_SAFE_INTEGER),
}))
},
// 获取秒杀活动详情
getDetail(id: number): Promise<ApiResponse<FlashSale>> {
return request.get(`/api/flashsale/${id}`)
return request.get<ApiResponse<any>>(`/api/flashsale/${id}`).then((res) => ({
...res,
data: normalizeFlashSale(res.data),
}))
},
// 参与秒杀
@@ -23,12 +54,33 @@ export const flashsaleApi = {
quantity: number;
timestamp?: number;
}): Promise<ApiResponse<{ orderId: number }>> {
return request.post('/api/flashsale/participate', data)
return request.post<ApiResponse<any>>('/api/flashsale/participate', data).then((res) => ({
...res,
data: {
orderId: Number(res.data?.orderId || res.data?.id || 0),
},
}))
},
// 获取用户参与记录
getUserRecords(): Promise<ApiResponse<any[]>> {
return request.get('/api/flashsale/user-records')
return request.post<ApiResponse<Record<string, any>>>('/api/order/my-orders', {
orderType: 2,
page: 0,
size: 100,
sortBy: 'createdAt',
sortDirection: 'desc',
}).then((res) => {
const content = Array.isArray(res.data?.content) ? res.data.content : []
return {
...res,
data: content.map((item: Record<string, any>) => ({
id: item.id,
success: mapOrderStatus(item.status) !== 'CANCELLED',
status: item.status,
})),
}
})
},
// 检查用户是否可以参与
@@ -37,6 +89,70 @@ export const flashsaleApi = {
reason?: string;
remainingQuota?: number;
}>> {
return request.get(`/api/flashsale/${flashSaleId}/check-eligibility`)
return this.getDetail(flashSaleId).then((res) => {
const eligible = res.data.status === 'ACTIVE' && res.data.remainingStock > 0
return {
code: 0,
success: true,
message: '检查成功',
data: {
eligible,
reason: eligible ? '' : '活动未开始、已结束或库存不足',
remainingQuota: res.data.limitPerUser,
},
}
})
},
}
create(data: {
productId: number
flashPrice: number
flashStock: number
startTime: string
endTime: string
}): Promise<ApiResponse<FlashSale>> {
return request.post<ApiResponse<any>>('/api/flashsale/create', data).then((res) => ({
...res,
data: normalizeFlashSale(res.data),
}))
},
update(id: number, data: Record<string, unknown>): Promise<ApiResponse<FlashSale>> {
return request.put<ApiResponse<any>>(`/api/flashsale/${id}`, data).then((res) => ({
...res,
data: normalizeFlashSale(res.data),
}))
},
delete(id: number): Promise<ApiResponse> {
return request.delete(`/api/flashsale/${id}`)
},
publish(id: number): Promise<ApiResponse<FlashSale>> {
return request.post<ApiResponse<any>>(`/api/flashsale/${id}/publish`).then((res) => ({
...res,
data: normalizeFlashSale(res.data),
}))
},
pause(id: number): Promise<ApiResponse<FlashSale>> {
return request.post<ApiResponse<any>>(`/api/flashsale/${id}/pause`).then((res) => ({
...res,
data: normalizeFlashSale(res.data),
}))
},
resume(id: number): Promise<ApiResponse<FlashSale>> {
return request.post<ApiResponse<any>>(`/api/flashsale/${id}/resume`).then((res) => ({
...res,
data: normalizeFlashSale(res.data),
}))
},
end(id: number): Promise<ApiResponse<FlashSale>> {
return request.post<ApiResponse<any>>(`/api/flashsale/${id}/end`).then((res) => ({
...res,
data: normalizeFlashSale(res.data),
}))
},
}

View File

@@ -1,57 +1,138 @@
import { request } from '../request'
import type { ApiResponse, Order, PageParams, PageResponse } from '@/types/api'
import { normalizeOrder } from '@/utils/normalizers'
const orderStatusToCode = (status?: string) => {
if (status === 'PENDING') return 1
if (status === 'PAID') return 2
if (status === 'SHIPPED') return 3
if (status === 'COMPLETED') return 4
if (status === 'CANCELLED') return 5
return undefined
}
const aggregateOrders = (rawOrders: Array<Record<string, any>>): Order[] => {
const groups = new Map<string, Array<Record<string, any>>>()
rawOrders.forEach((item) => {
const key = item.groupNo || item.orderNo || String(item.id)
if (!groups.has(key)) {
groups.set(key, [])
}
groups.get(key)!.push(item)
})
return Array.from(groups.values()).map((group) => {
const [anchor] = group
const normalizedAnchor = normalizeOrder(anchor)
const items = group.flatMap((item) => normalizeOrder(item).items)
const totalAmount = items.reduce((sum, item) => sum + item.subtotal, 0)
return {
...normalizedAnchor,
orderNo: anchor.groupNo || normalizedAnchor.orderNo,
totalAmount,
paymentAmount: totalAmount,
items,
}
})
}
export const orderApi = {
// 创建订单
create(data: {
items: Array<{ productId: number; quantity: number }>
addressId?: number
remark?: string
}): Promise<ApiResponse<Order>> {
return request.post('/api/order/create', data)
const [firstItem] = data.items
return request.post<ApiResponse<any>>('/api/order/create', {
productId: firstItem.productId,
quantity: firstItem.quantity,
remark: data.remark,
}).then((res) => ({
...res,
data: normalizeOrder(res.data),
}))
},
// 获取订单列表
getList(params?: PageParams & {
status?: string
}): Promise<ApiResponse<PageResponse<Order>>> {
return request.get('/api/order/list', params)
getList(params?: PageParams & { status?: string }): Promise<ApiResponse<PageResponse<Order>>> {
return request.post<ApiResponse<Record<string, any>>>('/api/order/my-orders', {
status: orderStatusToCode(params?.status),
page: params?.page ?? 0,
size: params?.size ?? 10,
sortBy: params?.sort || 'createdAt',
sortDirection: params?.order || 'desc',
}).then((res) => {
const rawContent = Array.isArray(res.data.content) ? res.data.content : []
const content = aggregateOrders(rawContent)
return {
...res,
data: {
content,
totalElements: Number(res.data.totalElements || content.length),
totalPages: Number(res.data.totalPages || 1),
size: res.data.size || content.length,
number: res.data.currentPage || 0,
first: true,
last: true,
},
}
})
},
// 获取订单详情
getDetail(id: number): Promise<ApiResponse<Order>> {
return request.get(`/api/order/${id}`)
return request.get<ApiResponse<any>>(`/api/order/${id}`).then(async (res) => {
if (res.data.groupNo) {
const groupRes = await request.get<ApiResponse<any[]>>(`/api/order/group/${res.data.groupNo}`)
return {
...res,
data: aggregateOrders(groupRes.data)[0],
}
}
return {
...res,
data: normalizeOrder(res.data),
}
})
},
// 取消订单
cancel(id: number): Promise<ApiResponse> {
return request.post(`/api/order/${id}/cancel`)
},
// 支付订单
pay(id: number, paymentMethod: string): Promise<ApiResponse> {
return request.post(`/api/order/${id}/pay`, { paymentMethod })
},
// 确认收货
ship(id: number): Promise<ApiResponse> {
return request.post(`/api/order/${id}/ship`)
},
updateStatus(id: number, status: number, remark?: string): Promise<ApiResponse> {
return request.put('/api/order/status', { orderId: id, status, remark })
},
confirm(id: number): Promise<ApiResponse> {
return request.post(`/api/order/${id}/confirm`)
},
// 删除订单
delete(id: number): Promise<ApiResponse> {
return request.delete(`/api/order/${id}`)
},
// 获取订单统计
getStatistics(): Promise<ApiResponse<{
total: number
pending: number
paid: number
shipped: number
completed: number
cancelled: number
}>> {
return request.get('/api/order/statistics')
getStatistics(): Promise<ApiResponse<{ total: number; pending: number; paid: number; shipped: number; completed: number; cancelled: number }>> {
return request.get<ApiResponse<any>>('/api/order/statistics').then((res) => ({
...res,
data: {
total: Number(res.data.totalOrders || 0),
pending: Number(res.data.pendingPaymentOrders || 0),
paid: Number(res.data.paidOrders || 0),
shipped: Number(res.data.shippedOrders || 0),
completed: Number(res.data.completedOrders || 0),
cancelled: Number(res.data.cancelledOrders || 0),
},
}))
},
}
}

View File

@@ -1,5 +1,6 @@
import { request } from '../request'
import type { ApiResponse, Product, PageParams, PageResponse } from '@/types/api'
import { normalizePage, normalizeProduct } from '@/utils/normalizers'
export const productApi = {
// 获取商品列表
@@ -9,26 +10,48 @@ export const productApi = {
minPrice?: number;
maxPrice?: number;
}): Promise<ApiResponse<PageResponse<Product>>> {
return request.get('/api/product/list', params)
return request.get<ApiResponse<Record<string, any>>>('/api/product/list', {
page: params?.page ?? 0,
size: params?.size ?? 10,
keyword: params?.keyword,
category: params?.category,
minPrice: params?.minPrice,
maxPrice: params?.maxPrice,
sortBy: params?.sort || 'id',
sortDirection: params?.order || 'desc',
status: 1,
}).then((res) => ({
...res,
data: normalizePage(res.data, normalizeProduct),
}))
},
// 获取热门商品
getHot(limit = 8): Promise<ApiResponse<Product[]>> {
return request.get('/api/product/hot', { limit })
return request.get<ApiResponse<any[]>>('/api/product/hot', { limit }).then((res) => ({
...res,
data: Array.isArray(res.data) ? res.data.map((item) => normalizeProduct(item)) : [],
}))
},
// 获取商品详情
getDetail(id: number): Promise<ApiResponse<Product>> {
return request.get(`/api/product/${id}`)
return request.get<ApiResponse<any>>(`/api/product/${id}`).then((res) => ({
...res,
data: normalizeProduct(res.data),
}))
},
// 搜索商品
search(keyword: string): Promise<ApiResponse<Product[]>> {
return request.get('/api/product/search', { keyword })
return this.getList({ keyword, page: 0, size: 50 }).then((res) => ({
...res,
data: res.data.content,
}))
},
// 获取商品分类
getCategories(): Promise<ApiResponse<string[]>> {
return request.get('/api/product/categories')
},
}
}

View File

@@ -0,0 +1,30 @@
import { request } from '../request'
import type { ApiResponse } from '@/types/api'
export interface ReviewItem {
id: number
productId: number
userId: number
orderId: number
username: string
rating: number
content: string
createdAt: string
updatedAt?: string
}
export interface ReviewSummary {
averageRating: number
totalReviews: number
reviews: ReviewItem[]
}
export const reviewApi = {
getProductReviews(productId: number): Promise<ApiResponse<ReviewSummary>> {
return request.get(`/api/review/product/${productId}`)
},
create(data: { orderId: number; productId: number; rating: number; content: string }): Promise<ApiResponse<ReviewItem>> {
return request.post('/api/review', data)
},
}

View File

@@ -1,34 +1,64 @@
import { request } from '../request'
import type { ApiResponse, User, LoginParams, RegisterParams } from '@/types/api'
import { normalizeUser } from '@/utils/normalizers'
export const userApi = {
// 登录
login(params: LoginParams): Promise<ApiResponse<{ token: string; user: User }>> {
return request.post('/api/auth/login', params)
return request.post<ApiResponse<{ token: string; user: any }>>('/api/user/login', params).then((res) => ({
...res,
data: {
token: res.data.token,
user: normalizeUser(res.data.user),
},
}))
},
// 注册
register(params: RegisterParams): Promise<ApiResponse<User>> {
return request.post('/api/auth/register', params)
return request.post<ApiResponse<any>>('/api/user/register', {
...params,
confirmPassword: params.password,
}).then((res) => ({
...res,
data: normalizeUser(res.data),
}))
},
// 退出登录
logout(): Promise<ApiResponse> {
return request.post('/api/auth/logout')
return request.post('/api/user/logout')
},
// 获取用户信息
getInfo(): Promise<ApiResponse<User>> {
return request.get('/api/user/info')
return request.get<ApiResponse<any>>('/api/user/current').then((res) => ({
...res,
data: normalizeUser(res.data),
}))
},
// 更新用户信息
updateInfo(data: Partial<User>): Promise<ApiResponse<User>> {
return request.put('/api/user/info', data)
return request.put<ApiResponse<any>>('/api/user/update', {
email: data.email,
phone: data.phone,
avatar: data.avatar,
}).then((res) => ({
...res,
data: normalizeUser(res.data),
}))
},
// 修改密码
changePassword(data: { oldPassword: string; newPassword: string }): Promise<ApiResponse> {
return request.post('/api/user/change-password', data)
changePassword(data: { oldPassword: string; newPassword: string; confirmPassword?: string }): Promise<ApiResponse> {
return request.post('/api/user/change-password', {
...data,
confirmPassword: data.confirmPassword || data.newPassword,
})
},
}
getProfileStats(): Promise<ApiResponse<{ totalOrders: number; totalAmount: number; flashSaleSuccess: number; favoriteCount: number }>> {
return request.get('/api/user/profile-stats')
},
}

View File

@@ -1,4 +1,9 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import axios, {
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useUserStore } from '@/stores/user'
import router from '@/router'
@@ -14,7 +19,7 @@ const service: AxiosInstance = axios.create({
// 请求拦截器
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
(config: InternalAxiosRequestConfig) => {
const userStore = useUserStore()
// 添加token
@@ -35,6 +40,20 @@ service.interceptors.request.use(
service.interceptors.response.use(
(response: AxiosResponse) => {
const res = response.data
if (typeof res?.success === 'boolean') {
if (!res.success) {
if (response.status === 401) {
ElMessage.error(res.message || '登录状态已失效')
} else {
ElMessage.error(res.message || '请求失败')
}
return Promise.reject(new Error(res.message || '请求失败'))
}
return res
}
// 自定义状态码处理
if (res.code !== 200 && res.code !== 0) {
@@ -110,9 +129,12 @@ export const request = {
return service.put(url, data)
},
delete<T = any>(url: string, params?: any): Promise<T> {
return service.delete(url, { params })
delete<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return service.delete(url, {
...config,
data,
})
},
}
export default service
export default service

View File

@@ -0,0 +1,7 @@
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" fill="#f8f9fa" stroke="#dee2e6" stroke-width="1"/>
<text x="50" y="35" font-family="Arial, sans-serif" font-size="12" fill="#6c757d" text-anchor="middle">商品</text>
<text x="50" y="50" font-family="Arial, sans-serif" font-size="12" fill="#6c757d" text-anchor="middle">图片</text>
<text x="50" y="70" font-family="Arial, sans-serif" font-size="10" fill="#adb5bd" text-anchor="middle">暂无图片
</text>
</svg>

After

Width:  |  Height:  |  Size: 533 B

View File

@@ -6,6 +6,10 @@ export function setupGuards(router: Router) {
// 路由前置守卫
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
if (userStore.token && !userStore.user) {
await userStore.getUserInfo()
}
// 设置页面标题
document.title = `${to.meta.title || '秒杀系统'} - 高并发电商抢购平台`
@@ -21,7 +25,7 @@ export function setupGuards(router: Router) {
}
// 需要管理员权限的页面
if (to.meta.requiresAdmin && userStore.user?.role !== 'ADMIN') {
if (to.meta.requiresAdmin && !userStore.isAdmin) {
ElMessage.error('无权访问')
next('/')
return
@@ -41,4 +45,4 @@ export function setupGuards(router: Router) {
// 页面切换后滚动到顶部
window.scrollTo(0, 0)
})
}
}

View File

@@ -20,6 +20,10 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/pages/flashsale/index.vue'),
meta: { title: '秒杀活动' }
},
{
path: 'flashsales',
redirect: '/flashsale'
},
{
path: 'flashsale/:id',
name: 'FlashSaleDetail',
@@ -32,6 +36,14 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/pages/product/index.vue'),
meta: { title: '商品列表' }
},
{
path: 'search',
redirect: (to) => ({ path: '/products', query: to.query })
},
{
path: 'category/:category',
redirect: (to) => ({ path: '/products', query: { category: String(to.params.category || '') } })
},
{
path: 'product/:id',
name: 'ProductDetail',
@@ -61,6 +73,18 @@ const routes: RouteRecordRaw[] = [
name: 'Profile',
component: () => import('@/pages/user/profile.vue'),
meta: { title: '个人中心', requiresAuth: true }
},
{
path: 'favorites',
name: 'Favorites',
component: () => import('@/pages/user/favorites.vue'),
meta: { title: '我的收藏', requiresAuth: true }
},
{
path: 'addresses',
name: 'Addresses',
component: () => import('@/pages/user/profile.vue'),
meta: { title: '地址管理', requiresAuth: true }
}
]
},
@@ -110,6 +134,24 @@ const routes: RouteRecordRaw[] = [
name: 'AdminUsers',
component: () => import('@/pages/admin/users.vue'),
meta: { title: '用户管理' }
},
{
path: 'reviews',
name: 'AdminReviews',
component: () => import('@/pages/admin/reviews.vue'),
meta: { title: '评价管理' }
},
{
path: 'favorites',
name: 'AdminFavorites',
component: () => import('@/pages/admin/favorites.vue'),
meta: { title: '收藏管理' }
},
{
path: 'monitor',
name: 'AdminMonitor',
component: () => import('@/pages/admin/monitor.vue'),
meta: { title: '系统监控' }
}
]
},
@@ -136,4 +178,4 @@ const router = createRouter({
// 设置路由守卫
setupGuards(router)
export default router
export default router

View File

@@ -12,7 +12,7 @@ export const useUserStore = defineStore('user', () => {
// 计算属性
const isLoggedIn = computed(() => !!token.value)
const isAdmin = computed(() => user.value?.role === 'ADMIN')
const isAdmin = computed(() => user.value?.role === 'ADMIN' || user.value?.username === 'admin')
const username = computed(() => user.value?.username || '')
// 从localStorage恢复登录状态
@@ -74,7 +74,15 @@ export const useUserStore = defineStore('user', () => {
}
// 退出登录
const logout = () => {
const logout = async () => {
try {
if (token.value) {
await userApi.logout()
}
} catch (error) {
console.error('退出登录失败:', error)
}
user.value = null
token.value = ''
@@ -94,11 +102,18 @@ export const useUserStore = defineStore('user', () => {
const res = await userApi.getInfo()
if (res.success) {
user.value = res.data
user.value = {
...res.data,
avatar: res.data.avatar || user.value?.avatar || '',
}
localStorage.setItem('user', JSON.stringify(user.value))
}
} catch (error) {
console.error('获取用户信息失败:', error)
user.value = null
token.value = ''
localStorage.removeItem('token')
localStorage.removeItem('user')
}
}
@@ -125,4 +140,4 @@ export const useUserStore = defineStore('user', () => {
getUserInfo,
updateUserInfo,
}
})
})

View File

@@ -0,0 +1,163 @@
export interface AdminDashboardStats {
totalUsers: number
totalProducts: number
totalOrders: number
totalAmount: number
todayOrders: number
paidOrders: number
pendingOrders: number
activeFlashSales: number
}
export interface AdminUserStats {
totalUsers: number
activeUsers: number
newUsers: number
onlineUsers: number
}
export interface AdminOrderStats {
totalOrders: number
paidOrders: number
pendingOrders: number
completedOrders: number
cancelledOrders: number
totalAmount: number
}
export interface AdminProductStats {
totalProducts: number
activeProducts: number
inactiveProducts: number
lowStockProducts: number
}
export interface AdminFlashSaleStats {
totalFlashSales: number
activeFlashSales: number
upcomingFlashSales: number
endedFlashSales: number
}
export interface AdminRecentOrderRow {
id: number
orderNo: string
username: string
productName: string
quantity: number
totalAmount: number
status: string
createdAt: string
isFlashSale: boolean
}
export interface AdminHotProductRow {
id: number
name: string
price: number
stock: number
sales: number
}
export interface AdminUserRow {
id: number
username: string
email: string
phone: string
status: number
statusText: string
role: 'USER' | 'ADMIN'
isOnline: boolean
createdAt: string
lastLogin?: string
}
export interface AdminOrderRow {
id: number
orderNo: string
username: string
productName: string
productId?: number
quantity: number
totalAmount: number
status: string
createdAt: string
isFlashSale: boolean
}
export interface AdminProductRow {
id: number
name: string
description: string
category: string
price: number
stock: number
status: number
imageUrl: string
createdAt: string
updatedAt?: string
totalSales?: number
totalRevenue?: number
viewCount?: number
rating?: number
}
export interface MonitorSystemStatus {
status: string
cpuUsage: number
memoryUsage: number
diskUsage: number
availableProcessors?: number
totalMemory?: string
usedMemory?: string
dbStatus?: string
redisStatus?: string
requestCountToday?: number
}
export interface RedisNodeStatus {
node: string
status: string
memory: string
connections: number
}
export interface AdminReviewStats {
totalReviews: number
todayReviews: number
averageRating: number
fiveStarReviews: number
}
export interface AdminFavoriteStats {
totalFavorites: number
favoriteUsers: number
favoriteProducts: number
todayFavorites: number
}
export interface AdminReviewRow {
id: number
productId: number
userId: number
orderId: number
productName: string
username: string
rating: number
content: string
status: number
statusText: string
adminReply?: string
repliedAt?: string
createdAt: string
}
export interface AdminFavoriteRow {
id: number
userId: number
productId: number
productName: string
productCategory: string
username: string
createdAt: string
}

View File

@@ -0,0 +1,42 @@
import defaultProductImage from '@/assets/default-product.svg'
export const DEFAULT_PRODUCT_IMAGE = defaultProductImage
const ABSOLUTE_URL_PATTERN = /^(https?:)?\/\//i
const SPECIAL_URL_PATTERN = /^(data:|blob:)/i
const normalizeBaseUrl = (value?: string) => {
if (!value) return ''
return value.endsWith('/') ? value.slice(0, -1) : value
}
export const resolveImageUrl = (value?: string | null) => {
if (!value || !String(value).trim()) {
return DEFAULT_PRODUCT_IMAGE
}
const imageUrl = String(value).trim()
if (ABSOLUTE_URL_PATTERN.test(imageUrl) || SPECIAL_URL_PATTERN.test(imageUrl)) {
return imageUrl
}
const baseUrl = normalizeBaseUrl(import.meta.env.VITE_API_BASE_URL)
if (!baseUrl) {
return imageUrl.startsWith('/') ? imageUrl : `/${imageUrl}`
}
return imageUrl.startsWith('/') ? `${baseUrl}${imageUrl}` : `${baseUrl}/${imageUrl}`
}
export const applyFallbackImage = (event: Event) => {
const target = event.target as HTMLImageElement | null
if (!target) return
if (target.dataset.fallbackApplied === 'true') {
return
}
target.dataset.fallbackApplied = 'true'
target.onerror = null
target.src = DEFAULT_PRODUCT_IMAGE
}

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

View File

@@ -2,7 +2,6 @@ import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
@@ -16,7 +15,19 @@ export default defineConfig({
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api'),
rewrite: (proxyPath) => proxyPath.replace(/^\/api/, '/api'),
},
'/images': {
target: 'http://localhost:8080',
changeOrigin: true,
},
'/uploads': {
target: 'http://localhost:8080',
changeOrigin: true,
},
'/static': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
@@ -24,12 +35,12 @@ export default defineConfig({
rollupOptions: {
output: {
manualChunks: {
'vendor': ['vue', 'vue-router', 'pinia'],
'element': ['element-plus', '@element-plus/icons-vue'],
'utils': ['axios', 'dayjs', '@vueuse/core'],
'charts': ['echarts', 'vue-echarts'],
vendor: ['vue', 'vue-router', 'pinia'],
element: ['element-plus', '@element-plus/icons-vue'],
utils: ['axios', 'dayjs', '@vueuse/core'],
charts: ['echarts', 'vue-echarts'],
},
},
},
},
})
})