Initial commit: 商品售卖网站
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
<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 {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.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>
|
||||
Reference in New Issue
Block a user