Oroqen-Vue/src/views/Products.vue
2025-08-10 21:46:20 +08:00

639 lines
15 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">
<!-- 页面头部 -->
<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 }]"
@click="selectedCategory = category.id; onCategoryChange()"
>
{{ 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">
<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"/>
</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">
<h3 class="product-name">{{ product.productName || '产品名称' }}</h3>
<p class="product-category">{{ getCategoryName(product.categoryId) }}</p>
<p class="product-description">{{ product.description || '暂无描述' }}</p>
<div class="product-meta">
<div class="craftsman-info" v-if="product.craftsmanInfo">
<el-icon><User /></el-icon>
<span>{{ product.craftsmanInfo }}</span>
</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'
import { productApi, productCategoryApi } from '../api/product'
import { cartApi } from '../api/cart'
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)
const currentPage = ref(1)
const pageSize = ref(12)
// 产品分类
const categories = ref<any[]>([
{ id: 'all', name: '全部产品' }
])
// 产品数据
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)
}
}
// 获取产品列表
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)]
}
// 计算属性
const filteredProducts = computed(() => {
let result = products.value
// 分类筛选
if (selectedCategory.value !== 'all') {
result = result.filter(p => p.categoryId === selectedCategory.value)
}
// 价格筛选
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 : ''
}
const addToCart = async (product: any) => {
if (product.stock === 0) {
ElMessage.warning('商品暂时缺货')
return
}
try {
// 这里使用默认用户ID实际应用中应该从用户状态获取
const userId = '1'
await cartApi.addToCart(userId, product.id, 1)
store.addToCart(product)
ElMessage.success('已添加到购物车')
} catch (error) {
console.error('添加到购物车失败:', error)
ElMessage.error('添加到购物车失败,请稍后重试')
}
}
const loadMore = () => {
getProducts(true)
}
// 监听分类变化
const onCategoryChange = () => {
getProducts()
}
// 处理图片路径
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'
}
}
// 生命周期
onMounted(() => {
getCategories()
getProducts()
// 处理搜索参数
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%;
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);
}
.image-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
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;
}
.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>