Files
FlashSaleSystem/flash-sale-frontend/src/pages/product/index.vue
YoVinchen 989c2741a2 后端功能增强:全局异常处理、API控制器、JSP视图和单元测试
- 添加 GlobalExceptionHandler 全局异常处理
- 添加 ApiController REST API 控制器
- 更新 WebConfig 跨域配置和 ProductRepository 查询方法
- 新增 monitor/product-detail/profile JSP 视图页面
- 添加 FlashSaleServiceTest 秒杀服务单元测试
- 更新 application.yml 配置
2026-03-05 20:30:48 +08:00

227 lines
6.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="products-page">
<div class="container mx-auto px-4 py-8">
<!-- 页面标题 -->
<div class="mb-8">
<h1 class="text-3xl font-bold mb-2 flex items-center">
<el-icon class="text-blue-500 mr-2"><ShoppingBag /></el-icon>
商品列表
</h1>
<p class="text-gray-600">精选好物品质保证</p>
</div>
<!-- 筛选栏 -->
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
<div class="flex flex-wrap gap-4 items-center">
<!-- 分类筛选 -->
<el-select
v-model="filters.category"
placeholder="选择分类"
clearable
style="width: 150px"
@change="loadProducts"
>
<el-option
v-for="cat in categories"
:key="cat"
:label="cat"
:value="cat"
/>
</el-select>
<!-- 价格区间 -->
<div class="flex items-center gap-2">
<el-input-number
v-model="filters.minPrice"
:min="0"
placeholder="最低价"
style="width: 120px"
@change="loadProducts"
/>
<span>-</span>
<el-input-number
v-model="filters.maxPrice"
:min="0"
placeholder="最高价"
style="width: 120px"
@change="loadProducts"
/>
</div>
<!-- 排序 -->
<el-radio-group v-model="filters.sort" @change="loadProducts">
<el-radio-button label="default">默认</el-radio-button>
<el-radio-button label="price-asc">价格升序</el-radio-button>
<el-radio-button label="price-desc">价格降序</el-radio-button>
<el-radio-button label="sales">销量</el-radio-button>
</el-radio-group>
<!-- 搜索 -->
<el-input
v-model="filters.keyword"
placeholder="搜索商品"
style="width: 200px"
clearable
@keyup.enter="loadProducts"
>
<template #suffix>
<el-icon class="cursor-pointer" @click="loadProducts">
<Search />
</el-icon>
</template>
</el-input>
</div>
</div>
<!-- 商品列表 -->
<div v-if="loading" class="text-center py-12">
<el-icon :size="40" class="animate-spin"><Loading /></el-icon>
<p class="mt-2 text-gray-500">加载中...</p>
</div>
<div v-else-if="products.length === 0" class="text-center py-12">
<el-empty description="暂无商品" />
</div>
<div v-else>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<ProductCard
v-for="item in products"
:key="item.id"
:data="item"
@add-to-cart="handleAddToCart"
/>
</div>
<!-- 分页 -->
<div class="mt-8 flex justify-center">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.size"
:total="pagination.total"
:page-sizes="[12, 24, 36, 48]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="loadProducts"
@current-change="loadProducts"
/>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import ProductCard from '@/components/business/ProductCard.vue'
import { productApi } from '@/api/modules/product'
import { useCartStore } from '@/stores/cart'
import { useUserStore } from '@/stores/user'
import type { Product } from '@/types/api'
const route = useRoute()
const router = useRouter()
const cartStore = useCartStore()
const userStore = useUserStore()
// 数据状态
const loading = ref(false)
const products = ref<Product[]>([])
const categories = ref<string[]>([])
// 筛选条件
const filters = reactive({
keyword: '',
category: '',
minPrice: undefined as number | undefined,
maxPrice: undefined as number | undefined,
sort: 'default'
})
// 分页
const pagination = reactive({
page: 1,
size: 12,
total: 0
})
// 加载商品列表
const loadProducts = async () => {
loading.value = true
try {
const params: any = {
page: pagination.page - 1,
size: pagination.size
}
if (filters.keyword) params.keyword = filters.keyword
if (filters.category) params.category = filters.category
if (filters.minPrice !== undefined) params.minPrice = filters.minPrice
if (filters.maxPrice !== undefined) params.maxPrice = filters.maxPrice
// 处理排序
if (filters.sort === 'price-asc') {
params.sort = 'price'
params.order = 'asc'
} else if (filters.sort === 'price-desc') {
params.sort = 'price'
params.order = 'desc'
} else if (filters.sort === 'sales') {
params.sort = 'sales'
params.order = 'desc'
}
const res = await productApi.getList(params)
if (res.success) {
products.value = res.data.content
pagination.total = res.data.totalElements
}
} catch (error) {
console.error('加载商品列表失败:', error)
} finally {
loading.value = false
}
}
// 加载分类
const loadCategories = async () => {
try {
const res = await productApi.getCategories()
if (res.success) {
categories.value = res.data
}
} catch (error) {
console.error('加载分类失败:', error)
}
}
// 添加到购物车
const handleAddToCart = async (productId: number) => {
if (!userStore.isLoggedIn) {
ElMessage.warning('请先登录')
router.push('/login')
return
}
await cartStore.addToCart(productId)
}
onMounted(() => {
// 从路由参数获取搜索关键词
if (route.query.keyword) {
filters.keyword = route.query.keyword as string
}
loadCategories()
loadProducts()
})
</script>
<style scoped lang="scss">
.products-page {
min-height: calc(100vh - 60px);
background-color: #f5f5f5;
}
</style>