| | |
| | | </view> |
| | | <view v-else class="merchant-title-row"> |
| | | <text class="merchant-title">{{merchantName}}</text> |
| | | <view class="merchant-tags"><text v-if="merchantHasCoupon && !isFuelMerchant" class="coupon-tag">券</text><text v-if="merchantDiscount != '' && !isFuelMerchant" class="discount-tag">{{merchantDiscount}}</text><text v-if="isHotelMerchant && merchantConsumeDiscount != ''" class="discount-tag">{{merchantConsumeDiscount}}</text></view> |
| | | <view v-if="!isFuelMerchant && merchantDisplayTags.length > 0" class="merchant-tags"><text v-for="tag in merchantDisplayTags" :key="tag" :class="tag == '券' ? 'coupon-tag' : 'discount-tag'">{{tag}}</text></view> |
| | | </view> |
| | | <view v-if="(!isFuelMerchant && !isHotelMerchant && merchantServiceLabel != '') || applicableCards != ''" class="service-tags"><text v-if="!isParkingMerchant && !isMedicalMerchant && !isFuelMerchant && !isHotelMerchant && merchantServiceLabel != ''">{{merchantServiceLabel}}</text><text v-if="applicableCards != ''">{{applicableCards}}</text></view> |
| | | <view v-if="!isParkingMerchant && merchantType != 'apartment'" class="merchant-address-row"> |
| | |
| | | <image class="package-image" :src="item.image" mode="aspectFill" @error="replaceImage(item)"></image> |
| | | <view class="package-info"> |
| | | <text class="package-name">{{item.name}}</text> |
| | | <view class="package-tags"><text v-if="merchantHasCoupon" class="coupon-tag">券</text><text v-if="merchantDiscount != ''" class="discount-tag">{{merchantDiscount}}</text><text v-if="isHotelMerchant && merchantConsumeDiscount != ''" class="discount-tag">{{merchantConsumeDiscount}}</text></view> |
| | | <view class="package-tags"><text v-for="tag in merchantDisplayTags" :key="tag" :class="tag == '券' ? 'coupon-tag' : 'discount-tag'">{{tag}}</text></view> |
| | | <view class="package-bottom"><view class="price-row"><text class="price-symbol">¥</text><text class="price">{{item.price}}</text><text v-if="!isHotelMerchant" class="old-price">¥{{item.originalPrice}}</text></view><view class="buy-button"><text>{{item.available === false ? '已售罄' : (isHotelMerchant ? '预约' : '购买')}}</text></view></view> |
| | | </view> |
| | | </view> |
| | |
| | | const merchantHasCoupon = ref(false) |
| | | const merchantDiscount = ref('') |
| | | const merchantConsumeDiscount = ref('') |
| | | const merchantBenefitTags = ref<string[]>([]) |
| | | const merchantBenefitTagsLoaded = ref(false) |
| | | const merchantDisplayTags = computed((): string[] => { |
| | | if (merchantBenefitTagsLoaded.value) return merchantBenefitTags.value |
| | | const tags: string[] = [] |
| | | if (merchantHasCoupon.value && !isFuelMerchant.value) tags.push('券') |
| | | if (merchantDiscount.value != '' && !isFuelMerchant.value) tags.push(merchantDiscount.value) |
| | | if (isHotelMerchant.value && merchantConsumeDiscount.value != '') tags.push(merchantConsumeDiscount.value) |
| | | return tags |
| | | }) |
| | | const readBenefitTags = (data: any): string[] => { |
| | | const tags: string[] = [] |
| | | const records: any[] = data != null && data.benefitTags instanceof Array ? data.benefitTags as any[] : [] |
| | | for (let index = 0; index < records.length; index++) { |
| | | const tag = String(records[index]).trim() |
| | | if (tag != '' && tags.indexOf(tag) < 0) tags.push(tag) |
| | | } |
| | | return tags |
| | | } |
| | | const normalizeHotelCardLabel = (value: string): string => { |
| | | let label = value.trim() |
| | | const separator = label.indexOf('|') |
| | |
| | | merchantHasCoupon.value = data != null && (data.couponAvailable === true || Number(data.couponAvailable) == 1) |
| | | const discountText = data != null && data.discountText != null ? data.discountText as string : '' |
| | | merchantDiscount.value = discountText == '人才专享' ? '' : discountText |
| | | merchantBenefitTagsLoaded.value = data != null && data.benefitTags instanceof Array |
| | | merchantBenefitTags.value = merchantBenefitTagsLoaded.value ? readBenefitTags(data) : [] |
| | | const configuredTag = data != null && data.rightsTag != null ? '' + data.rightsTag : '' |
| | | merchantServiceLabel.value = configuredTag != '' && data != null && data.serviceLabel != null ? data.serviceLabel as string : '' |
| | | applicableCards.value = data != null && data.applicableCards != null ? data.applicableCards as string : '' |
| | |
| | | const loadServices = (append: boolean) => { |
| | | if (!isApiConfigured() || activeTab.value == '') { initialLoading.value = false; loadingMore.value = false; initialError.value = true; return }; const requestVersion = listRequestVersion.value |
| | | if (!append && failInitialOnce.value) { failInitialOnce.value = false; initialLoading.value = false; initialError.value = true; return }; if (append && failLoadMoreOnce.value) { failLoadMoreOnce.value = false; loadingMore.value = false; loadMoreError.value = true; return } |
| | | getPremiumTopicServices(activeTab.value, currentPage.value + 1, 4, (data: any) => { if (requestVersion != listRequestVersion.value) return; const records: any[] = data != null && data.records instanceof Array ? data.records as any[] : []; const next = append ? visibleServices.value.slice() : []; for (let index = 0; index < records.length; index++) { const item = records[index]; next.push({ id: '' + item.id, businessType: item.businessType != null ? item.businessType as string : 'MERCHANT', merchantType: item.merchantType != null ? item.merchantType as string : 'service', name: item.merchantName != null ? item.merchantName as string : '臻享服务', benefit: activeTab.value == '免费体检' || activeTab.value == '配偶推荐就业' || item.businessType == 'ADMIN_APPROVAL' ? '免费' : (item.benefitText != null ? item.benefitText as string : '人才专享'), address: item.address != null ? item.address as string : '', image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage, available: item.available !== false }) }; visibleServices.value = next; currentPage.value++; hasMore.value = data != null && data.total != null ? next.length < data.total : false; initialLoading.value = false; loadingMore.value = false; hasLoaded.value = true }, () => { if (requestVersion != listRequestVersion.value) return; initialLoading.value = false; loadingMore.value = false; if (append) loadMoreError.value = true; else initialError.value = true }) |
| | | getPremiumTopicServices(activeTab.value, currentPage.value + 1, 4, (data: any) => { if (requestVersion != listRequestVersion.value) return; const records: any[] = data != null && data.records instanceof Array ? data.records as any[] : []; const next = append ? visibleServices.value.slice() : []; for (let index = 0; index < records.length; index++) { const item = records[index]; next.push({ id: '' + item.id, businessType: item.businessType != null ? item.businessType as string : 'MERCHANT', merchantType: item.merchantType != null ? item.merchantType as string : 'service', name: item.merchantName != null ? item.merchantName as string : '臻享服务', benefit: activeTab.value == '免费体检' || activeTab.value == '配偶推荐就业' || activeTab.value == '金融服务' || item.businessType == 'ADMIN_APPROVAL' || item.businessType == 'FINANCE' ? '免费' : (item.benefitText != null ? item.benefitText as string : '人才专享'), address: item.address != null ? item.address as string : '', image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage, available: item.available !== false }) }; visibleServices.value = next; currentPage.value++; hasMore.value = data != null && data.total != null ? next.length < data.total : false; initialLoading.value = false; loadingMore.value = false; hasLoaded.value = true }, () => { if (requestVersion != listRequestVersion.value) return; initialLoading.value = false; loadingMore.value = false; if (append) loadMoreError.value = true; else initialError.value = true }) |
| | | } |
| | | const loadMore = () => { if (initialLoading.value || loadingMore.value || !hasMore.value) return; loadMoreError.value = false; loadingMore.value = true; loadServices(true) } |
| | | const goBack = () => safeBack('/pages/benefits/benefits') |
| | |
| | | for (let index = 0; index < records.length; index++) { |
| | | const item = records[index] |
| | | const merchantType = item.merchantType != null ? item.merchantType as string : '' |
| | | const tags: string[] = [] |
| | | if (item.couponAvailable === true || Number(item.couponAvailable) == 1) tags.push('券') |
| | | const configuredTag = item.discountText != null ? String(item.discountText).trim() : '' |
| | | if (merchantType == 'culture' && (item.storeConsumeAvailable === true || Number(item.storeConsumeAvailable) == 1)) tags.push('到店消费') |
| | | if (merchantType != 'culture' && configuredTag != '' && tags.indexOf(configuredTag) < 0) tags.push(configuredTag) |
| | | const tags: string[] = [] |
| | | if (item.benefitTags instanceof Array) { |
| | | const tagRecords: any[] = item.benefitTags as any[] |
| | | for (let tagIndex = 0; tagIndex < tagRecords.length; tagIndex++) { |
| | | const tag = String(tagRecords[tagIndex]).trim() |
| | | if (tag != '' && tag != '人才专享' && tags.indexOf(tag) < 0) tags.push(tag) |
| | | } |
| | | } else { |
| | | if (item.couponAvailable === true || Number(item.couponAvailable) == 1) tags.push('券') |
| | | if (merchantType == 'culture' && (item.storeConsumeAvailable === true || Number(item.storeConsumeAvailable) == 1)) tags.push('到店消费') |
| | | if (merchantType != 'culture' && configuredTag != '' && tags.indexOf(configuredTag) < 0) tags.push(configuredTag) |
| | | } |
| | | next.push({ id: '' + item.id, merchantType: merchantType, name: item.merchantName != null && item.merchantName != '' ? item.merchantName as string : '商家名称未配置', discount: configuredTag, tags: tags, address: item.address != null ? item.address as string : '', image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage, available: item.available !== false, unavailableReason: item.unavailableReason != null && item.unavailableReason != '' ? item.unavailableReason as string : '该服务当前不可用' }) |
| | | } |
| | | visibleServices.value = next; currentPage.value++; hasMore.value = data != null && data.total != null ? next.length < data.total : false; initialLoading.value = false; loadingMore.value = false; hasLoaded.value = true |
| | |
| | | v-for="item in visibleServices" :key="item.id" @tap="openService(item)"> |
| | | <image class="service-image" :src="item.image" mode="aspectFill" |
| | | @error="replaceServiceImage(item)"></image> |
| | | <view class="service-info"><text class="service-name">{{item.name}}</text><view v-if="item.available" class="discount-tags"><text v-for="tag in item.discountTags" :key="tag" class="discount-tag">{{tag}}</text></view><text v-else class="discount-tag">已下架</text> |
| | | <view class="service-info"><text class="service-name">{{item.name}}</text><view v-if="item.available" class="discount-tags"><text v-for="tag in item.discountTags" :key="tag" :class="tag == '券' ? 'coupon-tag' : 'discount-tag'">{{tag}}</text></view><text v-else class="discount-tag">已下架</text> |
| | | <view v-if="item.merchantType != 'apartment'" class="address"> |
| | | <image src="/static/benefits-assets/map-pin.svg"></image><text>{{item.address}}</text> |
| | | </view> |
| | |
| | | const merchantName = readText(item.merchantName, item['merchant_name']) |
| | | const discountText = readText(item.discountText, item['discount_text']) |
| | | const configuredTags: any[] = item.benefitTags instanceof Array ? (item.benefitTags as any[]) : [] |
| | | const hasCoupon = item.couponAvailable === true || Number(item.couponAvailable) == 1 || configuredTags.some((tag: any) => { |
| | | const value = String(tag).trim() |
| | | return value == '券' || value == '惠' |
| | | }) |
| | | const displayTags: any[] = activeTab.value == 'hotel' |
| | | ? configuredTags.filter((tag: any) => String(tag).trim() != '券' && String(tag).trim() != '惠') |
| | | : configuredTags |
| | | if (activeTab.value == 'hotel' && hasCoupon) displayTags.unshift('券') |
| | | const displayTags: string[] = [] |
| | | for (let tagIndex = 0; tagIndex < configuredTags.length; tagIndex++) { |
| | | const tag = String(configuredTags[tagIndex]).trim() |
| | | if (tag != '' && tag != '人才专享' && displayTags.indexOf(tag) < 0) displayTags.push(tag) |
| | | } |
| | | const hasCoupon = item.couponAvailable === true || Number(item.couponAvailable) == 1 |
| | | if (displayTags.length == 0) { |
| | | if (hasCoupon) displayTags.push('券') |
| | | if (discountText != '' && discountText != '人才专享') displayTags.push(discountText) |
| | | } |
| | | next.push({ |
| | | id: '' + item.id, |
| | | merchantType: merchantType != '' ? merchantType : activeTab.value, |
| | | name: merchantName != '' ? merchantName : '\u4f4f\u5bbf\u670d\u52a1', |
| | | discount: discountText != '' ? discountText : '\u4eba\u624d\u4e13\u4eab', |
| | | discountTags: displayTags.length > 0 ? displayTags : [discountText != '' ? discountText : '\u4eba\u624d\u4e13\u4eab'], |
| | | discountTags: displayTags, |
| | | address: readText(item.address, item['merchant_address']), |
| | | image: coverUrl != '' ? resolveApiUrl(coverUrl) : fallbackImage, |
| | | available: readAvailable(item) |
| | |
| | | box-sizing: border-box; |
| | | padding: 3rpx 8rpx; |
| | | border-radius: 5rpx; |
| | | background-color: #FFF0EB; |
| | | color: #FF6239; |
| | | background-color: #FFF5E7; |
| | | color: #FF7C2B; |
| | | font-size: 21rpx; |
| | | line-height: 29rpx; |
| | | } |
| | | .coupon-tag { |
| | | display: block; |
| | | width: fit-content; |
| | | box-sizing: border-box; |
| | | padding: 3rpx 8rpx; |
| | | border-radius: 5rpx; |
| | | background-color: #FFF0EC; |
| | | color: #FF5D37; |
| | | font-size: 21rpx; |
| | | line-height: 29rpx; |
| | | } |
| | |
| | | <scroll-view class="page-scroll" scroll-y lower-threshold="80" @scroll="handleScroll" @scrolltolower="loadMore"> |
| | | <view class="hero"><image class="hero-image" :src="currentScene.image" mode="aspectFill" @error="replaceHeroImage"></image><view class="hero-shade"></view><text v-if="currentScene.name != ''" class="scene-name">{{currentScene.name}}</text><text v-if="scenes.length == 0" class="no-gallery">暂无图册内容</text><scroll-view v-else class="scene-scroll" scroll-x scroll-with-animation :scroll-left="sceneScrollLeft" @scrollend="handleSceneScrollEnd"><view class="scene-row" :style="'width:' + sceneTrackWidth + 'rpx'"><view class="scene-pad"></view><view v-for="(item, index) in scenes" :key="item.id" class="scene-item" :class="index == activeIndex ? 'active' : ''" @tap="selectScene(index)"><image :src="item.image" mode="aspectFill" @error="replaceSceneImage(index)"></image></view><view class="scene-pad"></view></view></scroll-view></view> |
| | | <view v-if="topicDescription != ''" class="topic-summary"><view class="intro-card"><text>{{topicDescription}}</text></view></view> |
| | | <view class="service-section"><view class="section-heading"><text>景区服务</text><view></view></view><view v-if="initialLoading" class="list-state"><view class="loading-spinner"></view><text>正在加载景区服务</text></view><view v-else-if="initialError" class="list-state error-state"><text>景区服务加载失败</text><text class="retry-text" @tap="retryInitialLoad">重新加载</text></view><view v-else-if="hasLoaded && visibleServices.length == 0" class="list-state"><text>暂无相关景区服务</text></view><view v-else-if="hasLoaded"><view class="service-item" :class="item.available ? '' : 'is-unavailable'" v-for="item in visibleServices" :key="item.id" @tap="openService(item)"><image class="service-image" :src="item.image" mode="aspectFill" @error="replaceServiceImage(item)"></image><view class="service-info"><text class="service-name">{{item.name}}</text><text class="service-discount">{{item.available ? item.discount : '已下架'}}</text><view class="address"><image src="/static/benefits-assets/map-pin.svg"></image><text>{{item.address}}</text></view></view></view></view><view v-if="loadingMore" class="list-state compact"><view class="loading-spinner"></view><text>正在加载</text></view><view v-else-if="loadMoreError" class="list-state compact load-error" @tap="retryLoadMore"><text>加载失败,点击重试</text></view><view v-else-if="hasLoaded && !hasMore && visibleServices.length > 0" class="no-more"><text>已经到底了</text></view></view><view class="bottom-space"></view> |
| | | <view class="service-section"><view class="section-heading"><text>景区服务</text><view></view></view><view v-if="initialLoading" class="list-state"><view class="loading-spinner"></view><text>正在加载景区服务</text></view><view v-else-if="initialError" class="list-state error-state"><text>景区服务加载失败</text><text class="retry-text" @tap="retryInitialLoad">重新加载</text></view><view v-else-if="hasLoaded && visibleServices.length == 0" class="list-state"><text>暂无相关景区服务</text></view><view v-else-if="hasLoaded"><view class="service-item" :class="item.available ? '' : 'is-unavailable'" v-for="item in visibleServices" :key="item.id" @tap="openService(item)"><image class="service-image" :src="item.image" mode="aspectFill" @error="replaceServiceImage(item)"></image><view class="service-info"><text class="service-name">{{item.name}}</text><view v-if="!item.available" class="service-tags"><text class="service-discount">已下架</text></view><view v-else-if="item.tags.length > 0" class="service-tags"><text v-for="tag in item.tags" :key="tag" :class="tag == '券' ? 'coupon-tag' : 'service-discount'">{{tag}}</text></view><view class="address"><image src="/static/benefits-assets/map-pin.svg"></image><text>{{item.address}}</text></view></view></view></view><view v-if="loadingMore" class="list-state compact"><view class="loading-spinner"></view><text>正在加载</text></view><view v-else-if="loadMoreError" class="list-state compact load-error" @tap="retryLoadMore"><text>加载失败,点击重试</text></view><view v-else-if="hasLoaded && !hasMore && visibleServices.length > 0" class="no-more"><text>已经到底了</text></view></view><view class="bottom-space"></view> |
| | | </scroll-view> |
| | | </view> |
| | | </template> |
| | |
| | | import { isApiConfigured } from '../../config/api.uts' |
| | | import { getTravelTopic, getTravelTopicMerchants } from '../../services/portal.uts' |
| | | import { resolveApiUrl } from '../../services/http.uts' |
| | | import { getSession } from '../../services/session.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | const navTop = ref(28) |
| | |
| | | const scenes = ref<any[]>([]) |
| | | const currentScene = ref({ id: 'fallback', name: '', image: fallbackImage }) |
| | | const topicDescription = ref('') |
| | | const talentId = ref('') |
| | | const visibleServices = ref<any[]>([]) |
| | | const initialLoading = ref(true) |
| | | const initialError = ref(false) |
| | |
| | | failLoadMoreOnce.value = state == 'loadMoreError' |
| | | } |
| | | loadTravelTopic() |
| | | const session = getSession() |
| | | if (session != null && session.talentId != null) talentId.value = session.talentId as string |
| | | loadInitialServices() |
| | | }) |
| | | |
| | |
| | | const requestVersion = listRequestVersion.value |
| | | if (!append && failInitialOnce.value) { failInitialOnce.value = false; initialLoading.value = false; initialError.value = true; return } |
| | | if (append && failLoadMoreOnce.value) { failLoadMoreOnce.value = false; loadingMore.value = false; loadMoreError.value = true; return } |
| | | getTravelTopicMerchants(currentPage.value + 1, 4, (data: any) => { |
| | | getTravelTopicMerchants(currentPage.value + 1, 4, talentId.value, (data: any) => { |
| | | if (requestVersion != listRequestVersion.value) return |
| | | const records: any[] = data != null && data.records instanceof Array ? data.records as any[] : [] |
| | | const next = append ? visibleServices.value.slice() : [] |
| | | for (let index = 0; index < records.length; index++) { |
| | | const item = records[index] |
| | | next.push({ id: '' + item.id, merchantType: item.merchantType != null ? item.merchantType as string : 'scenic', name: item.merchantName != null ? item.merchantName as string : '景区服务', discount: item.discountText != null ? item.discountText as string : '人才专享', address: item.address != null ? item.address as string : '', image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage, available: item.available !== false }) |
| | | const hasCoupon = item.couponAvailable === true || Number(item.couponAvailable) == 1 |
| | | const discount = item.discountText != null && item.discountText != '人才专享' ? item.discountText as string : '' |
| | | const tags: string[] = [] |
| | | if (item.benefitTags instanceof Array) { |
| | | const tagRecords: any[] = item.benefitTags as any[] |
| | | for (let tagIndex = 0; tagIndex < tagRecords.length; tagIndex++) { |
| | | const tag = String(tagRecords[tagIndex]).trim() |
| | | if (tag != '' && tag != '人才专享' && tags.indexOf(tag) < 0) tags.push(tag) |
| | | } |
| | | } else { |
| | | if (hasCoupon) tags.push('券') |
| | | if (discount != '') tags.push(discount) |
| | | } |
| | | next.push({ id: '' + item.id, merchantType: item.merchantType != null ? item.merchantType as string : 'scenic', name: item.merchantName != null ? item.merchantName as string : '景区服务', discount: discount, tags: tags, address: item.address != null ? item.address as string : '', image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage, available: item.available !== false }) |
| | | } |
| | | visibleServices.value = next |
| | | currentPage.value = currentPage.value + 1 |
| | |
| | | .scene-item { width: 106rpx; height: 106rpx; margin-right: 31rpx; box-sizing: border-box; overflow: hidden; border: 3rpx solid rgba(255,255,255,.9); border-radius: 8rpx; flex-shrink: 0; transition: transform 180ms ease-out; } .scene-item image { width: 100%; height: 100%; } .scene-item.active { border-width: 4rpx; border-color: #FFD329; transform: scale(1.28); box-shadow: 0 8rpx 18rpx rgba(0,0,0,.28); } |
| | | .topic-summary { margin-top: -66rpx; position: relative; z-index: 4; } .intro-card { margin: 0 31rpx; padding: 25rpx 31rpx; border-radius: 26rpx; background-image: linear-gradient(90deg,#FF5320,#FF8A22); box-shadow: 0 10rpx 22rpx rgba(255,104,28,.24); } .intro-card text { color: #FFFFFF; font-size: 23rpx; line-height: 34rpx; } |
| | | .service-section { padding: 48rpx 31rpx 0; } .section-heading { display: flex; width: 132rpx; height: 72rpx; flex-direction: column; align-items: center; } .section-heading text { color: #363636; font-size: 32rpx; font-weight: 700; } .section-heading view { width: 61rpx; height: 7rpx; margin-top: 12rpx; border-radius: 4rpx; background-image: linear-gradient(90deg,#FF4D36,#FFBF3A); } |
| | | .service-item { display: flex; min-height: 205rpx; margin-bottom: 20rpx; flex-direction: row; } .service-item.is-unavailable { opacity: .56; } .service-image { width: 184rpx; height: 184rpx; border-radius: 17rpx; flex-shrink: 0; background-color: #EEE; } .service-info { display: flex; min-width: 0; min-height: 184rpx; flex: 1; padding: 4rpx 0 4rpx 24rpx; flex-direction: column; justify-content: space-between; } .service-name { color: #292929; font-size: 29rpx; font-weight: 700; } .service-discount { display: block; width: fit-content; padding: 6rpx 9rpx; border-radius: 6rpx; background-color: #FFF5E7; color: #FF6A34; font-size: 21rpx; } .address { display: flex; min-width: 0; padding: 7rpx 10rpx; border-radius: 7rpx; background-color: #F1F2F3; } .address image { width: 27rpx; height: 27rpx; margin-right: 8rpx; } .address text { min-width: 0; flex: 1; color: #7E8490; font-size: 21rpx; line-height: 31rpx; } |
| | | .service-item { display: flex; min-height: 205rpx; margin-bottom: 20rpx; flex-direction: row; } .service-item.is-unavailable { opacity: .56; } .service-image { width: 184rpx; height: 184rpx; border-radius: 17rpx; flex-shrink: 0; background-color: #EEE; } .service-info { display: flex; min-width: 0; min-height: 184rpx; flex: 1; padding: 4rpx 0 4rpx 24rpx; flex-direction: column; justify-content: space-between; } .service-name { color: #292929; font-size: 29rpx; font-weight: 700; } .service-tags { display: flex; flex-direction: row; align-items: center; } .coupon-tag { padding: 6rpx 8rpx; border-radius: 6rpx; background-color: #FFF0EC; color: #FF5D37; font-size: 21rpx; } .service-discount { display: block; width: fit-content; margin-left: 0; padding: 6rpx 9rpx; border-radius: 6rpx; background-color: #FFF5E7; color: #FF6A34; font-size: 21rpx; } .service-tags .service-discount { margin-left: 11rpx; } .service-tags .coupon-tag + .service-discount { margin-left: 11rpx; } .address { display: flex; min-width: 0; padding: 7rpx 10rpx; border-radius: 7rpx; background-color: #F1F2F3; } .address image { width: 27rpx; height: 27rpx; margin-right: 8rpx; } .address text { min-width: 0; flex: 1; color: #7E8490; font-size: 21rpx; line-height: 31rpx; } |
| | | .list-state, .no-more { display: flex; height: 150rpx; color: #989DA5; font-size: 23rpx; justify-content: center; align-items: center; } .list-state.compact { height: 90rpx; } .error-state { flex-direction: column; } .retry-text, .load-error text { margin-top: 18rpx; color: #F26A38; } .loading-spinner { width: 26rpx; height: 26rpx; margin-right: 12rpx; box-sizing: border-box; border: 3rpx solid #FFD6C4; border-top-color: #FF6C2B; border-radius: 50%; animation: loading 720ms linear infinite; } @keyframes loading { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .bottom-space { height: calc(30rpx + env(safe-area-inset-bottom)); } |
| | | </style> |
| | |
| | | } |
| | | |
| | | export const getTravelTopic = (onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appTravelTopic'), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | export const getTravelTopicMerchants = (page: number, size: number, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appTravelTopicMerchants'), method: 'GET', skipAuth: true, data: { page: page, size: size } }, onSuccess, onError) |
| | | export const getTravelTopicMerchants = (page: number, size: number, talentId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => { |
| | | const data: any = { page: page, size: size } |
| | | if (talentId != '') data.talentId = Number(talentId) |
| | | return request<any>({ path: getApiEndpoint('appTravelTopicMerchants'), method: 'GET', skipAuth: true, data: data }, onSuccess, onError) |
| | | } |
| | | export const getStayTopic = (onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appStayTopic'), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | export const getStayTopicMerchants = (type: string, page: number, size: number, talentId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => { |
| | | const data: any = { type: type, page: page, size: size } |
| | |
| | | '停车服务': 'parking', parking: 'parking', |
| | | '加油服务': 'fuel', fuel: 'fuel', |
| | | '子女托管': 'children', children: 'children', |
| | | '学生托付': 'children', '学生托管': 'children', '子女托管服务': 'children', |
| | | '金融服务': 'finance', finance: 'finance', |
| | | '配偶推荐就业': 'employment', employment: 'employment', |
| | | '行政审批': 'approval', approval: 'approval', |
| | |
| | | const isSpouseEmploymentMerchant = computed(() => merchantTypeCode.value === 'employment') |
| | | const isApprovalMerchant = computed(() => merchantTypeCode.value === 'approval') |
| | | const isBookMerchant = computed(() => merchantTypeCode.value === 'culture') |
| | | const couponWithoutProducts = computed(() => isBookMerchant.value || isChildcareMerchant.value) |
| | | const isServiceMerchant = computed(() => isFoodMerchant.value || merchantTypeCode.value === 'scenic') |
| | | const supportsStoreConsumption = computed(() => [ |
| | | 'hotel', 'scenic', 'food', 'dental', 'medical', 'culture', |
| | | 'hotel', 'scenic', 'food', 'dental', 'culture', 'children', |
| | | ].includes(merchantTypeCode.value) || (isCarWashMerchant.value && form.miniProgramJumpType === 'normal')) |
| | | const merchantAppLoginOptionalTypes = [ |
| | | 'housing', 'car', 'appliance', 'digital', 'culture', 'telecom', 'dental', 'fuel', |
| | |
| | | } |
| | | |
| | | const tabs = computed(() => { |
| | | if (isBookMerchant.value) { |
| | | if (couponWithoutProducts.value) { |
| | | return [ |
| | | { label: '商家设置', value: 'settings' }, |
| | | { label: '商家权益', value: 'rights' }, |
| | |
| | | { label: '团单', value: 'products' }, |
| | | ] |
| | | } |
| | | if (isParkingMerchant.value || isChildcareMerchant.value || isFinancialMerchant.value || isMovieMerchant.value || isSpouseEmploymentMerchant.value || isApprovalMerchant.value) { |
| | | if (isParkingMerchant.value || isFinancialMerchant.value || isMovieMerchant.value || isSpouseEmploymentMerchant.value || isApprovalMerchant.value) { |
| | | return [ |
| | | { label: '商家设置', value: 'settings' }, |
| | | { label: '商家权益', value: 'rights' }, |
| | |
| | | }], |
| | | validTime: [{ required: true, message: '请选择优惠券有效期', trigger: 'change' }], |
| | | productScope: [{ |
| | | validator: (_rule, value, callback) => isBookMerchant.value || value !== 'selected' || selectedCouponProductIds.value.length > 0 |
| | | validator: (_rule, value, callback) => couponWithoutProducts.value || value !== 'selected' || selectedCouponProductIds.value.length > 0 |
| | | ? callback() |
| | | : callback(new Error('请至少选择一个可用商品')), |
| | | trigger: 'change', |
| | |
| | | |
| | | function defaultMerchantRights(): MerchantRightConfig[] { |
| | | const currentType = merchantTypeCode.value |
| | | const supportsDefaultStoreConsumption = ['food', 'scenic', 'dental', 'medical'].includes(currentType) |
| | | const supportsDefaultStoreConsumption = ['food', 'scenic', 'dental', 'culture', 'children'].includes(currentType) |
| | | || (currentType === 'carWash' && form.miniProgramJumpType === 'normal') |
| | | const isHotel = currentType === 'hotel' |
| | | const isCarWash = currentType === 'carWash' |
| | |
| | | enabled: true, |
| | | tag: '', |
| | | detail: '', |
| | | storeConsume: true, |
| | | storeConsume: false, |
| | | discount: '', |
| | | governmentSubsidy: true, |
| | | governmentSubsidy: false, |
| | | consumeDesc: '', |
| | | consumeEnabled: true, |
| | | consumeEnabled: false, |
| | | }, |
| | | { |
| | | level: '杰出人才卡', |
| | | enabled: true, |
| | | tag: '', |
| | | detail: '', |
| | | storeConsume: true, |
| | | storeConsume: false, |
| | | discount: '', |
| | | governmentSubsidy: true, |
| | | governmentSubsidy: false, |
| | | consumeDesc: '', |
| | | consumeEnabled: true, |
| | | consumeEnabled: false, |
| | | }, |
| | | { |
| | | level: '菁英人才卡', |
| | | enabled: true, |
| | | tag: '', |
| | | detail: '', |
| | | storeConsume: true, |
| | | storeConsume: false, |
| | | discount: '', |
| | | governmentSubsidy: true, |
| | | governmentSubsidy: false, |
| | | consumeDesc: '', |
| | | consumeEnabled: true, |
| | | consumeEnabled: false, |
| | | }, |
| | | ] |
| | | } |
| | |
| | | enabled: true, |
| | | tag: '', |
| | | detail: '', |
| | | storeConsume: false, |
| | | storeConsume: true, |
| | | discount: '', |
| | | governmentSubsidy: false, |
| | | governmentSubsidy: true, |
| | | consumeDesc: '', |
| | | consumeEnabled: false, |
| | | consumeEnabled: true, |
| | | }, |
| | | { |
| | | level: '杰出人才卡', |
| | | enabled: true, |
| | | tag: '', |
| | | detail: '', |
| | | storeConsume: false, |
| | | storeConsume: true, |
| | | discount: '', |
| | | governmentSubsidy: false, |
| | | governmentSubsidy: true, |
| | | consumeDesc: '', |
| | | consumeEnabled: false, |
| | | consumeEnabled: true, |
| | | }, |
| | | { |
| | | level: '菁英人才卡', |
| | | enabled: true, |
| | | tag: '', |
| | | detail: '', |
| | | storeConsume: false, |
| | | storeConsume: true, |
| | | discount: '', |
| | | governmentSubsidy: false, |
| | | governmentSubsidy: true, |
| | | consumeDesc: '', |
| | | consumeEnabled: false, |
| | | consumeEnabled: true, |
| | | }, |
| | | ] |
| | | } |
| | |
| | | ...(saved || {}), |
| | | } |
| | | if (isParkingMerchant.value && merged.parkingType === 'paid') merged.consumeEnabled = true |
| | | if (supportsStoreConsumption.value && merged.level !== '普通游客') merged.consumeEnabled = true |
| | | return merged |
| | | })) |
| | | syncCarWashJumpMeta() |
| | |
| | | } |
| | | |
| | | async function loadCouponProducts() { |
| | | if (isBookMerchant.value) { |
| | | if (couponWithoutProducts.value) { |
| | | couponProducts.value = [] |
| | | selectedCouponProductIds.value = [] |
| | | return |
| | |
| | | couponEditForm.couponAmount = '' |
| | | couponEditForm.talentLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡'] |
| | | couponEditForm.validTime = [] |
| | | couponEditForm.productScope = isBookMerchant.value ? 'all' : 'selected' |
| | | couponEditForm.productScope = couponWithoutProducts.value ? 'all' : 'selected' |
| | | couponEditForm.storeConsume = true |
| | | couponEditForm.governmentSubsidy = true |
| | | couponEditForm.status = 1 |
| | |
| | | try { savedQuantities = typeof detail.talentLevelQuantities === 'string' ? JSON.parse(detail.talentLevelQuantities) : (detail.talentLevelQuantities || {}) } catch { savedQuantities = {} } |
| | | couponEditForm.talentLevels.forEach((level) => { couponLevelQuantities[level] = Number(savedQuantities[level] || 1) }) |
| | | couponEditForm.validTime = detail.validStart && detail.validEnd ? [detail.validStart, detail.validEnd] : [] |
| | | couponEditForm.productScope = isBookMerchant.value || detail.productScope === 'ALL' ? 'all' : 'selected' |
| | | couponEditForm.productScope = couponWithoutProducts.value || detail.productScope === 'ALL' ? 'all' : 'selected' |
| | | couponEditForm.storeConsume = detail.storeConsume === true || detail.storeConsume === 1 |
| | | couponEditForm.governmentSubsidy = detail.governmentSubsidy === true || detail.governmentSubsidy === 1 |
| | | couponEditForm.status = detail.status ?? 1 |
| | |
| | | talentLevelQuantities: Object.fromEntries(couponEditForm.talentLevels.map((level) => [level, Number(couponLevelQuantities[level] || 1)])), |
| | | validStart: couponEditForm.validTime?.[0], |
| | | validEnd: couponEditForm.validTime?.[1], |
| | | productScope: isBookMerchant.value || couponEditForm.productScope === 'all' ? 'ALL' : 'SELECTED', |
| | | productIds: isBookMerchant.value || couponEditForm.productScope === 'all' ? [] : selectedCouponProductIds.value, |
| | | storeConsume: isBookMerchant.value || couponEditForm.storeConsume, |
| | | productScope: couponWithoutProducts.value || couponEditForm.productScope === 'all' ? 'ALL' : 'SELECTED', |
| | | productIds: couponWithoutProducts.value || couponEditForm.productScope === 'all' ? [] : selectedCouponProductIds.value, |
| | | storeConsume: couponWithoutProducts.value || couponEditForm.storeConsume, |
| | | governmentSubsidy: couponEditForm.governmentSubsidy, |
| | | status: couponEditForm.status, |
| | | } |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div v-if="isChildcareMerchant" class="merchant-rights-detail-row consume-desc-row"> |
| | | <label>到店消费使用描述</label> |
| | | <el-input v-model="item.consumeDesc" type="textarea" :rows="4" resize="none" placeholder="请输入" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <div v-if="isParkingMerchant && item.parkingType === 'paid'" class="merchant-rights-detail-row consume-desc-row"> |
| | |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <div v-if="isBookMerchant" class="coupon-edit-row coupon-switch-row"> |
| | | <div v-if="couponWithoutProducts" class="coupon-edit-row coupon-switch-row"> |
| | | <label>适用范围</label> |
| | | <span>到店消费</span> |
| | | </div> |
| | |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | |
| | | <div v-if="!isBookMerchant" class="coupon-product-select-table"> |
| | | <div v-if="!couponWithoutProducts" class="coupon-product-select-table"> |
| | | <el-table :data="couponProducts" border class="coupon-edit-product-table" :show-header="true"> |
| | | <el-table-column width="98" align="center"> |
| | | <template #default="{ row }"> |
| | |
| | | </el-table> |
| | | </div> |
| | | |
| | | <div v-if="!isBookMerchant" class="coupon-edit-row coupon-switch-row"> |
| | | <div v-if="!couponWithoutProducts" class="coupon-edit-row coupon-switch-row"> |
| | | <label>是否到店消费可用</label> |
| | | <el-switch v-model="couponEditForm.storeConsume" /> |
| | | </div> |
| | |
| | | } |
| | | const drawerTitle = computed(() => (editingId.value ? '编辑商家' : '新建商家')) |
| | | |
| | | const merchantTypeAliases: Record<string, string> = { |
| | | '餐饮服务': 'food', food: 'food', |
| | | '景区服务': 'scenic', scenic: 'scenic', |
| | | '酒店服务': 'hotel', hotel: 'hotel', |
| | | '公寓租赁': 'apartment', apartment: 'apartment', |
| | | '自助洗车': 'carWash', '自助洗车服务': 'carWash', carWash: 'carWash', |
| | | '免费体检': 'medical', medical: 'medical', healthCheck: 'medical', |
| | | '停车服务': 'parking', parking: 'parking', |
| | | '加油服务': 'fuel', fuel: 'fuel', |
| | | '子女托管': 'children', children: 'children', |
| | | '学生托付': 'children', '学生托管': 'children', '子女托管服务': 'children', |
| | | '金融服务': 'finance', finance: 'finance', |
| | | '配偶推荐就业': 'employment', employment: 'employment', |
| | | '行政审批': 'approval', approval: 'approval', |
| | | '观影服务': 'cinema', cinema: 'cinema', theatre: 'cinema', '电影院': 'cinema', '影院': 'cinema', '影城': 'cinema', '剧院': 'cinema', |
| | | '购房优惠': 'housing', housing: 'housing', |
| | | '购车服务': 'car', car: 'car', |
| | | '家电服务': 'appliance', appliance: 'appliance', |
| | | '数码服务': 'digital', digital: 'digital', |
| | | '文化服务': 'culture', culture: 'culture', book: 'culture', |
| | | '电信服务': 'telecom', telecom: 'telecom', |
| | | '口腔治疗保健': 'dental', dental: 'dental', |
| | | } |
| | | const productTabTypeCodes = new Set(['food', 'scenic', 'hotel', 'dental', 'medical']) |
| | | |
| | | function resolveMerchantTypeCode(row: Merchant) { |
| | | const raw = String(row.merchantTypeCode || row.merchantType || '').trim() |
| | | return merchantTypeAliases[raw] || raw |
| | | } |
| | | |
| | | function parseMerchantRights(config: Merchant['merchantRightsConfig']) { |
| | | if (Array.isArray(config)) return config |
| | | if (typeof config !== 'string' || !config.trim()) return [] |
| | | try { |
| | | const parsed = JSON.parse(config) |
| | | return Array.isArray(parsed) ? parsed : [] |
| | | } catch { |
| | | return [] |
| | | } |
| | | } |
| | | |
| | | function isCarWashMiniProgramJump(row: Merchant) { |
| | | const linkType = String(row.carWashExternalLinkType || '').trim() |
| | | if (linkType === 'miniProgram') return true |
| | | if (linkType === 'normal') return false |
| | | const jump = parseMerchantRights(row.merchantRightsConfig).find((entry) => ( |
| | | entry?.externalLinkType |
| | | || entry?.externalMiniProgramShortLink |
| | | || entry?.externalMiniProgramPath |
| | | || entry?.externalWebUrl |
| | | )) |
| | | if (!jump) return false |
| | | return jump.externalLinkType === 'miniProgram' || Boolean(String(jump.externalMiniProgramShortLink || '').trim()) |
| | | } |
| | | |
| | | function supportsGroupBuying(row: Merchant) { |
| | | const code = resolveMerchantTypeCode(row) |
| | | if (code === 'carWash') return !isCarWashMiniProgramJump(row) |
| | | return productTabTypeCodes.has(code) |
| | | } |
| | | |
| | | function resetForm() { |
| | | Object.assign(form, emptyForm()) |
| | | editingId.value = undefined |
| | |
| | | <el-button v-permission="'admin.merchants.disable'" link :type="row.status === 0 ? 'success' : 'danger'" @click="toggleStatus(row)"> |
| | | {{ row.status === 0 ? '启用' : '禁用' }} |
| | | </el-button> |
| | | <el-button link type="primary" :icon="Goods" @click="openProducts(row)">团购管理</el-button> |
| | | <el-button v-if="supportsGroupBuying(row)" link type="primary" :icon="Goods" @click="openProducts(row)">团购管理</el-button> |
| | | <el-button v-permission="'admin.merchants.edit'" link type="primary" :icon="Edit" @click="openEdit(row)">编辑</el-button> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | int size |
| | | ) { |
| | | StringBuilder sql = new StringBuilder(""" |
| | | select m.id, m.merchant_name, m.merchant_type, m.contact_name, m.contact_mobile, |
| | | select m.id, m.merchant_name, m.merchant_type, m.merchant_type_code, m.contact_name, m.contact_mobile, |
| | | m.logo_url, m.cover_url, m.address, m.is_recommend, m.sort_no, m.status, |
| | | m.created_at, |
| | | m.created_at, m.merchant_rights_config, |
| | | (select s.external_link_type from transport_car_wash_service s |
| | | where s.merchant_id = m.id and s.deleted = 0 order by s.id desc limit 1) as car_wash_external_link_type, |
| | | (select count(*) from merchant_product p where p.merchant_id = m.id and p.deleted = 0) as product_count, |
| | | (select count(*) from benefit_order o where o.merchant_id = m.id and o.deleted = 0) as order_count, |
| | | 0 as reservation_count |
| | |
| | | @GetMapping("/topics/travel/merchants") |
| | | public ApiResponse<PageResult<Map<String, Object>>> travelMerchants( |
| | | @RequestParam(defaultValue = "1") int page, |
| | | @RequestParam(defaultValue = "4") int size |
| | | @RequestParam(defaultValue = "4") int size, |
| | | @RequestParam(required = false) Long talentId |
| | | ) { |
| | | return ApiResponse.success(portalService.travelMerchants(page, size)); |
| | | return ApiResponse.success(portalService.travelMerchants(page, size, talentId)); |
| | | } |
| | | |
| | | @GetMapping("/topics/stay") |
| | |
| | | List<Map<String, Object>> records = result.records().stream().map(row -> { |
| | | Map<String, Object> item = enrichMerchantSearchRow(row); |
| | | Long merchantId = item.get("id") instanceof Number id ? id.longValue() : null; |
| | | item.put("couponAvailable", merchantId != null && hasActiveMerchantCoupon(merchantId)); |
| | | item.put("discountText", merchantId == null ? "" : merchantLowestTalentDiscount(merchantId, talentId)); |
| | | boolean couponAvailable = merchantId != null && hasActiveMerchantCoupon(merchantId); |
| | | String discountText = merchantId == null ? "" : merchantLowestTalentDiscount(merchantId, talentId); |
| | | item.put("couponAvailable", couponAvailable); |
| | | item.put("discountText", discountText); |
| | | item.put("benefitTags", merchantBenefitTags("food", couponAvailable, discountText, null)); |
| | | return item; |
| | | }).toList(); |
| | | return new PageResult<>(records, result.page(), result.size(), result.total()); |
| | |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> travelMerchants(int page, int size) { |
| | | return travelMerchants(page, size, null); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> travelMerchants(int page, int size, Long talentId) { |
| | | String merchantWhere = " where deleted = 0 and merchant_type_code = 'scenic'"; |
| | | PageResult<Map<String, Object>> result = page(""" |
| | | select id, merchant_name, merchant_type_code as merchant_type, contact_name, contact_mobile, logo_url, cover_url, address, |
| | |
| | | """ + merchantWhere + " order by sort_no asc, id asc", |
| | | "select count(*) from merchant" + merchantWhere, page, size); |
| | | List<Map<String, Object>> records = result.records().stream().map(row -> { |
| | | Map<String, Object> normalized = new LinkedHashMap<>(row); |
| | | normalized.put("merchantType", "scenic"); |
| | | return enrichMerchantSearchRow(normalized); |
| | | Map<String, Object> item = enrichMerchantSearchRow(row); |
| | | item.put("merchantType", "scenic"); |
| | | Long merchantId = item.get("id") instanceof Number id ? id.longValue() : null; |
| | | boolean couponAvailable = merchantId != null && hasActiveMerchantCoupon(merchantId); |
| | | String discountText = merchantId == null ? "" : merchantLowestTalentDiscount(merchantId, talentId); |
| | | item.put("couponAvailable", couponAvailable); |
| | | item.put("discountText", discountText); |
| | | item.put("benefitTags", merchantBenefitTags("scenic", couponAvailable, discountText, null)); |
| | | return item; |
| | | }).toList(); |
| | | return new PageResult<>(records, result.page(), result.size(), result.total()); |
| | | } |
| | |
| | | """ + merchantWhere + " order by sort_no asc, id asc", |
| | | "select count(*) from merchant" + merchantWhere, page, size); |
| | | List<Map<String, Object>> records = result.records().stream().map(row -> { |
| | | Map<String, Object> normalized = new LinkedHashMap<>(row); |
| | | normalized.put("merchantType", responseMerchantType); |
| | | String cardLevel = activeProductCardLevel(talentId); |
| | | Map<String, Object> right = matchingMerchantRight(merchantRightsConfigs(row.get("merchantRightsConfig")), cardLevel, activeTalentSourceAreaType(talentId), true); |
| | | normalized.put("benefitTags", stayBenefitTags(right, row)); |
| | | return enrichMerchantSearchRow(normalized); |
| | | Map<String, Object> item = enrichMerchantSearchRow(row); |
| | | item.put("merchantType", responseMerchantType); |
| | | Long merchantId = item.get("id") instanceof Number id ? id.longValue() : null; |
| | | boolean couponAvailable = merchantId != null && hasActiveMerchantCoupon(merchantId); |
| | | String discountText = merchantId == null ? "" : merchantLowestTalentDiscount(merchantId, talentId); |
| | | Map<String, Object> right = matchingMerchantRight( |
| | | merchantRightsConfigs(row.get("merchantRightsConfig")), |
| | | activeProductCardLevel(talentId), |
| | | activeTalentSourceAreaType(talentId), |
| | | true); |
| | | item.put("couponAvailable", couponAvailable); |
| | | item.put("discountText", discountText); |
| | | item.put("benefitTags", merchantBenefitTags(responseMerchantType, couponAvailable, discountText, right)); |
| | | item.remove("merchantRightsConfig"); |
| | | return item; |
| | | }).toList(); |
| | | return new PageResult<>(records, result.page(), result.size(), result.total()); |
| | | } |
| | | |
| | | private List<String> stayBenefitTags(Map<String, Object> right, Map<String, Object> merchant) { |
| | | Object merchantId = merchant.get("id"); |
| | | boolean couponAvailable = merchantId instanceof Number id && hasActiveMerchantCoupon(id.longValue()); |
| | | return stayBenefitTags(right, couponAvailable); |
| | | } |
| | | |
| | | private List<String> stayBenefitTags(Map<String, Object> right, boolean couponAvailable) { |
| | | List<String> tags = new ArrayList<>(); |
| | | if (couponAvailable) tags.add("券"); |
| | | if (right == null) return tags; |
| | | if (booleanValue(right.get("freeVoucherEnabled"))) { |
| | | String times = textOrDefault(right.get("freeVoucherTimes"), ""); |
| | | if (!times.isBlank()) tags.add("免费" + times + "次"); |
| | | } |
| | | if (booleanValue(right.get("storeConsume")) && booleanValue(right.get("consumeEnabled"))) { |
| | | String type = textOrDefault(right.get("consumeType"), ""); |
| | | String discount = textOrDefault(right.get("consumeDiscount"), ""); |
| | | if ("折扣".equals(type) && !discount.isBlank()) tags.add(discount + "折"); |
| | | } |
| | | return tags; |
| | | } |
| | | |
| | | private List<String> merchantListBenefitTags(Map<String, Object> item, Map<String, Object> row, Long talentId) { |
| | | String merchantType = MerchantTypeCatalog.canonicalCode(textOrDefault(item.get("merchantType"), "")); |
| | | boolean couponAvailable = booleanValue(item.get("couponAvailable")); |
| | | String discountText = textOrDefault(item.get("discountText"), "").trim(); |
| | | String serviceLabel = textOrDefault(row.get("serviceLabel"), "").trim(); |
| | | List<String> tags = new ArrayList<>(); |
| | | |
| | | if ("food".equals(merchantType)) { |
| | | if (couponAvailable) tags.add("券"); |
| | | if (!discountText.isBlank()) tags.add(discountText); |
| | | return tags; |
| | | } |
| | | if ("cinema".equals(merchantType)) { |
| | | if (couponAvailable) tags.add("券"); |
| | | return tags; |
| | | } |
| | | // The travel module displays the calculated discount, not the merchant's |
| | | // service label, so search results must use the same source. |
| | | if ("scenic".equals(merchantType)) { |
| | | if (!discountText.isBlank()) tags.add(discountText); |
| | | return tags; |
| | | } |
| | | |
| | | String cardLevel = activeProductCardLevel(talentId); |
| | | String sourceAreaType = activeTalentSourceAreaType(talentId); |
| | | Map<String, Object> right = matchingMerchantRight( |
| | | merchantRightsConfigs(row.get("merchantRightsConfig")), cardLevel, sourceAreaType, true); |
| | | if ("hotel".equals(merchantType) || "apartment".equals(merchantType)) { |
| | | tags.addAll(stayBenefitTags(right, couponAvailable)); |
| | | if (tags.isEmpty()) tags.add(!discountText.isBlank() ? discountText : "人才专享"); |
| | | return tags; |
| | | if (usesMerchantDetailHeaderTags(merchantType) || "cinema".equals(merchantType)) { |
| | | return merchantBenefitTags(merchantType, couponAvailable, discountText, right); |
| | | } |
| | | |
| | | boolean shoppingType = MerchantTypeCatalog.find(merchantType) |
| | | .map(type -> type.shoppingCategoryKey() != null) |
| | | .orElse(false); |
| | | if (shoppingType) { |
| | | List<String> tags = new ArrayList<>(); |
| | | if (couponAvailable) tags.add("券"); |
| | | if ("culture".equals(merchantType)) { |
| | | if (right != null && booleanValue(right.get("storeConsume")) && booleanValue(right.get("consumeEnabled"))) { |
| | | tags.add("到店消费"); |
| | | } |
| | | } else if (right != null) { |
| | | if (right != null) { |
| | | String configuredDiscount = merchantRightDiscountText(right); |
| | | if (!configuredDiscount.isBlank()) tags.add(configuredDiscount); |
| | | } |
| | | return tags; |
| | | } |
| | | |
| | | String serviceLabel = textOrDefault(row.get("serviceLabel"), "").trim(); |
| | | String categoryTag = !serviceLabel.isBlank() ? serviceLabel : discountText; |
| | | if (categoryTag.isBlank()) categoryTag = "人才专享"; |
| | | tags.add(categoryTag); |
| | | return List.of(categoryTag); |
| | | } |
| | | |
| | | private boolean usesMerchantDetailHeaderTags(String merchantType) { |
| | | return List.of("food", "scenic", "hotel", "apartment", "dental", "culture").contains(merchantType); |
| | | } |
| | | |
| | | private List<String> merchantBenefitTags( |
| | | String merchantType, |
| | | boolean couponAvailable, |
| | | String productDiscountText, |
| | | Map<String, Object> right |
| | | ) { |
| | | String type = MerchantTypeCatalog.canonicalCode(merchantType); |
| | | if ("cinema".equals(type)) { |
| | | return couponAvailable ? List.of("券") : List.of(); |
| | | } |
| | | List<String> tags = new ArrayList<>(); |
| | | if (couponAvailable) tags.add("券"); |
| | | String discount = textOrDefault(productDiscountText, "").trim(); |
| | | if ("人才专享".equals(discount)) discount = ""; |
| | | boolean hotelLike = "hotel".equals(type) || "apartment".equals(type); |
| | | if (hotelLike && right != null && booleanValue(right.get("freeVoucherEnabled"))) { |
| | | String times = textOrDefault(right.get("freeVoucherTimes"), "").trim(); |
| | | discount = times.isBlank() ? "免费" : "免费" + times + "次"; |
| | | } |
| | | if (!discount.isBlank() && !tags.contains(discount)) tags.add(discount); |
| | | if (hotelLike && right != null && booleanValue(right.get("storeConsume")) && booleanValue(right.get("consumeEnabled"))) { |
| | | String consumeType = textOrDefault(right.get("consumeType"), ""); |
| | | String consumeDiscount = textOrDefault(right.get("consumeDiscount"), "").trim(); |
| | | String consumeCoupon = textOrDefault(right.get("consumeCoupon"), "").trim(); |
| | | String consumeTag = ""; |
| | | if ("折扣".equals(consumeType) && !consumeDiscount.isBlank()) consumeTag = consumeDiscount + "折"; |
| | | else if ("优惠".equals(consumeType) && !consumeCoupon.isBlank()) consumeTag = "减" + consumeCoupon; |
| | | if (!consumeTag.isBlank() && !tags.contains(consumeTag)) tags.add(consumeTag); |
| | | } |
| | | return tags; |
| | | } |
| | | |
| | |
| | | boolean couponAvailable = booleanValue(item.get("couponAvailable")); |
| | | item.put("couponAvailable", couponAvailable); |
| | | item.put("discountText", couponAvailable ? "券" : ""); |
| | | item.put("benefitTags", merchantBenefitTags("cinema", couponAvailable, "", null)); |
| | | return item; |
| | | }).toList(); |
| | | return new PageResult<>(records, result.page(), result.size(), result.total()); |
| | |
| | | item.put("storeConsumeAvailable", configuredRight != null |
| | | && booleanValue(configuredRight.get("storeConsume")) |
| | | && booleanValue(configuredRight.get("consumeEnabled"))); |
| | | String merchantType = MerchantTypeCatalog.canonicalCode(textOrDefault(item.get("merchantType"), "")); |
| | | boolean couponAvailable = booleanValue(item.get("couponAvailable")); |
| | | if (configuredRight != null) { |
| | | item.put("discountText", merchantRightDiscountText(configuredRight)); |
| | | item.put("rightsAvailable", true); |
| | |
| | | } else { |
| | | item.put("discountText", textOrDefault(item.get("serviceLabel"), "")); |
| | | item.put("rightsAvailable", true); |
| | | } |
| | | if (usesMerchantDetailHeaderTags(merchantType)) { |
| | | String productDiscount = merchantId == null ? "" : merchantLowestTalentDiscount(merchantId, talentId); |
| | | item.put("discountText", productDiscount); |
| | | item.put("benefitTags", merchantBenefitTags(merchantType, couponAvailable, productDiscount, configuredRight)); |
| | | } else { |
| | | List<String> tags = new ArrayList<>(); |
| | | if (couponAvailable) tags.add("券"); |
| | | String storeDiscount = textOrDefault(item.get("discountText"), "").trim(); |
| | | if (!storeDiscount.isBlank() && !"人才专享".equals(storeDiscount)) tags.add(storeDiscount); |
| | | item.put("benefitTags", tags); |
| | | } |
| | | item.remove("merchantRightsConfig"); |
| | | return item; |
| | |
| | | merchant.put("consumeCoupon", consumeCoupon); |
| | | merchant.put("storeConsume", storeConsume); |
| | | merchant.put("consumeEnabled", consumeEnabled); |
| | | merchant.put("benefitTags", merchantBenefitTags( |
| | | textOrDefault(merchant.get("merchantType"), ""), |
| | | booleanValue(merchant.get("couponAvailable")), |
| | | textOrDefault(merchant.get("discountText"), ""), |
| | | configuredRight)); |
| | | merchant.put("usageInstructions", merchantUsageInstructions( |
| | | id, merchant.get("merchantRightsConfig"), cardLevel, merchant.get("usageInstructions"))); |
| | | List<String> heroImages = new ArrayList<>(); |
| | |
| | | |
| | | assertThat(foodTags).containsExactly("券", "8.5折"); |
| | | assertThat(cinemaTags).isEmpty(); |
| | | |
| | | List<String> scenicTags = (List<String>) method.invoke(service, |
| | | Map.of("merchantType", "scenic", "couponAvailable", true, "discountText", "8.8折"), |
| | | Map.of(), null); |
| | | List<String> cultureTags = (List<String>) method.invoke(service, |
| | | Map.of("merchantType", "culture", "couponAvailable", true, "discountText", ""), |
| | | Map.of(), null); |
| | | assertThat(scenicTags).containsExactly("券", "8.8折"); |
| | | assertThat(cultureTags).containsExactly("券"); |
| | | } |
| | | |
| | | @Test |
| | | @SuppressWarnings("unchecked") |
| | | void merchantDetailHeaderTagsDriveListTags() throws Exception { |
| | | Method method = PortalService.class.getDeclaredMethod( |
| | | "merchantBenefitTags", String.class, boolean.class, String.class, Map.class); |
| | | method.setAccessible(true); |
| | | |
| | | List<String> foodTags = (List<String>) method.invoke(service, "food", true, "8.8折", null); |
| | | List<String> hotelTags = (List<String>) method.invoke(service, "hotel", true, "7折", Map.of( |
| | | "freeVoucherEnabled", true, |
| | | "freeVoucherTimes", "2", |
| | | "storeConsume", true, |
| | | "consumeEnabled", true, |
| | | "consumeType", "折扣", |
| | | "consumeDiscount", "8.5")); |
| | | |
| | | assertThat(foodTags).containsExactly("券", "8.8折"); |
| | | assertThat(hotelTags).containsExactly("券", "免费2次", "8.5折"); |
| | | } |
| | | } |