fix: update portal and merchant detail displays
| | |
| | | |
| | | <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 { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | const navTop = ref(28) |
| | |
| | | const navWidth = ref(88) |
| | | const navRight = ref(10) |
| | | const merchantId = ref('') |
| | | const talentId = ref('') |
| | | const merchantName = ref('') |
| | | const discount = ref('') |
| | | const applicableCards = 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' |
| | | loadDetail() |
| | | }) |
| | | const loadDetail = () => { |
| | | if (merchantId.value == '') { detailLoading.value = false; detailError.value = true; return } |
| | | detailLoading.value = true |
| | | detailError.value = false |
| | | getFinanceMerchantDetail(merchantId.value, (data: any) => { |
| | | getFinanceMerchantDetail(merchantId.value, talentId.value, (data: any) => { |
| | | merchantName.value = data.merchantName || '' |
| | | discount.value = data.benefitSummary || '请咨询服务专员' |
| | | applicableCards.value = data.applicableCards || '请咨询服务专员' |
| | |
| | | <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="loadDetail">重新加载</text></view> |
| | | <view v-else> |
| | | <view class="package-info-card"><view class="package-title-row"><text class="package-title">{{packageName}}</text><text class="free-tag">{{freeLabel}}</text></view><view class="service-tags"><text>{{applicableCards}}</text></view><text class="hospital-name">{{hospitalName}}</text><view class="address-row"><image class="address-icon" src="/static/benefits-assets/map-pin.svg"></image><text class="address-text">{{hospitalAddress}}</text><view class="address-actions"><view :class="canNavigate ? 'round-action' : 'round-action is-disabled'" @tap="openMap"><image src="/static/merchant-assets/navigation.svg"></image></view><view :class="hospitalPhone != '' ? 'round-action' : 'round-action is-disabled'" @tap="callHospital"><image src="/static/merchant-assets/phone.svg"></image></view></view></view><text v-if="!packageAvailable" class="unavailable-text">{{unavailableReason}}</text></view> |
| | | <view class="package-info-card"><view class="package-title-row"><text class="package-title">{{packageName}}</text><text class="free-tag">{{freeLabel}}</text></view><view v-if="applicableCards != ''" class="service-tags"><text>{{applicableCards}}</text></view><text class="hospital-name">{{hospitalName}}</text><view class="address-row"><image class="address-icon" src="/static/benefits-assets/map-pin.svg"></image><text class="address-text">{{hospitalAddress}}</text><view class="address-actions"><view :class="canNavigate ? 'round-action' : 'round-action is-disabled'" @tap="openMap"><image src="/static/merchant-assets/navigation.svg"></image></view><view :class="hospitalPhone != '' ? 'round-action' : 'round-action is-disabled'" @tap="callHospital"><image src="/static/merchant-assets/phone.svg"></image></view></view></view><text v-if="!packageAvailable" class="unavailable-text">{{unavailableReason}}</text></view> |
| | | <view class="tabs"><view class="tab-item active"><text>详情</text><view class="tab-line"></view></view></view> |
| | | <view v-if="itemsLoading" class="list-state"><view class="loading-spinner"></view><text>正在加载检查项目</text></view><view v-else-if="itemsError" class="list-state error-state"><text>检查项目加载失败</text><text class="retry-text" @tap="loadItems">重新加载</text></view><view v-else-if="itemsLoaded && packageItems.length == 0" class="list-state"><text>暂无体检检查项目</text></view> |
| | | <view v-if="itemsLoaded && packageItems.length > 0" class="check-table"><view class="check-head"><view class="check-cell cell-item"><text>检查项目</text></view><view class="check-cell cell-meaning"><text>项目意义</text></view><view class="check-cell cell-gender"><text>男</text></view><view class="check-cell cell-gender cell-gender-end"><text>女</text></view></view><view class="check-row" v-for="item in packageItems" :key="item.id"><view class="check-cell cell-item"><text>{{item.name}}</text></view><view class="check-cell cell-meaning"><text>{{item.meaning}}</text></view><view class="check-cell cell-gender"><text>{{item.male ? '√' : ''}}</text></view><view class="check-cell cell-gender cell-gender-end"><text>{{item.female ? '√' : ''}}</text></view></view></view> |
| | |
| | | import { resolveApiUrl } from '../../services/http.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10); const isSolidNav = ref(false) |
| | | const packageId = ref(''); const hospitalId = ref(''); const detailLoading = ref(true); const detailError = ref(false); const itemsLoading = ref(false); const itemsLoaded = ref(false); const itemsError = ref(false) |
| | | const packageId = ref(''); const hospitalId = ref(''); const routeApplicableCards = ref(''); const detailLoading = ref(true); const detailError = ref(false); const itemsLoading = ref(false); const itemsLoaded = ref(false); const itemsError = ref(false) |
| | | const packageName = ref(''); const heroImage = ref(''); const freeLabel = ref('免费'); const applicableCards = ref(''); const packageAvailable = ref(false); const unavailableReason = ref('该套餐暂不可预约') |
| | | const hospitalName = ref(''); const hospitalAddress = ref(''); const hospitalPhone = ref(''); const latitude = ref(0); const longitude = ref(0); const packageItems = ref<any[]>([]) |
| | | const canNavigate = computed(() => latitude.value > 0 && longitude.value > 0) |
| | | const canBook = computed(() => !detailLoading.value && !detailError.value && packageAvailable.value) |
| | | const decodeRouteText = (value: any): string => { |
| | | const text = value == null ? '' : String(value) |
| | | try { return decodeURIComponent(text) } catch (error) { return text } |
| | | } |
| | | 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 |
| | | packageId.value = options != null && options['packageId'] != null ? options['packageId'] as string : ''; hospitalId.value = options != null && options['hospitalId'] != null ? options['hospitalId'] as string : ''; loadDetail() |
| | | packageId.value = options != null && options['packageId'] != null ? options['packageId'] as string : ''; hospitalId.value = options != null && options['hospitalId'] != null ? options['hospitalId'] as string : ''; routeApplicableCards.value = options != null ? decodeRouteText(options['applicableCards']) : ''; applicableCards.value = routeApplicableCards.value; loadDetail() |
| | | }) |
| | | const loadDetail = () => { |
| | | if (packageId.value == '') { detailLoading.value = false; detailError.value = true; return } |
| | | detailLoading.value = true; detailError.value = false |
| | | getMedicalPackage(packageId.value, (data: any) => { const hospital: any = data.hospital || {}; packageName.value = data.name || ''; heroImage.value = data.heroImage != null && data.heroImage != '' ? resolveApiUrl(data.heroImage as string) : ''; freeLabel.value = data.freeLabel || '免费'; applicableCards.value = data.applicableCards || '以医院说明为准'; packageAvailable.value = data.available === true; unavailableReason.value = data.unavailableReason || unavailableReason.value; hospitalId.value = String(hospital.hospitalId || hospitalId.value); hospitalName.value = hospital.name || ''; hospitalAddress.value = hospital.address || '医院暂未配置地址'; hospitalPhone.value = hospital.mobile || ''; latitude.value = Number(hospital.latitude); longitude.value = Number(hospital.longitude); detailLoading.value = false; loadItems() }, () => { detailLoading.value = false; detailError.value = true }) |
| | | getMedicalPackage(packageId.value, (data: any) => { const hospital: any = data.hospital || {}; packageName.value = data.name || ''; heroImage.value = data.heroImage != null && data.heroImage != '' ? resolveApiUrl(data.heroImage as string) : ''; freeLabel.value = data.freeLabel || '免费'; const apiApplicableCards = data.applicableCards != null ? String(data.applicableCards).trim() : ''; applicableCards.value = apiApplicableCards != '' ? apiApplicableCards : routeApplicableCards.value; packageAvailable.value = data.available === true; unavailableReason.value = data.unavailableReason || unavailableReason.value; hospitalId.value = String(hospital.hospitalId || hospitalId.value); hospitalName.value = hospital.name || ''; hospitalAddress.value = hospital.address || '医院暂未配置地址'; hospitalPhone.value = hospital.mobile || ''; latitude.value = Number(hospital.latitude); longitude.value = Number(hospital.longitude); detailLoading.value = false; loadItems() }, () => { detailLoading.value = false; detailError.value = true }) |
| | | } |
| | | const loadItems = () => { if (packageId.value == '') return; itemsLoading.value = true; itemsLoaded.value = false; itemsError.value = false; getMedicalPackageItems(packageId.value, (data: any) => { packageItems.value = data instanceof Array ? data as any[] : []; itemsLoading.value = false; itemsLoaded.value = true }, () => { itemsLoading.value = false; itemsError.value = true }) } |
| | | const goBack = () => safeBack('/pages/premium-benefits/premium-benefits') |
| | |
| | | |
| | | <style> |
| | | .page { width:100%; height:100vh; background-color:#F6F6F6; }.content { height:calc(100% - 150rpx); height:calc(100% - 150rpx - env(safe-area-inset-bottom)); background-color:#F6F6F6; }.hero { height:445rpx; position:relative; overflow:hidden; background-color:#3B3028; }.hero-image { position:absolute; left:0; top:0; width:100%; height:445rpx; }.hero-shade { position:absolute; left:0; right:0; top:0; height:190rpx; background-image:linear-gradient(180deg,rgba(0,0,0,.58),rgba(0,0,0,0)); }.custom-nav { position:fixed; z-index:20; left:0; right:0; top:0; padding-bottom:20rpx; }.custom-nav-bg { position:absolute; z-index:0; left:0; right:0; top:0; bottom:0; opacity:0; background-image:linear-gradient(180deg,#F26A38,#EE542B); transition:opacity 280ms ease-out; }.custom-nav.is-solid .custom-nav-bg { opacity:1; }.safe-space { height:28px; position:relative; z-index:2; }.nav-row { display:flex; box-sizing:border-box; padding-left:24rpx; position:relative; z-index:2; 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,.52); justify-content:center; align-items:center; }.custom-nav.is-solid .back-button { background-color:rgba(45,45,45,0); }.back-button image { width:34rpx; height:34rpx; }.nav-title { position:absolute; left:105rpx; right:105rpx; opacity:0; color:#FFF; font-size:29rpx; font-weight:600; text-align:center; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.custom-nav.is-solid .nav-title { opacity:1; }.capsule-space { height:100%; flex-shrink:0; }.package-panel { position:relative; z-index:3; min-height:600rpx; margin-top:-22rpx; border-radius:27rpx 27rpx 0 0; overflow:hidden; background-color:#F6F6F6; }.package-info-card { padding:25rpx 38rpx 20rpx; }.package-title-row { display:flex; min-height:50rpx; align-items:center; justify-content:space-between; }.package-title { min-width:0; flex:1; color:#292929; font-size:35rpx; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.free-tag { margin-left:15rpx; color:#FF7C2B; font-size:26rpx; }.service-tags { display:flex; margin-top:16rpx; }.service-tags text { padding:6rpx 11rpx; border:1rpx solid #FF9B70; border-radius:5rpx; color:#EF7046; font-size:24rpx; }.hospital-name { display:block; margin-top:20rpx; color:#292929; font-size:30rpx; font-weight:700; }.address-row { display:flex; min-height:72rpx; margin-top:12rpx; align-items:flex-start; }.address-icon { width:27rpx; height:27rpx; margin-top:4rpx; margin-right:8rpx; flex-shrink:0; }.address-text { width:420rpx; min-width:0; color:#7B8491; font-size:24rpx; line-height:34rpx; }.address-actions { display:flex; margin-left:auto; flex-shrink:0; }.round-action { display:flex; width:52rpx; height:52rpx; border-radius:50%; background-color:#F2F3F5; justify-content:center; align-items:center; }.round-action + .round-action { margin-left:30rpx; }.round-action image { width:27rpx; height:27rpx; }.is-disabled { opacity:.42; }.unavailable-text { display:block; margin-top:10rpx; color:#D96B3B; font-size:23rpx; }.tabs { display:flex; height:85rpx; padding:0 38rpx; align-items:center; }.tab-item { display:flex; height:85rpx; position:relative; color:#303030; font-size:32rpx; font-weight:600; align-items:center; }.tab-line { position:absolute; left:50%; bottom:0; width:59rpx; height:7rpx; margin-left:-30rpx; border-radius:4rpx; background-image:linear-gradient(90deg,#FF4D36,#FFBF3A); }.detail-state,.list-state { display:flex; height:330rpx; margin:16rpx 25rpx 0; color:#969CA5; font-size:24rpx; align-items:center; justify-content:center; }.detail-state { margin-top:0; }.error-state { flex-direction:column; }.retry-text { margin-top:20rpx; color:#F26A38; }.loading-spinner { width:28rpx; height:28rpx; margin-right:13rpx; box-sizing:border-box; border:3rpx solid #FFD6C4; border-top-color:#FF6C2B; border-radius:50%; animation:medical-loading 720ms linear infinite; } @keyframes medical-loading { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }.check-table { margin:16rpx 25rpx 0; border:1rpx solid #E3E6EB; border-radius:8rpx; overflow:hidden; background-color:#FFF; }.check-head,.check-row { display:flex; }.check-head { background-color:#F5F6F8; }.check-row { border-top:1rpx solid #E3E6EB; }.check-cell { display:flex; box-sizing:border-box; padding:18rpx 14rpx; border-left:1rpx solid #E3E6EB; align-items:center; justify-content:center; }.cell-item { width:180rpx; border-left:0; flex-shrink:0; }.cell-meaning { min-width:0; flex:1; justify-content:flex-start; }.cell-gender { width:84rpx; flex-shrink:0; }.check-head .check-cell text { color:#30343A; font-size:24rpx; font-weight:700; }.check-cell text { color:#4A5159; font-size:22rpx; line-height:32rpx; }.cell-gender text { color:#30343A; font-size:26rpx; }.content-bottom { height:35rpx; }.action-bar { display:flex; height:150rpx; height:calc(150rpx + env(safe-area-inset-bottom)); box-sizing:border-box; padding:20rpx 34rpx 2rpx; padding-bottom:calc(2rpx + env(safe-area-inset-bottom)); background:#FFF; box-shadow:0 -5rpx 15rpx rgba(0,0,0,.06); align-items:flex-end; }.booking-button { display:flex; width:100%; height:90rpx; border-radius:46rpx; background-image:linear-gradient(90deg,#FF4D32,#FF8A20); justify-content:center; align-items:center; }.booking-button text { color:#FFF; font-size:31rpx; }.booking-disabled { background-image:none; background-color:#C6CBD2; }.booking-button-active { opacity:.82; transform:scale(.99); } |
| | | .package-title-row, |
| | | .service-tags, |
| | | .address-row, |
| | | .address-actions, |
| | | .tabs, |
| | | .tab-item, |
| | | .detail-state, |
| | | .list-state, |
| | | .check-head, |
| | | .check-row { |
| | | display: flex; |
| | | flex-direction: row; |
| | | } |
| | | .service-tags { |
| | | align-items: flex-start; |
| | | } |
| | | .service-tags text { |
| | | flex-shrink: 0; |
| | | white-space: nowrap; |
| | | } |
| | | .error-state { |
| | | flex-direction: column; |
| | | } |
| | | .check-head, |
| | | .check-row { |
| | | width: 100%; |
| | | align-items: stretch; |
| | | } |
| | | .check-cell { |
| | | display: flex; |
| | | flex-direction: row; |
| | | } |
| | | </style> |
| | |
| | | <view class="merchant-tags"><text v-if="merchantHasCoupon" class="coupon-tag">券</text><text v-if="merchantDiscount != ''" class="discount-tag">{{merchantDiscount}}</text></view> |
| | | </view> |
| | | <view class="service-tags"><text v-if="!isParkingMerchant && !isMedicalMerchant">{{merchantServiceLabel}}</text><text>{{applicableCards}}</text></view> |
| | | <view v-if="!isParkingMerchant" class="merchant-address-row"> |
| | | <view v-if="!isParkingMerchant && merchantType != 'apartment'" class="merchant-address-row"> |
| | | <image class="address-icon" src="/static/benefits-assets/map-pin.svg"></image> |
| | | <text>{{merchantAddress}}</text> |
| | | <view class="merchant-actions"><view :class="merchantLocation.latitude > 0 && merchantLocation.longitude > 0 ? 'round-action' : 'round-action is-disabled'" @tap="openMap"><image src="/static/merchant-assets/navigation.svg"></image></view><view :class="merchantPhone != '' ? 'round-action' : 'round-action is-disabled'" @tap="callMerchant"><image src="/static/merchant-assets/phone.svg"></image></view></view> |
| | |
| | | <view class="medical-package-item" v-for="item in visibleMedicalPackages" :key="item.id" hover-class="package-item-active" @tap="openMedicalPackage(item)"> |
| | | <image class="medical-package-image" :src="item.image" mode="aspectFill" @error="replaceImage(item)"></image> |
| | | <view class="medical-package-info"> |
| | | <text class="medical-package-name">{{item.name}}</text> |
| | | <view class="medical-package-bottom"> |
| | | <view class="medical-open-row"><text class="medical-open-label">开放时间</text><text class="medical-open-value">{{item.openTime}}</text></view> |
| | | <view class="medical-package-top"> |
| | | <text class="medical-package-name">{{item.name}}</text> |
| | | <view class="medical-view-button"><text>查看</text></view> |
| | | </view> |
| | | <view class="medical-open-row"><text class="medical-open-label">开放时间</text><text class="medical-open-value">{{item.openTime}}</text></view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | |
| | | merchantHeroImages.value = nextHeroes |
| | | merchantAddress.value = data != null && data.address != null ? data.address as string : merchantAddress.value |
| | | const rawType = data != null && data.merchantType != null ? data.merchantType as string : merchantType.value |
| | | merchantType.value = rawType == '餐饮服务' ? 'food' : (rawType == '酒店服务' ? 'hotel' : rawType) |
| | | merchantType.value = rawType == '餐饮服务' ? 'food' : (rawType == '酒店服务' ? 'hotel' : ((rawType == '公寓租赁' || rawType == '公寓租赁服务') ? 'apartment' : rawType)) |
| | | merchantAvailable.value = data == null || data.available !== false |
| | | merchantPhone.value = data != null && data.contactMobile != null ? data.contactMobile as string : '' |
| | | merchantHasCoupon.value = data != null && (data.couponAvailable === true || Number(data.couponAvailable) == 1) |
| | |
| | | } |
| | | const openMedicalPackage = (item: any) => { |
| | | if (item.available === false) { uni.showToast({ title: item.unavailableReason || '该套餐暂不可预约', icon: 'none' }); return } |
| | | uni.navigateTo({ url: '/pages/medical-package/medical-package?packageId=' + encodeURIComponent(item.id) + '&hospitalId=' + encodeURIComponent(merchantId.value) + '&name=' + encodeURIComponent(item.name) + '&hospital=' + encodeURIComponent(merchantName.value) }) |
| | | uni.navigateTo({ url: '/pages/medical-package/medical-package?packageId=' + encodeURIComponent(item.id) + '&hospitalId=' + encodeURIComponent(merchantId.value) + '&name=' + encodeURIComponent(item.name) + '&hospital=' + encodeURIComponent(merchantName.value) + '&applicableCards=' + encodeURIComponent(applicableCards.value) }) |
| | | } |
| | | const showBottomVoucher = () => { |
| | | if (isHotelMerchant.value && hasFreeVoucher.value) { |
| | |
| | | .medical-package-item:last-child { margin-bottom: 0; } |
| | | .medical-package-image { width: 168rpx; height: 168rpx; border-radius: 14rpx; flex-shrink: 0; } |
| | | .medical-package-info { display: flex; min-width: 0; flex: 1; padding-left: 23rpx; flex-direction: column; } |
| | | .medical-package-name { color: #292929; font-size: 31rpx; font-weight: 700; } |
| | | .medical-package-bottom { display: flex; margin-top: auto; flex-direction: row; align-items: center; justify-content: space-between; } |
| | | .medical-open-row { display: flex; flex-direction: row; align-items: center; } |
| | | .medical-open-label { color: #8A919C; font-size: 24rpx; } |
| | | .medical-open-value { margin-left: 14rpx; color: #FF7C2B; font-size: 26rpx; font-weight: 600; } |
| | | .medical-view-button { display: flex; width: 132rpx; height: 57rpx; border-radius: 29rpx; background-image: linear-gradient(90deg, #FF5733 0%, #FF8A20 100%); justify-content: center; align-items: center; } |
| | | .medical-package-top { display: flex; min-width: 0; flex-direction: row; align-items: center; justify-content: space-between; } |
| | | .medical-package-name { min-width: 0; flex: 1; margin-right: 16rpx; color: #292929; font-size: 31rpx; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| | | .medical-open-row { display: flex; min-width: 0; margin-top: auto; flex-direction: row; align-items: center; } |
| | | .medical-open-label { color: #8A919C; font-size: 24rpx; flex-shrink: 0; } |
| | | .medical-open-value { min-width: 0; flex: 1; margin-left: 14rpx; color: #FF7C2B; font-size: 26rpx; font-weight: 600; } |
| | | .medical-view-button { display: flex; width: 132rpx; height: 57rpx; border-radius: 29rpx; background-image: linear-gradient(90deg, #FF5733 0%, #FF8A20 100%); flex-shrink: 0; justify-content: center; align-items: center; } |
| | | .medical-view-button text { color: #FFFFFF; font-size: 27rpx; } |
| | | .medical-usage-panel { min-height: 548rpx; margin: 16rpx 25rpx 0; box-sizing: border-box; padding: 31rpx 27rpx; border-radius: 15rpx; background-color: #EFF1F4; } |
| | | .medical-usage-panel text { color: #777F8B; font-size: 27rpx; line-height: 43rpx; white-space: pre-wrap; } |
| | |
| | | // #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'; submitKey.value = createIdempotencyKey('spouse-employment'); loadBenefit() |
| | | }) |
| | | const loadBenefit = () => { if (serviceId.value == '') { loading.value = false; loadError.value = true; return }; loading.value = true; loadError.value = false; getSpouseEmploymentBenefit(serviceId.value, (data: any) => { available.value = data.available === true; unavailableReason.value = data.unavailableReason || unavailableReason.value; const config = data.formConfig || {}; educationOptions.value = config.educationOptions instanceof Array && config.educationOptions.length > 0 ? config.educationOptions : ['高中及以下', '大专', '本科', '硕士研究生', '博士研究生']; institutionOptions.value = config.institutionOptions instanceof Array && config.institutionOptions.length > 0 ? config.institutionOptions : ['是', '否']; loading.value = false }, () => { loading.value = false; loadError.value = true }) } |
| | | 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) => { available.value = data.available === true; unavailableReason.value = data.unavailableReason || unavailableReason.value; const config = data.formConfig || {}; educationOptions.value = config.educationOptions instanceof Array && config.educationOptions.length > 0 ? config.educationOptions : ['高中及以下', '大专', '本科', '硕士研究生', '博士研究生']; institutionOptions.value = config.institutionOptions instanceof Array && config.institutionOptions.length > 0 ? config.institutionOptions : ['是', '否']; loading.value = false }, () => { loading.value = false; loadError.value = true }) } |
| | | const goBack = () => safeBack('/pages/spouse-employment/spouse-employment?serviceId=' + encodeURIComponent(serviceId.value)) |
| | | const changeEducation = (event: any) => { const index = parseInt(event.detail.value as string); educationIndex.value = index; const next = form.value; next.education = educationOptions.value[index]; form.value = next } |
| | | const changeInstitution = (event: any) => { const index = parseInt(event.detail.value as string); institutionIndex.value = index; const next = form.value; next.inInstitution = institutionOptions.value[index]; form.value = next } |
| | |
| | | if (!(/^(\d{15}|\d{17}[0-9Xx])$/).test(form.value.idCard.trim())) { uni.showToast({ title: '请填写正确的身份证号', icon: 'none' }); return } |
| | | if (!required(form.value.politicalStatus, '政治面貌') || !required(form.value.education, '学历') || !required(form.value.schoolMajor, '毕业院校及专业') || !required(form.value.workUnit, '现工作单位及职务') || !required(form.value.inInstitution, '是否在编') || !required(form.value.workExperience, '工作经历')) return |
| | | isSubmitting.value = true |
| | | submitSpouseEmploymentApplication(serviceId.value, { talentId: Number(talentId.value), name: form.value.name.trim(), mobile: form.value.mobile.trim(), idCard: form.value.idCard.trim(), politicalStatus: form.value.politicalStatus.trim(), education: form.value.education, schoolMajor: form.value.schoolMajor.trim(), workUnit: form.value.workUnit.trim(), inInstitution: form.value.inInstitution, workExperience: form.value.workExperience.trim() }, submitKey.value, (data: any) => { isSubmitting.value = false; uni.redirectTo({ url: '/pages/spouse-employment-success/spouse-employment-success?applicationId=' + encodeURIComponent(String(data.applicationId)) + '&applicationNo=' + encodeURIComponent(data.applicationNo || '') + '&estimatedProcessingTime=' + encodeURIComponent(data.estimatedProcessingTime || '') }) }, (error) => { isSubmitting.value = false; uni.showToast({ title: error.message, icon: 'none' }) }) |
| | | submitSpouseEmploymentApplication(serviceId.value, { talentId: Number(talentId.value), name: form.value.name.trim(), mobile: form.value.mobile.trim(), idCard: form.value.idCard.trim(), politicalStatus: form.value.politicalStatus.trim(), education: form.value.education, schoolMajor: form.value.schoolMajor.trim(), workUnit: form.value.workUnit.trim(), inInstitution: form.value.inInstitution, workExperience: form.value.workExperience.trim() }, submitKey.value, (data: any) => { isSubmitting.value = false; uni.redirectTo({ url: '/pages/spouse-employment-success/spouse-employment-success?applicationId=' + encodeURIComponent(String(data.applicationId)) + '&applicationNo=' + encodeURIComponent(data.applicationNo || '') }) }, (error) => { isSubmitting.value = false; uni.showToast({ title: error.message, icon: 'none' }) }) |
| | | } |
| | | </script> |
| | | |
| | |
| | | <template> |
| | | <view class="page"><view class="top-glow"></view><view class="custom-nav"><view class="safe-space" :style="'height:' + navTop + 'px'"></view><view class="nav-row" :style="'height:' + navHeight + 'px;padding-right:' + navRight + 'px'"><view class="back-button" @tap="goBack"><image src="/static/news-assets/arrow-left.svg" mode="aspectFit"></image></view><view class="capsule-space" :style="'width:' + navWidth + 'px'"></view></view></view><view class="result-content"><image class="result-image" src="/static/premium-assets/employment-success.svg" mode="aspectFit"></image><text class="result-title">已提交</text><text class="result-desc">服务专员将联系确认</text><view class="status-row"><text>当前状态:</text><text class="status-pill">待处理</text></view><text v-if="estimatedProcessingTime != ''" class="estimate">{{estimatedProcessingTime}}</text><view class="record-button" hover-class="record-button-active" @tap="viewRecords"><text>返回使用记录</text></view></view></view> |
| | | <view class="page"><view class="top-glow"></view><view class="custom-nav"><view class="safe-space" :style="'height:' + navTop + 'px'"></view><view class="nav-row" :style="'height:' + navHeight + 'px;padding-right:' + navRight + 'px'"><view class="back-button" @tap="goBack"><image src="/static/news-assets/arrow-left.svg" mode="aspectFit"></image></view><view class="capsule-space" :style="'width:' + navWidth + 'px'"></view></view></view><view class="result-content"><image class="result-image" src="/static/premium-assets/employment-success.svg" mode="aspectFit"></image><text class="result-title">已提交</text><text class="result-desc">服务专员将联系确认</text><view class="status-row"><text>当前状态:</text><text class="status-pill">待处理</text></view><view class="record-button" hover-class="record-button-active" @tap="viewRecords"><text>返回使用记录</text></view></view></view> |
| | | </template> |
| | | |
| | | <script setup lang="uts"> |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10); const estimatedProcessingTime = ref('') |
| | | const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10) |
| | | 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 |
| | | estimatedProcessingTime.value = options != null && options['estimatedProcessingTime'] != null ? decodeURIComponent(options['estimatedProcessingTime'] as string) : '' |
| | | }) |
| | | const goBack = () => safeBack('/pages/order/order?status=' + encodeURIComponent('待处理')) |
| | | const viewRecords = () => uni.reLaunch({ url: '/pages/order/order?status=' + encodeURIComponent('待处理') }) |
| | |
| | | |
| | | <style> |
| | | .page { width:100%; height:100vh; position:relative; overflow:hidden; background-color:#FFFFFF; }.top-glow { position:absolute; left:0; right:0; top:0; height:500rpx; background-image:radial-gradient(circle at 52% 0%,rgba(255,220,119,.72),rgba(255,240,197,.36) 34%,rgba(255,255,255,0) 75%); }.custom-nav { position:relative; z-index:3; }.safe-space { height:28px; }.nav-row { display:flex; box-sizing:border-box; padding-left:26rpx; flex-direction:row; align-items:center; justify-content:space-between; }.back-button { display:flex; width:60rpx; height:60rpx; margin-left:-8rpx; justify-content:center; align-items:center; }.back-button image { width:38rpx; height:38rpx; }.capsule-space { height:100%; flex-shrink:0; }.result-content { display:flex; position:relative; z-index:2; margin-top:80rpx; flex-direction:column; align-items:center; }.result-image { width:250rpx; height:250rpx; }.result-title { margin-top:30rpx; color:#23252A; font-size:31rpx; font-weight:500; }.result-desc { margin-top:24rpx; color:#8D96A5; font-size:24rpx; }.status-row { display:flex; margin-top:14rpx; color:#8D96A5; font-size:24rpx; align-items:center; }.status-pill { margin-left:10rpx; padding:5rpx 12rpx; border-radius:7rpx; background-color:#FFF4E6; color:#FF8A1E; }.estimate { max-width:560rpx; margin-top:16rpx; color:#8D96A5; font-size:22rpx; line-height:32rpx; text-align:center; }.record-button { display:flex; width:352rpx; height:78rpx; margin-top:44rpx; box-sizing:border-box; border:2rpx solid #DCE1E8; border-radius:40rpx; background-color:rgba(255,255,255,.72); justify-content:center; align-items:center; }.record-button text { color:#7B8495; font-size:27rpx; }.record-button-active { background-color:#F5F6F8; } |
| | | .status-row { flex-direction: row; } |
| | | </style> |
| | |
| | | <template> |
| | | <view class="page"> |
| | | <image class="hero-image" src="/static/stay-assets/hotel-vienna.jpg" mode="aspectFill"></image> |
| | | <image class="hero-image" :src="heroImage" mode="aspectFill" @error="replaceHeroImage"></image> |
| | | <view class="hero-shade"></view> |
| | | <view class="custom-nav"><view class="safe-space" :style="'height:' + navTop + 'px'"></view><view class="nav-row" :style="'height:' + navHeight + 'px;padding-right:' + navRight + 'px'"><view class="back-button" @tap="goBack"><image src="/static/food-assets/arrow-left-white.svg" mode="aspectFit"></image></view><view class="capsule-space" :style="'width:' + navWidth + 'px'"></view></view></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 { 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 loading = ref(true); const loadError = ref(false); const available = ref(false) |
| | | const title = ref('配偶推荐就业'); const freeLabel = ref('免费'); const rulesText = ref(''); const consultationPhone = ref(''); const unavailableReason = ref('该权益暂未开放') |
| | | 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 consultationPhone = 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 : '' |
| | | 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' |
| | | loadBenefit() |
| | | }) |
| | | const loadBenefit = () => { |
| | | if (serviceId.value == '') { loading.value = false; loadError.value = true; return } |
| | | loading.value = true; loadError.value = false |
| | | getSpouseEmploymentBenefit(serviceId.value, (data: any) => { |
| | | title.value = data.title || title.value; freeLabel.value = data.freeLabel || freeLabel.value; rulesText.value = data.rulesText || ''; consultationPhone.value = data.consultationPhone || ''; available.value = data.available === true; unavailableReason.value = data.unavailableReason || unavailableReason.value; loading.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 || ''; consultationPhone.value = data.consultationPhone || ''; 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 } |
| | | const goBack = () => safeBack('/pages/premium-benefits/premium-benefits') |
| | | const callConsultation = () => { if (consultationPhone.value != '') uni.makePhoneCall({ phoneNumber: consultationPhone.value }) } |
| | | const fillInformation = () => { if (!loading.value && !loadError.value && available.value) uni.navigateTo({ url: '/pages/spouse-employment-form/spouse-employment-form?serviceId=' + encodeURIComponent(serviceId.value) }) } |
| | |
| | | @error="replaceServiceImage(item)"></image> |
| | | <view class="service-info"><text class="service-name">{{item.name}}</text><text |
| | | class="discount-tag">{{item.available ? item.discount : '已下架'}}</text> |
| | | <view class="address"> |
| | | <view v-if="item.merchantType != 'apartment'" class="address"> |
| | | <image src="/static/benefits-assets/map-pin.svg"></image><text>{{item.address}}</text> |
| | | </view> |
| | | </view> |
| | |
| | | export const getShoppingTopicMerchants = (categoryId: string, page: number, size: number, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appShoppingTopicMerchants'), method: 'GET', skipAuth: true, data: { categoryId: categoryId, page: page, size: size } }, onSuccess, onError) |
| | | export const getPremiumTopic = (onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appPremiumTopic'), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | export const getPremiumTopicServices = (type: string, page: number, size: number, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appPremiumTopicServices'), method: 'GET', skipAuth: true, data: { type: type, page: page, size: size } }, onSuccess, onError) |
| | | export const getSpouseEmploymentBenefit = (serviceId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appSpouseEmploymentBenefit') + encodeURIComponent(serviceId), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | export const getSpouseEmploymentBenefit = (serviceId: string, talentId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => { |
| | | const data: any = {} |
| | | if (talentId != '') data.talentId = Number(talentId) |
| | | return request<any>({ path: getApiEndpoint('appSpouseEmploymentBenefit') + encodeURIComponent(serviceId), method: 'GET', skipAuth: true, data: data }, onSuccess, onError) |
| | | } |
| | | export const submitSpouseEmploymentApplication = (serviceId: string, payload: any, idempotencyKey: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appSpouseEmploymentBenefit') + encodeURIComponent(serviceId) + '/applications', method: 'POST', skipAuth: true, data: payload, idempotencyKey: idempotencyKey }, onSuccess, onError) |
| | | export const getAdminApprovalBenefit = (serviceId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appAdminApprovalBenefit') + encodeURIComponent(serviceId), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | export const getFinanceMerchantDetail = (merchantId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appFinanceMerchantDetail') + encodeURIComponent(merchantId), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | export const getFinanceMerchantDetail = (merchantId: string, talentId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => { |
| | | const data: any = {} |
| | | if (talentId != '') data.talentId = Number(talentId) |
| | | return request<any>({ path: getApiEndpoint('appFinanceMerchantDetail') + encodeURIComponent(merchantId), method: 'GET', skipAuth: true, data: data }, onSuccess, onError) |
| | | } |
| | | export const getChildrenEntrustmentBenefit = (serviceId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appChildrenEntrustmentBenefit') + encodeURIComponent(serviceId), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | export const getChildrenEntrustmentSchools = (serviceId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appChildrenEntrustmentBenefit') + encodeURIComponent(serviceId) + '/schools', method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | export const getChildrenEntrustmentSchool = (schoolId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appChildrenEntrustmentSchool') + encodeURIComponent(schoolId), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | |
| | | |
| | | const rightExampleImage = |
| | | 'https://images.unsplash.com/photo-1559847844-5315695dadae?auto=format&fit=crop&w=640&q=80' |
| | | const showRightsExamples = false |
| | | |
| | | const rightExampleLines = [ |
| | | '营业时间10:00-22:00', |
| | |
| | | </el-radio-group> |
| | | </div> |
| | | </div> |
| | | <div class="merchant-rights-example" v-if="isMovieMerchant"> |
| | | <div class="merchant-rights-example" v-if="showRightsExamples && isMovieMerchant"> |
| | | <span>示例:</span> |
| | | <div class="merchant-rights-phone-preview"> |
| | | <div class="phone-preview-hero"> |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="merchant-rights-example" v-else-if="isFinancialMerchant || isSpouseEmploymentMerchant"> |
| | | <div class="merchant-rights-example" v-else-if="showRightsExamples && (isFinancialMerchant || isSpouseEmploymentMerchant)"> |
| | | <span>示例:</span> |
| | | <div class="merchant-rights-phone-preview"> |
| | | <div class="phone-preview-hero"> |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="merchant-rights-example" v-else-if="!isParkingMerchant && (!isCarWashMerchant || form.miniProgramJumpType === 'normal')"> |
| | | <div class="merchant-rights-example" v-else-if="showRightsExamples && !isParkingMerchant && (!isCarWashMerchant || form.miniProgramJumpType === 'normal')"> |
| | | <span>示例:</span> |
| | | <div class="merchant-rights-phone-preview"> |
| | | <div class="phone-preview-hero"> |
| | |
| | | <div v-if="!isParkingMerchant" class="merchant-rights-detail-row consume-desc-row"> |
| | | <label>到店消费使用描述</label> |
| | | <el-input v-model="item.consumeDesc" type="textarea" :rows="4" resize="none" placeholder="请输入" /> |
| | | <div class="merchant-rights-example"> |
| | | <div v-if="showRightsExamples" class="merchant-rights-example"> |
| | | <span>示例:</span> |
| | | <div class="merchant-rights-phone-preview"> |
| | | <div class="phone-preview-hero"> |
| | |
| | | const productImage = computed(() => product.value?.coverUrl || fallbackImage) |
| | | const imageList = computed(() => (galleryImages.value.length ? galleryImages.value : [productImage.value])) |
| | | const productNameText = computed(() => product.value?.productName || '百杏厨房(万象城店)') |
| | | const productAmountText = computed(() => amountText(product.value?.salePrice ?? product.value?.originalPrice) || '158') |
| | | const isHotelProduct = computed(() => { |
| | | const merchantType = product.value?.merchantType || '' |
| | | return merchantType === '酒店服务' || merchantType === '酒店' |
| | | }) |
| | | const productAmountText = computed(() => { |
| | | const salePrice = product.value?.salePrice |
| | | const originalPrice = product.value?.originalPrice |
| | | if (isHotelProduct.value && salePrice !== undefined && originalPrice !== undefined) { |
| | | return `${amountText(salePrice)} - ${amountText(originalPrice)}` |
| | | } |
| | | return amountText(salePrice ?? originalPrice) || '158' |
| | | }) |
| | | const formatDateTime = (value?: string) => value ? value.replace('T', ' ') : '' |
| | | const createdAtText = computed(() => formatDateTime(product.value?.createdAt) || '2026-06-18 11:34:25') |
| | | const submitTimeText = computed(() => formatDateTime(product.value?.submitTime || product.value?.createdAt) || '2026-06-18 11:34:25') |
| | |
| | | return Number(value).toFixed(2).replace(/\.00$/, '') |
| | | } |
| | | |
| | | function parseNotice(value?: string | Record<string, unknown>) { |
| | | function parseNotice(value?: string | Record<string, unknown>, source?: Product) { |
| | | notice.validPeriod = '' |
| | | notice.useMethod = '' |
| | | notice.availableTime = '' |
| | | notice.otherRules = '' |
| | | if (!value) return |
| | | const fallback = source as (Product & Record<string, unknown>) | undefined |
| | | if (!value) { |
| | | notice.validPeriod = [fallback?.validStartTime, fallback?.validEndTime].filter(Boolean).join(' 至 ') |
| | | notice.useMethod = String(fallback?.usageInstructions || fallback?.usage || '') |
| | | notice.availableTime = String(fallback?.availableHours || fallback?.availableTime || '') |
| | | notice.otherRules = String(fallback?.otherRules || fallback?.rules || '') |
| | | return |
| | | } |
| | | |
| | | try { |
| | | const parsed = typeof value === 'string' ? JSON.parse(value) : value |
| | | notice.validPeriod = parsed.validPeriod || parsed.valid_period || parsed.validity || '' |
| | | notice.useMethod = parsed.useMethod || parsed.use_method || parsed.usage || '' |
| | | notice.availableTime = parsed.availableTime || parsed.available_time || '' |
| | | notice.otherRules = parsed.otherRules || parsed.other_rules || parsed.otherRule || parsed.other_rule || '' |
| | | notice.useMethod = parsed.useMethod || parsed.use_method || parsed.usage || parsed.usageInstructions || parsed.usage_instructions || '' |
| | | notice.availableTime = parsed.availableTime || parsed.available_time || parsed.availableHours || parsed.available_hours || '' |
| | | notice.otherRules = parsed.otherRules || parsed.other_rules || parsed.otherRule || parsed.other_rule || parsed.rules || '' |
| | | notice.validPeriod ||= [fallback?.validStartTime, fallback?.validEndTime].filter(Boolean).join(' 至 ') |
| | | notice.useMethod ||= String(fallback?.usageInstructions || fallback?.usage || '') |
| | | notice.availableTime ||= String(fallback?.availableHours || fallback?.availableTime || '') |
| | | notice.otherRules ||= String(fallback?.otherRules || fallback?.rules || '') |
| | | } catch { |
| | | notice.otherRules = typeof value === 'string' ? value : '' |
| | | } |
| | |
| | | return |
| | | } finally { |
| | | galleryImages.value = parseProductImages(product.value?.productImages, product.value?.coverUrl) |
| | | parseNotice(product.value?.purchaseNotice) |
| | | parseNotice(product.value?.purchaseNotice, product.value) |
| | | parseBenefits(product.value?.rightsConfig, product.value?.benefitDesc) |
| | | loading.value = false |
| | | } |
| | |
| | | } |
| | | |
| | | function amountText(row: Product) { |
| | | const amount = row.salePrice ?? row.originalPrice |
| | | const isHotel = row.merchantType === '酒店服务' || row.merchantType === '酒店' |
| | | if (isHotel && row.salePrice !== undefined && row.originalPrice !== undefined) { |
| | | return `${formatAmount(row.salePrice)} - ${formatAmount(row.originalPrice)}` |
| | | } |
| | | return formatAmount(row.salePrice ?? row.originalPrice) |
| | | } |
| | | |
| | | function formatAmount(amount?: number) { |
| | | if (amount === undefined || amount === null) return '' |
| | | return Number(amount).toFixed(2).replace(/\.00$/, '') |
| | | } |
| | |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PathVariable; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | @RestController |
| | |
| | | } |
| | | |
| | | @GetMapping("/{merchantId}") |
| | | public ApiResponse<Map<String, Object>> detail(@PathVariable Long merchantId) { |
| | | return ApiResponse.success(service.detail(merchantId)); |
| | | public ApiResponse<Map<String, Object>> detail(@PathVariable Long merchantId, @RequestParam(required = false) Long talentId) { |
| | | return ApiResponse.success(service.detail(merchantId, talentId)); |
| | | } |
| | | } |
| | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.talentcard.server.business.DatabaseRows; |
| | | import com.talentcard.server.common.BusinessException; |
| | | import com.talentcard.server.common.ErrorCode; |
| | |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> detail(Long merchantId) { |
| | | public Map<String, Object> detail(Long merchantId, Long talentId) { |
| | | if (merchantId == null || merchantId <= 0) throw new BusinessException(ErrorCode.BAD_REQUEST, "金融机构信息不能为空"); |
| | | Map<String, Object> row = one(""" |
| | | select m.id, m.merchant_name, m.cover_url, m.service_label, m.address, m.contact_mobile, |
| | | select m.id, m.merchant_name, m.cover_url, m.service_label, m.address, m.contact_mobile, m.contact_name, |
| | | m.rights_text, m.merchant_rights_config, |
| | | m.longitude, m.latitude, m.status as merchant_status, detail_config.talent_levels, |
| | | detail_config.benefit_detail, detail_config.advisor_id, detail_config.advisor_name, |
| | | detail_config.advisor_department, detail_config.advisor_mobile, detail_config.advisor_service_time, |
| | | detail_config.advisor_available, detail_config.unavailable_reason, detail_config.status as detail_status |
| | | from merchant m |
| | | join premium_topic_merchant mapping on mapping.merchant_id = m.id and mapping.category_key = 'finance' and mapping.deleted = 0 |
| | | join finance_service_detail detail_config on detail_config.merchant_id = m.id and detail_config.deleted = 0 |
| | | where m.id = ? and m.merchant_type = 'finance' and m.deleted = 0 |
| | | left join finance_service_detail detail_config on detail_config.merchant_id = m.id and detail_config.deleted = 0 |
| | | where m.id = ? and m.merchant_type in ('finance', '金融服务') and m.deleted = 0 |
| | | order by detail_config.id asc limit 1 |
| | | """, merchantId); |
| | | boolean available = number(row.get("merchantStatus")) == 1 && number(row.get("detailStatus")) == 1; |
| | | boolean available = number(row.get("merchantStatus")) == 1 && (row.get("detailStatus") == null || number(row.get("detailStatus")) == 1); |
| | | Map<String, Object> matchedRight = matchedRight(row.get("merchantRightsConfig"), activeCardLevel(talentId)); |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("merchantId", row.get("id")); |
| | | result.put("merchantName", row.get("merchantName")); |
| | | result.put("coverUrl", row.get("coverUrl")); |
| | | result.put("benefitSummary", text(row.get("serviceLabel"), "请咨询服务专员")); |
| | | result.put("applicableCards", text(row.get("talentLevels"), "请咨询服务专员")); |
| | | result.put("applicableCards", text(matchedRight.get("tag"), text(row.get("talentLevels"), "请咨询服务专员"))); |
| | | result.put("address", text(row.get("address"), "暂未配置机构地址")); |
| | | result.put("contactMobile", text(row.get("contactMobile"), "")); |
| | | result.put("longitude", row.get("longitude")); |
| | | result.put("latitude", row.get("latitude")); |
| | | result.put("benefitDetail", text(row.get("benefitDetail"), "以服务专员说明为准")); |
| | | result.put("benefitDetail", text(matchedRight.get("detail"), text(row.get("benefitDetail"), text(row.get("rightsText"), "以服务专员说明为准")))); |
| | | result.put("available", available); |
| | | result.put("unavailableReason", available ? "" : text(row.get("unavailableReason"), "该金融服务当前不可用")); |
| | | Map<String, Object> advisor = new LinkedHashMap<>(); |
| | | advisor.put("advisorId", row.get("advisorId")); |
| | | advisor.put("name", text(row.get("advisorName"), "")); |
| | | advisor.put("name", text(row.get("advisorName"), text(row.get("contactName"), "金融服务专员"))); |
| | | advisor.put("department", text(row.get("advisorDepartment"), "")); |
| | | advisor.put("mobile", text(row.get("advisorMobile"), "")); |
| | | advisor.put("mobile", text(row.get("advisorMobile"), text(row.get("contactMobile"), ""))); |
| | | advisor.put("serviceTime", text(row.get("advisorServiceTime"), "")); |
| | | advisor.put("available", number(row.get("advisorAvailable")) == 1 && !text(row.get("advisorMobile"), "").isEmpty()); |
| | | advisor.put("available", (row.get("advisorAvailable") == null ? !text(row.get("contactMobile"), "").isEmpty() : number(row.get("advisorAvailable")) == 1) && !text(row.get("advisorMobile"), text(row.get("contactMobile"), "")).isEmpty()); |
| | | result.put("advisor", advisor); |
| | | return result; |
| | | } |
| | | |
| | | private Map<String, Object> matchedRight(Object value, String cardLevel) { |
| | | if (cardLevel.isBlank() || value == null) return Map.of(); |
| | | try { |
| | | List<Map<String, Object>> rights = JSON.parseObject(String.valueOf(value), 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"), ""))) return right; |
| | | } |
| | | } |
| | | } catch (RuntimeException ignored) { |
| | | // Use the legacy finance detail fields when merchant rights are unavailable. |
| | | } |
| | | return Map.of(); |
| | | } |
| | | |
| | | private String activeCardLevel(Long talentId) { |
| | | if (talentId == null || talentId <= 0) return ""; |
| | | List<Map<String, Object>> cards = jdbc.queryForList(""" |
| | | select card_level from talent_card |
| | | where talent_id = ? and deleted = 0 and card_type = 'PERSONAL' and status = 1 |
| | | and (valid_end is null or valid_end >= current_date) |
| | | order by id desc limit 1 |
| | | """, talentId).stream().map(DatabaseRows::normalize).toList(); |
| | | return cards.isEmpty() ? "" : text(cards.get(0).get("cardLevel"), ""); |
| | | } |
| | | |
| | | private boolean booleanValue(Object value) { |
| | | if (value instanceof Boolean bool) return bool; |
| | | if (value instanceof Number number) return number.intValue() != 0; |
| | | String result = text(value, ""); |
| | | return "true".equalsIgnoreCase(result) || "1".equals(result) || "yes".equalsIgnoreCase(result); |
| | | } |
| | | |
| | | private Map<String, Object> one(String sql, Object... args) { |
| | | List<Map<String, Object>> rows = jdbc.queryForList(sql, args).stream().map(DatabaseRows::normalize).toList(); |
| | | if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "金融服务详情不存在"); |
| | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.talentcard.server.business.DatabaseRows; |
| | | import com.talentcard.server.common.BusinessException; |
| | | import com.talentcard.server.common.ErrorCode; |
| | |
| | | public List<Map<String, Object>> packages(Long merchantId) { |
| | | medicalMerchant(merchantId); |
| | | return query(""" |
| | | select id, package_name, hero_image, open_time, free_status, applicable_cards, stock, status, unavailable_reason |
| | | from medical_package where merchant_id = ? and deleted = 0 order by sort_no asc, id asc |
| | | select p.id, p.product_name as package_name, p.cover_url as hero_image, |
| | | case when coalesce(p.sale_price, 0) = 0 then 1 else 0 end as free_status, |
| | | '' as applicable_cards, |
| | | coalesce( |
| | | nullif(json_unquote(json_extract(p.purchase_notice, '$.availableTime')), ''), |
| | | nullif(json_unquote(json_extract(p.purchase_notice, '$.validPeriod')), ''), |
| | | '' |
| | | ) as open_time, |
| | | p.stock, p.status, '' as unavailable_reason |
| | | from merchant_product p |
| | | where p.merchant_id = ? and p.deleted = 0 and p.audit_status = 2 and p.status = 1 |
| | | order by p.sort_no desc, p.id desc |
| | | """, merchantId).stream().map(this::packageListView).toList(); |
| | | } |
| | | |
| | |
| | | |
| | | @Transactional(readOnly = true) |
| | | public List<Map<String, Object>> items(Long packageId) { |
| | | packageRow(packageId); |
| | | return query(""" |
| | | select id, item_name, item_meaning, male_applicable, female_applicable |
| | | from medical_package_item where package_id = ? and deleted = 0 order by sort_no asc, id asc |
| | | """, packageId).stream().map(row -> Map.<String, Object>of( |
| | | "id", row.get("id"), "name", row.get("itemName"), "meaning", row.get("itemMeaning"), |
| | | "male", number(row.get("maleApplicable")) == 1, "female", number(row.get("femaleApplicable")) == 1 |
| | | )).toList(); |
| | | Map<String, Object> row = packageRow(packageId); |
| | | String detailText = text(row.get("detailText")).trim(); |
| | | if (detailText.isBlank()) return List.of(); |
| | | try { |
| | | List<Map<String, Object>> records = JSON.parseObject(detailText, new com.alibaba.fastjson.TypeReference<>() {}); |
| | | if (records == null) return List.of(); |
| | | java.util.ArrayList<Map<String, Object>> result = new java.util.ArrayList<>(); |
| | | for (int index = 0; index < records.size(); index++) { |
| | | Map<String, Object> item = records.get(index); |
| | | List<String> genders = JSON.parseArray(JSON.toJSONString(item.get("gender")), String.class); |
| | | Map<String, Object> normalized = new LinkedHashMap<>(); |
| | | normalized.put("id", index + 1); |
| | | normalized.put("name", text(item.get("name"))); |
| | | normalized.put("meaning", text(item.get("meaning"))); |
| | | normalized.put("male", genders != null && genders.contains("男")); |
| | | normalized.put("female", genders != null && genders.contains("女")); |
| | | result.add(normalized); |
| | | } |
| | | return result; |
| | | } catch (RuntimeException ignored) { |
| | | return List.of(); |
| | | } |
| | | } |
| | | |
| | | private Map<String, Object> medicalMerchant(Long merchantId) { |
| | | if (merchantId == null || merchantId <= 0) throw new BusinessException(ErrorCode.BAD_REQUEST, "体检医院信息不能为空"); |
| | | List<Map<String, Object>> rows = query(""" |
| | | select m.id from merchant m join premium_topic_merchant mapping on mapping.merchant_id = m.id |
| | | where m.id = ? and m.merchant_type = 'medical' and m.deleted = 0 |
| | | where m.id = ? and m.merchant_type in ('medical', '免费体检', 'healthCheck') and m.deleted = 0 |
| | | and mapping.category_key = 'medical' and mapping.deleted = 0 |
| | | """, merchantId); |
| | | if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "体检医院不存在"); |
| | |
| | | private Map<String, Object> packageRow(Long packageId) { |
| | | if (packageId == null || packageId <= 0) throw new BusinessException(ErrorCode.BAD_REQUEST, "体检套餐信息不能为空"); |
| | | List<Map<String, Object>> rows = query(""" |
| | | select p.id as package_id, p.merchant_id, p.package_name, p.hero_image, p.free_status, p.applicable_cards, |
| | | p.open_time, p.stock, p.status, p.unavailable_reason, m.merchant_name as hospital_name, |
| | | select p.id as package_id, p.merchant_id, p.product_name as package_name, p.cover_url as hero_image, |
| | | case when coalesce(p.sale_price, 0) = 0 then 1 else 0 end as free_status, |
| | | '' as applicable_cards, |
| | | coalesce( |
| | | nullif(json_unquote(json_extract(p.purchase_notice, '$.availableTime')), ''), |
| | | nullif(json_unquote(json_extract(p.purchase_notice, '$.validPeriod')), ''), |
| | | '' |
| | | ) as open_time, |
| | | p.stock, p.status, '' as unavailable_reason, p.detail_text, |
| | | m.merchant_name as hospital_name, |
| | | m.address as hospital_address, m.contact_mobile as hospital_mobile, m.latitude as hospital_latitude, |
| | | m.longitude as hospital_longitude |
| | | from medical_package p join merchant m on m.id = p.merchant_id and m.deleted = 0 |
| | | from merchant_product p join merchant m on m.id = p.merchant_id and m.deleted = 0 |
| | | join premium_topic_merchant mapping on mapping.merchant_id = m.id and mapping.category_key = 'medical' and mapping.deleted = 0 |
| | | where p.id = ? and p.deleted = 0 |
| | | where p.id = ? and p.deleted = 0 and p.audit_status = 2 |
| | | """, packageId); |
| | | if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "体检套餐不存在"); |
| | | return rows.get(0); |
| | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> stayMerchants(String type, int page, int size) { |
| | | String requestedType = type == null ? "" : type.trim(); |
| | | String merchantType = switch (requestedType) { |
| | | case "hotel", "酒店服务" -> "酒店服务"; |
| | | case "apartment", "公寓租赁服务" -> "公寓租赁服务"; |
| | | boolean apartment = switch (requestedType) { |
| | | case "hotel", "酒店服务" -> false; |
| | | case "apartment", "公寓租赁", "公寓租赁服务" -> true; |
| | | default -> throw new BusinessException(ErrorCode.BAD_REQUEST, "不支持的住宿服务类型"); |
| | | }; |
| | | String responseMerchantType = merchantType.equals("公寓租赁服务") ? "apartment" : "hotel"; |
| | | String merchantWhere = " where deleted = 0 and merchant_type = ?"; |
| | | String responseMerchantType = apartment ? "apartment" : "hotel"; |
| | | String merchantWhere = apartment |
| | | ? " where deleted = 0 and merchant_type in ('apartment', '公寓租赁', '公寓租赁服务')" |
| | | : " where deleted = 0 and merchant_type in ('hotel', '酒店服务')"; |
| | | PageResult<Map<String, Object>> result = page(""" |
| | | select id, merchant_name, merchant_type, contact_name, contact_mobile, logo_url, cover_url, address, |
| | | status, sort_no, longitude, latitude, business_status, |
| | |
| | | (select min(p.original_price) from merchant_product p where p.merchant_id = merchant.id and p.deleted = 0 and p.status = 1 and p.audit_status = 2) as original_price |
| | | from merchant |
| | | """ + merchantWhere + " order by sort_no asc, id asc", |
| | | "select count(*) from merchant" + merchantWhere, page, size, merchantType); |
| | | "select count(*) from merchant" + merchantWhere, page, size); |
| | | List<Map<String, Object>> records = result.records().stream().map(row -> { |
| | | Map<String, Object> normalized = new LinkedHashMap<>(row); |
| | | normalized.put("merchantType", responseMerchantType); |
| | |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | import org.springframework.web.bind.annotation.RequestHeader; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | @RestController |
| | |
| | | } |
| | | |
| | | @GetMapping("/{merchantId}") |
| | | public ApiResponse<Map<String, Object>> detail(@PathVariable Long merchantId) { |
| | | return ApiResponse.success(service.detail(merchantId)); |
| | | public ApiResponse<Map<String, Object>> detail( |
| | | @PathVariable Long merchantId, |
| | | @RequestParam(required = false) Long talentId |
| | | ) { |
| | | return ApiResponse.success(service.detail(merchantId, talentId)); |
| | | } |
| | | |
| | | @PostMapping("/{merchantId}/applications") |
| | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.talentcard.server.business.BusinessIdGenerator; |
| | | import com.talentcard.server.business.DatabaseRows; |
| | | import com.talentcard.server.common.BusinessException; |
| | |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> detail(Long merchantId) { |
| | | public Map<String, Object> detail(Long merchantId, Long talentId) { |
| | | Map<String, Object> benefit = benefit(merchantId); |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("benefitId", benefit.get("id")); |
| | | result.put("benefitId", benefit.get("id") == null ? merchantId : benefit.get("id")); |
| | | result.put("serviceId", benefit.get("merchantId")); |
| | | result.put("title", benefit.get("title")); |
| | | result.put("heroImage", benefit.get("coverUrl")); |
| | | result.put("freeLabel", benefit.get("freeLabel")); |
| | | result.put("rulesText", benefit.get("rulesText")); |
| | | result.put("rulesText", configuredRulesText(benefit, talentId)); |
| | | result.put("consultationPhone", benefit.get("consultationPhone")); |
| | | result.put("available", number(benefit.get("enabled")) == 1); |
| | | result.put("unavailableReason", text(benefit.get("unavailableReason"), "该权益暂未开放")); |
| | |
| | | private Map<String, Object> benefit(Long merchantId) { |
| | | if (merchantId == null || merchantId <= 0) throw new BusinessException(ErrorCode.BAD_REQUEST, "权益信息不能为空"); |
| | | List<Map<String, Object>> rows = query(""" |
| | | select b.*, m.merchant_name |
| | | from spouse_employment_benefit b |
| | | join merchant m on m.id = b.merchant_id and m.deleted = 0 |
| | | select b.id, m.id as merchant_id, m.merchant_name, m.cover_url, |
| | | coalesce(nullif(b.title, ''), nullif(m.merchant_name, ''), '配偶推荐就业') as title, |
| | | coalesce(nullif(b.free_label, ''), '免费') as free_label, |
| | | coalesce(nullif(b.rules_text, ''), nullif(m.rights_text, ''), '请咨询服务单位了解配偶推荐就业权益详情') as rules_text, |
| | | coalesce(nullif(b.consultation_phone, ''), m.contact_mobile, '') as consultation_phone, |
| | | coalesce(nullif(b.expected_time, ''), '提交后 5 个工作日内由服务专员联系确认') as expected_time, |
| | | coalesce(nullif(b.education_options, ''), '高中及以下,大专,本科,硕士研究生,博士研究生') as education_options, |
| | | coalesce(nullif(b.institution_options, ''), '是,否') as institution_options, |
| | | coalesce(b.enabled, m.status, 0) as enabled, |
| | | coalesce(nullif(b.unavailable_reason, ''), '该权益暂未开放') as unavailable_reason, |
| | | m.merchant_rights_config |
| | | from merchant m |
| | | left join spouse_employment_benefit b on b.merchant_id = m.id and b.deleted = 0 |
| | | join premium_topic_merchant mapping on mapping.merchant_id = m.id and mapping.category_key = 'employment' and mapping.deleted = 0 |
| | | where b.merchant_id = ? and b.deleted = 0 |
| | | where m.id = ? and m.deleted = 0 |
| | | """, merchantId); |
| | | if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "配偶推荐就业权益不存在"); |
| | | 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. |
| | | } |
| | | } |
| | | return text(benefit.get("rulesText"), "请咨询服务单位了解配偶推荐就业权益详情"); |
| | | } |
| | | |
| | | private String activeCardLevel(Long talentId) { |
| | | if (talentId == null || talentId <= 0) return ""; |
| | | List<Map<String, Object>> cards = query(""" |
| | | select card_level from talent_card |
| | | where talent_id = ? and deleted = 0 and card_type = 'PERSONAL' and status = 1 |
| | | and (valid_end is null or valid_end >= current_date) |
| | | order by id desc limit 1 |
| | | """, talentId); |
| | | return cards.isEmpty() ? "" : text(cards.get(0).get("cardLevel"), ""); |
| | | } |
| | | |
| | | private boolean booleanValue(Object value) { |
| | | if (value instanceof Boolean bool) return bool; |
| | | if (value instanceof Number number) return number.intValue() != 0; |
| | | String result = text(value, ""); |
| | | return "true".equalsIgnoreCase(result) || "1".equals(result) || "yes".equalsIgnoreCase(result); |
| | | } |
| | | |
| | | private Map<String, Object> activeCard(Long talentId) { |
| | | if (talentId == null || talentId <= 0) throw new BusinessException(ErrorCode.BAD_REQUEST, "请先登录后提交"); |
| | | List<Map<String, Object>> cards = query(""" |
| | |
| | | @Select(""" |
| | | select p.id as packageId, |
| | | p.merchant_id as merchantId, |
| | | p.package_name as packageName, |
| | | p.product_name as packageName, |
| | | p.stock, |
| | | p.status, |
| | | p.unavailable_reason as unavailableReason |
| | | from medical_package p |
| | | case |
| | | when p.status != 1 then '套餐已停用' |
| | | when p.stock = 0 then '当前库存不足' |
| | | else '' |
| | | end as unavailableReason |
| | | from merchant_product p |
| | | where p.id = #{packageId} |
| | | and (#{merchantId} is null or p.merchant_id = #{merchantId}) |
| | | and p.deleted = 0 |
| | | and p.audit_status = 2 |
| | | limit 1 |
| | | """) |
| | | Map<String, Object> selectMedicalPackage(@Param("packageId") Long packageId, @Param("merchantId") Long merchantId); |
| | |
| | | for update |
| | | """) |
| | | ServiceApplication selectByIdForUpdate(@Param("id") Long id); |
| | | } |
| | | } |
| | |
| | | if (merchant == null || merchant.isEmpty()) { |
| | | throw new BusinessException(ErrorCode.NOT_FOUND, message); |
| | | } |
| | | if (expectedType != null && !expectedType.equalsIgnoreCase(text(merchant, "merchantType", "merchant_type"))) { |
| | | String actualType = text(merchant, "merchantType", "merchant_type"); |
| | | boolean medicalType = "medical".equalsIgnoreCase(expectedType) |
| | | && ("medical".equalsIgnoreCase(actualType) || "healthCheck".equalsIgnoreCase(actualType) || "免费体检".equals(actualType)); |
| | | if (expectedType != null && !medicalType && !expectedType.equalsIgnoreCase(actualType)) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, message); |
| | | } |
| | | return merchant; |