Files
hhrs/pages/discount/discount.vue
T
jacobxu666andClaude Fable 5 eeb6b91e0a feat: 场馆/折扣/权益待使用列表新增「所属县区 + 所属类型」筛选条
- 新增 components/hhrs-filter-bar:两个等宽入口平分并占满容器宽、无底色、蓝字选中态;
  面板首项「全部 X」即清空该维度(因此没有独立重置按钮)
- 三页接入;选项与各选项命中条数由列表接口在响应里一并返回(后端已按关键词与另一维度算好)
- 筛选与关键词为 AND;切筛选回第 1 页,复用各页现有 reqSeq 竞态保护
- 某维度无可选项时(如商铺未录入所在地区)该入口自动不渲染,避免点开空面板
- 三页头部间距统一为「搜索栏↔筛选条 = 筛选条↔列表 = 10px」

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-28 15:12:38 +08:00

325 lines
8.1 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>
<view class="page">
<!-- 搜索栏 -->
<view class="search-area">
<view class="nav-row">
<view class="back-btn" @click="goBack">
<u-icon name="arrow-left" color="#333333" size="40"></u-icon>
<text class="back-text">返回</text>
</view>
<view class="search-bar">
<u-icon name="search" color="#8e8e93" size="36"></u-icon>
<input class="search-input" v-model="keyword" placeholder="请搜索商户名" confirm-type="search" @confirm="onSearch" />
<view class="search-btn" @click="onSearch">搜索</view>
</view>
</view>
<!-- xuchao: 所属县区 + 所属类型筛选,选项与联动条数由列表接口一并返回 -->
<hhrs-filter-bar :districts="filters.districts" :categories="filters.categories"
:district-id="districtId" :category="category" @change="onFilterChange" />
</view>
<!-- 商户列表 -->
<view class="list-area">
<view class="venue-card" v-for="item in list" :key="item.shopId" @click="goDetail(item)">
<image class="card-img" :src="item.image" mode="aspectFill" />
<view class="card-info">
<text class="card-name">{{ item.name }}</text>
<text class="card-category">{{ item.category }}</text>
<!-- xuchao: 地址与距离同属一个模块 -->
<view class="card-address-row">
<text class="card-address">{{ item.address }}</text>
<view class="card-distance" v-if="item.distance !== null">
<u-icon name="map" color="#999999" size="24"></u-icon>
<text class="distance-text">{{ item.distance < 1 ? Math.round(item.distance * 1000) + 'm' : Math.round(item.distance) + 'km' }}</text>
</view>
</view>
</view>
</view>
<view v-if="hasScrolled" class="nomore-area">
<u-loadmore :status="loadStatus" :load-text="{ loadmore: '', loading: '加载中...', nomore: '我也是有底线的' }" />
</view>
</view>
</view>
</template>
<script>
// xuchao: 县区 + 类型筛选条,三个列表页共用(pages.json 的 easycom 只给 u- 前缀配了规则,这里显式引入)
import HhrsFilterBar from '@/components/hhrs-filter-bar/hhrs-filter-bar.vue'
export default {
components: { HhrsFilterBar },
data() {
return {
keyword: '',
list: [],
pageNo: 1,
pageSize: 10,
total: 0,
loading: false,
lng: '',
lat: '',
loadStatus: 'loadmore',
hasScrolled: false,
reqSeq: 0,
// xuchao: 筛选条件与面板选项(选项由列表响应带回)
districtId: '',
category: '',
filters: { districts: [], categories: [] }
}
},
onLoad() {
const hasLoc = uni.getStorageSync('lng') && uni.getStorageSync('lat')
if (hasLoc) {
this.lng = uni.getStorageSync('lng')
this.lat = uni.getStorageSync('lat')
this.loadList()
} else {
// xuchao: 定位仅在首页触发过,直接进本页无缓存时主动获取一次,保证距离列可展示
import('@/common/location.js').then(module => module.default.getLocation())
.then(({ lng, lat }) => {
this.lng = lng
this.lat = lat
})
.catch(() => {})
.finally(() => this.loadList())
}
},
// xuchao: 改用页面级触底加载(scroll-view 无固定高度时 scrolltolower 永不触发)
onReachBottom() {
this.loadMore()
},
methods: {
loadList() {
// xuchao: 请求序号 —— 新请求直接取代在途请求,过期响应整体丢弃(修复搜索竞态,故不再用 loading 提前 return)
const seq = ++this.reqSeq
this.loading = true
this.loadStatus = 'loading'
const params = {
pageNo: this.pageNo,
pageSize: this.pageSize,
keyword: this.keyword,
typeId: 2
}
if (this.lng) params.lng = this.lng
if (this.lat) params.lat = this.lat
if (this.districtId) params.districtId = this.districtId
if (this.category) params.category = this.category
this.$u.api.getStoreList(params).then(res => {
if (seq !== this.reqSeq) return
if (res.errcode === 0 && res.data) {
// xuchao: 面板选项随列表一起回来(后端已按关键词与另一维度算好条数)
this.filters = {
districts: res.data.districts || [],
categories: res.data.categories || []
}
const list = res.data.list || []
if (this.pageNo === 1) {
this.list = list
} else {
this.list = this.list.concat(list)
}
this.total = res.data.total || 0
// xuchao: 本页不足一页即已到尾部,钉住 total,防止 total 中途变大时空翻
if (list.length < this.pageSize) this.total = this.list.length
}
}).finally(() => {
if (seq !== this.reqSeq) return
this.loading = false
if (this.list.length === 0) {
this.loadStatus = 'nomore'
} else if (this.list.length >= this.total) {
this.loadStatus = 'nomore'
} else {
this.loadStatus = 'loadmore'
}
})
},
loadMore() {
this.hasScrolled = true
if (this.list.length < this.total && !this.loading) {
this.pageNo++
this.loadList()
}
},
goBack() {
// xuchao: 直接通过链接进入(无历史栈)时回到首页
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
uni.reLaunch({ url: '/pages/index/index' })
}
},
goDetail(item) {
// xuchao: type=2 标记商户折扣入口,详情页据此展示商品信息+商户优惠模块
uni.navigateTo({ url: '/pages/venue/detail?id=' + item.shopId + '&type=2' })
},
onSearch() {
this.pageNo = 1
this.list = []
this.loadList()
},
// xuchao: 换筛选条件即回第 1 页重查(关键词保持不变,两者为 AND)
onFilterChange(e) {
this.districtId = e.districtId
this.category = e.category
this.onSearch()
}
}
}
</script>
<style>
page {
background-color: #f2f2f7;
}
</style>
<style scoped lang="less">
.search-area {
background-color: #ffffff;
padding: 6px 12px 0;
// xuchao: 滚动时固定在页面顶部;z-index 高于筛选遮罩(90),保证面板与选中态不被压住
position: sticky;
top: 0;
z-index: 100;
}
.nav-row {
display: flex;
align-items: center;
gap: 12px;
}
.back-btn {
display: flex;
align-items: center;
justify-content: center;
height: 44px;
flex-shrink: 0;
gap: 2px;
}
.back-text {
font-size: 14px;
color: #333333;
}
.search-bar {
flex: 1;
display: flex;
align-items: center;
height: 44px;
background-color: #ffffff;
border: 1px solid #f2f2f7;
border-radius: 100px;
padding: 4px 8px;
gap: 12px;
}
.search-input {
flex: 1;
font-size: 16px;
color: #333;
}
.search-btn {
background-color: #0088ff;
color: #ffffff;
font-size: 14px;
padding: 4px 10px;
border-radius: 50px;
white-space: nowrap;
}
.list-area {
// xuchao: 顶部留白交给筛选条的 20rpx 下内边距,保证「搜索栏↔筛选条」与「筛选条↔列表」间距一致
padding: 0 12px 12px;
// xuchao: border-box 防止 padding 撑出页面横向滚动
box-sizing: border-box;
}
.venue-card {
display: flex;
align-items: flex-end;
box-sizing: border-box;
background-color: #ffffff;
border: 1px solid #f2f2f7;
border-radius: 6px;
width: 702rpx;
padding: 24px 12px;
gap: 12px;
box-shadow: 0 2px 3px #f2f2f7;
margin-bottom: 12px;
}
.card-img {
width: 75px;
height: 75px;
border-radius: 6px;
flex-shrink: 0;
}
.card-info {
flex: 1;
// xuchao: min-width:0 允许 flex 子项收缩到内容宽度以下,配合 nowrap 文本出省略号
min-width: 0;
display: flex;
flex-direction: column;
gap: 0;
}
.card-name {
font-size: 16px;
font-weight: 500;
color: #000000;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-category {
font-size: 14px;
color: #727272;
margin-top: 8px;
// xuchao: 分类为空也预留一行高度,防止卡片内容底部对齐时名称上下跳动
min-height: 20px;
line-height: 20px;
}
.card-address-row {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
margin-top: 8px;
}
.card-address {
flex: 1;
min-width: 0;
font-size: 14px;
color: #727272;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-distance {
display: flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
}
.distance-text {
font-size: 12px;
color: #727272;
}
.nomore-area {
padding-top: 24px;
}
</style>