Files
jurong_circle_frontdesk/src/views/Shop.vue
2025-08-27 17:00:20 +08:00

976 lines
22 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="shop-page">
<!-- 导航栏 -->
<nav class="navbar">
<!-- 移除 nav-left 部分 -->
<div class="nav-center">
<h1 class="nav-title">积分商城</h1>
</div>
<div class="nav-right">
<el-button
type="text"
@click="$router.push('#')"
class="points-btn"
>
<el-icon><Coin /></el-icon>
{{ userPoints }}
</el-button>
</div>
</nav>
<!-- 搜索栏 -->
<div class="search-section">
<el-input
v-model="searchKeyword"
placeholder="搜索商品"
class="search-input"
@input="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<!-- 分类筛选 -->
<div class="category-section">
<el-scrollbar>
<div class="category-list">
<div
v-for="category in categories"
:key="category.id"
:class="['category-item', { active: selectedCategory === category.id }]"
@click="selectCategory(category.id)"
>
<el-icon>{{ category.icon }}</el-icon>
<div class="category-name">{{ category.name }}</div>
</div>
</div>
</el-scrollbar>
</div>
<!-- 推荐 -->
<div class="recommend">
<!-- 热销推荐 -->
<div class="recommend-grid">
<div class="recommend-title">热销推荐</div>
<div v-for="(product, index) in hotProducts" :key="index" class="recommend-product" @click="goToProduct(product.id)">
<div class="recommend-image">
<img :src="product.image" :alt="product.name" />
<div v-if="product.discount" class="recommend-discount">
{{ product.discount }}
</div>
</div>
<div class="recommend-content">
<h4 class="recommend-name">{{ product.name }}</h4>
<div class="recommend-price-container">
<div class="recommend-price">
<el-icon><Coin /></el-icon>
{{ product.points }}
</div>
<div v-if="product.originalPoints" class="recommend-original-price">
{{ product.originalPoints }}
</div>
</div>
</div>
</div>
</div>
<!-- 秒杀推荐 -->
<div class="recommend-grid">
<div class="recommend-title">秒杀推荐</div>
<div v-for="(product, index) in cheapProducts" :key="index" class="recommend-product" @click="goToProduct(product.id)">
<div class="recommend-image">
<img :src="product.image" :alt="product.name" />
<div v-if="product.discount" class="recommend-discount">
{{ product.discount }}
</div>
</div>
<div class="recommend-content">
<h4 class="recommend-name">{{ product.name }}</h4>
<div class="recommend-price-container">
<div class="recommend-price">
<el-icon><Coin /></el-icon>
{{ product.points }}
</div>
<div v-if="product.originalPoints" class="recommend-original-price">
{{ product.originalPoints }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 商品列表 -->
<div class="products-section">
<div class="section-header">
<h3>热门商品</h3>
<el-dropdown @command="handleSort">
<span class="sort-btn">
{{ sortText }}
<el-icon><ArrowDown /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="default">默认排序</el-dropdown-item>
<el-dropdown-item command="price_asc">价格从低到高</el-dropdown-item>
<el-dropdown-item command="price_desc">价格从高到低</el-dropdown-item>
<el-dropdown-item command="sales">销量优先</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div v-loading="loading" class="products-grid">
<div
v-for="product in filteredProducts"
:key="product.id"
class="product-card"
@click="goToProduct(product.id)"
>
<div class="product-image">
<img :src="product.image" :alt="product.name" />
<div v-if="product.discount" class="discount-badge">
{{ product.discount }}
</div>
</div>
<h4 class="product-name">{{ product.name }}</h4>
<div class="product-price">
<span class="current-price">
<el-icon><Coin /></el-icon>
{{ product.points }}
</span>
<span v-if="product.originalPoints" class="original-price">
{{ product.originalPoints }}
</span>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="!loading && filteredProducts.length === 0" class="empty-state">
<el-icon size="60"><Box /></el-icon>
<p>暂无商品</p>
</div>
<!-- 加载更多 -->
<div v-if="hasMore" class="load-more">
<el-button @click="loadMore" :loading="loadingMore">
加载更多
</el-button>
</div>
</div>
<!-- 购物车悬浮按钮 -->
<div class="cart-fab" @click="showCart = true">
<el-badge :value="cartCount" :hidden="cartCount === 0">
<el-icon size="24"><ShoppingCart /></el-icon>
</el-badge>
</div>
<!-- 购物车抽屉 -->
<el-drawer
v-model="showCart"
title="购物车"
direction="rtl"
size="80%"
>
<div class="cart-content">
<div v-if="cartItems.length === 0" class="empty-cart">
<el-icon size="60"><ShoppingCart /></el-icon>
<p>购物车是空的</p>
</div>
<div v-else>
<div v-for="item in cartItems" :key="item.id" class="cart-item">
<img :src="item.image" :alt="item.name" class="item-image" />
<div class="item-info">
<h4>{{ item.name }}</h4>
<p class="item-price">
<el-icon><Coin /></el-icon>
{{ item.points }}
</p>
</div>
<div class="item-actions">
<el-input-number
v-model="item.quantity"
:min="1"
:max="item.stock"
size="small"
@change="updateCartItem(item)"
/>
<el-button
type="danger"
size="small"
@click="removeFromCart(item.id)"
>
删除
</el-button>
</div>
</div>
<div class="cart-footer">
<div class="total-points">
总计<el-icon><Coin /></el-icon>{{ totalPoints }}
</div>
<el-button
type="primary"
size="large"
@click="checkout"
:disabled="totalPoints > userPoints"
>
{{ totalPoints > userPoints ? '积分不足' : '立即兑换' }}
</el-button>
</div>
</div>
</div>
</el-drawer>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
ArrowLeft,
Coin,
Search,
ArrowDown,
Box,
ShoppingCart
} from '@element-plus/icons-vue'
import api from '@/utils/api'
import { debounce } from 'lodash-es'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
// 响应式数据
const loading = ref(false)
const loadingMore = ref(false)
const searchKeyword = ref('')
const selectedCategory = ref('all')
const sortBy = ref('default')
const products = ref([])
const page = ref(1)
const hasMore = ref(true)
const showCart = ref(false)
const cartItems = ref([])
// 用户积分
const userPoints = ref(0)
// 分类数据
const categories = ref([
{ id: 'all', name: '全部', icon: '🛍️' },
{ id: '数码产品', name: '电子数码', icon: '📱' },
{ id: '图书文具', name: '图书文具', icon: '📚' },
{ id: '生活用品', name: '日用百货', icon: '🏠' },
{ id: '食品饮料', name: '食品饮料', icon: '🍔' },
{ id: '服装配饰', name: '精美服饰', icon: '👕' },
{ id: '其他', name: '其他', icon: '📦' }
])
//热销商品数据
const hotProducts = ref([
{ id: '6', name: '1', discount: '100', points: '9999', originalPoints:'999999' },
{ id: '6', name: '2', discount: '100', points: '9999', originalPoints:'999999' }
])
//秒杀推荐数据
const cheapProducts = ref([
{ id: '6', name: '1', discount: '100', points: '9999', originalPoints:'999999' },
{ id: '6', name: '2', discount: '100', points: '9999', originalPoints:'999999' }
])
// 计算属性
const filteredProducts = computed(() => {
let result = products.value
// 分类筛选
if (selectedCategory.value !== 'all') {
result = result.filter(p => p.category === selectedCategory.value)
}
// 搜索筛选
if (searchKeyword.value) {
result = result.filter(p =>
p.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
p.description.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
}
// 排序
switch (sortBy.value) {
case 'price_asc':
result.sort((a, b) => a.points - b.points)
break
case 'price_desc':
result.sort((a, b) => b.points - a.points)
break
case 'sales':
result.sort((a, b) => b.sales - a.sales)
break
}
return result
})
const sortText = computed(() => {
const sortMap = {
default: '默认排序',
price_asc: '价格从低到高',
price_desc: '价格从高到低',
sales: '销量优先'
}
return sortMap[sortBy.value]
})
const cartCount = computed(() => {
return cartItems.value.reduce((sum, item) => sum + item.quantity, 0)
})
const totalPoints = computed(() => {
return cartItems.value.reduce((sum, item) => sum + (item.points * item.quantity), 0)
})
// 方法
const selectCategory = (categoryId) => {
selectedCategory.value = categoryId
}
const handleSort = (command) => {
sortBy.value = command
}
const handleSearch = debounce(() => {
// 搜索逻辑已在计算属性中处理
}, 300)
const goToProduct = (productId) => {
router.push(`/productsummary/${productId}`)
}
const addToCart = (product) => {
const existingItem = cartItems.value.find(item => item.id === product.id)
if (existingItem) {
if (existingItem.quantity < product.stock) {
existingItem.quantity++
ElMessage.success('已添加到购物车')
} else {
ElMessage.warning('库存不足')
}
} else {
cartItems.value.push({
...product,
quantity: 1
})
ElMessage.success('已添加到购物车')
}
}
const updateCartItem = (item) => {
// 数量更新逻辑
}
const removeFromCart = (productId) => {
const index = cartItems.value.findIndex(item => item.id === productId)
if (index > -1) {
cartItems.value.splice(index, 1)
ElMessage.success('已从购物车移除')
}
}
const checkout = async () => {
try {
await ElMessageBox.confirm(
`确定要花费 ${totalPoints.value} 积分兑换这些商品吗?`,
'确认兑换',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
const orderData = {
items: cartItems.value.map(item => ({
productId: item.id,
quantity: item.quantity,
points: item.points
})),
totalPoints: totalPoints.value
}
await api.post('/orders', orderData)
// 清空购物车
cartItems.value = []
showCart.value = false
// 更新用户积分
userPoints.value -= totalPoints.value
ElMessage.success('兑换成功!')
router.push('/orders')
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('兑换失败,请重试')
}
}
}
const getProducts = async (isLoadMore = false) => {
try {
if (!isLoadMore) {
loading.value = true
page.value = 1
} else {
loadingMore.value = true
}
const {data} = await api.get('/products', {
params: {
page: page.value,
limit: 20,
category: selectedCategory.value === 'all' ? '' : selectedCategory.value,
keyword: searchKeyword.value,
sort: sortBy.value
}
})
console.log(data,'response');
if (isLoadMore) {
products.value.push(...data.data.products)
} else {
products.value = data.data.products
}
hasMore.value = data.data.hasMore
page.value++
} catch (error) {
ElMessage.error('获取商品列表失败')
} finally {
loading.value = false
loadingMore.value = false
}
}
const loadMore = () => {
getProducts(true)
}
const getUserPoints = async () => {
try {
const {data} = await api.get('/user/points')
console.log(data,'points');
userPoints.value = data.points
} catch (error) {
console.error('获取用户积分失败:', error)
}
}
const truncateText = (text, maxLength) => {
if (text.length <= maxLength) return text
return text.substring(0, maxLength) + '...'
}
//获取热销推荐商品
const getHotProducts = async () => {
try {
const {data} = await api.get('/products/hot')
hotProducts.value = data.data.products
} catch (error) {
console.log(error)
} finally {
console.log('热销:',hotProducts)
}
}
//获取秒杀推荐商品
const getCheapProducts = async () => {
try {
const {data} = await api.get('/products/cheap')
cheapProducts.value = data.data.products
} catch (error) {
console.log(error)
} finally {
console.log('秒杀:',cheapProducts)
}
}
// 生命周期
onMounted(() => {
// 检查URL参数中是否有分类
const categoryFromQuery = route.query.category
if (categoryFromQuery && categories.value.some(cat => cat.id === categoryFromQuery)) {
selectedCategory.value = categoryFromQuery
}
getProducts()
getUserPoints()
getHotProducts()
getCheapProducts()
})
</script>
<style scoped>
.shop-page {
min-height: 100vh;
background: linear-gradient(to bottom, #ffae00, #f3f3f3);
}
/* 导航栏保持白色背景 */
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
height: 56px;
background: white;
border-bottom: 1px solid #eee;
position: sticky;
top: 0;
z-index: 100;
}
.nav-center {
position: absolute;
left: 0;
right: 0;
text-align: center;
pointer-events: none; /* 允许点击穿透到下方元素 */
}
.nav-title {
margin: 0 auto;
font-size: 18px;
font-weight: 500;
color: #333;
display: inline-block; /* 使文本能够真正居中 */
}
.nav-right {
margin-left: auto; /* 将积分按钮推到最右侧 */
}
.back-btn,
.points-btn {
color: #409eff;
font-size: 14px;
}
/* 搜索栏参与渐变 */
.search-section {
padding: 16px;
background: transparent;
border-bottom: none;
}
.search-input {
width: 100%;
}
.search-input :deep(.el-input__wrapper) {
border-radius: 1000px;
background-color: rgba(255,255,255,0.8);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
/* 分类筛选参与渐变 */
.category-section {
background: transparent;
padding: 16px 0;
border-bottom: none;
}
.category-list {
display: flex;
gap: 16px;
padding: 0 16px;
white-space: nowrap;
}
.category-item {
display: flex;
flex-direction: column;
align-items: center; /* 使子元素水平居中 */
justify-content: center; /* 使子元素垂直居中 */
gap: 4px;
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
min-width: 60px;
background: rgba(255,255,255,0.8);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.category-name {
font-size: 12px;
/* 移除以下三个属性,取消文本省略效果 */
/* white-space: nowrap; */
/* overflow: hidden; */
/* text-overflow: ellipsis; */
max-width: 100%;
text-align: center;
width: 100%;
display: block;
}
.category-item:hover {
background: rgba(240,249,255,0.9);
}
.category-item.active {
background: rgba(64,158,255,0.9);
color: white;
}
.category-item span {
font-size: 12px;
}
/* 商品列表参与渐变 */
.products-section {
padding: 16px;
background: transparent;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.section-header h3 {
margin: 0;
font-size: 16px;
color: #333;
text-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.sort-btn {
display: flex;
align-items: center;
gap: 4px;
color: #666;
font-size: 14px;
cursor: pointer;
text-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.products-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 20px;
}
.product-card {
width: 163px;
height: 217px;
background: rgba(255,255,255,0.9);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
cursor: pointer;
transition: all 0.3s;
display: flex;
flex-direction: column;
backdrop-filter: blur(2px);
}
.product-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0,0,0,0.15);
}
.product-image {
position: relative;
width: 100%;
height: 163px;
overflow: hidden;
flex-shrink: 0;
}
.product-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.discount-badge {
position: absolute;
top: 8px;
right: 8px;
background: #ff4757;
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
.product-name {
margin: 8px 8px 4px;
font-size: 14px;
font-weight: 500;
color: #333;
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.product-price {
display: flex;
align-items: center;
gap: 8px;
margin: 0 8px;
padding-bottom: 8px;
}
.current-price {
display: flex;
align-items: center;
gap: 2px;
color: #ff6b35;
font-weight: 600;
font-size: 14px;
}
.original-price {
color: #999;
font-size: 12px;
text-decoration: line-through;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.load-more {
text-align: center;
padding: 20px;
}
.cart-fab {
position: fixed;
bottom: 80px;
right: 20px;
width: 56px;
height: 56px;
background: #409eff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
cursor: pointer;
box-shadow: 0 4px 12px rgba(64,158,255,0.4);
z-index: 1000;
}
.cart-content {
height: 100%;
display: flex;
flex-direction: column;
background: white;
}
.empty-cart {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #999;
}
.cart-item {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 0;
border-bottom: 1px solid #eee;
}
.item-image {
width: 60px;
height: 60px;
border-radius: 8px;
object-fit: cover;
}
.item-info {
flex: 1;
}
.item-info h4 {
margin: 0 0 4px 0;
font-size: 14px;
color: #333;
}
.item-price {
display: flex;
align-items: center;
gap: 2px;
color: #ff6b35;
font-weight: 600;
margin: 0;
}
.item-actions {
display: flex;
flex-direction: column;
gap: 8px;
align-items: flex-end;
}
.cart-footer {
margin-top: auto;
padding: 20px 0;
border-top: 1px solid #eee;
}
.total-points {
display: flex;
align-items: center;
gap: 4px;
font-size: 18px;
font-weight: 600;
color: #ff6b35;
margin-bottom: 16px;
}
/* 响应式设计 */
@media (max-width: 480px) {
.products-grid {
grid-template-columns: repeat(2, 1fr);
justify-items: center;
}
.product-card {
width: calc(50vw - 24px);
max-width: 163px;
}
}
/* 推荐 */
.recommend {
display: flex;
justify-content: space-between;
padding: 16px;
margin: 16px;
background: rgba(255, 255, 255, 0.9);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.recommend-grid {
flex: 1;
margin: 0 8px;
}
.recommend-product {
display: flex;
align-items: center;
width: 144px;
height: 54px;
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
padding: 6px;
margin-bottom: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
.recommend-product:hover {
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.12);
}
.recommend-image {
position: relative;
width: 42px;
height: 42px;
flex-shrink: 0;
margin-right: 8px;
border-radius: 6px;
overflow: hidden;
}
.recommend-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.recommend-discount {
position: absolute;
top: 2px;
right: 2px;
background: #ff4757;
color: white;
padding: 1px 4px;
border-radius: 3px;
font-size: 10px;
line-height: 1;
}
.recommend-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
}
.recommend-name {
margin: 0;
font-size: 12px;
font-weight: 500;
color: #333;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 3px;
}
.recommend-price-container {
display: flex;
flex-direction: column;
gap: 1px;
}
.recommend-price {
display: flex;
align-items: center;
gap: 2px;
color: #ff6b35;
font-weight: 600;
font-size: 12px;
line-height: 1;
}
.recommend-original-price {
color: #999;
font-size: 10px;
text-decoration: line-through;
line-height: 1;
}
</style>