Oroqen-Vue/src/views/Products.vue

639 lines
15 KiB
Vue
Raw Normal View History

2025-08-07 21:29:04 +08:00
<template>
<div class="products">
<!-- 页面头部 -->
<section class="page-header">
<div class="container">
<div class="header-content">
<h1 class="page-title">特色产品</h1>
<p class="page-subtitle">精选鄂伦春族传统手工艺品与文创产品</p>
</div>
</div>
</section>
<!-- 产品筛选 -->
<section class="filters-section">
<div class="container">
<div class="filters">
<div class="filter-group">
<label>产品分类</label>
<div class="filter-buttons">
<button
v-for="category in categories"
:key="category.id"
:class="['filter-btn', { active: selectedCategory === category.id }]"
2025-08-10 20:42:49 +08:00
@click="selectedCategory = category.id; onCategoryChange()"
2025-08-07 21:29:04 +08:00
>
{{ category.name }}
</button>
</div>
</div>
<div class="filter-group">
<label>价格范围</label>
<el-select v-model="priceRange" placeholder="选择价格范围" style="width: 200px;">
<el-option label="全部价格" value="all" />
<el-option label="100元以下" value="0-100" />
<el-option label="100-300元" value="100-300" />
<el-option label="300-500元" value="300-500" />
<el-option label="500元以上" value="500+" />
</el-select>
</div>
<div class="filter-group">
<label>排序方式</label>
<el-select v-model="sortBy" placeholder="排序方式" style="width: 150px;">
<el-option label="默认排序" value="default" />
<el-option label="价格从低到高" value="price-asc" />
<el-option label="价格从高到低" value="price-desc" />
<el-option label="最新上架" value="newest" />
</el-select>
</div>
</div>
</div>
</section>
<!-- 产品列表 -->
<section class="products-section section">
<div class="container">
<div class="products-grid">
<div
v-for="product in filteredProducts"
:key="product.id"
class="product-card card"
@click="$router.push(`/product/${product.id}`)"
>
<div class="product-image">
2025-08-10 21:46:20 +08:00
<img
v-if="product.mainImage && getFileAccessHttpUrl(product.mainImage)"
:src="getFileAccessHttpUrl(product.mainImage)"
:alt="product.name"
class="product-cover-image"
@error="handleImageError"
/>
<div v-else class="image-placeholder">
<svg width="80" height="80" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21,15 16,10 5,21"/>
2025-08-07 21:29:04 +08:00
</svg>
</div>
<div class="product-badges">
<span v-if="product.isNew" class="badge new">新品</span>
<span v-if="product.isHot" class="badge hot">热销</span>
</div>
</div>
<div class="product-info">
2025-08-10 20:42:49 +08:00
<h3 class="product-name">{{ product.productName || '产品名称' }}</h3>
<p class="product-category">{{ getCategoryName(product.categoryId) }}</p>
<p class="product-description">{{ product.description || '暂无描述' }}</p>
2025-08-07 21:29:04 +08:00
<div class="product-meta">
2025-08-10 20:42:49 +08:00
<div class="craftsman-info" v-if="product.craftsmanInfo">
2025-08-07 21:29:04 +08:00
<el-icon><User /></el-icon>
2025-08-10 20:42:49 +08:00
<span>{{ product.craftsmanInfo }}</span>
2025-08-07 21:29:04 +08:00
</div>
<div class="stock-info">
<span :class="['stock-status', product.stock > 0 ? 'in-stock' : 'out-stock']">
{{ product.stock > 0 ? `库存${product.stock}` : '暂时缺货' }}
</span>
</div>
</div>
<div class="product-footer">
<div class="price-info">
<span class="current-price">¥{{ product.price }}</span>
<span v-if="product.originalPrice" class="original-price">¥{{ product.originalPrice }}</span>
</div>
<el-button
type="primary"
size="small"
:disabled="product.stock === 0"
@click.stop="addToCart(product)"
>
{{ product.stock > 0 ? '加入购物车' : '缺货' }}
</el-button>
</div>
</div>
</div>
</div>
<!-- 加载更多 -->
<div class="load-more" v-if="hasMore">
<el-button @click="loadMore" :loading="loading">加载更多</el-button>
</div>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { useMainStore } from '../stores'
import { User } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
2025-08-10 20:42:49 +08:00
import { productApi, productCategoryApi } from '../api/product'
import { cartApi } from '../api/cart'
2025-08-07 21:29:04 +08:00
const route = useRoute()
const store = useMainStore()
// 响应式数据
const selectedCategory = ref('all')
const priceRange = ref('all')
const sortBy = ref('default')
const loading = ref(false)
const hasMore = ref(true)
2025-08-10 20:42:49 +08:00
const currentPage = ref(1)
const pageSize = ref(12)
2025-08-07 21:29:04 +08:00
// 产品分类
2025-08-10 20:42:49 +08:00
const categories = ref<any[]>([
{ id: 'all', name: '全部产品' }
2025-08-07 21:29:04 +08:00
])
// 产品数据
2025-08-10 20:42:49 +08:00
const products = ref<any[]>([])
// 获取产品分类
const getCategories = async () => {
try {
const response = await productCategoryApi.getCategoryList({
pageNo: 1,
pageSize: 100
})
if (response && response.records) {
const categoryList = response.records.map((cat: any) => ({
id: cat.id,
name: cat.categoryName
}))
categories.value = [
{ id: 'all', name: '全部产品' },
...categoryList
]
}
} catch (error) {
console.error('获取分类失败:', error)
2025-08-07 21:29:04 +08:00
}
2025-08-10 20:42:49 +08:00
}
// 获取产品列表
const getProducts = async (loadMore = false) => {
try {
loading.value = true
const params: any = {
pageNo: loadMore ? currentPage.value + 1 : 1,
pageSize: pageSize.value,
status: 1 // 只获取上架的产品
}
// 添加分类筛选
if (selectedCategory.value !== 'all') {
params.categoryId = selectedCategory.value
}
const response = await productApi.getProductList(params)
if (response && response.records) {
const newProducts = response.records.map((product: any) => ({
...product,
color: getRandomColor(),
isNew: isNewProduct(product.createTime),
isHot: product.salesCount > 50 // 假设销量大于50为热销
}))
if (loadMore) {
products.value = [...products.value, ...newProducts]
currentPage.value++
} else {
products.value = newProducts
currentPage.value = 1
}
hasMore.value = response.current < response.pages
}
} catch (error) {
console.error('获取产品数据失败:', error)
ElMessage.error('获取产品数据失败,请稍后重试')
if (!loadMore) {
products.value = []
}
} finally {
loading.value = false
}
}
// 判断是否为新产品
const isNewProduct = (createTime: string) => {
if (!createTime) return false
const now = new Date()
const created = new Date(createTime)
const diffDays = (now.getTime() - created.getTime()) / (1000 * 3600 * 24)
return diffDays <= 30 // 30天内为新品
}
// 获取随机颜色
const getRandomColor = () => {
const colors = ['#8b4513', '#2d5016', '#8b4513', '#2d5016']
return colors[Math.floor(Math.random() * colors.length)]
}
2025-08-07 21:29:04 +08:00
// 计算属性
const filteredProducts = computed(() => {
let result = products.value
// 分类筛选
if (selectedCategory.value !== 'all') {
2025-08-10 20:42:49 +08:00
result = result.filter(p => p.categoryId === selectedCategory.value)
2025-08-07 21:29:04 +08:00
}
// 价格筛选
if (priceRange.value !== 'all') {
const [min, max] = priceRange.value.split('-').map(v => v === '+' ? Infinity : parseInt(v))
result = result.filter(p => {
if (max === undefined) return p.price >= min
return p.price >= min && p.price <= max
})
}
// 排序
switch (sortBy.value) {
case 'price-asc':
result.sort((a, b) => a.price - b.price)
break
case 'price-desc':
result.sort((a, b) => b.price - a.price)
break
case 'newest':
result.sort((a, b) => (b.isNew ? 1 : 0) - (a.isNew ? 1 : 0))
break
}
return result
})
// 方法
const getCategoryName = (categoryId: string) => {
const category = categories.value.find(c => c.id === categoryId)
return category ? category.name : ''
}
2025-08-10 20:42:49 +08:00
const addToCart = async (product: any) => {
2025-08-07 21:29:04 +08:00
if (product.stock === 0) {
ElMessage.warning('商品暂时缺货')
return
}
2025-08-10 20:42:49 +08:00
try {
// 这里使用默认用户ID实际应用中应该从用户状态获取
const userId = '1'
await cartApi.addToCart(userId, product.id, 1)
store.addToCart(product)
ElMessage.success('已添加到购物车')
} catch (error) {
console.error('添加到购物车失败:', error)
ElMessage.error('添加到购物车失败,请稍后重试')
}
2025-08-07 21:29:04 +08:00
}
const loadMore = () => {
2025-08-10 20:42:49 +08:00
getProducts(true)
}
// 监听分类变化
const onCategoryChange = () => {
getProducts()
2025-08-07 21:29:04 +08:00
}
2025-08-10 21:46:20 +08:00
// 处理图片路径
const getFileAccessHttpUrl = (path: string) => {
if (!path) return ''
// 如果已经是完整的HTTP URL或blob URL直接返回
if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('blob:')) {
return path
}
// 如果是相对路径转换为静态资源URL
if (path.startsWith('/')) {
return `http://localhost:8080/jeecg-boot/sys/common/static${path}`
}
// 为产品图片添加默认路径
return `http://localhost:8080/jeecg-boot/sys/common/static/${path}`
}
// 图片加载错误处理
const handleImageError = (event: Event) => {
const img = event.target as HTMLImageElement
img.style.display = 'none'
const placeholder = img.nextElementSibling as HTMLElement
if (placeholder) {
placeholder.style.display = 'flex'
}
}
2025-08-07 21:29:04 +08:00
// 生命周期
onMounted(() => {
2025-08-10 20:42:49 +08:00
getCategories()
getProducts()
2025-08-07 21:29:04 +08:00
// 处理搜索参数
const searchQuery = route.query.search as string
if (searchQuery) {
// 这里可以根据搜索关键词筛选产品
console.log('搜索关键词:', searchQuery)
}
})
</script>
<style scoped>
/* 页面头部 */
.page-header {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
padding: 4rem 0 2rem;
text-align: center;
}
.page-title {
font-size: 3rem;
font-weight: bold;
margin-bottom: 1rem;
}
.page-subtitle {
font-size: 1.2rem;
opacity: 0.9;
}
/* 筛选区域 */
.filters-section {
background: var(--background-light);
padding: 2rem 0;
border-bottom: 1px solid var(--border-color);
}
.filters {
display: flex;
flex-wrap: wrap;
gap: 2rem;
align-items: center;
}
.filter-group {
display: flex;
align-items: center;
gap: 1rem;
}
.filter-group label {
font-weight: 500;
color: var(--text-color);
white-space: nowrap;
}
.filter-buttons {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.filter-btn {
padding: 0.5rem 1rem;
border: 1px solid var(--border-color);
background: white;
color: var(--text-color);
border-radius: 20px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.9rem;
}
.filter-btn:hover,
.filter-btn.active {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
/* 产品网格 */
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
}
.product-card {
cursor: pointer;
transition: all 0.3s ease;
overflow: hidden;
}
.product-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.product-image {
position: relative;
width: 100%;
2025-08-10 21:46:20 +08:00
height: 250px;
border-radius: 12px;
overflow: hidden;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.product-cover-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: all 0.3s ease;
}
.product-cover-image:hover {
transform: scale(1.05);
2025-08-07 21:29:04 +08:00
}
.image-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
2025-08-10 21:46:20 +08:00
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border: 2px dashed #dee2e6;
}
.image-placeholder svg {
opacity: 0.6;
transition: opacity 0.3s ease;
}
.product-image:hover .image-placeholder svg {
opacity: 0.8;
2025-08-07 21:29:04 +08:00
}
.product-badges {
position: absolute;
top: 10px;
right: 10px;
display: flex;
flex-direction: column;
gap: 5px;
}
.badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: bold;
color: white;
}
.badge.new {
background: #ff4757;
}
.badge.hot {
background: #ff6b35;
}
.product-info {
padding: 1.5rem;
}
.product-name {
font-size: 1.2rem;
font-weight: bold;
color: var(--primary-color);
margin-bottom: 0.5rem;
}
.product-category {
color: var(--secondary-color);
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.product-description {
color: var(--text-light);
font-size: 0.9rem;
line-height: 1.5;
margin-bottom: 1rem;
}
.product-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.craftsman-info {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-light);
}
.stock-status {
font-size: 0.8rem;
padding: 2px 6px;
border-radius: 4px;
}
.stock-status.in-stock {
background: rgba(46, 160, 67, 0.1);
color: #2ea043;
}
.stock-status.out-stock {
background: rgba(218, 54, 51, 0.1);
color: #da3633;
}
.product-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.price-info {
display: flex;
align-items: center;
gap: 0.5rem;
}
.current-price {
font-size: 1.3rem;
font-weight: bold;
color: var(--secondary-color);
}
.original-price {
font-size: 1rem;
color: var(--text-light);
text-decoration: line-through;
}
/* 加载更多 */
.load-more {
text-align: center;
margin-top: 3rem;
}
/* 响应式设计 */
@media (max-width: 768px) {
.page-title {
font-size: 2rem;
}
.filters {
flex-direction: column;
align-items: stretch;
gap: 1rem;
}
.filter-group {
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
}
.filter-buttons {
justify-content: center;
}
.products-grid {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
.product-meta {
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
}
.product-footer {
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
}
@media (max-width: 480px) {
.products-grid {
grid-template-columns: 1fr;
}
.filter-btn {
flex: 1;
text-align: center;
}
}
</style>