feat: 前端基础设施更新 - API模块、路由、状态管理和工具类
- 新增 address/admin/favorite/review API 模块 - 更新已有 API 模块适配后端接口变更 - 新增 admin 类型定义和工具函数 - 添加静态资源文件 - 更新路由配置和守卫逻辑 - 更新 Vite 配置和依赖锁文件
This commit is contained in:
22
flash-sale-frontend/package-lock.json
generated
22
flash-sale-frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
52
flash-sale-frontend/src/api/modules/address.ts
Normal file
52
flash-sale-frontend/src/api/modules/address.ts
Normal 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}`)
|
||||
},
|
||||
}
|
||||
138
flash-sale-frontend/src/api/modules/admin.ts
Normal file
138
flash-sale-frontend/src/api/modules/admin.ts
Normal 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') },
|
||||
}
|
||||
@@ -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),
|
||||
}))
|
||||
},
|
||||
}
|
||||
|
||||
31
flash-sale-frontend/src/api/modules/favorite.ts
Normal file
31
flash-sale-frontend/src/api/modules/favorite.ts
Normal 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 })
|
||||
},
|
||||
}
|
||||
@@ -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),
|
||||
}))
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
30
flash-sale-frontend/src/api/modules/review.ts
Normal file
30
flash-sale-frontend/src/api/modules/review.ts
Normal 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)
|
||||
},
|
||||
}
|
||||
@@ -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')
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
7
flash-sale-frontend/src/assets/default-product.svg
Normal file
7
flash-sale-frontend/src/assets/default-product.svg
Normal 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 |
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
163
flash-sale-frontend/src/types/admin.ts
Normal file
163
flash-sale-frontend/src/types/admin.ts
Normal 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
|
||||
}
|
||||
42
flash-sale-frontend/src/utils/image.ts
Normal file
42
flash-sale-frontend/src/utils/image.ts
Normal 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
|
||||
}
|
||||
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),
|
||||
})
|
||||
@@ -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'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user