| | |
| | | </view> |
| | | </view> |
| | | |
| | | <scroll-view class="page-scroll" scroll-y="true" :show-scrollbar="false" lower-threshold="80" @scrolltolower="loadMoreMerchants"> |
| | | <scroll-view class="page-scroll" scroll-y="true" :show-scrollbar="false" lower-threshold="200" @scrolltolower="loadMoreMerchants"> |
| | | <view v-if="!isAllMode && !hasSearched" class="history-section"> |
| | | <view class="history-head"> |
| | | <text>搜索历史</text> |
| | |
| | | <image class="result-image" :src="item.image" mode="aspectFill" @error="replaceImage(item)"></image> |
| | | <view class="result-info"> |
| | | <text class="result-name">{{item.name}}</text> |
| | | <view class="result-tags"><text class="type-tag">{{item.category}}</text><text class="discount-tag">{{item.discount}}</text></view> |
| | | <view v-if="item.tags != null && item.tags.length > 0" class="result-tags"><text v-for="tag in item.tags" :key="tag" :class="tag == '券' ? 'coupon-tag' : 'benefit-tag'">{{tag}}</text></view> |
| | | <view class="result-address"><image src="/static/benefits-assets/map-pin.svg"></image><text>{{item.address}}</text></view> |
| | | </view> |
| | | </view> |
| | |
| | | <view v-else-if="hasLoaded" class="result-empty"><text>暂未搜索到相关店铺</text></view> |
| | | <view v-if="loadingMore" class="loading-row"><view class="loading-spinner"></view><text>正在加载</text></view> |
| | | <view v-else-if="loadMoreError" class="loading-row load-error" @tap="retryLoadMore"><text>加载失败,点击重试</text></view> |
| | | <view v-else-if="hasLoaded && hasMore && visibleMerchants.length > 0" class="load-more-action" @tap="loadMoreMerchants"><text>加载更多商家</text></view> |
| | | <view v-else-if="hasLoaded && !hasMore && visibleMerchants.length > 0" class="no-more"><view></view><text>已经到底了</text><view></view></view> |
| | | <view class="list-bottom"></view> |
| | | </view> |
| | |
| | | import { apiEnvironment, isApiConfigured } from '../../config/api.uts' |
| | | import { searchMerchants } from '../../services/portal.uts' |
| | | import { resolveApiUrl } from '../../services/http.uts' |
| | | import { resolveTalentId } from '../../utils/talent-id.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | import { openMerchantDetail } from '../../utils/merchant-navigation.uts' |
| | | |
| | | const navTop = ref(28) |
| | | const navHeight = ref(32) |
| | | const navWidth = ref(88) |
| | | const navRight = ref(10) |
| | | const fallbackImage = '/static/common/image-fallback.svg' |
| | | const replaceImage = (item: any) => { if (item.image != fallbackImage) item.image = fallbackImage } |
| | | const fallbackImage = '' |
| | | const replaceImage = (item: any) => { if (item.image != '') item.image = '' } |
| | | const keyword = ref('') |
| | | const hasSearched = ref(false) |
| | | const isAllMode = ref(false) |
| | | const pageSize = 6 |
| | | const pageSize = 10 |
| | | const allModePageSize = 100 |
| | | const talentId = ref('') |
| | | const merchantPool = ref([]) |
| | | const visibleMerchants = ref([]) |
| | | const loadedCount = ref(0) |
| | |
| | | const histories = ref<string[]>([]) |
| | | // 演示数据已按 sort 从小到大排列;后端接入后由接口返回同序分页结果。 |
| | | const allMerchants = [ |
| | | { id: 101, name: '乡菜郎·攀枝花特色延边菜', category: '暖心食享', merchantType: 'food', discount: '6折', address: '西区格里坪镇清香坪东街78号', image: '/static/food-assets/food-1.jpg', sort: 10 }, |
| | | { id: 102, name: '攀枝花希尔顿欢朋酒店', category: '舒心安居', merchantType: 'hotel', discount: '6折', address: '东区机场路10号', image: '/static/stay-assets/hotel-hampton.jpg', sort: 20 }, |
| | | { id: 103, name: '攀枝花万达影城', category: '怡乐生活', merchantType: 'cinema', discount: '免费券', address: '东区攀枝花大道东段699号', image: '/static/entertainment-assets/wanda-cinema.webp', sort: 30 }, |
| | | { id: 104, name: '攀枝花中国三线建设博物馆', category: '悦览花城', merchantType: 'scenic', discount: '优惠票', address: '仁和区花城新区干坝塘村', image: '/static/home-assets/city-intro.jpg', sort: 40 }, |
| | | { id: 105, name: '金府酒楼(西区店)', category: '暖心食享', merchantType: 'food', discount: '6折', address: '西区清香坪北街51号', image: '/static/food-assets/food-2.jpg', sort: 50 }, |
| | | { id: 106, name: '中国石油东区新福路加油站', category: '畅行无忧', merchantType: 'fuel', discount: '9.4折', address: '东区新福路20号附3-21号', image: '/static/home-assets/top.jpg', sort: 60 }, |
| | | { id: 107, name: '渡口公司管辖停车场', category: '畅行无忧', merchantType: 'parking', discount: '免费', address: '攀枝花市东区指定停车场', image: '/static/home-assets/city-intro.jpg', sort: 70 }, |
| | | { id: 108, name: '攀商未来城', category: '惠享好物', merchantType: 'shopping', discount: '购房优惠3%', address: '东区榕树街55号', image: '/static/home-assets/top.jpg', sort: 80 }, |
| | | { id: 109, name: '攀枝花市中心医院', category: '臻享礼遇', merchantType: 'medical', discount: '免费体检', address: '东区攀枝花大道中段益康街34号', image: '/static/stay-assets/hotel-vienna.jpg', sort: 90 }, |
| | | { id: 110, name: '人才金融服务中心', category: '臻享礼遇', merchantType: 'finance', discount: '专属金融方案', address: '东区人民街中心广场', image: '/static/stay-assets/hotel-hampton.jpg', sort: 100 }, |
| | | { id: 111, name: '东区人才自助洗车服务点', category: '畅行无忧', merchantType: 'carWash', discount: '专属优惠', address: '东区机场路人才服务站', image: '/static/home-assets/top.jpg', sort: 110 }, |
| | | { id: 112, name: '攀枝花维也纳国际酒店', category: '舒心安居', merchantType: 'hotel', discount: '免费5天', address: '东区攀枝花大道中段', image: '/static/stay-assets/hotel-vienna.jpg', sort: 120 }, |
| | | { id: 113, name: '百杏厨房(中环天地店)', category: '暖心食享', merchantType: 'food', discount: '8.8折', address: '东区炳三区新福路20号', image: '/static/food-assets/food-1.jpg', sort: 130 }, |
| | | { id: 114, name: '攀枝花大剧院', category: '怡乐生活', merchantType: 'cinema', discount: '演出优惠', address: '东区机场路文化艺术中心', image: '/static/entertainment-assets/entertainment-hero.webp', sort: 140 } |
| | | { id: 101, name: '乡菜郎·攀枝花特色延边菜', category: '暖心食享', merchantType: 'food', discount: '6折', address: '西区格里坪镇清香坪东街78号', image: '/static/common/image-placeholder.png', sort: 10 }, |
| | | { id: 102, name: '攀枝花希尔顿欢朋酒店', category: '舒心安居', merchantType: 'hotel', discount: '6折', address: '东区机场路10号', image: '/static/common/image-placeholder.png', sort: 20 }, |
| | | { id: 103, name: '攀枝花万达影城', category: '怡乐生活', merchantType: 'cinema', discount: '免费券', address: '东区攀枝花大道东段699号', image: '/static/common/image-placeholder.png', sort: 30 }, |
| | | { id: 104, name: '攀枝花中国三线建设博物馆', category: '悦览花城', merchantType: 'scenic', discount: '优惠票', address: '仁和区花城新区干坝塘村', image: '/static/common/image-placeholder.png', sort: 40 }, |
| | | { id: 105, name: '金府酒楼(西区店)', category: '暖心食享', merchantType: 'food', discount: '6折', address: '西区清香坪北街51号', image: '/static/common/image-placeholder.png', sort: 50 }, |
| | | { id: 106, name: '中国石油东区新福路加油站', category: '畅行无忧', merchantType: 'fuel', discount: '9.4折', address: '东区新福路20号附3-21号', image: '/static/common/image-placeholder.png', sort: 60 }, |
| | | { id: 107, name: '渡口公司管辖停车场', category: '畅行无忧', merchantType: 'parking', discount: '免费', address: '攀枝花市东区指定停车场', image: '/static/common/image-placeholder.png', sort: 70 }, |
| | | { id: 108, name: '攀商未来城', category: '惠享好物', merchantType: 'shopping', discount: '购房优惠3%', address: '东区榕树街55号', image: '/static/common/image-placeholder.png', sort: 80 }, |
| | | { id: 109, name: '攀枝花市中心医院', category: '臻享礼遇', merchantType: 'medical', discount: '免费体检', address: '东区攀枝花大道中段益康街34号', image: '/static/common/image-placeholder.png', sort: 90 }, |
| | | { id: 110, name: '人才金融服务中心', category: '臻享礼遇', merchantType: 'finance', discount: '专属金融方案', address: '东区人民街中心广场', image: '/static/common/image-placeholder.png', sort: 100 }, |
| | | { id: 111, name: '东区人才自助洗车服务点', category: '畅行无忧', merchantType: 'carWash', discount: '专属优惠', address: '东区机场路人才服务站', image: '/static/common/image-placeholder.png', sort: 110 }, |
| | | { id: 112, name: '攀枝花维也纳国际酒店', category: '舒心安居', merchantType: 'hotel', discount: '免费5天', address: '东区攀枝花大道中段', image: '/static/common/image-placeholder.png', sort: 120 }, |
| | | { id: 113, name: '百杏厨房(中环天地店)', category: '暖心食享', merchantType: 'food', discount: '8.8折', address: '东区炳三区新福路20号', image: '/static/common/image-placeholder.png', sort: 130 }, |
| | | { id: 114, name: '攀枝花大剧院', category: '怡乐生活', merchantType: 'cinema', discount: '演出优惠', address: '东区机场路文化艺术中心', image: '/static/common/image-placeholder.png', sort: 140 } |
| | | ] |
| | | const formatRemoteDiscount = (item: any): string => { |
| | | const sale = item.salePrice != null ? Number(item.salePrice) : 0 |
| | |
| | | for (let index = 0; index < records.length; index++) { |
| | | const item: any = records[index] |
| | | const rawType = item.merchantType != null ? item.merchantType as string : 'food' |
| | | const hasConfiguredTags = item.benefitTags instanceof Array |
| | | const configuredTags: any[] = hasConfiguredTags ? item.benefitTags as any[] : [] |
| | | const tags: string[] = [] |
| | | for (let tagIndex = 0; tagIndex < configuredTags.length; tagIndex++) { |
| | | const tag = String(configuredTags[tagIndex]).trim() |
| | | if (tag != '' && tags.indexOf(tag) < 0) tags.push(tag) |
| | | } |
| | | const discount = item.discountText != null && String(item.discountText).trim() != '' |
| | | ? String(item.discountText).trim() |
| | | : formatRemoteDiscount(item) |
| | | if (!hasConfiguredTags && discount != '') tags.push(discount) |
| | | mapped.push({ |
| | | id: '' + item.id, |
| | | name: item.merchantName != null ? item.merchantName as string : '人才权益商户', |
| | | category: item.categoryName != null ? item.categoryName as string : '', |
| | | merchantType: rawType, |
| | | discount: item.discountText != null ? item.discountText as string : formatRemoteDiscount(item), |
| | | discount: discount, |
| | | tags: tags, |
| | | address: item.address != null ? item.address as string : '', |
| | | image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : '/static/common/image-fallback.svg', |
| | | image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : '', |
| | | sort: item.sort != null ? Number(item.sort) : 0, |
| | | detailType: item.detailType != null ? item.detailType as string : 'MERCHANT', |
| | | detailBusinessId: item.detailBusinessId != null ? '' + item.detailBusinessId : '' + item.id, |
| | |
| | | visibleMerchants.value = [] |
| | | currentPage.value = 0 |
| | | } |
| | | searchMerchants(keyword.value.trim(), '', isAllMode.value ? 'all' : 'search', currentPage.value + 1, pageSize, (data: any) => { |
| | | const requestSize = isAllMode.value && !hasSearched.value ? allModePageSize : pageSize |
| | | searchMerchants(keyword.value.trim(), '', isAllMode.value ? 'all' : 'search', talentId.value, currentPage.value + 1, requestSize, (data: any) => { |
| | | if (requestVersion != listRequestVersion.value) return |
| | | const records: any[] = data != null && data.records instanceof Array ? data.records as any[] : [] |
| | | const mapped = mapRemoteMerchants(records) |
| | |
| | | visibleMerchants.value = next |
| | | merchantPool.value = next |
| | | loadedCount.value = next.length |
| | | currentPage.value = currentPage.value + 1 |
| | | hasMore.value = data != null && data.total != null ? next.length < data.total : false |
| | | const responsePage = data != null && data.page != null ? Number(data.page) : currentPage.value + 1 |
| | | const total = data != null && data.total != null ? Number(data.total) : -1 |
| | | currentPage.value = responsePage > 0 ? responsePage : currentPage.value + 1 |
| | | hasMore.value = records.length > 0 && (total >= 0 ? next.length < total : records.length >= requestSize) |
| | | initialLoading.value = false |
| | | loadingMore.value = false |
| | | hasLoaded.value = true |
| | |
| | | failLoadMoreOnce.value = previewState == 'loadMoreError' |
| | | } |
| | | loadHistories() |
| | | talentId.value = resolveTalentId() |
| | | if (options != null && options['mode'] == 'all') { |
| | | isAllMode.value = true |
| | | if (isApiConfigured()) loadRemoteMerchants(false) |
| | |
| | | keyword.value = value |
| | | performSearch() |
| | | } |
| | | const openMerchant = (item: any) => { |
| | | const action = item.targetAction != null ? item.targetAction as string : 'MERCHANT' |
| | | const detailBusinessId = item.detailBusinessId != null ? item.detailBusinessId as string : item.id as string |
| | | if (action == 'CINEMA') { |
| | | uni.navigateTo({ url: '/pages/cinema-merchant/cinema-merchant?merchantId=' + encodeURIComponent(detailBusinessId) + '&name=' + encodeURIComponent(item.name) }) |
| | | return |
| | | } |
| | | if (action == 'SHOPPING') { |
| | | uni.navigateTo({ url: '/pages/shopping-merchant/shopping-merchant?name=' + encodeURIComponent(item.name) + '&discount=' + encodeURIComponent(item.discount) + '&address=' + encodeURIComponent(item.address) }) |
| | | return |
| | | } |
| | | if (action == 'MEDICAL') { |
| | | uni.navigateTo({ url: '/pages/merchant/merchant?type=medical&name=' + encodeURIComponent(item.name) }) |
| | | return |
| | | } |
| | | if (action == 'FINANCE') { |
| | | uni.navigateTo({ url: '/pages/finance-merchant/finance-merchant?name=' + encodeURIComponent(item.name) + '&discount=' + encodeURIComponent(item.discount) + '&address=' + encodeURIComponent(item.address) + '&image=' + encodeURIComponent(item.image) }) |
| | | return |
| | | } |
| | | if (action == 'CAR_WASH') { |
| | | uni.navigateTo({ url: '/pages/car-wash-merchant/car-wash-merchant?merchantId=' + encodeURIComponent(detailBusinessId) + '&name=' + encodeURIComponent(item.name) }) |
| | | return |
| | | } |
| | | if (action == 'FUEL' || action == 'PARKING') { |
| | | const transportAction = action == 'PARKING' ? 'plate' : 'mobile' |
| | | uni.navigateTo({ url: '/pages/merchant/merchant?type=transport&name=' + encodeURIComponent(item.name) + '&action=' + transportAction + '&plateMode=withParking' }) |
| | | return |
| | | } |
| | | uni.navigateTo({ |
| | | url: '/pages/merchant/merchant?merchantType=' + encodeURIComponent(item.merchantType) |
| | | + '&merchantId=' + encodeURIComponent(detailBusinessId) |
| | | + '&name=' + encodeURIComponent(item.name) |
| | | + '&image=' + encodeURIComponent(item.image) |
| | | + '&address=' + encodeURIComponent(item.address) |
| | | + '&discount=' + encodeURIComponent(item.discount) |
| | | }) |
| | | } |
| | | const openMerchant = (item: any) => openMerchantDetail(item) |
| | | const loadMoreMerchants = () => { |
| | | if (initialLoading.value || loadingMore.value || !hasMore.value) return |
| | | const requestVersion = listRequestVersion.value |
| | |
| | | .result-info { display: flex; height: 138rpx; min-width: 0; flex: 1; box-sizing: border-box; padding: 1rpx 0 1rpx 22rpx; flex-direction: column; justify-content: space-between; } |
| | | .result-name { color: #292929; font-size: 26rpx; line-height: 36rpx; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| | | .result-tags { display: flex; flex-direction: row; align-items: center; } |
| | | .type-tag { max-width: 150rpx; padding: 5rpx 8rpx; border-radius: 5rpx; background-color: #FFF0EC; color: #FF5D37; font-size: 19rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| | | .discount-tag { margin-left: 10rpx; padding: 5rpx 8rpx; border-radius: 5rpx; background-color: #FFF5E7; color: #FF7C2B; font-size: 19rpx; } |
| | | .result-tags text { margin-right: 10rpx; padding: 5rpx 8rpx; border-radius: 5rpx; font-size: 19rpx; white-space: nowrap; } |
| | | .coupon-tag { background-color: #FFF0EC; color: #FF5D37; } |
| | | .benefit-tag { max-width: 220rpx; overflow: hidden; text-overflow: ellipsis; background-color: #FFF5E7; color: #FF7C2B; } |
| | | .result-address { display: flex; min-width: 0; flex-direction: row; align-items: center; } |
| | | .result-address image { width: 25rpx; height: 25rpx; margin-right: 7rpx; flex-shrink: 0; } |
| | | .result-address text { min-width: 0; flex: 1; color: #7E8490; font-size: 19rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| | |
| | | .loading-spinner { width: 28rpx; height: 28rpx; margin-right: 13rpx; box-sizing: border-box; border-width: 3rpx; border-style: solid; border-color: #FFD6C4; border-top-color: #FF6C2B; border-radius: 50%; animation: merchant-loading 720ms linear infinite; } |
| | | .loading-row text { color: #989DA5; font-size: 22rpx; } |
| | | .load-error text { color: #F26A38; } |
| | | .load-more-action { display: flex; height: 90rpx; justify-content: center; align-items: center; } |
| | | .load-more-action text { color: #F26A38; font-size: 23rpx; } |
| | | .no-more { display: flex; height: 90rpx; flex-direction: row; justify-content: center; align-items: center; } |
| | | .no-more view { width: 70rpx; height: 1rpx; background-color: #E7E7E7; } |
| | | .no-more text { margin: 0 18rpx; color: #B1B3B7; font-size: 21rpx; } |