Oroqen-Vue/src/views/Products.vue

582 lines
13 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 }]"
@click="selectedCategory = category.id"
>
{{ 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">
<div class="image-placeholder">
<svg viewBox="0 0 250 200" width="100%" height="200">
<rect width="250" height="200" :fill="product.color" opacity="0.2"/>
<circle cx="125" cy="100" r="40" :fill="product.color" opacity="0.5"/>
<text x="125" y="110" text-anchor="middle" :fill="product.color"
font-size="14" font-weight="bold">{{ product.name.substring(0, 2) }}</text>
</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.name }}</h3>
<p class="product-category">{{ getCategoryName(product.category) }}</p>
<p class="product-description">{{ product.description }}</p>
<div class="product-meta">
<div class="craftsman-info" v-if="product.craftsman">
<el-icon><User /></el-icon>
<span>{{ product.craftsman }}</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'
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 categories = ref([
{ id: 'all', name: '全部产品' },
{ id: 'handicrafts', name: '手工艺品' },
{ id: 'food', name: '特色食品' },
{ id: 'cultural', name: '文创产品' },
{ id: 'clothing', name: '服饰配饰' }
])
// 产品数据
const products = ref([
{
id: 1,
name: '桦皮工艺盒',
category: 'handicrafts',
description: '传统桦皮制作工艺,精美实用的收纳盒',
price: 168,
originalPrice: 198,
stock: 15,
craftsman: '关小云',
isNew: true,
isHot: false,
color: '#8b4513'
},
{
id: 2,
name: '鄂伦春族刺绣挂画',
category: 'handicrafts',
description: '手工刺绣,图案精美,寓意吉祥',
price: 288,
stock: 8,
craftsman: '孟淑珍',
isNew: false,
isHot: true,
color: '#2d5016'
},
{
id: 3,
name: '传统民族服饰',
category: 'clothing',
description: '复刻传统服饰,展现民族风采',
price: 588,
stock: 5,
craftsman: '白热布',
isNew: false,
isHot: true,
color: '#8b4513'
},
{
id: 4,
name: '手工木雕摆件',
category: 'handicrafts',
description: '精美木雕工艺,展现森林文化',
price: 128,
stock: 20,
craftsman: '关金山',
isNew: false,
isHot: false,
color: '#2d5016'
},
{
id: 5,
name: '野生蓝莓干',
category: 'food',
description: '大兴安岭野生蓝莓,天然无添加',
price: 58,
originalPrice: 68,
stock: 50,
isNew: true,
isHot: false,
color: '#8b4513'
},
{
id: 6,
name: '鄂伦春文化笔记本',
category: 'cultural',
description: '融入民族元素的精美笔记本',
price: 35,
stock: 100,
isNew: false,
isHot: false,
color: '#2d5016'
},
{
id: 7,
name: '桦皮茶具套装',
category: 'handicrafts',
description: '传统桦皮工艺制作的茶具',
price: 368,
stock: 3,
craftsman: '关小云',
isNew: false,
isHot: true,
color: '#8b4513'
},
{
id: 8,
name: '民族风头饰',
category: 'clothing',
description: '传统图案设计的精美头饰',
price: 88,
stock: 12,
isNew: true,
isHot: false,
color: '#2d5016'
}
])
// 计算属性
const filteredProducts = computed(() => {
let result = products.value
// 分类筛选
if (selectedCategory.value !== 'all') {
result = result.filter(p => p.category === 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 = (product: any) => {
if (product.stock === 0) {
ElMessage.warning('商品暂时缺货')
return
}
store.addToCart(product)
ElMessage.success('已添加到购物车')
}
const loadMore = () => {
loading.value = true
// 模拟加载更多数据
setTimeout(() => {
loading.value = false
hasMore.value = false
}, 1000)
}
// 生命周期
onMounted(() => {
// 处理搜索参数
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: 200px;
}
.image-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: var(--background-light);
}
.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>