Files
jurong_circle_frontdesk/src/views/Shop.vue
2025-09-28 11:53:37 +08:00

856 lines
19 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">
<div class="points-btn">
<el-icon><Coin /></el-icon>
积分余额
{{ userPoints }}
</div>
<div class="points-btn">
<div class="beans-container">
<img src='/imgs/profile/rongdou.png' alt="融豆" class="rongdou-icon" />
融豆余额
{{ userBeans }}
</div>
</div>
</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="main-price">
<img src='/imgs/profile/rongdou.png' alt="融豆" class="rongdou-icon" />
<span class="rongdou-price">{{ product.rongdou_price }}</span>
</div>
<div class="sub-price">
<el-icon class="points-icon"><Coin /></el-icon>
<span class="points-price">{{ product.points_price }}</span>
</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="main-price">
<img src='/imgs/profile/rongdou.png' alt="融豆" class="rongdou-icon" />
<span class="rongdou-price">{{ product.flash_price }}</span>
</div>
<div class="sub-price">
<el-icon class="points-icon"><Coin /></el-icon>
<span class="points-price">{{ product.flash_price*10000 }}</span>
</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">
<div class="main-price">
<img src='/imgs/profile/rongdou.png' alt="融豆" class="rongdou-icon" />
<span class="rongdou-price">{{ product.rongdou_price }}</span>
</div>
<div class="sub-price">
<el-icon class="points-icon"><Coin /></el-icon>
<span class="points-price">{{ product.points_price }}</span>
</div>
</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>
</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
} from '@element-plus/icons-vue'
import api from '@/utils/api'
import { debounce } from 'lodash-es'
import { getImageUrl } from '@/config'
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 userPoints = ref(0)
// 用户融豆
const userBeans = 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 selectCategory = (categoryId) => {
selectedCategory.value = categoryId
}
const handleSort = (command) => {
sortBy.value = command
}
const handleSearch = debounce(() => {
// 搜索逻辑已在计算属性中处理
}, 300)
const goToProduct = (productId) => {
router.push(`/productsummary/${productId}`)
}
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) {
const newProducts = data.data.products
// 处理新加载商品图片路径
newProducts.forEach(product => {
if (product.image) {
product.image = getImageUrl(product.image)
}
})
products.value.push(...newProducts)
} else {
products.value = data.data.products
// 处理商品图片路径
products.value.forEach(product => {
if (product.image) {
product.image = getImageUrl(product.image)
}
})
}
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 getUserBeans = async () => {
try {
const {data} = await api.get('/user/profile')
console.log(data.user.balance,'beans');
userBeans.value = data.user.balance
} 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
// 处理热销商品图片路径
hotProducts.value.forEach(product => {
if (product.image) {
product.image = getImageUrl(product.image)
}
})
} 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
// 处理秒杀商品图片路径
cheapProducts.value.forEach(product => {
if (product.image) {
product.image = getImageUrl(product.image)
}
})
} 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()
getUserBeans()
getHotProducts()
getCheapProducts()
})
</script>
<style scoped>
.shop-page {
min-height: 100vh;
background: linear-gradient(to bottom, #72c9ffae, #f3f3f3);
}
/* 导航栏保持白色背景 */
.navbar {
display: flex;
align-items: center;
padding: 0 16px;
justify-content: space-between;
height: 56px;
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: #ffffff;
display: inline-block; /* 使文本能够真正居中 */
}
.nav-right {
margin-left: auto;
}
.points-btn {
display: flex;
align-items: center;
gap: 4px;
color: #409eff;
font-size: 14px;
white-space: nowrap; /* 防止换行 */
}
.beans-container {
display: flex;
align-items: center;
gap: 4px;
}
.beans-container img {
margin-right: -2px;
}
.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 {
margin: 0 8px;
padding-bottom: 8px;
position: relative;
}
.main-price {
display: flex;
align-items: center;
gap: 4px;
}
.rongdou-icon {
width: 16px;
height: 16px;
object-fit: contain;
}
.rongdou-price {
color: #ff6b35;
font-weight: bold;
font-size: 16px;
}
.sub-price {
display: flex;
align-items: center;
gap: 2px;
margin-top: 4px;
}
.points-icon {
font-size: 10px;
width: 10px;
height: 10px;
}
.points-price {
color: #999;
font-size: 10px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.load-more {
text-align: center;
padding: 20px;
}
/* 响应式设计 */
@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 {
position: relative;
}
.recommend-price-container .main-price {
display: flex;
align-items: center;
gap: 2px;
}
.recommend-price-container .rongdou-icon {
width: 12px;
height: 12px;
object-fit: contain;
}
.recommend-price-container .rongdou-price {
color: #ff6b35;
font-weight: 600;
font-size: 12px;
line-height: 1;
}
.recommend-price-container .sub-price {
display: flex;
align-items: center;
gap: 1px;
margin-top: 2px;
}
.recommend-price-container .points-icon {
font-size: 8px;
width: 8px;
height: 8px;
}
.recommend-price-container .points-price {
color: #999;
font-size: 8px;
line-height: 1;
}
</style>