shinnosuke
5 天以前 e52934f6527027bd6008c2ade61c3083159efd4a
rencaika/pages/benefit-search/benefit-search.uvue
@@ -16,7 +16,7 @@
         </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>
@@ -39,7 +39,7 @@
                  <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>
@@ -47,6 +47,7 @@
            <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>
@@ -58,18 +59,22 @@
   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)
@@ -87,20 +92,20 @@
   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
@@ -113,14 +118,26 @@
      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,
@@ -143,7 +160,8 @@
         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)
@@ -152,8 +170,10 @@
         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
@@ -181,6 +201,7 @@
         failLoadMoreOnce.value = previewState == 'loadMoreError'
      }
      loadHistories()
      talentId.value = resolveTalentId()
      if (options != null && options['mode'] == 'all') {
         isAllMode.value = true
         if (isApiConfigured()) loadRemoteMerchants(false)
@@ -279,43 +300,7 @@
      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
@@ -400,8 +385,9 @@
   .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; }
@@ -413,6 +399,8 @@
   .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; }