fix: align merchant list benefit tags
| | |
| | | <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> |
| | |
| | | import { apiEnvironment, isApiConfigured } from '../../config/api.uts' |
| | | import { searchMerchants } 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 hasSearched = ref(false) |
| | | const isAllMode = ref(false) |
| | | const pageSize = 6 |
| | | const allModePageSize = 100 |
| | | const talentId = ref('') |
| | | const merchantPool = ref([]) |
| | | const visibleMerchants = ref([]) |
| | | const loadedCount = ref(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 ? item.discountText as string : 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) : '', |
| | | sort: item.sort != null ? Number(item.sort) : 0, |
| | |
| | | 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) |
| | |
| | | failLoadMoreOnce.value = previewState == 'loadMoreError' |
| | | } |
| | | loadHistories() |
| | | const session = getSession() |
| | | if (session != null && session.talentId != null) talentId.value = '' + session.talentId |
| | | if (options != null && options['mode'] == 'all') { |
| | | isAllMode.value = true |
| | | if (isApiConfigured()) loadRemoteMerchants(false) |
| | |
| | | .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; } |
| | |
| | | <image class="merchant-image" :src="item.image" mode="aspectFill" @error="replaceImage(item)"></image> |
| | | <view class="benefit-info"> |
| | | <text class="benefit-name">{{item.name}}</text> |
| | | <view v-if="item.hasCoupon || item.discount != ''" class="tag-row"><text v-if="item.hasCoupon" class="coupon-tag">券</text><text v-if="item.discount != ''" :class="item.hasCoupon ? 'discount-tag' : 'discount-tag no-coupon'">{{item.discount}}</text></view> |
| | | <view v-if="item.tags != null && item.tags.length > 0" class="tag-row"><text v-for="tag in item.tags" :key="tag" :class="tag == '券' ? 'coupon-tag' : 'discount-tag'">{{tag}}</text></view> |
| | | <view class="address-row"><image src="/static/benefits-assets/map-pin.svg"></image><text>{{item.address}}</text></view> |
| | | </view> |
| | | </view> |
| | |
| | | const item: any = records[index] |
| | | const rawType = item.merchantType != null ? item.merchantType as string : 'food' |
| | | const merchantType = rawType == '餐饮服务' ? 'food' : rawType |
| | | const hasCoupon = item.couponAvailable === true || Number(item.couponAvailable) == 1 |
| | | const discount = item.discountText != null ? item.discountText as string : '' |
| | | 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) |
| | | } |
| | | if (!hasConfiguredTags) { |
| | | if (hasCoupon) tags.push('券') |
| | | if (discount != '' && discount != '券') tags.push(discount) |
| | | } |
| | | mapped.push({ |
| | | id: '' + item.id, |
| | | name: item.merchantName != null ? item.merchantName as string : '人才权益商户', |
| | | merchantType: merchantType, |
| | | hasCoupon: item.couponAvailable === true || Number(item.couponAvailable) == 1, |
| | | discount: item.discountText != null ? item.discountText as string : '', |
| | | hasCoupon: hasCoupon, |
| | | discount: discount, |
| | | tags: tags, |
| | | address: item.address != null ? item.address as string : '', |
| | | image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage |
| | | }) |
| | |
| | | .benefit-info { height: 184rpx; flex: 1; min-width: 0; box-sizing: border-box; padding: 8rpx 0 7rpx 26rpx; justify-content: space-between; } |
| | | .benefit-name { color: #292929; font-size: 29rpx; line-height: 40rpx; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| | | .tag-row { flex-direction: row; align-items: center; } |
| | | .tag-row text { margin-right: 11rpx; } |
| | | .coupon-tag { padding: 6rpx 8rpx; border-radius: 6rpx; background-color: #FFF0EC; color: #FF5D37; font-size: 21rpx; } |
| | | .discount-tag { margin-left: 11rpx; padding: 6rpx 9rpx; border-radius: 6rpx; background-color: #FFF5E7; color: #FF7C2B; font-size: 21rpx; }.discount-tag.no-coupon { margin-left: 0; } |
| | | .discount-tag { padding: 6rpx 9rpx; border-radius: 6rpx; background-color: #FFF5E7; color: #FF7C2B; font-size: 21rpx; } |
| | | .address-row { display: flex; min-width: 0; box-sizing: border-box; padding: 7rpx 10rpx; border-radius: 7rpx; background-image: linear-gradient(90deg, rgba(239,240,242,.98) 0%, rgba(242,243,245,.76) 58%, rgba(255,255,255,0) 100%); flex-direction: row; align-items: center; } |
| | | .address-row image { width: 27rpx; height: 27rpx; margin-right: 9rpx; flex-shrink: 0; } |
| | | .address-row text { flex: 1; color: #7E8490; font-size: 21rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| | |
| | | export const getTicketPackageClaim = (claimId: string, talentId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appTicketPackageClaims') + encodeURIComponent(claimId), method: 'GET', skipAuth: true, data: { talentId: Number(talentId) } }, onSuccess, onError) |
| | | export const getCinemaCodeImageUrl = (claimId: string): string => resolveApiUrl(getApiEndpoint('appBenefitVerificationCodes') + '/' + encodeURIComponent(claimId) + '/image') |
| | | |
| | | export const searchMerchants = (keyword: string, categoryId: string, mode: string, page: number, size: number, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => { |
| | | export const searchMerchants = (keyword: string, categoryId: string, mode: string, talentId: string, page: number, size: number, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => { |
| | | const data: any = { keyword: keyword, categoryId: categoryId, mode: mode, page: page, size: size, sortField: 'sort', sortOrder: 'asc' } |
| | | if (talentId != '') data.talentId = Number(talentId) |
| | | return request<any>({ |
| | | path: getApiEndpoint('appMerchants'), |
| | | method: 'GET', |
| | | skipAuth: true, |
| | | data: { keyword: keyword, categoryId: categoryId, mode: mode, page: page, size: size, sortField: 'sort', sortOrder: 'asc' } |
| | | data: data |
| | | }, onSuccess, onError) |
| | | } |
| | | |
| | |
| | | } |
| | | 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, |
| | | is_recommend, sort_no, longitude, latitude, business_status, |
| | | is_recommend, sort_no, longitude, latitude, business_status, service_label, merchant_rights_config, |
| | | (exists(select 1 from merchant_coupon c |
| | | where c.merchant_id = merchant.id and c.deleted = 0 and c.status = 1 |
| | | and (c.valid_start is null or c.valid_start <= current_date) |
| | | and (c.valid_end is null or c.valid_end >= current_date)) |
| | | or (merchant.merchant_type_code = 'cinema' and exists(select 1 from ticket_package tp |
| | | where tp.deleted = 0 and tp.status = 1 |
| | | and (tp.valid_start is null or tp.valid_start <= current_date) |
| | | and (tp.valid_end is null or tp.valid_end >= current_date) |
| | | and (tp.merchant_id = merchant.id or exists ( |
| | | select 1 from ticket_package_merchant tpm |
| | | where tpm.package_id = tp.id and tpm.merchant_id = merchant.id and tpm.deleted = 0 |
| | | ))))) as coupon_available, |
| | | (select min(p.sale_price) from merchant_product p where p.merchant_id = merchant.id and p.deleted = 0 and p.status = 1 and p.audit_status = 2) as sale_price, |
| | | (select min(p.original_price) from merchant_product p where p.merchant_id = merchant.id and p.deleted = 0 and p.status = 1 and p.audit_status = 2) as original_price |
| | | from merchant |
| | |
| | | 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("couponAvailable", booleanValue(row.get("couponAvailable"))); |
| | | item.put("discountText", merchantId == null ? "" : merchantLowestTalentDiscount(merchantId, talentId)); |
| | | item.put("benefitTags", merchantListBenefitTags(item, row, talentId)); |
| | | 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) { |
| | | List<String> tags = new ArrayList<>(); |
| | | Object merchantId = merchant.get("id"); |
| | | if (merchantId instanceof Number id && hasActiveMerchantCoupon(id.longValue())) tags.add("券"); |
| | | 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"), ""); |
| | |
| | | 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; |
| | | } |
| | | |
| | | 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; |
| | | } |
| | | |
| | | boolean shoppingType = MerchantTypeCatalog.find(merchantType) |
| | | .map(type -> type.shoppingCategoryKey() != null) |
| | | .orElse(false); |
| | | if (shoppingType) { |
| | | 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) { |
| | | String configuredDiscount = merchantRightDiscountText(right); |
| | | if (!configuredDiscount.isBlank()) tags.add(configuredDiscount); |
| | | } |
| | | return tags; |
| | | } |
| | | |
| | | String categoryTag = !serviceLabel.isBlank() ? serviceLabel : discountText; |
| | | if (categoryTag.isBlank()) categoryTag = "人才专享"; |
| | | tags.add(categoryTag); |
| | | return tags; |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> entertainmentTopic() { |
| | | Map<String, Object> topic = one(""" |
| | |
| | | |
| | | assertThat(discount).isEqualTo("8.5折"); |
| | | } |
| | | |
| | | @Test |
| | | @SuppressWarnings("unchecked") |
| | | void recommendedMerchantTagsFollowCategoryListRules() throws Exception { |
| | | Method method = PortalService.class.getDeclaredMethod( |
| | | "merchantListBenefitTags", Map.class, Map.class, Long.class); |
| | | method.setAccessible(true); |
| | | |
| | | List<String> foodTags = (List<String>) method.invoke(service, |
| | | Map.of("merchantType", "food", "couponAvailable", true, "discountText", "8.5折"), |
| | | Map.of(), null); |
| | | List<String> cinemaTags = (List<String>) method.invoke(service, |
| | | Map.of("merchantType", "cinema", "couponAvailable", false, "discountText", "7折"), |
| | | Map.of(), null); |
| | | |
| | | assertThat(foodTags).containsExactly("券", "8.5折"); |
| | | assertThat(cinemaTags).isEmpty(); |
| | | } |
| | | } |