| | |
| | | </view> |
| | | |
| | | <view class="content-sheet"> |
| | | <view class="service-row"><text class="service-name">{{benefitTitle}}</text><text class="free-label">{{freeLabel}}</text></view> |
| | | <view class="service-row"><text class="service-name">{{benefitTitle}}</text></view> |
| | | <view class="section-title"><text>权益详情</text><view></view></view> |
| | | <scroll-view class="org-scroll" scroll-y :show-scrollbar="false"> |
| | | <view v-if="listLoading" class="list-state"><view class="loading-spinner"></view><text>正在加载服务机构</text></view> |
| | |
| | | const serviceId = ref('') |
| | | const heroImage = ref('') |
| | | const benefitTitle = ref('行政审批') |
| | | const freeLabel = ref('免费') |
| | | const benefitAvailable = ref(true) |
| | | const unavailableReason = ref('该权益暂未开放') |
| | | const orgs = ref<any[]>([]) |
| | |
| | | if (serviceId.value == '') { listLoading.value = false; listError.value = true; return } |
| | | getAdminApprovalBenefit(serviceId.value, (data: any) => { |
| | | benefitTitle.value = data.title || '行政审批' |
| | | freeLabel.value = data.freeLabel || '免费' |
| | | heroImage.value = data.heroImage != null && data.heroImage != '' ? resolveApiUrl(data.heroImage as string) : '' |
| | | benefitAvailable.value = data.available === true |
| | | unavailableReason.value = data.unavailableReason || '该权益暂未开放' |
| | |
| | | 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 { resolveTalentId } from '../../utils/talent-id.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | import { openMerchantDetail } from '../../utils/merchant-navigation.uts' |
| | | |
| | |
| | | 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 && item.benefitTags.length > 0 |
| | | 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++) { |
| | |
| | | failLoadMoreOnce.value = previewState == 'loadMoreError' |
| | | } |
| | | loadHistories() |
| | | const session = getSession() |
| | | if (session != null && session.talentId != null) talentId.value = '' + session.talentId |
| | | talentId.value = resolveTalentId() |
| | | if (options != null && options['mode'] == 'all') { |
| | | isAllMode.value = true |
| | | if (isApiConfigured()) loadRemoteMerchants(false) |
| | |
| | | import { isApiConfigured } from '../../config/api.uts' |
| | | import { getMerchants } from '../../services/portal.uts' |
| | | import { resolveApiUrl } from '../../services/http.uts' |
| | | import { getSession } from '../../services/session.uts' |
| | | import { resolveTalentId } from '../../utils/talent-id.uts' |
| | | import { openMerchantDetail } from '../../utils/merchant-navigation.uts' |
| | | |
| | | const navTop = ref(28) |
| | |
| | | failInitialOnce.value = previewState == 'initialError' |
| | | failLoadMoreOnce.value = previewState == 'loadMoreError' |
| | | } |
| | | const session = getSession() |
| | | if (session != null && session.talentId != null) talentId.value = '' + session.talentId |
| | | talentId.value = resolveTalentId() |
| | | loadInitialBenefits() |
| | | }) |
| | | |
| | |
| | | |
| | | <script setup lang="uts"> |
| | | import { getChildrenEntrustmentBenefit, getChildrenEntrustmentSchools } from '../../services/portal.uts' |
| | | import { apiEnvironment } from '../../config/api.uts' |
| | | import { resolveApiUrl } from '../../services/http.uts' |
| | | import { getSession } from '../../services/session.uts' |
| | | import { resolveTalentId } from '../../utils/talent-id.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | const navTop = ref(28) |
| | |
| | | navRight.value = info.windowWidth - capsule.right |
| | | // #endif |
| | | serviceId.value = options != null && options['serviceId'] != null ? options['serviceId'] as string : '' |
| | | const session = getSession() |
| | | if (session != null && session.talentId != null) talentId.value = session.talentId as string |
| | | if (talentId.value == '' && apiEnvironment == 'development') talentId.value = '2' |
| | | talentId.value = resolveTalentId() |
| | | loadBenefit() |
| | | loadSchools() |
| | | }) |
| | |
| | | const canNavigate = (item: any): boolean => Number(item.latitude) > 0 && Number(item.longitude) > 0 |
| | | const openLocation = (item: any) => { if (!canNavigate(item)) { uni.showToast({ title: '学校暂未配置位置', icon: 'none' }); return }; uni.openLocation({ latitude: Number(item.latitude), longitude: Number(item.longitude), name: item.name, address: item.address, scale: 18, fail: () => uni.showToast({ title: '暂时无法打开地图导航', icon: 'none' }) }) } |
| | | const callSchool = (item: any) => { if (item.phone == null || item.phone == '') return; uni.makePhoneCall({ phoneNumber: item.phone, fail: () => uni.showToast({ title: '暂时无法拨打电话', icon: 'none' }) }) } |
| | | const showTalentCode = () => { const session = getSession(); const talentId = session != null && session.talentId != null ? session.talentId as string : (apiEnvironment == 'development' ? '2' : ''); uni.navigateTo({ url: '/pages/talent-code/talent-code?talentId=' + encodeURIComponent(talentId) + '&merchantId=' + encodeURIComponent(serviceId.value) + '&scene=children' }) } |
| | | const showTalentCode = () => { const talentId = resolveTalentId(); uni.navigateTo({ url: '/pages/talent-code/talent-code?talentId=' + encodeURIComponent(talentId) + '&merchantId=' + encodeURIComponent(serviceId.value) + '&scene=children' }) } |
| | | const goChildrenInfo = () => uni.navigateTo({ url: '/pages/children-info/children-info' }) |
| | | </script> |
| | | |
| | |
| | | <view v-if="detailLoading" class="detail-state"><view class="loading-spinner"></view><text>正在加载影城信息</text></view> |
| | | <view v-else-if="detailError" class="detail-state error-state"><text>影城信息加载失败</text><text class="retry-text" @tap="loadCinema">重新加载</text></view> |
| | | <view v-else class="merchant-detail"> |
| | | <view class="merchant-info"><view class="title-row"><text class="merchant-name">{{cinemaName}}</text><view class="coupon-badge"><text>券</text></view></view><view class="service-tag"><text>{{talentCardTag}}</text></view><view class="address-row"><image class="address-icon" src="/static/benefits-assets/map-pin.svg" mode="aspectFit"></image><text class="address-text">{{cinemaAddress}}</text><view class="merchant-actions"><view class="action-button" @tap="openMap"><image src="/static/merchant-assets/navigation.svg" mode="aspectFit"></image></view><view v-if="cinemaMobile != ''" class="action-button" @tap="callCinema"><image src="/static/merchant-assets/phone.svg" mode="aspectFit"></image></view></view></view></view> |
| | | <view class="merchant-info"><view class="title-row"><text class="merchant-name">{{cinemaName}}</text><view class="coupon-badge"><text>券</text></view></view><view v-if="talentCardTag != ''" class="service-tag"><text>{{talentCardTag}}</text></view><view class="address-row"><image class="address-icon" src="/static/benefits-assets/map-pin.svg" mode="aspectFit"></image><text class="address-text">{{cinemaAddress}}</text><view class="merchant-actions"><view class="action-button" @tap="openMap"><image src="/static/merchant-assets/navigation.svg" mode="aspectFit"></image></view><view v-if="cinemaMobile != ''" class="action-button" @tap="callCinema"><image src="/static/merchant-assets/phone.svg" mode="aspectFit"></image></view></view></view></view> |
| | | <view class="tabs"><view class="tab-item" :class="currentTab == 'purchase' ? 'active' : ''" @tap="changeTab('purchase')"><text>消费购买</text><view></view></view><view class="tab-item" :class="currentTab == 'rights' ? 'active' : ''" @tap="changeTab('rights')"><text>权益详情</text><view></view></view></view> |
| | | <view v-if="currentTab == 'purchase'" class="coupon-list"><view v-if="listLoading" class="list-state"><view class="loading-spinner"></view><text>正在加载可领电影券</text></view><view v-else-if="listError" class="list-state error-state"><text>电影券加载失败</text><text class="retry-text" @tap="loadCoupons">重新加载</text></view><view v-else-if="listLoaded && coupons.length == 0" class="list-state"><text>暂无可领取电影券</text></view><view class="coupon-item" v-for="item in coupons" :key="item.id"><image class="coupon-image" :src="item.image" mode="aspectFill" @error="replaceCouponImage(item)"></image><view class="coupon-info"><text class="coupon-name">{{item.name}}</text><view class="stock-row"><text>剩余可领</text><text>{{item.remainingCount}}</text><text>张</text></view><view class="coupon-bottom"><view class="coupon-price"><text>¥</text><text>{{item.price}}</text></view><view class="claim-button" :class="item.claimed ? 'claimed' : (item.soldOut ? 'sold-out' : '')" @tap="requestClaimCoupon(item)"><text>{{item.buttonText}}</text></view></view></view></view></view> |
| | | <view v-else class="rights-panel"><text class="rights-description">{{rightsDescription}}</text></view> |
| | |
| | | </template> |
| | | |
| | | <script setup lang="uts"> |
| | | import { apiEnvironment, isApiConfigured } from '../../config/api.uts' |
| | | import { isApiConfigured } from '../../config/api.uts' |
| | | import { getCinemaMerchant, getTalentCardProfile, getTicketPackages, claimTicketPackage } from '../../services/portal.uts' |
| | | import { resolveApiUrl } from '../../services/http.uts' |
| | | import { getSession } from '../../services/session.uts' |
| | | import { resolveTalentId } from '../../utils/talent-id.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | const fallbackImage = '' |
| | | const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10) |
| | | const isSolidNav = ref(false); const currentTab = ref('purchase'); const merchantId = ref(''); const talentId = ref(''); const cardId = ref('') |
| | | const cinemaName = ref('影城服务'); const cinemaAddress = ref(''); const cinemaHero = ref(fallbackImage); const cinemaMobile = ref(''); const cinemaLatitude = ref(0); const cinemaLongitude = ref(0) |
| | | const talentCardTag = ref('人才卡专享'); const rightsTitle = ref('人才观影专属权益'); const rightsDescription = ref('') |
| | | const talentCardTag = ref(''); const rightsTitle = ref('人才观影专属权益'); const rightsDescription = ref('') |
| | | const detailLoading = ref(true); const detailError = ref(false); const quotaRemaining = ref(0); const coupons = ref<any[]>([]); const listLoading = ref(false); const listLoaded = ref(false); const listError = ref(false) |
| | | const showClaimDialog = ref(false); const isSubmitting = ref(false); const pendingCouponId = ref(''); const pendingCouponStock = ref(0) |
| | | |
| | |
| | | // #endif |
| | | if (options != null && options['merchantId'] != null) merchantId.value = options['merchantId'] as string |
| | | if (options != null && options['name'] != null) cinemaName.value = decodeURIComponent(options['name'] as string) |
| | | const session = getSession(); if (session != null && session.talentId != null) talentId.value = session.talentId as string |
| | | if (talentId.value == '' && apiEnvironment == 'development') talentId.value = '2' |
| | | talentId.value = resolveTalentId() |
| | | if (talentId.value != '') { |
| | | getTalentCardProfile(talentId.value, (data: any) => { |
| | | const card = data != null ? data.card : null |
| | |
| | | cinemaHero.value = data.heroImage != null && data.heroImage != '' ? resolveApiUrl(data.heroImage as string) : fallbackImage |
| | | cinemaMobile.value = data.mobile != null ? data.mobile as string : '' |
| | | cinemaLatitude.value = data.latitude != null ? Number(data.latitude) : 0; cinemaLongitude.value = data.longitude != null ? Number(data.longitude) : 0 |
| | | talentCardTag.value = data.talentCardTag != null ? data.talentCardTag as string : talentCardTag.value; rightsTitle.value = data.rightsTitle != null ? data.rightsTitle as string : rightsTitle.value; rightsDescription.value = data.rightsDescription != null ? data.rightsDescription as string : '以商户说明为准' |
| | | talentCardTag.value = data.talentCardTag != null ? String(data.talentCardTag) : '' |
| | | rightsTitle.value = data.rightsTitle != null ? data.rightsTitle as string : rightsTitle.value |
| | | rightsDescription.value = data.rightsDescription != null ? data.rightsDescription as string : '以商户说明为准' |
| | | loadCoupons(); loadQuota() |
| | | }, () => { detailLoading.value = false; detailError.value = true }) |
| | | } |
| | |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="content"> |
| | | <view class="content" :style="'padding-bottom:' + (navTop + navHeight) + 'px'"> |
| | | <view v-if="accessLoading" class="state-block"><view class="loading-spinner"></view><text>正在加载家属卡服务状态</text></view> |
| | | <view v-else-if="accessError" class="state-block error-state"><text>家属卡服务状态加载失败</text><text class="retry-text" @tap="loadAccess">重新加载</text></view> |
| | | <view v-else class="notice-block"> |
| | |
| | | .back-button-active { opacity: .55; } |
| | | .nav-title { position: absolute; left: 50%; top: 50%; color: #171B22; font-family: sans-serif; font-size: 37rpx; line-height: 48rpx; font-weight: 700; transform: translate(-50%, -50%); white-space: nowrap; } |
| | | .capsule-space { width: 88px; height: 100%; flex-shrink: 0; } |
| | | .content { display: flex; min-height: 0; flex: 1; justify-content: center; align-items: flex-start; } |
| | | .notice-block { display: flex; width: 100%; box-sizing: border-box; padding: 118rpx 48rpx 0; flex-direction: column; align-items: center; } |
| | | .state-block { display: flex; margin-top: 118rpx; color: #7C8797; font-size: 26rpx; flex-direction: row; align-items: center; } |
| | | .content { display: flex; min-height: 0; flex: 1; box-sizing: border-box; flex-direction: column; justify-content: center; align-items: center; } |
| | | .notice-block { display: flex; width: 100%; box-sizing: border-box; padding: 0 48rpx; flex-direction: column; align-items: center; } |
| | | .state-block { display: flex; color: #7C8797; font-size: 26rpx; flex-direction: row; align-items: center; } |
| | | .error-state { flex-direction: column; } |
| | | .retry-text { margin-top: 22rpx; color: #F26A38; } |
| | | .loading-spinner { width: 30rpx; height: 30rpx; margin-right: 13rpx; box-sizing: border-box; border: 3rpx solid #FFD6C4; border-top-color: #FF6C2B; border-radius: 50%; animation: access-loading 720ms linear infinite; } |
| | |
| | | <view v-else-if="detailError" class="detail-state error-state"><text>金融服务加载失败</text><text class="retry-text" @tap="loadDetail">重新加载</text></view> |
| | | <view v-else class="detail-content"> |
| | | <view class="merchant-block"> |
| | | <view class="title-row"><text class="merchant-name">{{merchantName}}</text><text class="discount-label">{{discount}}</text></view> |
| | | <view class="talent-tag"><text>{{applicableCards}}</text></view> |
| | | <view class="title-row"><text class="merchant-name">{{merchantName}}</text></view> |
| | | <view v-if="applicableCards != ''" class="talent-tag"><text>{{applicableCards}}</text></view> |
| | | <view class="address-row"> |
| | | <image class="location-icon" src="/static/benefits-assets/map-pin.svg" mode="aspectFit"></image> |
| | | <text class="merchant-address">{{address}}</text> |
| | |
| | | |
| | | <script setup lang="uts"> |
| | | import { getFinanceMerchantDetail } from '../../services/portal.uts' |
| | | import { apiEnvironment } from '../../config/api.uts' |
| | | import { resolveApiUrl } from '../../services/http.uts' |
| | | import { getSession } from '../../services/session.uts' |
| | | import { resolveTalentId } from '../../utils/talent-id.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | const navTop = ref(28) |
| | |
| | | const merchantId = ref('') |
| | | const talentId = ref('') |
| | | const merchantName = ref('') |
| | | const discount = ref('') |
| | | const applicableCards = ref('') |
| | | const address = ref('') |
| | | const heroImage = ref('') |
| | |
| | | navRight.value = info.windowWidth - capsule.right |
| | | // #endif |
| | | merchantId.value = options != null && options['merchantId'] != null ? options['merchantId'] as string : (options != null && options['serviceId'] != null ? options['serviceId'] as string : '') |
| | | const session = getSession(); if (session != null && session.talentId != null) talentId.value = session.talentId as string; if (talentId.value == '' && apiEnvironment == 'development') talentId.value = '2' |
| | | talentId.value = resolveTalentId() |
| | | loadDetail() |
| | | }) |
| | | const loadDetail = () => { |
| | |
| | | detailError.value = false |
| | | getFinanceMerchantDetail(merchantId.value, talentId.value, (data: any) => { |
| | | merchantName.value = data.merchantName || '' |
| | | discount.value = data.benefitSummary || '请咨询服务专员' |
| | | applicableCards.value = data.applicableCards || '请咨询服务专员' |
| | | applicableCards.value = data.applicableCards != null ? String(data.applicableCards) : '' |
| | | address.value = data.address || '暂未配置机构地址' |
| | | heroImage.value = data.coverUrl != null && data.coverUrl != '' ? resolveApiUrl(data.coverUrl as string) : '' |
| | | benefitDetail.value = data.benefitDetail || '以服务专员说明为准' |
| | |
| | | <image class="merchant-image" :src="item.image" mode="aspectFill" @error="replaceMerchantImage(item)"></image> |
| | | <view class="merchant-info"> |
| | | <text class="merchant-name">{{item.name}}</text> |
| | | <view v-if="!item.available || item.hasCoupon || item.discount != ''" class="tag-row"><text v-if="item.hasCoupon" class="coupon-tag">券</text><text v-if="!item.available || item.discount != ''" class="discount-tag">{{item.available ? item.discount : '已下架'}}</text></view> |
| | | <view v-if="!item.available" class="tag-row"><text class="discount-tag">已下架</text></view> |
| | | <view v-else-if="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> |
| | |
| | | import { isApiConfigured } from '../../config/api.uts' |
| | | import { getFoodTopic, getFoodTopicMerchants } from '../../services/portal.uts' |
| | | import { resolveApiUrl } from '../../services/http.uts' |
| | | import { getSession } from '../../services/session.uts' |
| | | import { resolveTalentId } from '../../utils/talent-id.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | const navTop = ref(28) |
| | |
| | | const currentPage = ref(0) |
| | | const listRequestVersion = ref(0) |
| | | const talentId = ref('') |
| | | const readListTags = (item: any, hasCoupon: boolean, discount: string): string[] => { |
| | | const tags: string[] = [] |
| | | if (item != null && item.benefitTags instanceof Array) { |
| | | const records: any[] = item.benefitTags as any[] |
| | | for (let index = 0; index < records.length; index++) { |
| | | const tag = String(records[index]).trim() |
| | | if (tag != '' && tag != '人才专享' && tags.indexOf(tag) < 0) tags.push(tag) |
| | | } |
| | | return tags |
| | | } |
| | | if (hasCoupon) tags.push('券') |
| | | if (discount != '' && discount != '人才专享') tags.push(discount) |
| | | return tags |
| | | } |
| | | |
| | | onLoad((options: any) => { |
| | | // #ifdef MP-WEIXIN |
| | |
| | | failInitialOnce.value = previewState == 'initialError' |
| | | failLoadMoreOnce.value = previewState == 'loadMoreError' |
| | | } |
| | | const session = getSession() |
| | | if (session != null && session.talentId != null) talentId.value = session.talentId as string |
| | | talentId.value = resolveTalentId() |
| | | loadFoodTopic() |
| | | loadInitialMerchants() |
| | | }) |
| | |
| | | const next = append ? visibleMerchants.value.slice() : [] |
| | | for (let index = 0; index < records.length; index++) { |
| | | const item = records[index] |
| | | const hasCoupon = item.couponAvailable === true || Number(item.couponAvailable) == 1 |
| | | const discount = item.discountText != null && item.discountText != '人才专享' ? item.discountText as string : '' |
| | | next.push({ |
| | | id: '' + item.id, |
| | | name: item.merchantName != null ? item.merchantName as string : '餐饮商户', |
| | | merchantType: item.merchantType != null ? item.merchantType as string : 'food', |
| | | discount: item.discountText != null && item.discountText != '人才专享' ? item.discountText as string : '', |
| | | hasCoupon: item.couponAvailable === true || Number(item.couponAvailable) == 1, |
| | | discount: discount, |
| | | hasCoupon: hasCoupon, |
| | | tags: readListTags(item, hasCoupon, discount), |
| | | address: item.address != null ? item.address as string : '', |
| | | image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackFoodImage, |
| | | available: item.available !== false |
| | |
| | | .merchant-info { display: flex; min-width: 0; min-height: 184rpx; flex: 1; box-sizing: border-box; padding: 4rpx 0 4rpx 24rpx; flex-direction: column; justify-content: space-between; } |
| | | .merchant-name { color: #292929; font-size: 29rpx; line-height: 40rpx; font-weight: 700; } |
| | | .tag-row { display: flex; flex-direction: row; align-items: center; } |
| | | .tag-row text:first-child { margin-left: 0; } |
| | | .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; } |
| | | .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: flex-start; } |
| | |
| | | <view v-if="!merchantAvailable" class="merchant-unavailable"><text>{{merchantUnavailableReason}}</text></view> |
| | | <view v-if="isParkingMerchant" class="parking-title-block"> |
| | | <view class="parking-title-row"><text class="merchant-title">{{merchantName}}</text><view class="round-action" @tap="callMerchant"><image src="/static/merchant-assets/phone.svg"></image></view></view> |
| | | <text class="parking-free">{{parkingBenefitText}}</text> |
| | | <text class="parking-free">{{merchantServiceLabel != '' ? merchantServiceLabel : parkingBenefitText}}</text> |
| | | </view> |
| | | <view v-else-if="isMedicalMerchant" class="merchant-title-row"> |
| | | <text class="merchant-title">{{merchantName}}</text> |
| | |
| | | </view> |
| | | <view v-else class="merchant-title-row"> |
| | | <text class="merchant-title">{{merchantName}}</text> |
| | | <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 v-if="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"> |
| | |
| | | import { apiEnvironment, isApiConfigured } from '../../config/api.uts' |
| | | import { getFuelMerchantDetail, getMedicalPackages, getMerchantDetail, getParkingMerchantDetail, getProducts, getTalentCardProfile, submitFuelMobileRegistration } from '../../services/portal.uts' |
| | | import { resolveApiUrl } from '../../services/http.uts' |
| | | import { getSession } from '../../services/session.uts' |
| | | import { resolveTalentId } from '../../utils/talent-id.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | import { submitBusiness } from '../../services/business.uts' |
| | | |
| | |
| | | const merchantBenefitTags = ref<string[]>([]) |
| | | const merchantBenefitTagsLoaded = ref(false) |
| | | const merchantDisplayTags = computed((): string[] => { |
| | | if (isFuelMerchant.value || ['employment', 'finance', 'approval', 'apartment', 'housing', 'car', 'appliance', 'digital', 'telecom'].indexOf(merchantType.value) >= 0) return [] |
| | | 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 (merchantHasCoupon.value) tags.push('券') |
| | | if (merchantDiscount.value != '') tags.push(merchantDiscount.value) |
| | | if (isHotelMerchant.value && merchantConsumeDiscount.value != '') tags.push(merchantConsumeDiscount.value) |
| | | return tags |
| | | }) |
| | |
| | | } |
| | | return tags |
| | | } |
| | | const normalizeHotelCardLabel = (value: string): string => { |
| | | let label = value.trim() |
| | | const separator = label.indexOf('|') |
| | | if (separator >= 0) label = label.substring(0, separator).trim() |
| | | label = label.replace('(外地)', '').replace('(本地)', '').trim() |
| | | if (label == '') return '' |
| | | return label.endsWith('可用') ? label : label + '可用' |
| | | } |
| | | const merchantAddress = ref('四川省攀枝花市东区炳三区新福路20号中环天地负一层') |
| | | const merchantServiceLabel = ref('到店消费') |
| | | const applicableCards = ref('卓越卡可用') |
| | | const merchantServiceLabel = ref('') |
| | | const applicableCards = ref('') |
| | | const fuelBenefitId = ref('') |
| | | const parkingBenefitId = ref('') |
| | | const parkingBenefitText = ref('免费') |
| | |
| | | } |
| | | } |
| | | } |
| | | const session = getSession() |
| | | if (session != null && session.talentId != null) talentId.value = session.talentId as string |
| | | if (talentId.value == '' && apiEnvironment == 'development') talentId.value = '2' |
| | | talentId.value = resolveTalentId() |
| | | if ((isFuelMerchant.value || isParkingMerchant.value) && talentId.value != '') { |
| | | getTalentCardProfile(talentId.value, (data: any) => { |
| | | const card = data != null ? data.card : null |
| | |
| | | merchantAddress.value = data != null && data.address != null ? data.address as string : '' |
| | | merchantPhone.value = data != null && data.contactMobile != null ? data.contactMobile as string : '' |
| | | merchantAvailable.value = data != null && (data.available === true || Number(data.available) == 1) |
| | | merchantDiscount.value = data != null && data.discountText != null ? data.discountText as string : '请咨询商户' |
| | | merchantServiceLabel.value = data != null && data.serviceLabel != null ? data.serviceLabel as string : '请咨询商户' |
| | | applicableCards.value = data != null && data.applicableCards != null ? data.applicableCards as string : '请咨询商户' |
| | | merchantServiceLabel.value = '' |
| | | merchantDiscount.value = '' |
| | | merchantBenefitTagsLoaded.value = true |
| | | merchantBenefitTags.value = [] |
| | | applicableCards.value = data != null && data.applicableCards != null ? String(data.applicableCards) : '' |
| | | usageInstructions.value = data != null && data.usageInstructions != null ? data.usageInstructions as string : '请咨询商户' |
| | | merchantRightsText.value = data != null && data.rightsText != null ? data.rightsText as string : '请咨询商户' |
| | | groupBuyingEnabled.value = false |
| | |
| | | merchantUnavailableReason.value = data != null && data.unavailableReason != null && data.unavailableReason != '' ? data.unavailableReason as string : '该停车权益暂不可用' |
| | | merchantServiceLabel.value = data != null && data.serviceLabel != null ? data.serviceLabel as string : '停车服务' |
| | | parkingBenefitText.value = data != null && data.parkingType == 'free' ? '免费' : '付费' |
| | | applicableCards.value = data != null && data.applicableCards != null ? data.applicableCards as string : '请咨询商户' |
| | | applicableCards.value = data != null && data.applicableCards != null ? String(data.applicableCards) : '' |
| | | const parkingRightDetail = data != null && data.merchantRight != null && data.merchantRight.detail != null |
| | | ? String(data.merchantRight.detail).trim() |
| | | : '' |
| | |
| | | applyMedicalRightsConfig(data) |
| | | // 普通自助洗车复用餐饮商家的团单展示;外链洗车不会进入此通用详情页。 |
| | | const isNormalCarWash = merchantType.value == 'carWash' |
| | | groupBuyingEnabled.value = merchantType.value == 'culture' |
| | | ? false |
| | | : isNormalCarWash |
| | | ? true |
| | | : (data == null || data.groupBuyingEnabled == null ? groupBuyingEnabled.value : data.groupBuyingEnabled === true || Number(data.groupBuyingEnabled) == 1) |
| | | groupBuyingEnabled.value = merchantType.value == 'food' |
| | | || merchantType.value == 'scenic' |
| | | || merchantType.value == 'hotel' |
| | | || merchantType.value == 'dental' |
| | | || merchantType.value == 'medical' |
| | | || isNormalCarWash |
| | | if (merchantType.value == 'culture') { |
| | | const cultureStoreConsume = data != null && (data.storeConsume === true || Number(data.storeConsume) == 1) && (data.consumeEnabled === true || Number(data.consumeEnabled) == 1) |
| | | merchantServiceLabel.value = cultureStoreConsume ? '到店消费' : '' |
| | | merchantPrimaryTag.value = merchantHasCoupon.value ? '券' : '权益' |
| | | } |
| | | isHotelMerchant.value = merchantType.value == 'hotel' |
| | | merchantConsumeDiscount.value = '' |
| | | if (isHotelMerchant.value) { |
| | | const configuredServiceLabel = data != null && data.serviceLabel != null ? String(data.serviceLabel) : '' |
| | | const configuredCardTag = data != null && data.applicableCards != null ? String(data.applicableCards) : '' |
| | | const hasStoreConsumeTag = configuredCardTag.indexOf('到店消费') >= 0 |
| | | const hotelStoreConsume = (data != null && (data.storeConsume === true || Number(data.storeConsume) == 1) && (data.consumeEnabled === true || Number(data.consumeEnabled) == 1)) || configuredServiceLabel.indexOf('到店') >= 0 || hasStoreConsumeTag |
| | | merchantServiceLabel.value = hotelStoreConsume ? '到店消费' : '住宿服务' |
| | | const normalizedCardLabel = normalizeHotelCardLabel(configuredCardTag) |
| | | if (normalizedCardLabel != '') applicableCards.value = normalizedCardLabel |
| | | const hotelStoreConsume = data != null && (data.storeConsume === true || Number(data.storeConsume) == 1) && (data.consumeEnabled === true || Number(data.consumeEnabled) == 1) |
| | | const consumeType = data != null && data.consumeType != null ? String(data.consumeType) : '' |
| | | const consumeDiscount = data != null && data.consumeDiscount != null ? String(data.consumeDiscount).trim() : '' |
| | | const consumeCoupon = data != null && data.consumeCoupon != null ? String(data.consumeCoupon).trim() : '' |
| | |
| | | <view class="nav-shell"> |
| | | <view class="safe-space" :style="'height:' + navTop + 'px'"></view> |
| | | <view class="nav-row" :style="'height:' + navHeight + 'px;padding-right:' + navRight + 'px'"> |
| | | <view class="back" @tap="goBack"><view class="back-arrow"></view></view> |
| | | <view class="back" @tap="goBack"><image src="/static/news-assets/arrow-left.svg" mode="aspectFit"></image></view> |
| | | <text class="nav-title">资讯详情</text> |
| | | <view class="capsule-reserve" :style="'width:' + navWidth + 'px'"></view> |
| | | </view> |
| | |
| | | .safe-space { height: 28px; } |
| | | .nav-row { display: flex; position: relative; height: 32px; align-items: center; justify-content: flex-end; box-sizing: border-box; } |
| | | .back { display: flex; position: absolute; z-index: 2; left: 24rpx; top: 0; width: 72rpx; height: 100%; justify-content: center; align-items: center; } |
| | | .back-arrow { width: 22rpx; height: 22rpx; margin-left: 10rpx; border-left-width: 4rpx; border-left-style: solid; border-left-color: #2A3441; border-bottom-width: 4rpx; border-bottom-style: solid; border-bottom-color: #2A3441; transform: rotate(45deg); } |
| | | .back image { width: 38rpx; height: 38rpx; } |
| | | .nav-title { position: absolute; left: 190rpx; right: 190rpx; color: #171717; font-size: 35rpx; font-weight: 700; text-align: center; white-space: nowrap; } |
| | | .capsule-reserve { width: 88px; height: 100%; flex-shrink: 0; } |
| | | .nav-bottom-space { height: 34rpx; } |
| | |
| | | <view v-if="showParkingResults" class="parking-results"> |
| | | <view v-if="parkingSearchLoading" class="parking-result-empty"><text>正在搜索停车场</text></view> |
| | | <scroll-view v-else-if="filteredParkingLots.length > 0" class="parking-result-scroll" scroll-y> |
| | | <view class="parking-result-item" v-for="item in filteredParkingLots" :key="item.id" @tap="selectParking(item)"><text class="parking-result-name">{{item.name}}</text><text class="parking-result-address">{{item.address}}</text></view> |
| | | <view class="parking-result-item" v-for="item in filteredParkingLots" :key="item.id" @tap.stop="selectParking(item)"><text class="parking-result-name">{{item.name}}</text><text class="parking-result-address">{{item.address}}</text></view> |
| | | </scroll-view> |
| | | <view v-else class="parking-result-empty"><text>暂无匹配停车场</text></view> |
| | | </view> |
| | |
| | | .capsule-space { height: 100%; flex-shrink: 0; } |
| | | .form-content { padding: 47rpx 54rpx 0; } |
| | | .field-group { display: flex; position: relative; flex-direction: column; } |
| | | .parking-field { z-index: 20; } |
| | | .plate-field { z-index: 1; } |
| | | .field-label { color: #70798A; font-size: 25rpx; line-height: 36rpx; font-weight: 600; } |
| | | .input-box { display: flex; height: 86rpx; margin-top: 17rpx; box-sizing: border-box; padding: 0 24rpx; border-width: 2rpx; border-style: solid; border-color: #E1E5EB; border-radius: 14rpx; background-color: #FFFFFF; flex-direction: row; align-items: center; } |
| | | .input-box input { height: 82rpx; min-width: 0; flex: 1; color: #2D333C; font-size: 28rpx; } |
| | |
| | | <view v-if="topicError" class="topic-error"><text>专题配置加载失败</text><text @tap="loadPremiumTopic">重新加载</text></view> |
| | | <view v-if="topicDescription != ''" class="topic-summary"><view class="intro-card"><text>{{topicDescription}}</text></view></view> |
| | | <view class="service-section"><scroll-view class="tab-scroll" scroll-x :show-scrollbar="false"><view class="service-tabs" :style="'width:' + Math.max(750, serviceTabs.length * 234) + 'rpx'"><view v-for="tab in serviceTabs" :key="tab.id" class="service-tab" :class="tab.id == activeTab ? 'active' : ''" @tap="selectTab(tab.id)"><text>{{tab.name}}</text><view class="tab-line"></view></view></view></scroll-view> |
| | | <view v-if="initialLoading" class="service-empty"><view class="loading-spinner"></view><text>正在加载臻享服务</text></view><view v-else-if="initialError" class="service-empty error-state"><text>臻享服务加载失败</text><text class="retry-text" @tap="retryInitialLoad">重新加载</text></view><view v-else-if="hasLoaded && visibleServices.length == 0" class="service-empty"><text>暂无相关臻享服务</text></view><view v-else-if="hasLoaded" class="service-list"><view v-for="item in visibleServices" :key="item.id" class="service-item" :class="item.available ? '' : 'is-unavailable'" @tap="openService(item)"><image class="service-image" :src="item.image" mode="aspectFill" @error="replaceImage(item)"></image><view class="service-info"><text class="service-name">{{item.name}}</text><text class="service-benefit">{{item.available ? item.benefit : '已停用'}}</text><view class="address-row"><image src="/static/benefits-assets/map-pin.svg" mode="aspectFit"></image><text>{{item.address}}</text></view></view></view></view> |
| | | <view v-if="initialLoading" class="service-empty"><view class="loading-spinner"></view><text>正在加载臻享服务</text></view><view v-else-if="initialError" class="service-empty error-state"><text>臻享服务加载失败</text><text class="retry-text" @tap="retryInitialLoad">重新加载</text></view><view v-else-if="hasLoaded && visibleServices.length == 0" class="service-empty"><text>暂无相关臻享服务</text></view><view v-else-if="hasLoaded" class="service-list"><view v-for="item in visibleServices" :key="item.id" class="service-item" :class="item.available ? '' : 'is-unavailable'" @tap="openService(item)"><image class="service-image" :src="item.image" mode="aspectFill" @error="replaceImage(item)"></image><view class="service-info"><text class="service-name">{{item.name}}</text><text v-if="!item.available || item.benefitTagVisible" class="service-benefit">{{item.available ? item.benefit : '已停用'}}</text><view class="address-row"><image src="/static/benefits-assets/map-pin.svg" mode="aspectFit"></image><text>{{item.address}}</text></view></view></view></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 && visibleServices.length > 0" class="no-more"><view></view><text>已经到底了</text><view></view></view> |
| | | </view><view class="bottom-space"></view> |
| | | </scroll-view> |
| | |
| | | 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 == '配偶推荐就业' || 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 }) |
| | | 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]; const tagVisible = item.benefitTagVisible !== false; 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: tagVisible ? (item.benefitText != null ? item.benefitText as string : '') : '', benefitTagVisible: tagVisible, 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') |
| | |
| | | import { isApiConfigured } from '../../config/api.uts' |
| | | import { getShoppingTopic, getShoppingTopicMerchants } from '../../services/portal.uts' |
| | | import { resolveApiUrl } from '../../services/http.uts' |
| | | import { getSession } from '../../services/session.uts' |
| | | import { resolveTalentId } from '../../utils/talent-id.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10); const windowWidth = ref(375) |
| | |
| | | // #ifdef MP-WEIXIN |
| | | const capsule = uni.getMenuButtonBoundingClientRect(); const info = uni.getSystemInfoSync(); windowWidth.value = info.windowWidth; navTop.value = capsule.top; navHeight.value = capsule.height; navWidth.value = capsule.width; navRight.value = info.windowWidth - capsule.right |
| | | // #endif |
| | | const session = getSession(); if (session != null && session.talentId != null) talentId.value = session.talentId as string |
| | | talentId.value = resolveTalentId() |
| | | if (options != null && options['previewState'] != null) { const state = options['previewState'] as string; failInitialOnce.value = state == 'initialError'; failLoadMoreOnce.value = state == 'loadMoreError' } |
| | | selectScene(0); loadShoppingTopic() |
| | | }) |
| | |
| | | <view v-if="detailLoading" class="page-state"><view class="loading-spinner"></view><text>正在加载购物服务</text></view> |
| | | <view v-else-if="detailError" class="page-state error-state"><text>购物服务加载失败</text><text class="retry-text" @tap="loadMerchantDetail">重新加载</text></view> |
| | | <view v-else class="detail-content"> |
| | | <view class="merchant-block"><view class="title-row"><text class="merchant-name">{{merchantName}}</text><text v-if="discount != ''" class="discount-label">{{discount}}</text></view><view v-if="applicableCards != ''" class="talent-tag"><text>{{applicableCards}}</text></view><view v-if="address != '' || canNavigate() || merchantPhone != ''" class="address-row"><image v-if="address != ''" class="location-icon" src="/static/benefits-assets/map-pin.svg" mode="aspectFit"></image><text v-if="address != ''" class="merchant-address">{{address}}</text><view class="address-actions"><view class="round-action" :class="!available || !canNavigate() ? 'is-disabled' : ''" @tap="openLocation"><image src="/static/merchant-assets/navigation.svg" mode="aspectFit"></image></view><view class="round-action" :class="!available || merchantPhone == '' ? 'is-disabled' : ''" @tap="callMerchant"><image src="/static/merchant-assets/phone.svg" mode="aspectFit"></image></view></view></view><view v-if="!available" class="unavailable-row"><text>{{unavailableReason}}</text></view></view> |
| | | <view class="merchant-block"><view class="title-row"><text class="merchant-name">{{merchantName}}</text><view v-if="displayTags.length > 0" class="discount-tags"><text v-for="tag in displayTags" :key="tag" class="discount-label">{{tag}}</text></view></view><view v-if="applicableCards != ''" class="talent-tag"><text>{{applicableCards}}</text></view><view v-if="address != '' || canNavigate() || merchantPhone != ''" class="address-row"><image v-if="address != ''" class="location-icon" src="/static/benefits-assets/map-pin.svg" mode="aspectFit"></image><text v-if="address != ''" class="merchant-address">{{address}}</text><view class="address-actions"><view class="round-action" :class="!available || !canNavigate() ? 'is-disabled' : ''" @tap="openLocation"><image src="/static/merchant-assets/navigation.svg" mode="aspectFit"></image></view><view class="round-action" :class="!available || merchantPhone == '' ? 'is-disabled' : ''" @tap="callMerchant"><image src="/static/merchant-assets/phone.svg" mode="aspectFit"></image></view></view></view><view v-if="!available" class="unavailable-row"><text>{{unavailableReason}}</text></view></view> |
| | | <view class="detail-section"><view class="section-title"><text>权益详情</text><view></view></view><view class="detail-card"><text>{{benefitDetail == '' ? '权益详情暂未配置' : benefitDetail}}</text></view></view> |
| | | </view> |
| | | </view> |
| | |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10) |
| | | const merchantId = ref(''); const talentId = ref(''); const heroImage = ref(''); const merchantName = ref(''); const discount = ref(''); const applicableCards = ref(''); const address = ref(''); const benefitDetail = ref('') |
| | | const merchantId = ref(''); const talentId = ref(''); const heroImage = ref(''); const merchantName = ref(''); const displayTags = ref<string[]>([]); const applicableCards = ref(''); const address = ref(''); const benefitDetail = ref('') |
| | | const merchantPhone = ref(''); const latitude = ref(0); const longitude = ref(0); const available = ref(false); const unavailableReason = ref('该购物服务当前不可用'); const advisorAvailable = ref(false); const advisorPhone = ref('') |
| | | const detailLoading = ref(true); const detailError = ref(false) |
| | | const canNavigate = () => latitude.value != 0 && longitude.value != 0 |
| | |
| | | getShoppingMerchantDetail(merchantId.value, talentId.value, (data: any) => { |
| | | if (data == null) { detailLoading.value = false; detailError.value = true; return } |
| | | merchantName.value = data.merchantName != null ? data.merchantName as string : '' |
| | | discount.value = data.discountText != null ? data.discountText as string : '' |
| | | const tags: any[] = data.displayTags instanceof Array ? data.displayTags as any[] : [] |
| | | displayTags.value = [] |
| | | for (let index = 0; index < tags.length; index++) { |
| | | const tag = String(tags[index]).trim() |
| | | if (tag != '' && displayTags.value.indexOf(tag) < 0) displayTags.value.push(tag) |
| | | } |
| | | applicableCards.value = data.applicableCards != null ? data.applicableCards as string : '' |
| | | address.value = data.address != null ? data.address as string : '' |
| | | heroImage.value = data.coverUrl != null && data.coverUrl != '' ? resolveApiUrl(data.coverUrl as string) : '' |
| | |
| | | <style> |
| | | .page { width:100%; height:100vh; position:relative; overflow:hidden; background-color:#F5F5F5; }.hero-image { position:absolute; left:0; right:0; top:0; width:100%; height:445rpx; background-color:#D8DDE3; }.hero-shade { position:absolute; z-index:1; left:0; right:0; top:0; height:210rpx; background-image:linear-gradient(180deg,rgba(0,0,0,.45),rgba(0,0,0,0)); }.custom-nav { position:absolute; z-index:10; left:0; right:0; top:0; }.safe-space { height:28px; }.nav-row { display:flex; box-sizing:border-box; padding-left:24rpx; flex-direction:row; align-items:center; justify-content:space-between; }.back-button { display:flex; width:54rpx; height:54rpx; border-radius:50%; background-color:rgba(45,45,45,.48); justify-content:center; align-items:center; }.back-button image { width:38rpx; height:38rpx; }.capsule-space { height:100%; flex-shrink:0; } |
| | | .content-sheet { display:flex; position:absolute; z-index:3; left:0; right:0; top:415rpx; bottom:128rpx; bottom:calc(128rpx + env(safe-area-inset-bottom)); overflow:hidden; border-radius:30rpx 30rpx 0 0; background-color:#F5F5F5; flex-direction:column; }.detail-content { display:flex; min-height:0; flex:1; flex-direction:column; }.page-state { display:flex; min-height:0; flex:1; background-color:#FFFFFF; flex-direction:column; justify-content:center; align-items:center; }.page-state text { color:#949BA5; font-size:26rpx; }.page-state.error-state .retry-text { margin-top:22rpx; color:#F26A38; }.loading-spinner { width:30rpx; height:30rpx; margin-bottom:16rpx; box-sizing:border-box; border:3rpx solid #FFD6C4; border-top-color:#FF6C2B; border-radius:50%; animation:shopping-detail-loading 720ms linear infinite; } |
| | | .merchant-block { flex-shrink:0; box-sizing:border-box; padding:28rpx 32rpx 23rpx; background-color:#FFFFFF; }.title-row { display:flex; min-width:0; flex-direction:row; justify-content:space-between; align-items:center; }.merchant-name { min-width:0; flex:1; margin-right:20rpx; color:#282828; font-size:35rpx; line-height:48rpx; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.discount-label { max-width:250rpx; flex-shrink:0; color:#FF7A27; font-size:24rpx; line-height:34rpx; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.talent-tag { display:flex; max-width:100%; width:fit-content; min-height:47rpx; margin-top:18rpx; box-sizing:border-box; padding:8rpx 12rpx; border:1rpx solid #FFB98E; border-radius:5rpx; justify-content:center; align-items:center; }.talent-tag text { max-width:570rpx; color:#D87842; font-size:23rpx; line-height:31rpx; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.address-row { display:flex; min-width:0; min-height:60rpx; margin-top:18rpx; flex-direction:row; align-items:center; }.location-icon { width:27rpx; height:27rpx; margin-right:10rpx; flex-shrink:0; }.merchant-address { min-width:0; flex:1; max-height:62rpx; overflow:hidden; color:#8B919A; font-size:22rpx; line-height:31rpx; }.address-actions { display:flex; margin-left:20rpx; flex-direction:row; align-items:center; flex-shrink:0; }.round-action { display:flex; width:58rpx; height:58rpx; margin-left:12rpx; border-radius:50%; background-color:#F6F7F9; justify-content:center; align-items:center; }.round-action.is-disabled { opacity:.42; }.round-action image { width:31rpx; height:31rpx; }.unavailable-row { margin-top:12rpx; }.unavailable-row text { color:#D86B35; font-size:22rpx; line-height:31rpx; } |
| | | .merchant-block { flex-shrink:0; box-sizing:border-box; padding:28rpx 32rpx 23rpx; background-color:#FFFFFF; }.title-row { display:flex; min-width:0; flex-direction:row; justify-content:space-between; align-items:center; }.merchant-name { min-width:0; flex:1; margin-right:20rpx; color:#282828; font-size:35rpx; line-height:48rpx; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.discount-tags { display:flex; max-width:300rpx; flex-shrink:0; flex-direction:row; align-items:center; }.discount-label { max-width:250rpx; margin-left:10rpx; flex-shrink:0; color:#FF7A27; font-size:24rpx; line-height:34rpx; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.talent-tag { display:flex; max-width:100%; width:fit-content; min-height:47rpx; margin-top:18rpx; box-sizing:border-box; padding:8rpx 12rpx; border:1rpx solid #FFB98E; border-radius:5rpx; justify-content:center; align-items:center; }.talent-tag text { max-width:570rpx; color:#D87842; font-size:23rpx; line-height:31rpx; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.address-row { display:flex; min-width:0; min-height:60rpx; margin-top:18rpx; flex-direction:row; align-items:center; }.location-icon { width:27rpx; height:27rpx; margin-right:10rpx; flex-shrink:0; }.merchant-address { min-width:0; flex:1; max-height:62rpx; overflow:hidden; color:#8B919A; font-size:22rpx; line-height:31rpx; }.address-actions { display:flex; margin-left:20rpx; flex-direction:row; align-items:center; flex-shrink:0; }.round-action { display:flex; width:58rpx; height:58rpx; margin-left:12rpx; border-radius:50%; background-color:#F6F7F9; justify-content:center; align-items:center; }.round-action.is-disabled { opacity:.42; }.round-action image { width:31rpx; height:31rpx; }.unavailable-row { margin-top:12rpx; }.unavailable-row text { color:#D86B35; font-size:22rpx; line-height:31rpx; } |
| | | .detail-section { display:flex; min-height:0; flex:1; box-sizing:border-box; padding:24rpx 26rpx 18rpx; flex-direction:column; }.section-title { display:flex; height:66rpx; padding-left:20rpx; flex-direction:column; align-items:flex-start; flex-shrink:0; }.section-title text { color:#766A64; font-size:31rpx; line-height:42rpx; font-weight:600; }.section-title view { width:61rpx; height:7rpx; margin:9rpx 0 0 29rpx; border-radius:4rpx; background-image:linear-gradient(90deg,#FF4C56,#FFD43A); }.detail-card { min-height:0; flex:1; box-sizing:border-box; margin-top:13rpx; padding:31rpx 47rpx; overflow:hidden; border-radius:15rpx; background-color:#FFFFFF; }.detail-card text { display:block; max-height:228rpx; overflow:hidden; color:#161616; font-size:24rpx; line-height:38rpx; } |
| | | .action-bar { display:flex; position:absolute; z-index:8; left:0; right:0; bottom:0; height:128rpx; height:calc(128rpx + env(safe-area-inset-bottom)); box-sizing:border-box; padding:17rpx 31rpx 13rpx; padding-bottom:calc(13rpx + env(safe-area-inset-bottom)); background-color:#FFFFFF; align-items:flex-start; }.contact-button { display:flex; width:100%; height:82rpx; border-radius:43rpx; background-image:linear-gradient(90deg,#FF4B32,#FF8A1E); flex-direction:row; justify-content:center; align-items:center; }.contact-button-disabled { background-image:none; background-color:#C7CDD5; }.contact-button image { width:35rpx; height:35rpx; margin-right:14rpx; }.contact-button text { color:#FFFFFF; font-size:29rpx; font-weight:600; }.contact-button-active { opacity:.8; transform:scale(.99); }@keyframes shopping-detail-loading { from { transform:rotate(0deg); } to { transform:rotate(360deg); } } |
| | | </style> |
| | |
| | | <view v-if="loading" class="state-box"><text>正在加载权益信息</text></view> |
| | | <view v-else-if="loadError" class="state-box"><text>权益信息加载失败</text><text class="retry" @tap="loadBenefit">重新加载</text></view> |
| | | <view v-else> |
| | | <view class="service-row"><text class="service-name">{{title}}</text><text class="free-label">{{freeLabel}}</text></view> |
| | | <view class="service-row"><text class="service-name">{{title}}</text></view> |
| | | <view v-if="applicableCards != ''" class="talent-tag"><text>{{applicableCards}}</text></view> |
| | | <view class="section-title"><text>权益详情</text><view></view></view> |
| | | <view class="detail-card"><text>{{rulesText}}</text></view> |
| | | <view v-if="!available" class="unavailable"><text>{{unavailableReason}}</text></view> |
| | |
| | | </template> |
| | | |
| | | <script setup lang="uts"> |
| | | import { apiEnvironment } from '../../config/api.uts' |
| | | import { getSpouseEmploymentBenefit } from '../../services/portal.uts' |
| | | import { resolveApiUrl } from '../../services/http.uts' |
| | | import { getSession } from '../../services/session.uts' |
| | | import { resolveTalentId } from '../../utils/talent-id.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10) |
| | | const serviceId = ref(''); const talentId = ref(''); const loading = ref(true); const loadError = ref(false); const available = ref(false) |
| | | const fallbackHeroImage = '/static/stay-assets/hotel-vienna.jpg'; const heroImage = ref(fallbackHeroImage); const title = ref('配偶推荐就业'); const freeLabel = ref('免费'); const rulesText = ref(''); const unavailableReason = ref('该权益暂未开放') |
| | | const fallbackHeroImage = '/static/stay-assets/hotel-vienna.jpg'; const heroImage = ref(fallbackHeroImage); const title = ref('配偶推荐就业'); const applicableCards = ref(''); const rulesText = ref(''); const unavailableReason = ref('该权益暂未开放') |
| | | onLoad((options: any) => { |
| | | // #ifdef MP-WEIXIN |
| | | const capsule = uni.getMenuButtonBoundingClientRect(); const info = uni.getSystemInfoSync(); navTop.value = capsule.top; navHeight.value = capsule.height; navWidth.value = capsule.width; navRight.value = info.windowWidth - capsule.right |
| | | // #endif |
| | | serviceId.value = options != null && options['serviceId'] != null ? options['serviceId'] as string : ''; const session = getSession(); if (session != null && session.talentId != null) talentId.value = session.talentId as string; if (talentId.value == '' && apiEnvironment == 'development') talentId.value = '2' |
| | | serviceId.value = options != null && options['serviceId'] != null ? options['serviceId'] as string : ''; talentId.value = resolveTalentId() |
| | | loadBenefit() |
| | | }) |
| | | const loadBenefit = () => { |
| | | if (serviceId.value == '') { loading.value = false; loadError.value = true; return } |
| | | loading.value = true; loadError.value = false |
| | | getSpouseEmploymentBenefit(serviceId.value, talentId.value, (data: any) => { |
| | | title.value = data.title || title.value; heroImage.value = data.heroImage != null && data.heroImage != '' ? resolveApiUrl(data.heroImage as string) : fallbackHeroImage; freeLabel.value = data.freeLabel || freeLabel.value; rulesText.value = data.rulesText || ''; available.value = data.available === true; unavailableReason.value = data.unavailableReason || unavailableReason.value; loading.value = false |
| | | title.value = data.title || title.value; heroImage.value = data.heroImage != null && data.heroImage != '' ? resolveApiUrl(data.heroImage as string) : fallbackHeroImage; applicableCards.value = data.applicableCards != null ? String(data.applicableCards) : ''; rulesText.value = data.rulesText || ''; available.value = data.available === true; unavailableReason.value = data.unavailableReason || unavailableReason.value; loading.value = false |
| | | }, () => { loading.value = false; loadError.value = true }) |
| | | } |
| | | const replaceHeroImage = () => { heroImage.value = fallbackHeroImage } |
| | |
| | | .page { width:100%; height:100vh; position:relative; overflow:hidden; background-color:#F5F5F5; } |
| | | .hero-image { position:absolute; left:0; right:0; top:0; width:100%; height:425rpx; background-color:#D4DAE1; } .hero-shade { position:absolute; z-index:1; left:0; right:0; top:0; height:210rpx; background-image:linear-gradient(180deg,rgba(0,0,0,.55),rgba(0,0,0,0)); } |
| | | .custom-nav { position:absolute; z-index:10; left:0; right:0; top:0; } .safe-space { height:28px; } .nav-row { display:flex; box-sizing:border-box; padding-left:24rpx; flex-direction:row; align-items:center; justify-content:space-between; } .back-button { display:flex; width:54rpx; height:54rpx; border-radius:50%; background-color:rgba(45,45,45,.38); justify-content:center; align-items:center; } .back-button image { width:38rpx; height:38rpx; } .capsule-space { height:100%; flex-shrink:0; } |
| | | .content-sheet { position:absolute; z-index:3; left:0; right:0; top:405rpx; bottom:128rpx; bottom:calc(128rpx + env(safe-area-inset-bottom)); box-sizing:border-box; padding:28rpx 25rpx 20rpx; border-radius:30rpx 30rpx 0 0; background-color:#F5F5F5; } .service-row { display:flex; height:64rpx; padding:0 10rpx; flex-direction:row; justify-content:space-between; align-items:center; } .service-name { color:#252525; font-size:35rpx; line-height:48rpx; font-weight:700; } .free-label { color:#FF7A27; font-size:24rpx; } .section-title { display:flex; height:84rpx; margin-top:15rpx; padding-left:20rpx; flex-direction:column; align-items:flex-start; } .section-title text { color:#756B67; font-size:31rpx; line-height:42rpx; font-weight:600; } .section-title view { width:61rpx; height:7rpx; margin:10rpx 0 0 28rpx; border-radius:4rpx; background-image:linear-gradient(90deg,#FF4C56,#FFD43A); } .detail-card { box-sizing:border-box; padding:43rpx 47rpx; border-radius:15rpx; background-color:#FFFFFF; } .detail-card text { display:block; color:#727983; font-size:25rpx; line-height:52rpx; } .unavailable { margin-top:20rpx; text-align:center; color:#9A725A; font-size:24rpx; } |
| | | .content-sheet { position:absolute; z-index:3; left:0; right:0; top:405rpx; bottom:128rpx; bottom:calc(128rpx + env(safe-area-inset-bottom)); box-sizing:border-box; padding:28rpx 25rpx 20rpx; border-radius:30rpx 30rpx 0 0; background-color:#F5F5F5; } .service-row { display:flex; height:64rpx; padding:0 10rpx; flex-direction:row; justify-content:space-between; align-items:center; } .service-name { color:#252525; font-size:35rpx; line-height:48rpx; font-weight:700; } .free-label { color:#FF7A27; font-size:24rpx; } .talent-tag { display:flex; align-self:flex-start; min-height:42rpx; max-width:100%; margin:8rpx 10rpx 0; box-sizing:border-box; padding:0 14rpx; border:1rpx solid #FFB98E; border-radius:5rpx; justify-content:center; align-items:center; } .talent-tag text { max-width:640rpx; color:#D87842; font-size:23rpx; line-height:30rpx; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } .section-title { display:flex; height:84rpx; margin-top:15rpx; padding-left:20rpx; flex-direction:column; align-items:flex-start; } .section-title text { color:#756B67; font-size:31rpx; line-height:42rpx; font-weight:600; } .section-title view { width:61rpx; height:7rpx; margin:10rpx 0 0 28rpx; border-radius:4rpx; background-image:linear-gradient(90deg,#FF4C56,#FFD43A); } .detail-card { box-sizing:border-box; padding:43rpx 47rpx; border-radius:15rpx; background-color:#FFFFFF; } .detail-card text { display:block; color:#727983; font-size:25rpx; line-height:52rpx; } .unavailable { margin-top:20rpx; text-align:center; color:#9A725A; font-size:24rpx; } |
| | | .state-box { display:flex; height:230rpx; color:#8D96A5; font-size:27rpx; flex-direction:column; align-items:center; justify-content:center; } .retry { margin-top:24rpx; color:#FF7A27; } .action-bar { display:flex; position:absolute; z-index:8; left:0; right:0; bottom:0; height:128rpx; height:calc(128rpx + env(safe-area-inset-bottom)); box-sizing:border-box; padding:17rpx 31rpx 13rpx; padding-bottom:calc(13rpx + env(safe-area-inset-bottom)); background-color:#FFFFFF; } .fill-button { display:flex; width:100%; height:82rpx; border-radius:43rpx; background-image:linear-gradient(90deg,#FF4B32,#FF8A1E); justify-content:center; align-items:center; } .fill-button text { color:#FFFFFF; font-size:29rpx; font-weight:600; } .disabled-button { background-image:none; background-color:#C7CDD5; } .fill-button-active { opacity:.8; transform:scale(.99); } |
| | | </style> |
| | |
| | | import { getStayTopic, getStayTopicMerchants } from '../../services/portal.uts' |
| | | import { resolveApiUrl } from '../../services/http.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | import { getSession } from '../../services/session.uts' |
| | | import { resolveTalentId } from '../../utils/talent-id.uts' |
| | | |
| | | const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10) |
| | | const isSolidNav = ref(false); const windowWidth = ref(375); const fallbackImage = '' |
| | |
| | | const capsule = uni.getMenuButtonBoundingClientRect(); const info = uni.getSystemInfoSync(); windowWidth.value = info.windowWidth; navTop.value = capsule.top; navHeight.value = capsule.height; navWidth.value = capsule.width; navRight.value = info.windowWidth - capsule.right |
| | | // #endif |
| | | if (options != null && options['previewState'] != null) { const state = options['previewState'] as string; failInitialOnce.value = state == 'initialError'; failLoadMoreOnce.value = state == 'loadMoreError' } |
| | | const session = getSession(); if (session != null && session.talentId != null) talentId.value = session.talentId as string |
| | | talentId.value = resolveTalentId() |
| | | loadStayTopic(); loadInitialServices() |
| | | }) |
| | | const readText = (primary : any, fallback : any) : string => { |
| | |
| | | const merchantType = readText(item.merchantType, item['merchant_type']) |
| | | 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 hasConfiguredTags = item.benefitTags instanceof Array |
| | | const configuredTags: any[] = hasConfiguredTags ? (item.benefitTags as any[]) : [] |
| | | 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 (!hasConfiguredTags) { |
| | | if (hasCoupon) displayTags.push('券') |
| | | if (discountText != '' && discountText != '人才专享') displayTags.push(discountText) |
| | | } |
| | |
| | | import { isApiConfigured } from '../../config/api.uts' |
| | | import { getTransportTopic, getTransportTopicMerchants } from '../../services/portal.uts' |
| | | import { resolveApiUrl } from '../../services/http.uts' |
| | | import { getSession } from '../../services/session.uts' |
| | | import { resolveTalentId } from '../../utils/talent-id.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10) |
| | |
| | | const capsule = uni.getMenuButtonBoundingClientRect(); const info = uni.getSystemInfoSync(); windowWidth.value = info.windowWidth; navTop.value = capsule.top; navHeight.value = capsule.height; navWidth.value = capsule.width; navRight.value = info.windowWidth - capsule.right |
| | | // #endif |
| | | if (options != null && options['previewState'] != null) { const state = options['previewState'] as string; failInitialOnce.value = state == 'initialError'; failLoadMoreOnce.value = state == 'loadMoreError' } |
| | | const session = getSession(); if (session != null && session.talentId != null) talentId.value = session.talentId as string |
| | | talentId.value = resolveTalentId() |
| | | selectScene(0); loadTransportTopic(); loadInitialServices() |
| | | }) |
| | | const loadTransportTopic = () => { |
| | |
| | | getTransportTopicMerchants(activeTab.value, talentId.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]; const merchantType = item.merchantType != null ? item.merchantType as string : activeTab.value; const discount = item.discountText != null ? item.discountText as string : ''; next.push({ id: '' + item.id, merchantType: merchantType, name: item.merchantName != null ? item.merchantName as string : '行服务', discount: discount, benefitTagVisible: merchantType == 'carWash' ? item.benefitTagVisible === true : discount != '', address: item.address != null ? item.address as string : '', image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage, available: item.available !== false, externalLinkType: item.externalLinkType != null ? item.externalLinkType as string : '' }) } |
| | | for (let index = 0; index < records.length; index++) { const item = records[index]; const merchantType = item.merchantType != null ? item.merchantType as string : activeTab.value; const discount = merchantType == 'fuel' ? '' : (item.discountText != null ? item.discountText as string : ''); next.push({ id: '' + item.id, merchantType: merchantType, name: item.merchantName != null ? item.merchantName as string : '行服务', discount: discount, benefitTagVisible: merchantType == 'fuel' ? false : (merchantType == 'carWash' ? item.benefitTagVisible === true : discount != ''), address: item.address != null ? item.address as string : '', image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage, available: item.available !== false, externalLinkType: item.externalLinkType != null ? item.externalLinkType 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 |
| | | }, () => { if (requestVersion != listRequestVersion.value) return; initialLoading.value = false; loadingMore.value = false; if (append) loadMoreError.value = true; else initialError.value = true }) |
| | | } |
| | |
| | | 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 { resolveTalentId } from '../../utils/talent-id.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | const navTop = ref(28) |
| | |
| | | failLoadMoreOnce.value = state == 'loadMoreError' |
| | | } |
| | | loadTravelTopic() |
| | | const session = getSession() |
| | | if (session != null && session.talentId != null) talentId.value = session.talentId as string |
| | | talentId.value = resolveTalentId() |
| | | loadInitialServices() |
| | | }) |
| | | |
| | |
| | | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#E9924D" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> |
| | | <path d="M6.3 11.1 7.6 8a2 2 0 0 1 1.84-1.24h5.12A2 2 0 0 1 16.4 8l1.3 3.1"/> |
| | | <path d="M5.4 11.1h13.2a1.8 1.8 0 0 1 1.8 1.8v4.2a1.8 1.8 0 0 1-1.8 1.8H5.4a1.8 1.8 0 0 1-1.8-1.8v-4.2a1.8 1.8 0 0 1 1.8-1.8Z"/> |
| | | <path d="M6.6 18.9v1.2M17.4 18.9v1.2M7.3 14.4h.01M16.7 14.4h.01"/> |
| | | <path d="M18.6.8c-.9 1.1-1.45 1.9-1.45 2.62a1.45 1.45 0 0 0 2.9 0c0-.72-.55-1.52-1.45-2.62Z"/> |
| | | <path d="M21.5 3.9c-.56.68-.9 1.17-.9 1.61a.9.9 0 0 0 1.8 0c0-.44-.34-.93-.9-1.61Z"/> |
| | | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#FF7A2F" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"> |
| | | <path d="m3.8 13 1.1-2.6A1.6 1.6 0 0 1 6.4 9.4h5.2a1.6 1.6 0 0 1 1.5 1l1.1 2.6"/> |
| | | <rect x="2.2" y="13" width="13.6" height="6.6" rx="1.6"/> |
| | | <path d="M4.2 19.6v1.4M13.8 19.6v1.4M5.3 16.1h.01M12.7 16.1h.01"/> |
| | | <path d="M20 2.4c-1.45 1.65-2.55 3.15-2.55 4.45a2.55 2.55 0 0 0 5.1 0C22.55 5.55 21.45 4.05 20 2.4Z"/> |
| | | <path d="M15.45 4.55c-.72.82-1.25 1.55-1.25 2.25a1.25 1.25 0 0 0 2.5 0c0-.7-.53-1.43-1.25-2.25Z"/> |
| | | </svg> |
| New file |
| | |
| | | import { getSession } from '../services/session.uts' |
| | | |
| | | /** |
| | | * 解析当前请求使用的人才ID。 |
| | | * |
| | | * 列表和详情必须使用同一来源:折扣、券、免费次数这些标签都要先按人才卡等级匹配, |
| | | * 一旦某一侧拿不到人才ID,就会出现同一商家"列表没有标签、详情有标签"的差异。 |
| | | * |
| | | * 游客态没有人才卡,按产品要求不展示任何权益标签,因此这里不做开发环境兜底: |
| | | * 未登录时返回空串,让列表和详情一起收敛为不显示。 |
| | | */ |
| | | export const resolveTalentId = (): string => { |
| | | const session = getSession() |
| | | if (session != null && session.talentId != null) return '' + session.talentId |
| | | return '' |
| | | } |
| | |
| | | for (let index = 0; index < rules.value.length; index++) { |
| | | if (rules.value[index].id == selectedRuleId.value) { |
| | | const rule = rules.value[index] |
| | | if (rule.benefitText.length > 0) return rule.benefitText |
| | | if (rule.fixedReduction > 0) return '到店消费立减' + formatBenefitNumber(rule.fixedReduction) + '元' |
| | | if (rule.discountRate > 0) return '到店消费' + formatBenefitNumber(rule.discountRate * 10) + '折' |
| | | if (rule.benefitText.length > 0) return rule.benefitText |
| | | return rule.name |
| | | } |
| | | } |
| | |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .merchant-management-table .merchant-action-cell { |
| | | display: grid; |
| | | grid-template-columns: 32px 48px 80px; |
| | | justify-content: center; |
| | | align-items: center; |
| | | column-gap: 4px; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .merchant-management-table .merchant-action-cell .el-button { |
| | | margin-left: 0; |
| | | width: 100%; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .merchant-create-page { |
| | | display: grid; |
| | | gap: 14px; |
| | |
| | | |
| | | .hotel-consume-row > strong { |
| | | grid-column: 1; |
| | | align-self: center; |
| | | align-self: start; |
| | | margin-top: 7px; |
| | | } |
| | | |
| | | .hotel-consume-row > .el-switch { |
| | | grid-column: 2; |
| | | align-self: center; |
| | | align-self: start; |
| | | } |
| | | |
| | | .hotel-consume-options { |
| | |
| | | padding: 18px 22px 20px; |
| | | } |
| | | |
| | | .business-fill-page.role-management-page > .panel { |
| | | padding-left: 16px; |
| | | padding-right: 16px; |
| | | } |
| | | |
| | | .business-fill-filter-form { |
| | | display: grid; |
| | | grid-template-columns: minmax(250px, 1.15fr) minmax(240px, 1fr) minmax(220px, 0.9fr) minmax(260px, 1.1fr) auto; |
| | |
| | | |
| | | /* 角色管理筛选项与标题左边缘对齐。 */ |
| | | .role-management-filter-form .el-form-item__label { |
| | | flex: 0 0 86px; |
| | | width: 86px !important; |
| | | flex: 0 0 auto; |
| | | width: auto; |
| | | justify-content: flex-start; |
| | | } |
| | | |
| | |
| | | gap: 12px; |
| | | } |
| | | |
| | | .movie-ticket-code-pool-filters { |
| | | container-type: inline-size; |
| | | container-name: movie-ticket-code-pool; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .movie-ticket-code-pool-filter-form .el-form-item, |
| | | .movie-ticket-code-pool-filter-form .el-form-item__content { |
| | | min-width: 0; |
| | | } |
| | | |
| | | @container movie-ticket-code-pool (max-width: 1311px) { |
| | | .movie-ticket-code-pool-filter-form { |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)) 80px; |
| | | } |
| | | |
| | | .movie-ticket-code-pool-filter-form .movie-ticket-package-filter-actions { |
| | | grid-column: 3; |
| | | } |
| | | } |
| | | |
| | | @container movie-ticket-code-pool (max-width: 620px) { |
| | | .movie-ticket-code-pool-filter-form { |
| | | grid-template-columns: minmax(0, 1fr); |
| | | } |
| | | |
| | | .movie-ticket-code-pool-filter-form .movie-ticket-package-filter-actions { |
| | | grid-column: 1; |
| | | grid-row: auto; |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | } |
| | | } |
| | | |
| | | .movie-ticket-package-table { |
| | | color: #111827; |
| | | font-size: 12px; |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="page-stack prototype-page business-fill-page"> |
| | | <div class="page-stack prototype-page business-fill-page" :class="{ 'role-management-page': isRole }"> |
| | | <section class="panel"> |
| | | <div class="panel-header"><h3>{{ title }}</h3></div> |
| | | <el-form class="business-fill-filter-form" :class="{ 'role-management-filter-form': isRole, 'upgrade-management-filter-form': isUpgrade }" label-width="86px"> |
| | | <el-form class="business-fill-filter-form" :class="{ 'role-management-filter-form': isRole, 'upgrade-management-filter-form': isUpgrade }" :label-width="isRole ? undefined : '86px'"> |
| | | <el-form-item :label="isUpgrade ? '持卡人' : isRole ? '角色' : '员工'"> |
| | | <el-input v-model="filters.keyword" :placeholder="isUpgrade ? '持卡人姓名、手机号、卡号' : '请输入关键词'" clearable /> |
| | | </el-form-item> |
| | |
| | | const isApprovalMerchant = computed(() => merchantTypeCode.value === 'approval') |
| | | const isBookMerchant = computed(() => merchantTypeCode.value === 'culture') |
| | | const couponWithoutProducts = computed(() => isBookMerchant.value || isChildcareMerchant.value) |
| | | const couponStoreConsumeOnly = computed(() => couponWithoutProducts.value || isHotelMerchant.value) |
| | | const isServiceMerchant = computed(() => isFoodMerchant.value || merchantTypeCode.value === 'scenic') |
| | | const supportsStoreConsumption = computed(() => [ |
| | | 'hotel', 'scenic', 'food', 'dental', 'culture', 'children', |
| | |
| | | const merchantTypeCode = merchantTypeAliases[merchantType] || merchantType |
| | | form.recommend = merchantTypeCode === 'food' || merchantTypeCode === 'scenic' ? 1 : 0 |
| | | form.sortNo = undefined |
| | | } |
| | | |
| | | function requiresRightsTag() { |
| | | return !(isCarWashMerchant.value && form.miniProgramJumpType === 'miniprogram') && !isApprovalMerchant.value |
| | | } |
| | | |
| | | function hasMissingRightsTag() { |
| | | return requiresRightsTag() && rightsConfigs.some((item) => !String(item.tag || '').trim()) |
| | | } |
| | | |
| | | const tabs = computed(() => { |
| | |
| | | }], |
| | | validTime: [{ required: true, message: '请选择优惠券有效期', trigger: 'change' }], |
| | | productScope: [{ |
| | | validator: (_rule, value, callback) => couponWithoutProducts.value || value !== 'selected' || selectedCouponProductIds.value.length > 0 |
| | | validator: (_rule, value, callback) => couponStoreConsumeOnly.value || value !== 'selected' || selectedCouponProductIds.value.length > 0 |
| | | ? callback() |
| | | : callback(new Error('请至少选择一个可用商品')), |
| | | trigger: 'change', |
| | |
| | | return |
| | | } |
| | | |
| | | if (hasMissingRightsTag()) { |
| | | ElMessage.warning('请填写各个人才卡等级的标签') |
| | | return |
| | | } |
| | | |
| | | rightsSaving.value = true |
| | | try { |
| | | await updateMerchant(merchantId, { |
| | |
| | | } |
| | | |
| | | async function loadCouponProducts() { |
| | | if (couponWithoutProducts.value) { |
| | | if (couponStoreConsumeOnly.value) { |
| | | couponProducts.value = [] |
| | | selectedCouponProductIds.value = [] |
| | | return |
| | |
| | | couponEditForm.couponAmount = '' |
| | | couponEditForm.talentLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡'] |
| | | couponEditForm.validTime = [] |
| | | couponEditForm.productScope = couponWithoutProducts.value ? 'all' : 'selected' |
| | | couponEditForm.productScope = couponStoreConsumeOnly.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 = couponWithoutProducts.value || detail.productScope === 'ALL' ? 'all' : 'selected' |
| | | couponEditForm.productScope = couponStoreConsumeOnly.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: couponWithoutProducts.value || couponEditForm.productScope === 'all' ? 'ALL' : 'SELECTED', |
| | | productIds: couponWithoutProducts.value || couponEditForm.productScope === 'all' ? [] : selectedCouponProductIds.value, |
| | | storeConsume: couponWithoutProducts.value || couponEditForm.storeConsume, |
| | | productScope: couponStoreConsumeOnly.value || couponEditForm.productScope === 'all' ? 'ALL' : 'SELECTED', |
| | | productIds: couponStoreConsumeOnly.value || couponEditForm.productScope === 'all' ? [] : selectedCouponProductIds.value, |
| | | storeConsume: couponStoreConsumeOnly.value || couponEditForm.storeConsume, |
| | | governmentSubsidy: couponEditForm.governmentSubsidy, |
| | | status: couponEditForm.status, |
| | | } |
| | |
| | | </el-form-item> |
| | | |
| | | <template v-if="!isApartmentMerchant && !usesSimplifiedMerchantSettings"> |
| | | <div class="merchant-form-row recommend-row"> |
| | | <label>开启团购</label> |
| | | <el-switch v-model="form.groupBuyingEnabled" :active-value="1" :inactive-value="0" /> |
| | | </div> |
| | | |
| | | <div class="merchant-form-row"> |
| | | <label>服务标签</label> |
| | | <el-input v-model="form.serviceLabel" placeholder="如:到店消费" /> |
| | |
| | | <section class="merchant-rights-panel" :class="{ 'movie-merchant-rights-panel': isMovieMerchant }"> |
| | | <h3 v-if="isMovieMerchant" class="merchant-rights-section-title">商家权益</h3> |
| | | |
| | | <div v-if="!isApartmentMerchant && !isCarWashMerchant && !isHealthCheckMerchant && !isParkingMerchant && !isChildcareMerchant && !isFinancialMerchant && !isMovieMerchant && !isSpouseEmploymentMerchant && !isApprovalMerchant" class="merchant-rights-top-row"> |
| | | <strong>团单服务</strong> |
| | | <el-switch v-model="form.groupBuyingEnabled" :active-value="1" :inactive-value="0" /> |
| | | </div> |
| | | |
| | | <div v-if="isCarWashMerchant" class="merchant-rights-top-row carwash-miniprogram-row"> |
| | | <el-radio-group v-model="form.miniProgramJumpType" class="carwash-jump-radio"> |
| | | <el-radio label="miniprogram">小程序跳转</el-radio> |
| | |
| | | <div class="merchant-rights-level-row"> |
| | | <strong>{{ item.level }}</strong> |
| | | <el-switch v-model="item.enabled" /> |
| | | <span>标签</span> |
| | | <el-input v-model="item.tag" placeholder="请输入" /> |
| | | <template v-if="requiresRightsTag()"> |
| | | <span><i class="merchant-rights-required">*</i>标签</span> |
| | | <el-input v-model="item.tag" placeholder="请输入" /> |
| | | </template> |
| | | </div> |
| | | |
| | | <div class="merchant-rights-detail-row"> |
| | |
| | | <span>券</span> |
| | | </div> |
| | | </div> |
| | | <div class="phone-preview-tag">{{ item.tag || '人才卡专享' }}</div> |
| | | <div class="phone-preview-tag">{{ item.tag || '请填写标签' }}</div> |
| | | <div class="phone-preview-address"> |
| | | <span>⌖</span> |
| | | <p>{{ form.address || '请填写商家地址' }}</p> |
| | |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <div v-if="couponWithoutProducts" class="coupon-edit-row coupon-switch-row"> |
| | | <div v-if="couponStoreConsumeOnly" class="coupon-edit-row coupon-switch-row"> |
| | | <label>适用范围</label> |
| | | <span>到店消费</span> |
| | | </div> |
| | |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | |
| | | <div v-if="!couponWithoutProducts" class="coupon-product-select-table"> |
| | | <div v-if="!couponStoreConsumeOnly" 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="!couponWithoutProducts" class="coupon-edit-row coupon-switch-row"> |
| | | <div v-if="!couponStoreConsumeOnly" class="coupon-edit-row coupon-switch-row"> |
| | | <label>是否到店消费可用</label> |
| | | <el-switch v-model="couponEditForm.storeConsume" /> |
| | | </div> |
| | |
| | | } |
| | | const productTabTypeCodes = new Set(['food', 'scenic', 'hotel', 'dental', 'medical']) |
| | | |
| | | const merchantTypeDisplayNames: Record<string, string> = { |
| | | food: '餐饮服务', |
| | | scenic: '景区服务', |
| | | hotel: '酒店服务', |
| | | apartment: '公寓租赁', |
| | | carWash: '自助洗车', |
| | | medical: '免费体检', |
| | | parking: '停车服务', |
| | | fuel: '加油服务', |
| | | children: '子女托管', |
| | | finance: '金融服务', |
| | | employment: '配偶推荐就业', |
| | | approval: '行政审批', |
| | | cinema: '观影服务', |
| | | housing: '购房优惠', |
| | | car: '购车服务', |
| | | appliance: '家电服务', |
| | | digital: '数码服务', |
| | | culture: '文化服务', |
| | | telecom: '电信服务', |
| | | dental: '口腔治疗保健', |
| | | } |
| | | |
| | | function resolveMerchantTypeCode(row: Merchant) { |
| | | const raw = String(row.merchantTypeCode || row.merchantType || '').trim() |
| | | return merchantTypeAliases[raw] || raw |
| | | } |
| | | |
| | | function displayMerchantType(row: Merchant) { |
| | | const code = resolveMerchantTypeCode(row) |
| | | return merchantTypeDisplayNames[code] || displayDash(row.merchantType) |
| | | } |
| | | |
| | | function parseMerchantRights(config: Merchant['merchantRightsConfig']) { |
| | |
| | | <template #default="{ row }">{{ displayDash(row.address) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="merchantType" label="商家类型" width="112"> |
| | | <template #default="{ row }">{{ displayDash(row.merchantType) }}</template> |
| | | <template #default="{ row }">{{ displayMerchantType(row) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="orderCount" label="订单总数" width="76" align="center"> |
| | | <template #default="{ row }">{{ displayDash(row.orderCount) }}</template> |
| | |
| | | <el-table-column label="状态" width="68" align="center"> |
| | | <template #default="{ row }">{{ statusText(row.status) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="174" fixed="right" align="center"> |
| | | <el-table-column label="操作" width="220" fixed="right" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-button v-permission="'admin.merchants.disable'" link :type="row.status === 0 ? 'success' : 'danger'" @click="toggleStatus(row)"> |
| | | {{ row.status === 0 ? '启用' : '禁用' }} |
| | | </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> |
| | | <div class="merchant-action-cell"> |
| | | <div> |
| | | <el-button v-permission="'admin.merchants.disable'" link :type="row.status === 0 ? 'success' : 'danger'" @click="toggleStatus(row)"> |
| | | {{ row.status === 0 ? '启用' : '禁用' }} |
| | | </el-button> |
| | | </div> |
| | | <div> |
| | | <el-button v-permission="'admin.merchants.edit'" link type="primary" :icon="Edit" @click="openEdit(row)">编辑</el-button> |
| | | </div> |
| | | <div> |
| | | <el-button v-if="supportsGroupBuying(row)" link type="primary" :icon="Goods" @click="openProducts(row)">团购管理</el-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | </template> |
| | | |
| | | <template v-else-if="mode === 'codes'"> |
| | | <el-form class="movie-ticket-package-filter-form" :model="codeFilters" label-width="72px"> |
| | | <el-form-item label="兑换码"> |
| | | <el-input v-model="codeFilters.keyword" placeholder="码ID、兑换码" clearable /> |
| | | </el-form-item> |
| | | <el-form-item label="票包"> |
| | | <el-input v-model="codeFilters.packageKeyword" placeholder="票包名称、编号" clearable /> |
| | | </el-form-item> |
| | | <el-form-item label="适用商家"> |
| | | <el-input v-model="codeFilters.merchantKeyword" placeholder="商家名称" clearable /> |
| | | </el-form-item> |
| | | <el-form-item label="状态"> |
| | | <el-select v-model="codeFilters.status" placeholder="全部" clearable> |
| | | <el-option v-for="item in codeStatuses" :key="item.value" :label="item.label" :value="item.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <div class="movie-ticket-package-filter-actions"> |
| | | <el-button type="primary" :icon="Search" @click="search">查询</el-button> |
| | | <el-button :icon="Refresh" @click="resetCodeFilters">重置</el-button> |
| | | </div> |
| | | </el-form> |
| | | <div class="movie-ticket-code-pool-filters"> |
| | | <el-form class="movie-ticket-package-filter-form movie-ticket-code-pool-filter-form" :model="codeFilters" label-width="72px"> |
| | | <el-form-item label="兑换码"> |
| | | <el-input v-model="codeFilters.keyword" placeholder="码ID、兑换码" clearable /> |
| | | </el-form-item> |
| | | <el-form-item label="票包"> |
| | | <el-input v-model="codeFilters.packageKeyword" placeholder="票包名称、编号" clearable /> |
| | | </el-form-item> |
| | | <el-form-item label="适用商家"> |
| | | <el-input v-model="codeFilters.merchantKeyword" placeholder="商家名称" clearable /> |
| | | </el-form-item> |
| | | <el-form-item label="状态"> |
| | | <el-select v-model="codeFilters.status" placeholder="全部" clearable> |
| | | <el-option v-for="item in codeStatuses" :key="item.value" :label="item.label" :value="item.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <div class="movie-ticket-package-filter-actions"> |
| | | <el-button type="primary" :icon="Search" @click="search">查询</el-button> |
| | | <el-button :icon="Refresh" @click="resetCodeFilters">重置</el-button> |
| | | </div> |
| | | </el-form> |
| | | </div> |
| | | <div class="movie-ticket-package-tools movie-ticket-code-pool-tools"> |
| | | <el-button v-permission="'admin.movie-tickets.import'" type="primary" @click="openImport(selectedPackage || {})">导入码</el-button> |
| | | <el-button @click="openGenerate(selectedPackage || {})">批量生成</el-button> |
| | |
| | | const saving = ref(false) |
| | | const formRef = ref<FormInstance>() |
| | | const cardFormRef = ref<FormInstance>() |
| | | const serviceStaff = ref<number | string | undefined>() |
| | | const staff = ref<AdminStaff[]>([]) |
| | | const talentLevelConfirmed = ref(false) |
| | | const cardDialogVisible = ref(false) |
| | |
| | | |
| | | const levels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '普通游客'] |
| | | |
| | | const form = reactive<TalentPayload>({ |
| | | const form = reactive<TalentPayload & { serviceStaff?: number | string }>({ |
| | | name: '', |
| | | mobile: '', |
| | | idCardNo: '', |
| | |
| | | sourceAreaType: 'LOCAL', |
| | | workAddress: '', |
| | | contactAddress: '', |
| | | serviceStaff: undefined, |
| | | }) |
| | | |
| | | const rules: FormRules = { |
| | |
| | | : callback(new Error('请先确认人才等级、来源地和人才卡号')), |
| | | trigger: 'change', |
| | | }], |
| | | serviceStaff: [{ |
| | | validator: (_rule, _value, callback) => serviceStaff.value ? callback() : callback(new Error('请选择专属服务人员')), |
| | | trigger: 'change', |
| | | }], |
| | | serviceStaff: [{ required: true, message: '请选择专属服务人员', trigger: 'change' }], |
| | | } |
| | | const cardRules: FormRules = { |
| | | talentLevel: [{ required: true, message: '请选择人才等级', trigger: 'change' }], |
| | |
| | | saving.value = true |
| | | try { |
| | | // 后端仍保留 workAddress 历史字段;新建时同步写入,保证新旧端均可回显。 |
| | | const talent = await createTalent({ ...form, workAddress: form.workUnit }) |
| | | await updateTalentServiceStaff(talent.id, serviceStaff.value) |
| | | const { serviceStaff, ...talentForm } = form |
| | | const talent = await createTalent({ ...talentForm, workAddress: talentForm.workUnit }) |
| | | await updateTalentServiceStaff(talent.id, serviceStaff) |
| | | ElMessage.success('持卡人已保存') |
| | | router.push('/talents') |
| | | } finally { |
| | |
| | | <h3>专属服务人员</h3> |
| | | <el-form-item prop="serviceStaff" class="service-staff-form-item" required label-width="0"> |
| | | <div class="service-staff-panel"> |
| | | <el-select v-model="serviceStaff" clearable placeholder="请选择专属服务人员" @change="formRef?.validateField('serviceStaff')"> |
| | | <el-select v-model="form.serviceStaff" clearable placeholder="请选择专属服务人员"> |
| | | <el-option |
| | | v-for="item in staff" |
| | | :key="item.id" |
| | |
| | | } |
| | | |
| | | private List<Map<String, Object>> rightsConfigs(Long merchantId) { |
| | | List<Map<String, Object>> merchants = rows("select merchant_rights_config from merchant where id = ? and deleted = 0 limit 1", merchantId); |
| | | List<Map<String, Object>> merchants = rows("select merchant_type_code, merchant_rights_config from merchant where id = ? and deleted = 0 limit 1", merchantId); |
| | | if (merchants.isEmpty()) return List.of(); |
| | | Object raw = merchants.get(0).get("merchantRightsConfig"); |
| | | if (raw == null) { |
| | | for (Map.Entry<String, Object> entry : merchants.get(0).entrySet()) { |
| | | if (entry.getKey() != null && entry.getKey().toLowerCase().contains("rights")) { |
| | | raw = entry.getValue(); |
| | | break; |
| | | } |
| | | List<Map<String, Object>> configs = parseRightsConfigs(merchants.get(0).get("merchantRightsConfig")); |
| | | if (!"parking".equalsIgnoreCase(string(merchants.get(0).get("merchantTypeCode")))) return configs; |
| | | |
| | | // Parking's dedicated card-level configuration takes precedence over legacy JSON. |
| | | List<Map<String, Object>> parkingRights = rows(""" |
| | | select card_level as level, enabled, label as tag, benefit_detail as detail, |
| | | charge_type, store_consume_enabled, discount_text, government_subsidy, |
| | | consume_usage_text as consume_desc, status |
| | | from transport_parking_card_level_right |
| | | where merchant_id = ? and deleted = 0 |
| | | """, merchantId); |
| | | for (Map<String, Object> parkingRight : parkingRights) { |
| | | String level = string(parkingRight.get("level")); |
| | | Map<String, Object> merged = configs.stream() |
| | | .filter(right -> matchesCardLevel(string(right.get("level")), level)) |
| | | .findFirst().map(LinkedHashMap::new).orElseGet(LinkedHashMap::new); |
| | | for (Map.Entry<String, Object> entry : parkingRight.entrySet()) { |
| | | Object value = entry.getValue(); |
| | | if (value != null && (!(value instanceof String text) || !text.isBlank())) merged.put(entry.getKey(), value); |
| | | } |
| | | merged.put("enabled", booleanValue(parkingRight.get("enabled")) && booleanValue(parkingRight.get("status"))); |
| | | merged.put("parkingType", "PAID".equalsIgnoreCase(string(parkingRight.get("chargeType"))) ? "paid" : "free"); |
| | | merged.put("storeConsume", booleanValue(parkingRight.get("storeConsumeEnabled"))); |
| | | String discount = string(parkingRight.get("discountText")).replace("折", "").trim(); |
| | | if (!discount.isBlank()) merged.put("discount", discount); |
| | | configs.removeIf(right -> matchesCardLevel(string(right.get("level")), level)); |
| | | configs.add(merged); |
| | | } |
| | | if (raw == null || string(raw).isBlank()) return List.of(); |
| | | return configs; |
| | | } |
| | | |
| | | private List<Map<String, Object>> parseRightsConfigs(Object raw) { |
| | | if (raw == null || string(raw).isBlank()) return new ArrayList<>(); |
| | | String json = raw instanceof byte[] bytes ? new String(bytes, java.nio.charset.StandardCharsets.UTF_8) : string(raw); |
| | | List<Map<String, Object>> configs = new ArrayList<>(); |
| | | try { |
| | |
| | | } |
| | | } |
| | | } catch (Exception ignored) { |
| | | return List.of(); |
| | | return new ArrayList<>(); |
| | | } |
| | | return configs; |
| | | } |
| | |
| | | |
| | | boolean configuredStoreConsumptionEnabled(Map<String, Object> right) { |
| | | if (!booleanValue(right.get("storeConsume"))) return false; |
| | | return booleanValue(right.get("consumeEnabled")) || "paid".equalsIgnoreCase(string(right.get("parkingType"))); |
| | | if (!string(right.get("parkingType")).isBlank()) return "paid".equalsIgnoreCase(string(right.get("parkingType"))); |
| | | return booleanValue(right.get("consumeEnabled")); |
| | | } |
| | | |
| | | Map<String, Object> configuredRule(Map<String, Object> right) { |
| | |
| | | } |
| | | |
| | | private List<Map<String, Object>> coupons(Long merchantId, Long talentId, String cardLevel) { |
| | | if (!storeConsumptionCouponsEnabled(merchantId)) return List.of(); |
| | | return rows(""" |
| | | select c.id, c.coupon_name as name, c.coupon_type as couponType, c.coupon_amount as couponAmount, c.talent_levels as talentLevels, |
| | | c.condition_type as conditionType, c.condition_amount as conditionAmount, c.valid_end as validEnd |
| | | select c.id, c.coupon_name as name, c.coupon_type, c.coupon_amount, c.talent_levels, |
| | | c.condition_type, c.condition_amount, c.valid_end |
| | | from merchant_coupon c |
| | | where c.merchant_id = ? and c.deleted = 0 and c.status = 1 and c.store_consume = 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) |
| | |
| | | |
| | | private Map<String, Object> usableCoupon(Long merchantId, Long talentId, String cardLevel, Long couponId, BigDecimal amount) { |
| | | if (couponId == null) return null; |
| | | if (!storeConsumptionCouponsEnabled(merchantId)) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "文化服务到店消费仅支持人才卡折扣,不支持优惠券"); |
| | | } |
| | | List<Map<String,Object>> found = rows(""" |
| | | select c.id, c.coupon_amount as couponAmount, c.condition_type as conditionType, c.condition_amount as conditionAmount, |
| | | c.talent_levels as talentLevels |
| | | select c.id, c.coupon_amount, c.condition_type, c.condition_amount, |
| | | c.talent_levels |
| | | from merchant_coupon c |
| | | where c.id = ? and c.merchant_id = ? and c.deleted = 0 and c.status = 1 and c.store_consume = 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) |
| | |
| | | return coupon; |
| | | } |
| | | |
| | | private boolean storeConsumptionCouponsEnabled(Long merchantId) { |
| | | List<Map<String, Object>> merchants = rows("select merchant_type_code from merchant where id = ? and deleted = 0 limit 1", merchantId); |
| | | return !merchants.isEmpty() && !"culture".equalsIgnoreCase(string(merchants.get(0).get("merchantTypeCode"))); |
| | | } |
| | | |
| | | private boolean supportsCard(String levels, String cardLevel) { |
| | | if (levels == null || levels.isBlank()) return true; |
| | | return java.util.Arrays.stream(levels.split("[,、,]" )).anyMatch(v -> matchesCardLevel(v, cardLevel)); |
| | |
| | | |
| | | private BigDecimal configuredDiscountRate(Map<String, Object> right) { |
| | | try { |
| | | Object value = "折扣".equals(string(right.get("consumeType"))) ? right.get("consumeDiscount") : right.get("discount"); |
| | | Object value = !string(right.get("parkingType")).isBlank() ? right.get("discount") |
| | | : "折扣".equals(string(right.get("consumeType"))) ? right.get("consumeDiscount") : right.get("discount"); |
| | | BigDecimal discount = decimal(string(value)); |
| | | return discount.signum() > 0 && discount.compareTo(BigDecimal.TEN) <= 0 ? discount.divide(BigDecimal.TEN, 4, RoundingMode.HALF_UP) : BigDecimal.ZERO; |
| | | } catch (Exception ignored) { |
| | |
| | | } |
| | | |
| | | private BigDecimal configuredFixedReduction(Map<String, Object> right) { |
| | | if (!string(right.get("parkingType")).isBlank()) return BigDecimal.ZERO; |
| | | if (!"优惠".equals(string(right.get("consumeType")))) return BigDecimal.ZERO; |
| | | try { |
| | | BigDecimal reduction = decimal(string(right.get("consumeCoupon"))); |
| | |
| | | } |
| | | |
| | | private boolean configuredSubsidyEnabled(Map<String, Object> right) { |
| | | if (!string(right.get("parkingType")).isBlank()) return booleanValue(right.get("governmentSubsidy")); |
| | | String consumeType = string(right.get("consumeType")); |
| | | if ("折扣".equals(consumeType)) return booleanValue(right.get("consumeDiscountSubsidy")); |
| | | if ("优惠".equals(consumeType)) return booleanValue(right.get("consumeCouponSubsidy")); |
| | |
| | | } |
| | | |
| | | private String benefitText(Long merchantId, String cardLevel, String sourceAreaType) { |
| | | List<Map<String, Object>> merchants = rows("select merchant_type_code from merchant where id = ? and deleted = 0 limit 1", merchantId); |
| | | String merchantType = merchants.isEmpty() ? "" : string(merchants.get(0).get("merchantTypeCode")); |
| | | if ("children".equalsIgnoreCase(merchantType) || "culture".equalsIgnoreCase(merchantType)) { |
| | | List<Map<String, Object>> configs = rightsConfigs(merchantId).stream() |
| | | .filter(right -> booleanValue(right.get("enabled"))) |
| | | .toList(); |
| | | return firstRightsText(configs, cardLevel, sourceAreaType, |
| | | "culture".equalsIgnoreCase(merchantType) ? "consumeDesc" : "detail"); |
| | | } |
| | | String configuredDescription = consumeDescFromRights(merchantId, cardLevel, sourceAreaType); |
| | | if (!configuredDescription.isBlank()) return configuredDescription; |
| | | List<Map<String, Object>> benefits = rows(""" |
| | |
| | | |
| | | /** Build the displayed benefit from the same structured fields used for pricing. */ |
| | | private String configuredBenefitText(Map<String, Object> right) { |
| | | if (!string(right.get("parkingType")).isBlank()) { |
| | | BigDecimal rate = configuredDiscountRate(right); |
| | | return rate.signum() > 0 ? "到店消费" + rate.movePointRight(1).stripTrailingZeros().toPlainString() + "折" : ""; |
| | | } |
| | | List<String> parts = new ArrayList<>(); |
| | | String consumeType = string(right.get("consumeType")); |
| | | if ("优惠".equals(consumeType)) { |
| | |
| | | result.put("longitude", merchant.get("longitude")); |
| | | result.put("available", number(merchant.get("status")) == 1); |
| | | result.put("serviceLabel", text(merchant.get("serviceLabel"), "人才观影券")); |
| | | result.put("talentCardTag", rightsTag(displayRight, merchant)); |
| | | // 游客没有人才卡等级时不展示人才卡标签,只保留权益说明内容。 |
| | | boolean hasCardLevel = cardLevel != null && !cardLevel.isBlank(); |
| | | result.put("talentCardTag", hasCardLevel ? rightsTag(displayRight) : ""); |
| | | result.put("rightsTitle", rightsTitle(displayRight)); |
| | | result.put("rightsDescription", rightsDescription(displayRight, merchant)); |
| | | result.put("applicableCards", applicableCards(rights, displayRight, ticketPackage)); |
| | | result.put("applicableCards", hasCardLevel ? rightsTag(displayRight) : ""); |
| | | result.put("claimPrice", claimPriceText(ticketPackage)); |
| | | result.put("usage", usageText(ticketPackage)); |
| | | result.put("scope", scopeText(ticketPackage, merchant)); |
| | |
| | | return rows.isEmpty() ? new LinkedHashMap<>() : rows.get(0); |
| | | } |
| | | |
| | | private String rightsTag(Map<String, Object> right, Map<String, Object> merchant) { |
| | | if (right != null) { |
| | | String tag = text(right.get("tag"), ""); |
| | | if (!tag.isBlank()) return tag; |
| | | } |
| | | return text(merchant.get("serviceLabel"), "人才卡专享"); |
| | | private String rightsTag(Map<String, Object> right) { |
| | | return right == null ? "" : text(right.get("tag"), ""); |
| | | } |
| | | |
| | | private String rightsTitle(Map<String, Object> right) { |
| | |
| | | detail = text(right.get("rightsText"), ""); |
| | | if (!detail.isBlank()) return detail; |
| | | return text(right.get("benefit_text"), ""); |
| | | } |
| | | |
| | | private String applicableCards(List<Map<String, Object>> rights, Map<String, Object> right, Map<String, Object> ticketPackage) { |
| | | if (!rights.isEmpty()) { |
| | | List<String> levels = rights.stream() |
| | | .filter(item -> booleanValue(item.get("enabled"))) |
| | | .map(item -> normalizedCardLevel(text(item.get("level"), ""))) |
| | | .filter(level -> !level.isBlank()) |
| | | .distinct() |
| | | .toList(); |
| | | if (!levels.isEmpty()) return String.join("、", levels); |
| | | } |
| | | if (right != null) { |
| | | String level = normalizedCardLevel(text(right.get("level"), "")); |
| | | if (!level.isBlank()) return level; |
| | | } |
| | | String packageLevels = text(ticketPackage.get("talentLevels"), ""); |
| | | if (!packageLevels.isBlank()) return packageLevels; |
| | | return "卓越、杰出、菁英人才卡"; |
| | | } |
| | | |
| | | private String claimPriceText(Map<String, Object> ticketPackage) { |
| | |
| | | result.put("merchantName", row.get("merchantName")); |
| | | result.put("coverUrl", row.get("coverUrl")); |
| | | result.put("benefitSummary", text(row.get("serviceLabel"), "请咨询服务专员")); |
| | | result.put("applicableCards", text(matchedRight.get("tag"), text(row.get("talentLevels"), "请咨询服务专员"))); |
| | | result.put("applicableCards", text(matchedRight.get("tag"), "")); |
| | | result.put("address", text(row.get("address"), "暂未配置机构地址")); |
| | | result.put("contactMobile", text(row.get("contactMobile"), "")); |
| | | result.put("longitude", row.get("longitude")); |
| | |
| | | result.put("latitude", merchant.get("latitude")); |
| | | result.put("longitude", merchant.get("longitude")); |
| | | result.put("available", available); |
| | | result.put("serviceLabel", text(merchant.get("serviceLabel"), "请咨询商户")); |
| | | result.put("discountText", text(benefit.get("discountText"), "请咨询商户")); |
| | | result.put("serviceLabel", ""); |
| | | result.put("discountText", ""); |
| | | result.put("benefitTags", List.of()); |
| | | result.put("rightsTag", text(right == null ? null : right.get("tag"), "")); |
| | | result.put("applicableCards", text(right == null ? null : right.get("tag"), text(benefit.get("talentLevels"), "请咨询商户"))); |
| | | result.put("applicableCards", text(right == null ? null : right.get("tag"), "")); |
| | | result.put("groupBuyingEnabled", false); |
| | | String configuredDetail = text(right == null ? null : right.get("detail"), ""); |
| | | result.put("usageInstructions", text(configuredDetail, text(benefit.get("usageInstructions"), "请咨询商户"))); |
| | |
| | | package com.talentcard.server.business.portal; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.ArrayList; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Locale; |
| | |
| | | result.put("longitude", merchant.get("longitude")); |
| | | result.put("available", available); |
| | | result.put("unavailableReason", available ? "" : unavailableReason(merchant, right)); |
| | | result.put("serviceLabel", text(merchant.get("serviceLabel"), "停车服务")); |
| | | // 游客没有人才卡等级,不具备可展示的权益标签,避免把默认权益当成该用户的标签。 |
| | | boolean hasCardLevel = !cardLevel.isBlank(); |
| | | List<String> benefitTags = hasCardLevel ? parkingBenefitTags(merchantId, right) : List.of(); |
| | | String displayTag = benefitTags.isEmpty() ? "" : benefitTags.get(0); |
| | | result.put("serviceLabel", displayTag); |
| | | result.put("discountText", displayTag); |
| | | result.put("benefitTags", benefitTags); |
| | | result.put("benefitText", benefitText(merchant, right, benefit)); |
| | | result.put("applicableCards", applicableCards(right, benefit)); |
| | | result.put("applicableCards", hasCardLevel ? applicableCards(right, benefit) : ""); |
| | | result.put("usageInstructions", usageInstructions(merchant, right)); |
| | | result.put("rightsText", rightsText(merchant, right)); |
| | | String parkingType = text(right == null ? null : right.get("parkingType"), "paid"); |
| | |
| | | } |
| | | |
| | | private String applicableCards(Map<String, Object> right, Map<String, Object> benefit) { |
| | | return text(right == null ? null : right.get("level"), text(benefit.get("talentLevels"), "请咨询商户")); |
| | | return text(right == null ? null : right.get("tag"), ""); |
| | | } |
| | | |
| | | private String rightsText(Map<String, Object> merchant, Map<String, Object> right) { |
| | |
| | | return text(benefit.get("benefitText"), text(merchant.get("rightsText"), "免费停车")); |
| | | } |
| | | |
| | | private List<String> parkingBenefitTags(Long merchantId, Map<String, Object> right) { |
| | | if (right == null || !booleanValue(right.get("enabled")) || !rightStatusEnabled(right)) return List.of(); |
| | | String parkingType = text(right.get("parkingType"), "free"); |
| | | if ("free".equalsIgnoreCase(parkingType)) return List.of("免费"); |
| | | |
| | | List<String> tags = new ArrayList<>(); |
| | | if (hasActiveMerchantCoupon(merchantId)) tags.add("券"); |
| | | boolean hasStoredConsumeSetting = right.containsKey("storeConsumeEnabled"); |
| | | boolean storeConsume = hasStoredConsumeSetting |
| | | ? booleanValue(right.get("storeConsumeEnabled")) |
| | | : booleanValue(right.get("storeConsume")); |
| | | if (storeConsume) { |
| | | String rawDiscount = text(right.get("discount"), text(right.get("discountText"), "")) |
| | | .replace("折", "").trim(); |
| | | try { |
| | | BigDecimal discount = new BigDecimal(rawDiscount).stripTrailingZeros(); |
| | | if (discount.compareTo(BigDecimal.ZERO) > 0 && discount.compareTo(BigDecimal.TEN) <= 0) { |
| | | tags.add(discount.toPlainString() + "折"); |
| | | } |
| | | } catch (NumberFormatException ignored) { |
| | | // Invalid or missing discounts do not produce a display tag. |
| | | } |
| | | } |
| | | return tags; |
| | | } |
| | | |
| | | private boolean hasActiveMerchantCoupon(Long merchantId) { |
| | | Integer count = jdbc.queryForObject(""" |
| | | select count(*) from merchant_coupon |
| | | where merchant_id = ? and deleted = 0 and status = 1 |
| | | and (valid_start is null or valid_start <= current_date) |
| | | and (valid_end is null or valid_end >= current_date) |
| | | """, Integer.class, merchantId); |
| | | return count != null && count > 0; |
| | | } |
| | | |
| | | private String unavailableReason(Map<String, Object> merchant, Map<String, Object> right) { |
| | | if (number(merchant.get("status")) != 1) return "停车服务当前不可用"; |
| | | if (right == null) return "当前人才卡暂无可用停车权益"; |
| | |
| | | public class PortalService { |
| | | |
| | | private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); |
| | | private static final List<String> TAGLESS_MERCHANT_TYPES = List.of( |
| | | "employment", "finance", "approval", "apartment", "fuel", |
| | | "housing", "car", "appliance", "digital", "telecom"); |
| | | private final JdbcTemplate jdbcTemplate; |
| | | |
| | | public PortalService(JdbcTemplate jdbcTemplate) { |
| | |
| | | |
| | | private List<String> merchantListBenefitTags(Map<String, Object> item, Map<String, Object> row, Long talentId) { |
| | | String merchantType = MerchantTypeCatalog.canonicalCode(textOrDefault(item.get("merchantType"), "")); |
| | | if (TAGLESS_MERCHANT_TYPES.contains(merchantType)) return List.of(); |
| | | boolean couponAvailable = booleanValue(item.get("couponAvailable")); |
| | | String discountText = textOrDefault(item.get("discountText"), "").trim(); |
| | | String cardLevel = activeProductCardLevel(talentId); |
| | | String sourceAreaType = activeTalentSourceAreaType(talentId); |
| | | Map<String, Object> right = matchingMerchantRight( |
| | | merchantRightsConfigs(row.get("merchantRightsConfig")), cardLevel, sourceAreaType, true); |
| | | List<Map<String, Object>> rights = merchantRightsConfigs(row.get("merchantRightsConfig")); |
| | | Map<String, Object> right = matchingMerchantRight(rights, cardLevel, sourceAreaType, true); |
| | | if ("parking".equals(merchantType)) { |
| | | Long merchantId = item.get("id") instanceof Number number ? number.longValue() : null; |
| | | return parkingBenefitTags(merchantId, rights, cardLevel, sourceAreaType); |
| | | } |
| | | if ("carWash".equals(merchantType)) { |
| | | String tag = carWashListDiscountText(carWashExternalLinkType(rights), right); |
| | | return tag.isBlank() ? List.of() : List.of(tag); |
| | | } |
| | | if (usesMerchantDetailHeaderTags(merchantType) || "cinema".equals(merchantType)) { |
| | | return merchantBenefitTags(merchantType, couponAvailable, discountText, right); |
| | | } |
| | |
| | | Map<String, Object> right |
| | | ) { |
| | | String type = MerchantTypeCatalog.canonicalCode(merchantType); |
| | | if (TAGLESS_MERCHANT_TYPES.contains(type)) return List.of(); |
| | | if ("cinema".equals(type)) { |
| | | return couponAvailable ? List.of("券") : List.of(); |
| | | } |
| | |
| | | from merchant |
| | | """ + merchantWhere + " order by sort_no asc, id asc", |
| | | "select count(*) from merchant" + merchantWhere, page, size); |
| | | String cardLevel = "carWash".equals(requestedType) ? activeProductCardLevel(talentId) : null; |
| | | String sourceAreaType = "carWash".equals(requestedType) ? activeTalentSourceAreaType(talentId) : ""; |
| | | boolean personalizedTags = "carWash".equals(requestedType) || "parking".equals(requestedType); |
| | | String cardLevel = personalizedTags ? activeProductCardLevel(talentId) : null; |
| | | String sourceAreaType = personalizedTags ? activeTalentSourceAreaType(talentId) : ""; |
| | | List<Map<String, Object>> records = result.records().stream().map(row -> { |
| | | Map<String, Object> item = enrichMerchantSearchRow(row); |
| | | item.put("merchantType", requestedType); |
| | |
| | | item.put("externalLinkType", externalLinkType); |
| | | item.put("discountText", discountText); |
| | | item.put("benefitTagVisible", !discountText.isBlank()); |
| | | } else if ("fuel".equals(requestedType)) { |
| | | item.put("discountText", ""); |
| | | item.put("benefitTags", List.of()); |
| | | item.put("benefitTagVisible", false); |
| | | } else { |
| | | item.put("discountText", textOrDefault(item.get("serviceLabel"), textOrDefault(item.get("discountText"), "人才专享"))); |
| | | Long merchantId = item.get("id") instanceof Number number ? number.longValue() : null; |
| | | List<String> tags = parkingBenefitTags( |
| | | merchantId, merchantRightsConfigs(row.get("merchantRightsConfig")), cardLevel, sourceAreaType); |
| | | item.put("discountText", tags.isEmpty() ? "" : tags.get(0)); |
| | | item.put("benefitTags", tags); |
| | | item.put("benefitTagVisible", !tags.isEmpty()); |
| | | } |
| | | item.remove("merchantRightsConfig"); |
| | | return item; |
| | |
| | | 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.put("discountText", merchantId == null ? "" : merchantLowestTalentDiscount(merchantId, talentId)); |
| | | } |
| | | List<String> displayTags = merchantListBenefitTags(item, row, talentId); |
| | | item.put("benefitTags", displayTags); |
| | | item.put("discountText", displayTags.isEmpty() ? "" : displayTags.get(0)); |
| | | item.remove("merchantRightsConfig"); |
| | | return item; |
| | | }).toList(); |
| | |
| | | } catch (NumberFormatException ignored) { |
| | | return ""; |
| | | } |
| | | } |
| | | |
| | | private List<String> parkingBenefitTags( |
| | | Long merchantId, |
| | | List<Map<String, Object>> legacyRights, |
| | | String cardLevel, |
| | | String sourceAreaType |
| | | ) { |
| | | if (merchantId == null || cardLevel == null || cardLevel.isBlank()) return List.of(); |
| | | Map<String, Object> right = matchingMerchantRight(legacyRights, cardLevel, sourceAreaType, false); |
| | | List<Map<String, Object>> configuredRows = query(""" |
| | | select enabled, label as tag, benefit_detail as detail, charge_type as charge_type, |
| | | store_consume_enabled as store_consume_enabled, discount_text as discount_text, |
| | | status as right_status |
| | | from transport_parking_card_level_right |
| | | where merchant_id = ? and card_level = ? and deleted = 0 |
| | | limit 1 |
| | | """, merchantId, cardLevel); |
| | | if (!configuredRows.isEmpty()) right = mergeParkingRight(right, configuredRows.get(0)); |
| | | if (right == null |
| | | || (right.containsKey("enabled") && !booleanValue(right.get("enabled"))) |
| | | || (right.get("rightStatus") != null && !booleanValue(right.get("rightStatus")))) { |
| | | return List.of(); |
| | | } |
| | | |
| | | String chargeType = textOrDefault(right.get("chargeType"), ""); |
| | | String parkingType = textOrDefault(right.get("parkingType"), |
| | | "PAID".equalsIgnoreCase(chargeType) ? "paid" : "free"); |
| | | if ("free".equalsIgnoreCase(parkingType)) return List.of("免费"); |
| | | |
| | | List<String> tags = new ArrayList<>(); |
| | | if (hasActiveMerchantCoupon(merchantId)) tags.add("券"); |
| | | String discount = parkingDiscountText(right); |
| | | if (!discount.isBlank()) tags.add(discount); |
| | | return tags; |
| | | } |
| | | |
| | | private String parkingDiscountText(Map<String, Object> right) { |
| | | boolean hasStoredConsumeSetting = right.containsKey("storeConsumeEnabled"); |
| | | boolean storeConsume = hasStoredConsumeSetting |
| | | ? booleanValue(right.get("storeConsumeEnabled")) |
| | | : booleanValue(right.get("storeConsume")); |
| | | if (!storeConsume) return ""; |
| | | String discount = textOrDefault(right.get("discount"), textOrDefault(right.get("discountText"), "")) |
| | | .replace("折", "").trim(); |
| | | if (discount.isBlank()) return ""; |
| | | try { |
| | | BigDecimal value = new BigDecimal(discount).stripTrailingZeros(); |
| | | if (value.compareTo(BigDecimal.ZERO) <= 0 || value.compareTo(BigDecimal.TEN) > 0) return ""; |
| | | return value.toPlainString() + "折"; |
| | | } catch (NumberFormatException ignored) { |
| | | return ""; |
| | | } |
| | | } |
| | | |
| | | private Map<String, Object> mergeParkingRight(Map<String, Object> legacyRight, Map<String, Object> configuredRight) { |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | if (legacyRight != null) result.putAll(legacyRight); |
| | | if (configuredRight != null) { |
| | | for (Map.Entry<String, Object> entry : configuredRight.entrySet()) { |
| | | Object value = entry.getValue(); |
| | | if (value != null && (!(value instanceof String text) || !text.isBlank())) result.put(entry.getKey(), value); |
| | | } |
| | | String chargeType = textOrDefault(configuredRight.get("chargeType"), "FREE"); |
| | | result.put("parkingType", "PAID".equalsIgnoreCase(chargeType) ? "paid" : "free"); |
| | | boolean storeConsume = booleanValue(configuredRight.get("storeConsumeEnabled")); |
| | | result.put("storeConsume", storeConsume); |
| | | result.put("consumeEnabled", storeConsume); |
| | | String discount = textOrDefault(configuredRight.get("discountText"), "").replace("折", "").trim(); |
| | | if (!discount.isBlank()) result.put("discount", discount); |
| | | } |
| | | return result.isEmpty() ? null : result; |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | |
| | | Map<String, Object> item = enrichMerchantSearchRow(row); |
| | | item.put("businessType", merchantType.premiumBusinessType()); |
| | | item.put("targetAction", merchantType.premiumBusinessType()); |
| | | item.put("benefitText", textOrDefault(row.get("serviceLabel"), "人才专享")); |
| | | item.put("discountText", textOrDefault(row.get("serviceLabel"), "人才专享")); |
| | | boolean tagVisible = !TAGLESS_MERCHANT_TYPES.contains(merchantType.code()); |
| | | String benefitText = tagVisible ? textOrDefault(row.get("serviceLabel"), "人才专享") : ""; |
| | | item.put("benefitText", benefitText); |
| | | item.put("discountText", benefitText); |
| | | item.put("benefitTags", tagVisible ? List.of(benefitText) : List.of()); |
| | | item.put("benefitTagVisible", tagVisible); |
| | | return item; |
| | | }).toList(); |
| | | return new PageResult<>(records, result.page(), result.size(), result.total()); |
| | |
| | | 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("benefitTags", merchantListBenefitTags(merchant, merchant, talentId)); |
| | | merchant.put("usageInstructions", merchantUsageInstructions( |
| | | id, merchant.get("merchantRightsConfig"), cardLevel, merchant.get("usageInstructions"))); |
| | | List<String> heroImages = new ArrayList<>(); |
| | |
| | | result.put("categoryId", row.get("categoryKey")); |
| | | result.put("merchantName", row.get("merchantName")); |
| | | result.put("coverUrl", row.get("coverUrl")); |
| | | result.put("discountText", text(row.get("serviceLabel"), "")); |
| | | List<String> displayTags = displayTags(configuredMerchant); |
| | | result.put("displayTags", displayTags); |
| | | result.put("discountText", displayTags.isEmpty() ? "" : displayTags.get(0)); |
| | | result.put("applicableCards", text(configuredMerchant.get("applicableCards"), text(configuredMerchant.get("rightsTag"), ""))); |
| | | result.put("address", text(row.get("address"), "")); |
| | | result.put("contactMobile", text(row.get("contactMobile"), "")); |
| | |
| | | return result; |
| | | } |
| | | |
| | | private List<String> displayTags(Map<String, Object> configuredMerchant) { |
| | | List<String> result = new java.util.ArrayList<>(); |
| | | Object value = configuredMerchant.get("benefitTags"); |
| | | if (value instanceof List<?> tags) { |
| | | for (Object tag : tags) { |
| | | String text = text(tag, "").trim(); |
| | | if (!text.isBlank() && !result.contains(text)) result.add(text); |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Map<String, Object> accountAdvisor(Long talentId) { |
| | | if (talentId == null) return Map.of("available", false); |
| | | Object source = talentCardPortalService.profile(talentId).get("advisor"); |
| | |
| | | import java.util.Arrays; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Locale; |
| | | import java.util.Map; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("benefitId", benefit.get("id") == null ? merchantId : benefit.get("id")); |
| | | result.put("serviceId", benefit.get("merchantId")); |
| | | Map<String, Object> matchedRight = matchedRight(benefit.get("merchantRightsConfig"), activeCardLevel(talentId)); |
| | | result.put("title", benefit.get("title")); |
| | | result.put("heroImage", benefit.get("coverUrl")); |
| | | result.put("freeLabel", benefit.get("freeLabel")); |
| | | result.put("rulesText", configuredRulesText(benefit, talentId)); |
| | | result.put("applicableCards", text(matchedRight.get("tag"), "")); |
| | | result.put("rulesText", text(matchedRight.get("detail"), text(benefit.get("rulesText"), "请咨询服务单位了解配偶推荐就业权益详情"))); |
| | | result.put("consultationPhone", benefit.get("consultationPhone")); |
| | | result.put("available", number(benefit.get("enabled")) == 1); |
| | | result.put("unavailableReason", text(benefit.get("unavailableReason"), "该权益暂未开放")); |
| | |
| | | return rows.get(0); |
| | | } |
| | | |
| | | private String configuredRulesText(Map<String, Object> benefit, Long talentId) { |
| | | String cardLevel = activeCardLevel(talentId); |
| | | String config = text(benefit.get("merchantRightsConfig"), ""); |
| | | if (!cardLevel.isBlank() && !config.isBlank()) { |
| | | try { |
| | | List<Map<String, Object>> rights = JSON.parseObject(config, new com.alibaba.fastjson.TypeReference<>() {}); |
| | | if (rights != null) { |
| | | for (Map<String, Object> right : rights) { |
| | | if (!booleanValue(right.get("enabled")) || !cardLevel.equals(text(right.get("level"), ""))) continue; |
| | | String detail = text(right.get("detail"), ""); |
| | | if (!detail.isBlank()) return detail; |
| | | } |
| | | } |
| | | } catch (RuntimeException ignored) { |
| | | // Fall back to the merchant-level rights description below. |
| | | private Map<String, Object> matchedRight(Object config, String cardLevel) { |
| | | if (cardLevel.isBlank() || config == null) return Map.of(); |
| | | String raw = text(config, ""); |
| | | if (raw.isBlank()) return Map.of(); |
| | | try { |
| | | List<Map<String, Object>> rights = JSON.parseObject(raw, new com.alibaba.fastjson.TypeReference<>() {}); |
| | | if (rights == null) return Map.of(); |
| | | for (Map<String, Object> right : rights) { |
| | | if (booleanValue(right.get("enabled")) && sameCardLevel(right.get("level"), cardLevel)) return right; |
| | | } |
| | | } catch (RuntimeException ignored) { |
| | | // Fall back to the merchant-level rights description below. |
| | | } |
| | | return text(benefit.get("rulesText"), "请咨询服务单位了解配偶推荐就业权益详情"); |
| | | return Map.of(); |
| | | } |
| | | |
| | | private boolean sameCardLevel(Object configured, String actual) { |
| | | String left = normalizedCardLevel(configured); |
| | | return !left.isBlank() && left.equals(normalizedCardLevel(actual)); |
| | | } |
| | | |
| | | private String normalizedCardLevel(Object value) { |
| | | String level = value == null ? "" : String.valueOf(value).trim().toUpperCase(Locale.ROOT); |
| | | if (level.contains("卓越") || "EXCELLENT".equals(level)) return "EXCELLENT"; |
| | | if (level.contains("杰出") || "OUTSTANDING".equals(level)) return "OUTSTANDING"; |
| | | if (level.contains("菁英") || "ELITE".equals(level)) return "ELITE"; |
| | | return ""; |
| | | } |
| | | |
| | | private String activeCardLevel(Long talentId) { |
| | |
| | | package com.talentcard.server.business.merchant; |
| | | |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | import static org.assertj.core.api.Assertions.assertThatThrownBy; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.LinkedHashMap; |
| | |
| | | import com.talentcard.server.business.merchant.auth.MerchantPrincipal; |
| | | import org.junit.jupiter.api.BeforeEach; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.junit.jupiter.params.ParameterizedTest; |
| | | import org.junit.jupiter.params.provider.ValueSource; |
| | | import org.springframework.jdbc.core.JdbcTemplate; |
| | | import org.springframework.jdbc.datasource.DriverManagerDataSource; |
| | | |
| | |
| | | @BeforeEach |
| | | void setUp() { |
| | | DriverManagerDataSource dataSource = new DriverManagerDataSource( |
| | | "jdbc:h2:mem:manual-consumption-" + System.nanoTime() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", ""); |
| | | "jdbc:h2:mem:manual-consumption-" + System.nanoTime() + ";MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1", "sa", ""); |
| | | jdbc = new JdbcTemplate(dataSource); |
| | | jdbc.execute(""" |
| | | create table talent_dynamic_code ( |
| | |
| | | jdbc.execute(""" |
| | | create table merchant ( |
| | | id bigint primary key, |
| | | merchant_type_code varchar(64), |
| | | merchant_rights_config varchar(4000), |
| | | deleted tinyint not null default 0 |
| | | ) |
| | |
| | | assertThat(service.configuredStoreConsumptionEnabled(right)).isTrue(); |
| | | right.put("parkingType", "free"); |
| | | assertThat(service.configuredStoreConsumptionEnabled(right)).isFalse(); |
| | | right.put("consumeEnabled", true); |
| | | assertThat(service.configuredStoreConsumptionEnabled(right)).isFalse(); |
| | | } |
| | | |
| | | @Test |
| | | void paidParkingUsesParkingDiscountInsteadOfUnrelatedConsumeFields() { |
| | | Map<String, Object> right = configuredRight("卓越人才卡", "6"); |
| | | right.put("parkingType", "paid"); |
| | | right.put("consumeType", "优惠"); |
| | | right.put("consumeCoupon", "8"); |
| | | right.put("consumeDiscount", "9"); |
| | | Map<String, Object> rule = service.configuredRule(right); |
| | | assertThat(rule).containsEntry("benefitText", "到店消费6折"); |
| | | assertThat(service.quote(rule, null, new BigDecimal("10"))) |
| | | .containsEntry("payAmount", 600L).containsEntry("discountAmount", 400L); |
| | | right.put("consumeType", "折扣"); |
| | | assertThat(service.configuredRule(right)).containsEntry("benefitText", "到店消费6折"); |
| | | } |
| | | |
| | | @Test |
| | | void parkingDedicatedRightsOverrideLegacyAndRespectDisabledAndFreeLevels() { |
| | | jdbc.execute(""" |
| | | create table transport_parking_card_level_right ( |
| | | merchant_id bigint, card_level varchar(64), enabled tinyint, label varchar(128), |
| | | benefit_detail varchar(500), charge_type varchar(32), store_consume_enabled tinyint, |
| | | discount_text varchar(64), government_subsidy tinyint, consume_usage_text varchar(500), |
| | | status tinyint, deleted tinyint default 0 |
| | | ) |
| | | """); |
| | | jdbc.update("insert into merchant(id, merchant_type_code, merchant_rights_config) values (88, 'parking', null)"); |
| | | jdbc.update(""" |
| | | insert into transport_parking_card_level_right |
| | | (merchant_id, card_level, enabled, charge_type, store_consume_enabled, discount_text, status) |
| | | values (88, '卓越人才卡', 1, 'PAID', 1, '6折', 1) |
| | | """); |
| | | assertThat(service.hasApplicableRule(88L, "EXCELLENT", "")).isTrue(); |
| | | assertThat(service.hasApplicableRule(88L, "杰出人才卡", "")).isFalse(); |
| | | assertParkingDetailAndQuote(); |
| | | jdbc.update("update merchant set merchant_rights_config = ? where id = 88", |
| | | "[{\"level\":\"卓越人才卡\",\"enabled\":true,\"storeConsume\":true,\"consumeEnabled\":true,\"parkingType\":\"paid\",\"discount\":\"9\"}]"); |
| | | jdbc.update("update transport_parking_card_level_right set status = 0"); |
| | | assertThat(service.hasApplicableRule(88L, "卓越人才卡", "")).isFalse(); |
| | | jdbc.update("update transport_parking_card_level_right set status = 1, charge_type = 'FREE'"); |
| | | assertThat(service.hasApplicableRule(88L, "卓越人才卡", "")).isFalse(); |
| | | jdbc.update("update transport_parking_card_level_right set charge_type = 'PAID', store_consume_enabled = 0"); |
| | | assertThat(service.hasApplicableRule(88L, "卓越人才卡", "")).isFalse(); |
| | | } |
| | | |
| | | private void assertParkingDetailAndQuote() { |
| | | prepareConsumptionDetail(); |
| | | for (String discount : new String[] {"6折", "7折", "10折"}) { |
| | | jdbc.update("update transport_parking_card_level_right set discount_text = ?", discount); |
| | | Map<String, Object> detail = service.detail(principal, "parking-scan"); |
| | | assertThat(detail).containsEntry("cardLevel", "卓越人才卡"); |
| | | java.util.List<?> rules = (java.util.List<?>) detail.get("rules"); |
| | | assertThat(rules).hasSize(1); |
| | | Map<?, ?> rule = (Map<?, ?>) rules.get(0); |
| | | assertThat(rule.get("benefitText")).isEqualTo("到店消费" + discount); |
| | | Map<String, Object> quote = service.quote(principal, new ManualConsumptionCreateRequest( |
| | | "parking-scan", ((Number) rule.get("id")).longValue(), new BigDecimal("10"))); |
| | | assertThat(quote.get("payAmount")).isEqualTo(Long.parseLong(discount.replace("折", "")) * 100); |
| | | } |
| | | } |
| | | |
| | | @ParameterizedTest |
| | | @ValueSource(strings = {"carWash", "dental"}) |
| | | void merchantUsesEachScannedCardLevelDiscountInsteadOfItsLabel(String merchantType) { |
| | | prepareConsumptionDetail(); |
| | | String[] levels = {"卓越人才卡", "杰出人才卡", "菁英人才卡"}; |
| | | String[] discounts = {"7", "8.5", "9"}; |
| | | java.util.List<Map<String, Object>> rights = new java.util.ArrayList<>(); |
| | | for (int index = 0; index < levels.length; index++) { |
| | | Map<String, Object> right = configuredRight(levels[index], discounts[index]); |
| | | right.put("tag", levels[index] + "到店消费可用"); |
| | | right.put("enabled", true); |
| | | right.put("storeConsume", true); |
| | | right.put("consumeEnabled", true); |
| | | rights.add(right); |
| | | } |
| | | jdbc.update("insert into merchant(id, merchant_type_code, merchant_rights_config) values (88, ?, ?)", |
| | | merchantType, com.alibaba.fastjson.JSON.toJSONString(rights)); |
| | | for (int index = 0; index < levels.length; index++) { |
| | | jdbc.update("update talent_card set card_level = ? where id = 1", levels[index]); |
| | | Map<String, Object> detail = service.detail(principal, "parking-scan"); |
| | | assertThat(detail).containsEntry("cardLevel", levels[index]); |
| | | java.util.List<?> rules = (java.util.List<?>) detail.get("rules"); |
| | | assertThat(rules).hasSize(1); |
| | | Map<?, ?> rule = (Map<?, ?>) rules.get(0); |
| | | assertThat(rule.get("benefitText")).isEqualTo("到店消费" + discounts[index] + "折"); |
| | | Map<String, Object> quote = service.quote(principal, new ManualConsumptionCreateRequest( |
| | | "parking-scan", ((Number) rule.get("id")).longValue(), new BigDecimal("100"))); |
| | | assertThat(quote.get("payAmount")).isEqualTo(new BigDecimal(discounts[index]).multiply(new BigDecimal("1000")).longValue()); |
| | | } |
| | | } |
| | | |
| | | private void prepareConsumptionDetail() { |
| | | jdbc.execute("alter table talent_dynamic_code add column talent_id bigint"); |
| | | jdbc.execute("alter table talent_dynamic_code add column card_id bigint"); |
| | | jdbc.execute(""" |
| | | create table merchant_verification_attempt ( |
| | | id bigint, dynamic_code_id bigint, status tinyint, expires_at timestamp, manual_order_id bigint, |
| | | verification_id varchar(64), credential_type varchar(64), merchant_id bigint, store_id bigint, operator_id bigint |
| | | ) |
| | | """); |
| | | jdbc.execute("create table talent_card (id bigint, card_level varchar(64), deleted tinyint)"); |
| | | jdbc.execute("create table talent_user (id bigint, name varchar(64), mobile varchar(32), gender tinyint, source_area_type varchar(32), deleted tinyint)"); |
| | | jdbc.execute("create table merchant_card_benefit (id bigint, merchant_id bigint, card_level varchar(64), benefit_text varchar(500), status tinyint, sort_no int)"); |
| | | jdbc.execute(""" |
| | | create table merchant_coupon ( |
| | | id bigint, merchant_id bigint, coupon_name varchar(64), coupon_type varchar(64), coupon_amount decimal, |
| | | talent_levels varchar(64), condition_type varchar(64), condition_amount decimal, |
| | | valid_start date, valid_end date, deleted tinyint, status tinyint, store_consume tinyint |
| | | ) |
| | | """); |
| | | jdbc.execute("create table benefit_order (coupon_id bigint, talent_id bigint, deleted tinyint, order_status tinyint, expires_at timestamp)"); |
| | | jdbc.update("insert into talent_dynamic_code(id, status, talent_id, card_id) values (1, 0, 1, 1)"); |
| | | jdbc.update("insert into talent_card values (1, '卓越人才卡', 0)"); |
| | | jdbc.update("insert into talent_user values (1, '测试', '13800000000', 1, 'LOCAL', 0)"); |
| | | jdbc.update("insert into merchant_verification_attempt values (1, 1, 0, null, null, 'parking-scan', 'TALENT_CODE', 88, 7, 9)"); |
| | | } |
| | | |
| | | @Test |
| | | void cultureHidesLegacyCouponsAndRejectsCouponQuotesAndOrders() { |
| | | prepareConsumptionDetail(); |
| | | Map<String, Object> right = configuredRight("卓越人才卡", "8"); |
| | | right.put("enabled", true); |
| | | right.put("storeConsume", true); |
| | | right.put("consumeEnabled", true); |
| | | jdbc.update("insert into merchant(id, merchant_type_code, merchant_rights_config) values (88, 'culture', ?)", |
| | | com.alibaba.fastjson.JSON.toJSONString(java.util.List.of(right))); |
| | | jdbc.update(""" |
| | | insert into merchant_coupon (id, merchant_id, coupon_name, coupon_amount, talent_levels, |
| | | condition_type, deleted, status, store_consume) |
| | | values (1, 88, '旧优惠券', 20, '卓越人才卡', 'NONE', 0, 1, 1) |
| | | """); |
| | | Map<String, Object> detail = service.detail(principal, "parking-scan"); |
| | | assertThat((java.util.List<?>) detail.get("coupons")).isEmpty(); |
| | | ManualConsumptionCreateRequest discountOnly = new ManualConsumptionCreateRequest("parking-scan", -1L, new BigDecimal("100")); |
| | | assertThat(service.quote(principal, discountOnly)).containsEntry("payAmount", 8000L).containsEntry("couponAmount", 0L); |
| | | ManualConsumptionCreateRequest withCoupon = new ManualConsumptionCreateRequest("parking-scan", -1L, new BigDecimal("100"), 1L); |
| | | assertThatThrownBy(() -> service.quote(principal, withCoupon)) |
| | | .isInstanceOf(com.talentcard.server.common.BusinessException.class).hasMessageContaining("不支持优惠券"); |
| | | assertThatThrownBy(() -> service.create(principal, withCoupon, "culture-coupon-test")) |
| | | .isInstanceOf(com.talentcard.server.common.BusinessException.class).hasMessageContaining("不支持优惠券"); |
| | | assertThat(jdbc.queryForObject("select status from talent_dynamic_code where id = 1", Integer.class)).isZero(); |
| | | assertThat(jdbc.queryForObject("select count(*) from benefit_order", Integer.class)).isZero(); |
| | | |
| | | jdbc.update("update merchant set merchant_type_code = 'food' where id = 88"); |
| | | assertThat((java.util.List<?>) service.detail(principal, "parking-scan").get("coupons")).hasSize(1); |
| | | assertThat(service.quote(principal, withCoupon)).containsEntry("payAmount", 6000L).containsEntry("couponAmount", 2000L); |
| | | } |
| | | |
| | | @Test |
| | | void cultureCurrentBenefitUsesOnlyTheScannedLevelsStoreConsumptionDescription() { |
| | | prepareConsumptionDetail(); |
| | | String[] levels = {"卓越人才卡", "杰出人才卡", "菁英人才卡"}; |
| | | java.util.List<Map<String, Object>> rights = new java.util.ArrayList<>(); |
| | | for (String level : levels) { |
| | | Map<String, Object> right = configuredRight(level, "8"); |
| | | right.put("enabled", true); |
| | | right.put("storeConsume", true); |
| | | right.put("consumeEnabled", true); |
| | | right.put("detail", "其他权益详情"); |
| | | right.put("consumeDesc", level + "到店消费权益\n第二行 保留空格"); |
| | | rights.add(right); |
| | | } |
| | | jdbc.update("insert into merchant(id, merchant_type_code, merchant_rights_config) values (88, 'culture', ?)", |
| | | com.alibaba.fastjson.JSON.toJSONString(rights)); |
| | | jdbc.update("insert into merchant_card_benefit values (1, 88, 'EXCELLENT', '旧权益文案', 1, 0)"); |
| | | for (String level : levels) { |
| | | jdbc.update("update talent_card set card_level = ? where id = 1", level); |
| | | String expected = level + "到店消费权益\n第二行 保留空格"; |
| | | assertThat(service.detail(principal, "parking-scan")) |
| | | .containsEntry("cardLevel", level).containsEntry("benefitText", expected); |
| | | assertThat(service.benefitTextForMerchant(88L, level)).isEqualTo(expected); |
| | | } |
| | | rights.get(0).put("consumeDesc", ""); |
| | | rights.get(1).put("enabled", false); |
| | | jdbc.update("update merchant set merchant_rights_config = ? where id = 88", com.alibaba.fastjson.JSON.toJSONString(rights)); |
| | | assertThat(service.benefitTextForMerchant(88L, levels[0])).isEmpty(); |
| | | assertThat(service.benefitTextForMerchant(88L, levels[1])).isEmpty(); |
| | | assertThat(service.benefitTextForMerchant(88L, "普通游客")).isEmpty(); |
| | | } |
| | | |
| | | @Test |
| | | void childcareCurrentBenefitUsesTheScannedLevelsRightsDescription() { |
| | | prepareConsumptionDetail(); |
| | | String[] levels = {"卓越人才卡", "杰出人才卡", "菁英人才卡"}; |
| | | java.util.List<Map<String, Object>> rights = new java.util.ArrayList<>(); |
| | | for (String level : levels) { |
| | | Map<String, Object> right = configuredRight(level, "8"); |
| | | right.put("enabled", true); |
| | | right.put("storeConsume", true); |
| | | right.put("consumeEnabled", true); |
| | | right.put("detail", level + "托管权益描述\n第二行内容"); |
| | | right.put("consumeDesc", "不同的到店消费使用说明"); |
| | | rights.add(right); |
| | | } |
| | | jdbc.update("insert into merchant(id, merchant_type_code, merchant_rights_config) values (88, 'children', ?)", |
| | | com.alibaba.fastjson.JSON.toJSONString(rights)); |
| | | jdbc.update("insert into merchant_card_benefit values (1, 88, 'EXCELLENT', '旧权益文案', 1, 0)"); |
| | | for (String level : levels) { |
| | | jdbc.update("update talent_card set card_level = ? where id = 1", level); |
| | | assertThat(service.detail(principal, "parking-scan")) |
| | | .containsEntry("cardLevel", level) |
| | | .containsEntry("benefitText", level + "托管权益描述\n第二行内容"); |
| | | assertThat(service.benefitTextForMerchant(88L, level)).isEqualTo(level + "托管权益描述\n第二行内容"); |
| | | } |
| | | |
| | | rights.get(0).put("detail", ""); |
| | | rights.get(1).put("enabled", false); |
| | | jdbc.update("update merchant set merchant_rights_config = ? where id = 88", com.alibaba.fastjson.JSON.toJSONString(rights)); |
| | | assertThat(service.benefitTextForMerchant(88L, levels[0])).isEmpty(); |
| | | assertThat(service.benefitTextForMerchant(88L, levels[1])).isEmpty(); |
| | | assertThat(service.benefitTextForMerchant(88L, "普通游客")).isEmpty(); |
| | | } |
| | | |
| | | @Test |
| | |
| | | .hasMessageContaining("未配置适用于该人才卡的到店消费权益"); |
| | | } |
| | | |
| | | @Test |
| | | void parkingScanShowsAndPricesTheScannedCardLevelDiscount() { |
| | | jdbc.update("update merchant set merchant_type_code = 'parking', merchant_rights_config = ? where id = ?", |
| | | "[{\"level\":\"卓越人才卡\",\"enabled\":true,\"storeConsume\":true,\"consumeEnabled\":false,\"parkingType\":\"paid\",\"discount\":\"9\"}]", merchantId); |
| | | jdbc.update(""" |
| | | insert into transport_parking_card_level_right |
| | | (merchant_id, card_level, enabled, charge_type, store_consume_enabled, discount_text, government_subsidy, status) |
| | | values (?, '卓越人才卡', 1, 'PAID', 1, '6折', 0, 1), |
| | | (?, '杰出人才卡', 1, 'PAID', 1, '7折', 0, 1) |
| | | """, merchantId, merchantId); |
| | | Map<String, Object> scanned = verificationService.scan(principal, new VerificationScanRequest(token)); |
| | | String verificationId = String.valueOf(scanned.get("verificationId")); |
| | | Map<String, Object> detail = manualConsumptionService.detail(principal, verificationId); |
| | | assertThat(detail).containsEntry("cardLevel", "卓越人才卡"); |
| | | java.util.List<?> rules = (java.util.List<?>) detail.get("rules"); |
| | | assertThat(rules).hasSize(1); |
| | | Map<?, ?> rule = (Map<?, ?>) rules.get(0); |
| | | assertThat(rule.get("benefitText")).isEqualTo("到店消费6折"); |
| | | ManualConsumptionCreateRequest request = new ManualConsumptionCreateRequest(verificationId, |
| | | ((Number) rule.get("id")).longValue(), new BigDecimal("10")); |
| | | assertThat(manualConsumptionService.quote(principal, request)) |
| | | .containsEntry("payAmount", 600L).containsEntry("discountAmount", 400L); |
| | | Map<String, Object> created = manualConsumptionService.create(principal, request, "parking-test-" + System.nanoTime()); |
| | | assertThat(created).containsEntry("payAmount", 600L); |
| | | } |
| | | |
| | | @Test |
| | | void talentCodeStoreConsumptionUsesConfiguredCouponAfterTalentDiscount() { |
| | | Map<String, Object> scanned = verificationService.scan(principal, new VerificationScanRequest(token)); |
| New file |
| | |
| | | package com.talentcard.server.business.portal; |
| | | |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | import static org.mockito.ArgumentMatchers.any; |
| | | import static org.mockito.ArgumentMatchers.anyString; |
| | | import static org.mockito.ArgumentMatchers.eq; |
| | | import static org.mockito.Mockito.when; |
| | | |
| | | import java.lang.reflect.Method; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | import org.junit.jupiter.api.Test; |
| | | import org.junit.jupiter.api.extension.ExtendWith; |
| | | import org.mockito.InjectMocks; |
| | | import org.mockito.Mock; |
| | | import org.mockito.junit.jupiter.MockitoExtension; |
| | | import org.springframework.jdbc.core.JdbcTemplate; |
| | | |
| | | @ExtendWith(MockitoExtension.class) |
| | | class ParkingMerchantPortalServiceTests { |
| | | |
| | | @Mock |
| | | private JdbcTemplate jdbc; |
| | | |
| | | @InjectMocks |
| | | private ParkingMerchantPortalService service; |
| | | |
| | | @Test |
| | | @SuppressWarnings("unchecked") |
| | | void freeAndPaidParkingUseDifferentTagRules() throws Exception { |
| | | when(jdbc.queryForObject(anyString(), eq(Integer.class), any(Object[].class))).thenReturn(0); |
| | | Method method = ParkingMerchantPortalService.class.getDeclaredMethod( |
| | | "parkingBenefitTags", Long.class, Map.class); |
| | | method.setAccessible(true); |
| | | |
| | | List<String> freeTags = (List<String>) method.invoke(service, 1L, Map.of( |
| | | "enabled", true, "parkingType", "free")); |
| | | List<String> paidTags = (List<String>) method.invoke(service, 1L, Map.of( |
| | | "enabled", true, "parkingType", "paid", "storeConsume", true, |
| | | "consumeEnabled", false, "discount", "8.50")); |
| | | |
| | | assertThat(freeTags).containsExactly("免费"); |
| | | assertThat(paidTags).containsExactly("8.5折"); |
| | | } |
| | | } |
| | |
| | | |
| | | assertThat(foodTags).containsExactly("券", "8.5折"); |
| | | assertThat(cinemaTags).isEmpty(); |
| | | for (String type : List.of("employment", "finance", "approval", "apartment", "fuel", |
| | | "housing", "car", "appliance", "digital", "telecom")) { |
| | | List<String> tags = (List<String>) method.invoke(service, |
| | | Map.of("merchantType", type, "couponAvailable", true, "discountText", "7折"), |
| | | Map.of("serviceLabel", "人才专享"), null); |
| | | assertThat(tags).as(type).isEmpty(); |
| | | } |
| | | |
| | | List<String> scenicTags = (List<String>) method.invoke(service, |
| | | Map.of("merchantType", "scenic", "couponAvailable", true, "discountText", "8.8折"), |
| | |
| | | assertThat(foodTags).containsExactly("券", "8.8折"); |
| | | assertThat(hotelTags).containsExactly("券", "免费2次", "8.5折"); |
| | | } |
| | | |
| | | @Test |
| | | @SuppressWarnings("unchecked") |
| | | void parkingTagsUseFreeForFreeRightsAndDiscountForPaidRights() throws Exception { |
| | | when(jdbc.queryForList(anyString(), any(Object[].class))).thenReturn(List.of()); |
| | | when(jdbc.queryForObject(anyString(), eq(Long.class), any(Object[].class))).thenReturn(0L); |
| | | Method method = PortalService.class.getDeclaredMethod( |
| | | "parkingBenefitTags", Long.class, List.class, String.class, String.class); |
| | | method.setAccessible(true); |
| | | |
| | | List<String> freeTags = (List<String>) method.invoke(service, 1L, List.of(Map.of( |
| | | "level", "卓越人才卡", "enabled", true, "parkingType", "free")), "卓越人才卡", "LOCAL"); |
| | | List<String> paidTags = (List<String>) method.invoke(service, 1L, List.of(Map.of( |
| | | "level", "卓越人才卡", "enabled", true, "parkingType", "paid", |
| | | "storeConsume", true, "consumeEnabled", false, "discount", "8.50")), "卓越人才卡", "LOCAL"); |
| | | |
| | | assertThat(freeTags).containsExactly("免费"); |
| | | assertThat(paidTags).containsExactly("8.5折"); |
| | | } |
| | | } |