| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212 |
- <template>
- <div class="home">
- <div class="search-bar">
- <el-input
- v-model="keyword"
- placeholder="搜索闲置物品"
- :prefix-icon="Search"
- size="large"
- clearable
- @keyup.enter="onSearch"
- />
- </div>
- <el-tabs v-model="activeCategory" class="category-tabs" @tab-change="onCategoryChange">
- <el-tab-pane label="全部" name="" />
- <el-tab-pane v-for="cat in categories" :key="cat.id" :label="cat.name" :name="String(cat.id)" />
- </el-tabs>
- <el-row :gutter="12" class="item-grid">
- <el-col
- v-for="item in itemList"
- :key="item.id"
- :xs="24"
- :sm="12"
- :md="8"
- :lg="6"
- class="item-col"
- >
- <el-card :body-style="{ padding: '0px' }" shadow="hover" class="item-card" @click="goDetail(item.id)">
- <el-image
- :src="item.images?.[0]"
- fit="cover"
- class="item-image"
- >
- <template #error>
- <div class="image-placeholder">
- <el-icon :size="32"><Picture /></el-icon>
- </div>
- </template>
- </el-image>
- <div class="item-info">
- <h4 class="item-title">{{ item.title }}</h4>
- <div class="item-meta">
- <span class="price">¥{{ item.price }}</span>
- <span class="seller">{{ item.sellerNickname }}</span>
- </div>
- </div>
- </el-card>
- </el-col>
- </el-row>
- <div v-if="loading" class="loading-skeleton">
- <el-skeleton :rows="3" animated />
- </div>
- <div v-if="!loading && itemList.length === 0" class="empty-state">
- <el-empty description="暂无物品" />
- </div>
- <el-pagination
- v-if="total > pageSize"
- v-model:current-page="pageNum"
- :total="total"
- :page-size="pageSize"
- layout="prev, pager, next"
- class="pagination"
- @current-change="loadItems"
- />
- </div>
- </template>
- <script setup lang="ts">
- import { ref, onMounted } from 'vue'
- import { useRouter } from 'vue-router'
- import { Search, Picture } from '@element-plus/icons-vue'
- import { getItems } from '../api/endpoints/items'
- import { getCategories } from '../api/endpoints/categories'
- import type { ItemVO, Category } from '../api/models'
- const router = useRouter()
- const keyword = ref('')
- const activeCategory = ref('')
- const itemList = ref<ItemVO[]>([])
- const categories = ref<Category[]>([])
- const total = ref(0)
- const pageNum = ref(1)
- const pageSize = 10
- const loading = ref(false)
- const loadItems = async () => {
- loading.value = true
- try {
- const categoryId = activeCategory.value ? Number(activeCategory.value) : undefined
- const api = getItems()
- const params = { pageNum: pageNum.value, pageSize }
- const res = categoryId
- ? await api.byCategory(categoryId, params)
- : await api.list(params)
- if (res.code === 200) {
- itemList.value = res.data?.list ?? []
- total.value = res.data?.total ?? 0
- }
- } finally {
- loading.value = false
- }
- }
- const onSearch = () => {
- if (keyword.value.trim()) {
- router.push({ path: '/search', query: { keyword: keyword.value.trim() } })
- }
- }
- const onCategoryChange = () => {
- pageNum.value = 1
- loadItems()
- }
- const goDetail = (id?: number) => {
- if (id) router.push(`/item/${id}`)
- }
- onMounted(async () => {
- await loadItems()
- try {
- const res = await getCategories().list1()
- if (res.code === 200) {
- categories.value = res.data ?? []
- }
- } catch {
- // categories optional
- }
- })
- </script>
- <style scoped>
- .home {
- max-width: 1200px;
- margin: 0 auto;
- padding: 20px;
- }
- .search-bar {
- margin-bottom: 20px;
- }
- .category-tabs {
- margin-bottom: 20px;
- }
- .item-grid {
- margin-bottom: 20px;
- }
- .item-col {
- margin-bottom: 12px;
- }
- .item-card {
- cursor: pointer;
- border-radius: 8px;
- overflow: hidden;
- transition: transform 0.2s;
- }
- .item-card:hover {
- transform: translateY(-2px);
- }
- .item-image {
- width: 100%;
- height: 180px;
- display: block;
- }
- .image-placeholder {
- height: 180px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #f5f7fa;
- color: #c0c4cc;
- }
- .item-info {
- padding: 12px;
- }
- .item-title {
- margin: 0 0 8px;
- font-size: 14px;
- font-weight: 500;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .item-meta {
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .price {
- color: #f56c6c;
- font-size: 16px;
- font-weight: 600;
- }
- .seller {
- font-size: 12px;
- color: #909399;
- }
- .loading-skeleton {
- padding: 20px;
- }
- .empty-state {
- padding: 40px 0;
- }
- .pagination {
- display: flex;
- justify-content: center;
- padding: 20px 0;
- }
- </style>
|