3a898e8aa0
后端修复: - 验证码存储与校验机制 - 订单创建事务+库存扣减 - GetStats字段名错误 - VerifyEmail改为POST - 供应商更新字段白名单 - 文件删除安全检查 - 抽奖安全随机数 - 用户管理CRUD - 订单取消/确认收货 - 工单回复 - Toggle返回新数据 - 移除死代码 前端修复: - 404兜底路由 - 401软跳转 - API层统一 - 面包屑补充banners - 国际化完善 - 购物车并行删除 - 退出清理购物车 - 供应商Dashboard数据 - 工单详情页 - 订单取消/确认收货
286 lines
7.0 KiB
Vue
286 lines
7.0 KiB
Vue
<template>
|
|
<div class="products-page">
|
|
<div class="page-header">
|
|
<BackNav />
|
|
<div class="filter-bar">
|
|
<el-select v-model="categoryId" :placeholder="$t('product.filterByCategory')" popper-class="dark-select-dropdown" @change="fetchProducts">
|
|
<el-option label="全部分类" :value="undefined" />
|
|
<el-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id" />
|
|
</el-select>
|
|
<el-select v-model="brandId" placeholder="筛选品牌" popper-class="dark-select-dropdown" @change="fetchProducts">
|
|
<el-option label="全部品牌" :value="undefined" />
|
|
<el-option v-for="b in brands" :key="b.id" :label="b.name" :value="b.id" />
|
|
</el-select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="product-grid">
|
|
<div v-for="p in products" :key="p.id" class="product-card" @click="$router.push(`/products/${p.id}`)">
|
|
<div class="product-image">
|
|
<el-image v-if="getFirstImage(p.images)" :src="getFirstImage(p.images)" fit="contain" lazy>
|
|
<template #error>
|
|
<div class="image-placeholder">
|
|
<el-icon :size="40"><Picture /></el-icon>
|
|
</div>
|
|
</template>
|
|
</el-image>
|
|
<div v-else class="image-placeholder">
|
|
<el-icon :size="40"><Picture /></el-icon>
|
|
</div>
|
|
</div>
|
|
<div class="product-info">
|
|
<h3 class="product-name">{{ p.name }}</h3>
|
|
<div class="product-meta">
|
|
<span class="price">¥{{ p.price }}</span>
|
|
<el-tag v-if="p.brand" size="small" type="info">{{ p.brand?.name }}</el-tag>
|
|
</div>
|
|
<div class="product-tags">
|
|
<el-tag v-if="p.require_credit" size="small" type="warning">需要资格</el-tag>
|
|
<el-tag v-if="p.credit_reward > 0" size="small" type="success">奖励 +{{ p.credit_reward }}</el-tag>
|
|
<el-tag v-if="p.stock !== undefined && p.stock <= 10" size="small" type="danger">仅剩{{ p.stock }}件</el-tag>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<el-empty v-if="!products.length" :description="$t('common.noData')" />
|
|
|
|
<div class="pagination-wrap" v-if="total > pageSize">
|
|
<el-pagination v-model:current-page="page" :page-size="pageSize" :total="total" layout="prev, pager, next" @current-change="fetchProducts" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { Picture } from '@element-plus/icons-vue'
|
|
import { productApi, categoryApi, brandApi } from '../../api'
|
|
import { getFirstImage } from '../../utils/image'
|
|
import BackNav from '../../components/BackNav.vue'
|
|
|
|
const route = useRoute()
|
|
const products = ref<any[]>([])
|
|
const categories = ref<any[]>([])
|
|
const brands = ref<any[]>([])
|
|
const categoryId = ref<number | undefined>(undefined)
|
|
const brandId = ref<number | undefined>(undefined)
|
|
const page = ref(1)
|
|
const pageSize = 20
|
|
const total = ref(0)
|
|
|
|
async function fetchProducts() {
|
|
const res: any = await productApi.list({
|
|
page: page.value,
|
|
page_size: pageSize,
|
|
category_id: categoryId.value,
|
|
brand_id: brandId.value,
|
|
})
|
|
products.value = res.data || []
|
|
total.value = res.pagination?.total || 0
|
|
}
|
|
|
|
onMounted(async () => {
|
|
if (route.query.category_id) categoryId.value = Number(route.query.category_id)
|
|
const [catRes, brandRes]: any[] = await Promise.all([
|
|
categoryApi.list(),
|
|
brandApi.list()
|
|
])
|
|
categories.value = catRes.data || []
|
|
brands.value = brandRes.data || []
|
|
await fetchProducts()
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.products-page {
|
|
padding: 0;
|
|
}
|
|
|
|
.page-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 24px;
|
|
|
|
:deep(.breadcrumb-nav) {
|
|
margin-bottom: 0;
|
|
}
|
|
}
|
|
|
|
.filter-bar {
|
|
display: flex;
|
|
gap: 12px;
|
|
}
|
|
|
|
.filter-bar :deep(.el-select) {
|
|
width: 160px;
|
|
}
|
|
|
|
.product-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
.product-card {
|
|
background: #2d2d44;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
cursor: pointer;
|
|
transition: all 0.25s ease;
|
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
}
|
|
|
|
.product-card:hover {
|
|
transform: translateY(-4px);
|
|
background: #35355a;
|
|
border-color: rgba(78, 110, 242, 0.3);
|
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
|
|
}
|
|
|
|
.product-image {
|
|
aspect-ratio: 1;
|
|
background: rgba(255, 255, 255, 0.04);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
position: relative;
|
|
|
|
.el-image {
|
|
width: 100%;
|
|
height: 100%;
|
|
|
|
img {
|
|
object-fit: contain;
|
|
background: rgba(255, 255, 255, 0.02);
|
|
}
|
|
}
|
|
|
|
.image-placeholder {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(255, 255, 255, 0.03);
|
|
color: rgba(255, 255, 255, 0.15);
|
|
}
|
|
}
|
|
|
|
.product-info {
|
|
padding: 16px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.product-name {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #fff;
|
|
margin: 0;
|
|
line-height: 1.4;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
min-height: 40px;
|
|
}
|
|
|
|
.product-meta {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.price {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: #4e6ef2;
|
|
}
|
|
|
|
.product-tags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
}
|
|
|
|
.pagination-wrap {
|
|
margin-top: 32px;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.pagination-wrap :deep(.el-pagination) {
|
|
--el-pagination-bg-color: rgba(255, 255, 255, 0.06);
|
|
--el-pagination-text-color: rgba(255, 255, 255, 0.6);
|
|
--el-pagination-button-disabled-bg-color: rgba(255, 255, 255, 0.04);
|
|
--el-pagination-button-bg-color: rgba(255, 255, 255, 0.06);
|
|
--el-pagination-hover-color: #4e6ef2;
|
|
}
|
|
|
|
.pagination-wrap :deep(.el-pagination .el-pager li) {
|
|
background: rgba(255, 255, 255, 0.06);
|
|
color: rgba(255, 255, 255, 0.6);
|
|
border-radius: 6px;
|
|
margin: 0 4px;
|
|
}
|
|
|
|
.pagination-wrap :deep(.el-pagination .el-pager li:hover),
|
|
.pagination-wrap :deep(.el-pagination .el-pager li.is-active) {
|
|
background: #4e6ef2;
|
|
color: #fff;
|
|
}
|
|
|
|
.pagination-wrap :deep(.el-pagination .btn-prev),
|
|
.pagination-wrap :deep(.el-pagination .btn-next) {
|
|
background: rgba(255, 255, 255, 0.06);
|
|
color: rgba(255, 255, 255, 0.6);
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.pagination-wrap :deep(.el-pagination .btn-prev:hover),
|
|
.pagination-wrap :deep(.el-pagination .btn-next:hover) {
|
|
background: rgba(78, 110, 242, 0.2);
|
|
color: #4e6ef2;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.page-header {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
}
|
|
|
|
.filter-bar {
|
|
width: 100%;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.filter-bar :deep(.el-select) {
|
|
flex: 1;
|
|
min-width: 140px;
|
|
}
|
|
|
|
.product-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 12px;
|
|
}
|
|
|
|
.product-info {
|
|
padding: 12px;
|
|
}
|
|
|
|
.product-name {
|
|
font-size: 13px;
|
|
min-height: 36px;
|
|
}
|
|
|
|
.price {
|
|
font-size: 16px;
|
|
}
|
|
}
|
|
</style>
|