fix: sync miniapp merchant benefit data
| | |
| | | const replaceHero = () => { heroImage.value = '' } |
| | | const openSchoolDetail = (item: any) => { |
| | | if (item.detailAvailable !== true) { uni.showToast({ title: '学校介绍暂未开放', icon: 'none' }); return } |
| | | uni.navigateTo({ url: '/pages/school-detail/school-detail?id=' + item.id + '&name=' + encodeURIComponent(item.name) + '&address=' + encodeURIComponent(item.address) }) |
| | | uni.navigateTo({ url: '/pages/school-detail/school-detail?serviceId=' + encodeURIComponent(serviceId.value) + '&id=' + encodeURIComponent('' + item.id) + '&name=' + encodeURIComponent(item.name) + '&address=' + encodeURIComponent(item.address) }) |
| | | } |
| | | 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' }) }) } |
| | |
| | | <view v-if="topicDescription != ''" class="topic-summary"><view class="intro-card"><text>{{topicDescription}}</text></view></view> |
| | | <view class="service-section"> |
| | | <view class="section-heading"><text>观影服务</text><view></view></view> |
| | | <view class="ticket-entry" @tap="openTicketPackage"><text>电影票兑换券领取</text><text class="ticket-arrow">›</text></view> |
| | | <view v-if="initialLoading" class="list-state"><view class="loading-spinner"></view><text>正在加载观影服务</text></view> |
| | | <view v-else-if="initialError" class="list-state error-state"><text>观影服务加载失败</text><text class="retry-text" @tap="retryInitialLoad">重新加载</text></view> |
| | | <view v-else-if="hasLoaded && visibleServices.length == 0" class="list-state"><text>暂无相关观影服务</text></view> |
| | |
| | | const replaceHeroImage = () => { currentScene.value.image = fallbackImage } |
| | | const replaceSceneImage = (index: number) => { if (index >= 0 && index < scenes.value.length) scenes.value[index].image = fallbackImage } |
| | | const replaceServiceImage = (item: any) => { item.image = fallbackImage } |
| | | const openTicketPackage = () => uni.navigateTo({ url: '/pages/ticket-package/ticket-package' }) |
| | | const goBack = () => safeBack('/pages/benefits/benefits') |
| | | const openService = (item: any) => { |
| | | if (!item.available) { uni.showToast({ title: '该服务已下架', icon: 'none' }); return } |
| | |
| | | .hero { height: 730rpx; position: relative; overflow: visible; background-color: #241C2A; }.hero-image { position: absolute; left: 0; top: 0; width: 100%; height: 730rpx; }.hero-shade { position: absolute; left: 0; right: 0; top: 0; height: 360rpx; background-image: linear-gradient(180deg, rgba(0,0,0,.6) 0%, rgba(0,0,0,0) 100%); }.scene-name { position: absolute; z-index: 2; left: 38rpx; top: 300rpx; color: #FFFFFF; font-size: 47rpx; line-height: 62rpx; font-weight: 500; text-shadow: 0 3rpx 10rpx rgba(0,0,0,.55); }.no-gallery { position: absolute; z-index: 2; left: 0; right: 0; top: 480rpx; color: #FFFFFF; font-size: 25rpx; text-align: center; } |
| | | .scene-scroll { position: absolute; z-index: 4; left: 0; right: 0; top: 469rpx; width: 100%; height: 150rpx; white-space: nowrap; }.scene-row { display: flex; height: 150rpx; padding: 0; flex-direction: row; align-items: center; }.scene-pad { width: 322rpx; height: 1rpx; flex-shrink: 0; }.scene-item { display: flex; width: 106rpx; height: 106rpx; margin-right: 31rpx; box-sizing: border-box; overflow: hidden; position: relative; z-index: 1; border-width: 3rpx; border-style: solid; border-color: rgba(255,255,255,.9); border-radius: 8rpx; flex-shrink: 0; transition: transform 180ms ease-out; }.scene-item image { width: 100%; height: 100%; }.scene-item.active { z-index: 3; border-width: 4rpx; border-color: #FFD329; transform: scale(1.28); box-shadow: 0 8rpx 18rpx rgba(0,0,0,.28); } |
| | | .topic-summary { position: relative; z-index: 4; margin-top: -66rpx; }.intro-card { margin: 0 31rpx; box-sizing: border-box; padding: 25rpx 31rpx; border-radius: 26rpx; background-image: linear-gradient(90deg, #FF5320 0%, #FF8A22 100%); box-shadow: 0 10rpx 22rpx rgba(255,104,28,.24); }.intro-card text { color: #FFFFFF; font-size: 23rpx; line-height: 34rpx; } |
| | | .service-section { padding: 48rpx 31rpx 0; }.section-heading { display: flex; width: 132rpx; height: 72rpx; flex-direction: column; align-items: center; }.section-heading text { display: block; color: #363636; font-size: 32rpx; font-weight: 700; }.section-heading view { width: 61rpx; height: 7rpx; margin: 12rpx 0 18rpx; border-radius: 4rpx; background-image: linear-gradient(90deg, #FF4D36 0%, #FFBF3A 100%); }.service-item { display: flex; min-height: 205rpx; margin-bottom: 20rpx; flex-direction: row; }.service-item.is-unavailable { opacity: .56; }.service-image { width: 184rpx; height: 184rpx; border-radius: 17rpx; flex-shrink: 0; background-color: #EEE; }.service-info { display: flex; min-width: 0; min-height: 184rpx; flex: 1; padding: 4rpx 0 4rpx 24rpx; flex-direction: column; justify-content: space-between; }.service-name { color: #292929; font-size: 29rpx; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }.discount-tag { display: block; width: fit-content; padding: 6rpx 9rpx; border-radius: 6rpx; background-color: #FFF0EC; color: #FF5D37; font-size: 21rpx; }.address { 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; }.address image { width: 27rpx; height: 27rpx; margin-right: 8rpx; flex-shrink: 0; }.address text { min-width: 0; flex: 1; color: #7E8490; font-size: 21rpx; line-height: 31rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| | | .service-section { padding: 32rpx 31rpx 0; }.section-heading { display: flex; width: 132rpx; height: 72rpx; flex-direction: column; align-items: center; }.section-heading text { display: block; color: #363636; font-size: 32rpx; font-weight: 700; }.section-heading view { width: 61rpx; height: 7rpx; margin: 12rpx 0 18rpx; border-radius: 4rpx; background-image: linear-gradient(90deg, #FF4D36 0%, #FFBF3A 100%); }.service-item { display: flex; min-height: 205rpx; margin-bottom: 20rpx; flex-direction: row; }.service-item.is-unavailable { opacity: .56; }.service-image { width: 184rpx; height: 184rpx; border-radius: 17rpx; flex-shrink: 0; background-color: #EEE; }.service-info { display: flex; min-width: 0; min-height: 184rpx; flex: 1; padding: 4rpx 0 4rpx 24rpx; flex-direction: column; justify-content: space-between; }.service-name { color: #292929; font-size: 29rpx; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }.discount-tag { display: block; width: fit-content; padding: 6rpx 9rpx; border-radius: 6rpx; background-color: #FFF0EC; color: #FF5D37; font-size: 21rpx; }.address { 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; }.address image { width: 27rpx; height: 27rpx; margin-right: 8rpx; flex-shrink: 0; }.address text { min-width: 0; flex: 1; color: #7E8490; font-size: 21rpx; line-height: 31rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| | | .list-state, .no-more { display: flex; height: 150rpx; color: #989DA5; font-size: 23rpx; justify-content: center; align-items: center; }.list-state.compact { height: 90rpx; }.error-state { flex-direction: column; }.retry-text, .load-error text { margin-top: 18rpx; color: #F26A38; }.loading-spinner { width: 26rpx; height: 26rpx; margin-right: 12rpx; box-sizing: border-box; border: 3rpx solid #FFD6C4; border-top-color: #FF6C2B; border-radius: 50%; animation: entertainment-loading 720ms linear infinite; }@keyframes entertainment-loading { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }.bottom-space { height: calc(30rpx + env(safe-area-inset-bottom)); } |
| | | .ticket-entry { display: flex; height: 72rpx; margin: 20rpx 0 4rpx; box-sizing: border-box; padding: 0 26rpx; border-radius: 18rpx; background-color: #FFFFFF; flex-direction: row; align-items: center; justify-content: space-between; } |
| | | .ticket-entry text { color: #FF5935; font-size: 25rpx; font-weight: 600; } |
| | | .ticket-arrow { color: #C2C8D0; font-size: 34rpx; } |
| | | </style> |
| | |
| | | <view class="field-card"><input class="field-input" v-model="userName" type="text" maxlength="30" placeholder="请输入姓名" placeholder-class="field-placeholder" /></view> |
| | | |
| | | <text class="field-label">手机号</text> |
| | | <view class="field-card"><input class="field-input" v-model="userMobile" type="number" maxlength="11" placeholder="请输入手机号" placeholder-class="field-placeholder" /></view> |
| | | <view class="field-card"><input class="field-input" :value="userMobile" type="number" maxlength="11" placeholder="请输入手机号" placeholder-class="field-placeholder" @input="handleMobileInput" /></view> |
| | | |
| | | <text class="field-label">身份证号</text> |
| | | <view class="field-card"><input class="field-input" :value="userIdCard" type="idcard" maxlength="18" placeholder="请输入身份证号" placeholder-class="field-placeholder" @input="handleIdCardInput" /></view> |
| | |
| | | |
| | | const goBack = () => safeBack('/pages/premium-benefits/premium-benefits') |
| | | const changeExpectDate = (event: any) => { expectDate.value = event.detail.value as string } |
| | | const handleMobileInput = (event: any) => { |
| | | const raw = event != null && event.detail != null && event.detail.value != null ? event.detail.value as string : '' |
| | | const normalized = raw.replace(/[^0-9]/g, '').slice(0, 11) |
| | | userMobile.value = normalized |
| | | return normalized |
| | | } |
| | | const handleIdCardInput = (event: any) => { |
| | | const raw = event != null && event.detail != null && event.detail.value != null ? event.detail.value as string : '' |
| | | userIdCard.value = raw.toUpperCase().replace(/[^0-9A-Z]/g, '').slice(0, 18) |
| | | const normalized = raw.toUpperCase().replace(/[^0-9A-Z]/g, '').slice(0, 18) |
| | | userIdCard.value = normalized |
| | | return normalized |
| | | } |
| | | const submitBooking = () => { |
| | | if (isSubmitting.value) return |
| | |
| | | <view v-if="isParkingMerchant && parkingActionMode == 'talentCode'" class="talent-code-button" @tap="showBottomVoucher"><image src="/static/merchant-assets/qr-code.svg"></image><text>出示人才码</text></view> |
| | | <view v-else-if="isParkingMerchant" class="talent-code-button" @tap="openPlatePage"><text>填写车牌</text></view> |
| | | <view v-else-if="isTransportMerchant" class="talent-code-button" @tap="openMobileDialog"><text>填写手机号</text></view> |
| | | <view v-else-if="isApartmentMerchant" class="talent-code-button" :class="advisorMobile == '' ? 'talent-code-button-disabled' : ''" @tap="contactSpecialist"><image src="/static/merchant-assets/headset-white.svg"></image><text>联系专员</text></view> |
| | | <view v-else class="talent-code-button" @tap="showBottomVoucher"><image :src="hasFreeVoucher ? '/static/merchant-assets/coupon.svg' : '/static/merchant-assets/qr-code.svg'"></image><text>{{hasFreeVoucher ? '出示免费券' : '出示人才码'}}</text></view> |
| | | </view> |
| | | |
| | |
| | | const merchantPhone = ref('08123335555') |
| | | const merchantId = ref('merchant-food-default') |
| | | const merchantType = ref('food') |
| | | const isApartmentMerchant = computed(() : boolean => merchantType.value == 'apartment') |
| | | const advisorMobile = ref('') |
| | | const merchantName = ref('百杏厨房(中环天地店)') |
| | | const currentCardLevel = ref('') |
| | | const merchantHero = ref('') |
| | |
| | | } else if (isFuelMerchant.value || isParkingMerchant.value) { |
| | | loadTransportMerchantDetail() |
| | | } else { |
| | | if (isHotelMerchant.value && talentId.value != '') { |
| | | if ((isHotelMerchant.value || isApartmentMerchant.value) && talentId.value != '') { |
| | | getTalentCardProfile(talentId.value, (data: any) => { |
| | | const card = data != null ? data.card : null |
| | | talentCardId.value = card != null && card.id != null ? '' + card.id : talentCardId.value |
| | | currentCardLevel.value = card != null && card.level != null ? card.level as string : currentCardLevel.value |
| | | const advisor: any = data != null ? data.advisor : null |
| | | advisorMobile.value = advisor != null && advisor.mobile != null ? advisor.mobile as string : '' |
| | | }, () => {}) |
| | | } |
| | | loadRemoteMerchant() |
| | |
| | | } |
| | | uni.makePhoneCall({ phoneNumber: merchantPhone.value }) |
| | | } |
| | | const contactSpecialist = () => { |
| | | if (advisorMobile.value == '') { |
| | | uni.showToast({ title: '暂未配置服务专员', icon: 'none' }) |
| | | return |
| | | } |
| | | uni.makePhoneCall({ phoneNumber: advisorMobile.value, fail: () => uni.showToast({ title: '暂时无法拨打电话', icon: 'none' }) }) |
| | | } |
| | | const openStoreLocation = (item: any) => { |
| | | if (item.canNavigate === false || Number(item.latitude) <= 0 || Number(item.longitude) <= 0) { |
| | | uni.showToast({ title: '该门店暂未配置位置', icon: 'none' }) |
| | |
| | | .medical-usage-panel text { color: #777F8B; font-size: 27rpx; line-height: 43rpx; white-space: pre-wrap; } |
| | | .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-color: #FFFFFF; box-shadow: 0 -5rpx 15rpx rgba(0,0,0,.06); flex-direction: column; justify-content: flex-end; } |
| | | .talent-code-button { display: flex; width: 100%; height: 90rpx; flex-shrink: 0; border-radius: 46rpx; background-image: linear-gradient(90deg, #FF4D32 0%, #FF8A20 100%); flex-direction: row; justify-content: center; align-items: center; } |
| | | .talent-code-button-disabled { background-image: none; background-color: #C7CDD5; } |
| | | .talent-code-button image { width: 29rpx; height: 29rpx; margin-right: 14rpx; } |
| | | .talent-code-button text { color: #FFFFFF; font-size: 31rpx; font-weight: 500; } |
| | | .mobile-dialog-mask { display: flex; position: fixed; z-index: 100; left: 0; right: 0; top: 0; bottom: 0; background-color: rgba(24,29,38,.48); justify-content: center; align-items: center; } |
| | |
| | | <view v-else-if="loadError" class="state-panel"><text>预约信息加载失败</text><view class="state-action" @tap="loadApplication"><text>重新加载</text></view><view class="state-action secondary" @tap="goBack"><text>返回使用记录</text></view></view> |
| | | <view v-else class="content"> |
| | | <view class="content-inner"> |
| | | <view class="merchant-card"> |
| | | <view v-if="!isSpouseEmployment" class="merchant-card"> |
| | | <view class="merchant-info"><text class="merchant-name">{{institutionName}}</text><text class="merchant-address">{{institutionAddress}}</text></view> |
| | | <view class="merchant-actions"> |
| | | <view class="merchant-action" hover-class="action-active" @tap="openNavigation"><image src="/static/merchant-assets/navigation.svg" mode="aspectFit"></image><text>导航</text></view> |
| | |
| | | </view> |
| | | |
| | | <view class="appointment-card"> |
| | | <text class="appointment-title">预约详情</text> |
| | | <view class="appointment-row"><text>预约内容:</text><text>{{appointmentContent}}</text></view> |
| | | <view class="appointment-row"><text>预约时间:</text><text>{{appointmentTime}}</text></view> |
| | | <view class="appointment-row"><text>预约单号:</text><text>{{applicationNo}}</text></view> |
| | | <text class="appointment-title">{{isSpouseEmployment ? '提交详情' : '预约详情'}}</text> |
| | | <view v-if="isSpouseEmployment" class="appointment-row"><text>提交类型:</text><text>配偶推荐就业</text></view> |
| | | <view v-if="!isSpouseEmployment" class="appointment-row"><text>预约类型:</text><text>{{appointmentTypeLabel}}</text></view> |
| | | <view v-if="!isSpouseEmployment" class="appointment-row"><text>预约内容:</text><text>{{appointmentContent}}</text></view> |
| | | <view v-if="!isSpouseEmployment" class="appointment-row"><text>预约时间:</text><text>{{appointmentTime}}</text></view> |
| | | <view v-if="!isSpouseEmployment" class="appointment-row"><text>预约单号:</text><text>{{applicationNo}}</text></view> |
| | | <view class="appointment-row"><text>提交时间:</text><text>{{submittedAt}}</text></view> |
| | | <view class="appointment-row"><text>预计处理:</text><text>{{estimatedProcessingTime}}</text></view> |
| | | <view v-if="advisorName != ''" class="appointment-row"><text>服务专员:</text><text>{{advisorName}} {{advisorDepartment}}</text></view> |
| | | <view v-if="!isSpouseEmployment && advisorName != ''" class="appointment-row"><text>服务专员:</text><text>{{advisorName}} {{advisorDepartment}}</text></view> |
| | | </view> |
| | | <view class="content-bottom"></view> |
| | | </view> |
| | |
| | | const navRight = ref(10) |
| | | const recordId = ref('') |
| | | const applicationNo = ref('') |
| | | const applicationType = ref('') |
| | | const isSpouseEmployment = computed(() : boolean => applicationType.value == 'SPOUSE_EMPLOYMENT') |
| | | const appointmentTypeLabel = computed(() : string => { |
| | | if (applicationType.value == 'HOTEL_RESERVATION') return '酒店预约' |
| | | if (applicationType.value == 'MEDICAL_RESERVATION') return '体检预约' |
| | | if (applicationType.value == 'FAST_TRACK') return '快捷通道预约' |
| | | if (applicationType.value == 'CHILD_ENTRUSTMENT') return '子女托管预约' |
| | | return '服务预约' |
| | | }) |
| | | const talentId = ref('') |
| | | const loading = ref(true) |
| | | const loadError = ref(false) |
| | |
| | | const appointmentContent = ref('') |
| | | const appointmentTime = ref('以专员通知为准') |
| | | const submittedAt = ref('') |
| | | const estimatedProcessingTime = ref('以专员通知为准') |
| | | const advisorName = ref('') |
| | | const advisorDepartment = ref('') |
| | | const advisorMobile = ref('') |
| | |
| | | loading.value = true |
| | | loadError.value = false |
| | | getProcessingApplication(applicationNo.value, talentId.value, (data: any) => { |
| | | applicationType.value = data.applicationType || '' |
| | | statusTitle.value = data.statusTitle || '待处理' |
| | | statusDescription.value = data.statusDescription || '等待专员处理' |
| | | institutionName.value = data.institutionName || '人才服务机构' |
| | |
| | | appointmentContent.value = data.appointmentContent || '服务预约' |
| | | appointmentTime.value = data.appointmentTime || '以专员通知为准' |
| | | submittedAt.value = ('' + data.submittedAt).replace('T', ' ').substring(0, 19) |
| | | estimatedProcessingTime.value = data.estimatedProcessingTime || '以专员通知为准' |
| | | const advisor: any = data.advisor || {} |
| | | advisorName.value = advisor.name || '' |
| | | advisorDepartment.value = advisor.department || '' |
| | |
| | | id: source != null && source.recordId != null ? source.recordId as string : '', |
| | | businessId: source != null && source.businessId != null ? source.businessId as string : '', |
| | | recordType: source != null && source.recordType != null ? source.recordType as string : '', |
| | | targetDetailId: source != null && source.targetDetailId != null ? '' + source.targetDetailId : '', |
| | | targetDetailType: source != null && source.targetDetailType != null ? source.targetDetailType as string : '', |
| | | merchantId: source != null && source.merchantId != null ? '' + source.merchantId : '', |
| | | name: source != null && source.merchantName != null ? source.merchantName as string : '人才权益服务', |
| | |
| | | <view class="plate-composer"> |
| | | <picker class="plate-picker" :range="provinces" :value="provinceIndex" @change="changeProvince"><view class="plate-prefix" :class="provinceIndex == 0 ? 'placeholder' : 'selected'"><text>{{provinces[provinceIndex]}}</text><view class="picker-caret"></view></view></picker> |
| | | <picker class="plate-picker letter-picker" :range="letters" :value="letterIndex" @change="changeLetter"><view class="plate-prefix" :class="letterIndex == 0 ? 'placeholder' : 'selected'"><text>{{letters[letterIndex]}}</text><view class="picker-caret"></view></view></picker> |
| | | <view class="plate-suffix"><input :value="plateSuffix" maxlength="5" placeholder="5位数字或字母" placeholder-class="input-placeholder" @input="handlePlateSuffixInput" /></view> |
| | | <view class="plate-suffix"><input :value="plateSuffix" maxlength="6" placeholder="5-6位数字或字母" placeholder-class="input-placeholder" @input="handlePlateSuffixInput" /></view> |
| | | </view> |
| | | <text v-if="plateError != ''" class="field-error">{{plateError}}</text> |
| | | </view> |
| | |
| | | const changeLetter = (event: any) => { letterIndex.value = parseInt(event.detail.value as string); plateError.value = '' } |
| | | const handlePlateSuffixInput = (event: any) => { |
| | | const raw = (event.detail.value as string).toUpperCase() |
| | | plateSuffix.value = raw.replace(/[^A-Z0-9]/g, '').slice(0, 5) |
| | | plateSuffix.value = raw.replace(/[^A-Z0-9]/g, '').slice(0, 6) |
| | | plateError.value = '' |
| | | } |
| | | const submitPlate = () => { |
| | |
| | | if (requiresParking.value && selectedParkingId.value == 0) parkingError.value = '请从搜索结果中选择停车场' |
| | | if (provinceIndex.value == 0) plateError.value = '请选择省份简称' |
| | | else if (letterIndex.value == 0) plateError.value = '请选择车牌首字母' |
| | | else if (!/^[A-Z0-9]{5}$/.test(plateSuffix.value)) plateError.value = '请输入后 5 位数字或字母' |
| | | else if (!/^[A-Z0-9]{5,6}$/.test(plateSuffix.value)) plateError.value = '请输入后 5-6 位数字或字母' |
| | | if (parkingError.value != '' || plateError.value != '') return |
| | | if (merchantId.value == '' || benefitId.value == '' || talentId.value == '') { |
| | | plateError.value = merchantId.value == '' || benefitId.value == '' ? '停车权益暂未配置' : '请登录后提交车牌' |
| | |
| | | const navHeight = ref(32) |
| | | const navWidth = ref(88) |
| | | const navRight = ref(10) |
| | | const schoolId = ref(0) |
| | | const serviceId = ref('') |
| | | const schoolId = ref('') |
| | | const schoolName = ref('东区实验学校') |
| | | const schoolAddress = ref('东区机场路教育园区') |
| | | const isLoading = ref(true) |
| | |
| | | navRight.value = info.windowWidth - capsule.right |
| | | // #endif |
| | | if (options != null) { |
| | | if (options['id'] != null) schoolId.value = parseInt(options['id'] as string) |
| | | if (options['serviceId'] != null) serviceId.value = decodeURIComponent(options['serviceId'] as string) |
| | | if (options['id'] != null) schoolId.value = decodeURIComponent(options['id'] as string) |
| | | if (options['name'] != null) schoolName.value = decodeURIComponent(options['name'] as string) |
| | | if (options['address'] != null) schoolAddress.value = decodeURIComponent(options['address'] as string) |
| | | } |
| | |
| | | const loadSchoolDetail = () => { |
| | | loadFailed.value = false |
| | | isLoading.value = true |
| | | if (schoolId.value <= 0) { isLoading.value = false; loadFailed.value = true; return } |
| | | getChildrenEntrustmentSchool('' + schoolId.value, (data: any) => { |
| | | if (serviceId.value == '' || schoolId.value == '') { isLoading.value = false; loadFailed.value = true; return } |
| | | getChildrenEntrustmentSchool(serviceId.value, schoolId.value, (data: any) => { |
| | | schoolName.value = data.name != null ? data.name as string : schoolName.value |
| | | schoolAddress.value = data.address != null ? data.address as string : schoolAddress.value |
| | | schoolContent.value = sanitizeRichText(data.content != null ? data.content as string : '') |
| | |
| | | <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="openMerchant(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-discount">{{item.available ? item.discount : '已停用'}}</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-else-if="hasLoaded" class="service-list"><view v-for="item in visibleServices" :key="item.id" class="service-item" :class="item.available ? '' : 'is-unavailable'" @tap="openMerchant(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.discount != ''" class="service-discount">{{item.discount}}</text><text v-else-if="!item.available" class="service-discount">{{item.unavailableReason}}</text><view v-if="item.address != ''" 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> |
| | |
| | | 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 { 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) |
| | |
| | | const topicTitle = ref('惠享好物'); const topicDescription = ref(''); const topicError = ref(false) |
| | | const scenes = ref<any[]>(fallbackScenes); const activeIndex = ref(0); const activeScene = ref<any>({ id: '', name: '', image: '' }); const sceneScrollLeft = ref(0); const sceneTrackWidth = ref(644) |
| | | const serviceTabs = ref<any[]>([]); const activeTab = ref(''); const visibleServices = ref<any[]>([]) |
| | | const talentId = ref('') |
| | | const initialLoading = ref(true); const loadingMore = ref(false); const hasLoaded = ref(false); const hasMore = ref(false); const initialError = ref(false); const loadMoreError = ref(false); const currentPage = ref(0); const listRequestVersion = ref(0); const failInitialOnce = ref(false); const failLoadMoreOnce = ref(false) |
| | | |
| | | onLoad((options: any) => { |
| | | // #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 |
| | | if (options != null && options['previewState'] != null) { const state = options['previewState'] as string; failInitialOnce.value = state == 'initialError'; failLoadMoreOnce.value = state == 'loadMoreError' } |
| | | selectScene(0); loadShoppingTopic() |
| | | }) |
| | |
| | | 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 } |
| | | getShoppingTopicMerchants(activeTab.value, currentPage.value + 1, 4, (data: any) => { |
| | | getShoppingTopicMerchants(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]; next.push({ id: '' + item.id, merchantType: item.merchantType != null ? item.merchantType as string : '', name: item.merchantName != null ? item.merchantName as string : '购物服务', discount: item.discountText != null ? item.discountText as string : '人才专享', address: item.address != null ? item.address as string : '', image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage, available: item.available !== false }) } |
| | | for (let index = 0; index < records.length; index++) { const item = records[index]; next.push({ id: '' + item.id, merchantType: item.merchantType != null ? item.merchantType as string : '', name: item.merchantName != null && item.merchantName != '' ? item.merchantName as string : '商家名称未配置', discount: item.discountText != null ? item.discountText as string : '', address: item.address != null ? item.address as string : '', image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage, available: item.available !== false, unavailableReason: item.unavailableReason != null && item.unavailableReason != '' ? item.unavailableReason as string : '该服务当前不可用' }) } |
| | | visibleServices.value = next; currentPage.value++; hasMore.value = data != null && data.total != null ? next.length < data.total : false; initialLoading.value = false; loadingMore.value = false; hasLoaded.value = true |
| | | }, () => { 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') |
| | | const openMerchant = (item: any) => { |
| | | if (!item.available) { uni.showToast({ title: '该服务已停用', icon: 'none' }); return } |
| | | if (!item.available) { uni.showToast({ title: item.unavailableReason, icon: 'none' }); return } |
| | | if (item.merchantType == 'dental') { |
| | | uni.navigateTo({ url: '/pages/merchant/merchant?merchantId=' + encodeURIComponent(item.id) + '&merchantType=dental&name=' + encodeURIComponent(item.name) + '&image=' + encodeURIComponent(item.image) + '&address=' + encodeURIComponent(item.address) + '&discount=' + encodeURIComponent(item.discount) }) |
| | | return |
| | |
| | | .page,.page-scroll { width:100%; height:100vh; background-color:#FFFFFF; }.custom-nav { position:fixed; z-index:20; left:0; right:0; top:0; padding-bottom:20rpx; }.nav-bg { position:absolute; 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 .nav-bg { opacity:1; }.safe-space,.nav-row { position:relative; z-index:1; }.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; position:relative; z-index:2; border-radius:50%; background-color:rgba(45,45,45,.52); justify-content:center; align-items:center; }.custom-nav.is-solid .back-button { background-color:transparent; }.back-button image { width:38rpx; height:38rpx; }.nav-title { position:absolute; z-index:1; left:0; right:0; color:#FFFFFF; font-size:31rpx; font-weight:500; text-align:center; text-shadow:0 2rpx 8rpx rgba(0,0,0,.35); }.capsule-space { height:100%; flex-shrink:0; } |
| | | .hero { height:730rpx; position:relative; overflow:visible; background-color:#1C315E; }.hero-image { position:absolute; left:0; top:0; width:100%; height:730rpx; }.hero-top-shade { position:absolute; left:0; right:0; top:0; height:245rpx; background-image:linear-gradient(180deg,rgba(0,0,0,.62),rgba(0,0,0,0)); }.hero-bottom-shade { position:absolute; left:0; right:0; bottom:0; height:230rpx; background-image:linear-gradient(180deg,rgba(0,0,0,0),rgba(0,0,0,.42)); }.scene-name { position:absolute; z-index:3; left:38rpx; top:318rpx; color:#FFFFFF; font-family:'FoodCalligraphy'; font-size:48rpx; line-height:66rpx; text-shadow:0 3rpx 10rpx rgba(0,0,0,.55); }.scene-scroll { position:absolute; z-index:4; left:0; right:0; top:469rpx; width:100%; height:150rpx; white-space:nowrap; }.scene-row { display:flex; height:150rpx; flex-direction:row; align-items:center; }.scene-pad { width:322rpx; height:1rpx; flex-shrink:0; }.scene-item { display:flex; width:106rpx; height:106rpx; margin-right:31rpx; box-sizing:border-box; overflow:hidden; position:relative; z-index:1; border:3rpx solid rgba(255,255,255,.9); border-radius:8rpx; flex-shrink:0; transition:transform 180ms ease-out; }.scene-item image { width:100%; height:100%; }.scene-item.active { z-index:3; border-width:4rpx; border-color:#FFD329; transform:scale(1.28); box-shadow:0 8rpx 18rpx rgba(0,0,0,.28); } |
| | | .topic-error { display:flex; height:76rpx; padding:0 31rpx; background-color:#FFF8F4; flex-direction:row; justify-content:center; align-items:center; }.topic-error text { color:#A48A7A; font-size:22rpx; }.topic-error text + text { margin-left:22rpx; color:#F26A38; }.topic-summary { position:relative; z-index:4; margin-top:-66rpx; }.intro-card { margin:0 31rpx; box-sizing:border-box; padding:25rpx 31rpx; border-radius:26rpx; background-image:linear-gradient(90deg,#FF5320,#FF8A22); box-shadow:0 10rpx 22rpx rgba(255,104,28,.24); }.intro-card text { color:#FFFFFF; font-size:23rpx; line-height:34rpx; }.service-section { padding:32rpx 31rpx 0; }.tab-scroll { width:100%; height:78rpx; white-space:nowrap; }.service-tabs { display:flex; height:72rpx; flex-direction:row; align-items:flex-start; }.service-tab { display:flex; width:144rpx; margin-right:42rpx; flex-direction:column; align-items:center; flex-shrink:0; }.service-tab text { color:#8A8D92; font-size:29rpx; font-weight:600; }.service-tab.active text { color:#363636; font-weight:700; }.tab-line { width:0; height:7rpx; margin-top:12rpx; border-radius:4rpx; background-image:linear-gradient(90deg,#FF4D36,#FFBF3A); transition:width 180ms ease-out; }.service-tab.active .tab-line { width:61rpx; } |
| | | .service-list { padding-top:10rpx; }.service-item { display:flex; min-height:215rpx; margin-bottom:22rpx; flex-direction:row; align-items:flex-start; }.service-item.is-unavailable { opacity:.55; }.service-image { width:184rpx; height:184rpx; border-radius:17rpx; flex-shrink:0; background-color:#EEEEEE; }.service-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; }.service-name { color:#292929; font-size:29rpx; line-height:40rpx; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.service-discount { align-self:flex-start; max-width:400rpx; padding:6rpx 9rpx; border-radius:6rpx; background-color:#FFF5E7; color:#FF7C2B; font-size:21rpx; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.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),rgba(242,243,245,.76) 58%,rgba(255,255,255,0)); flex-direction:row; align-items:flex-start; }.address-row image { width:27rpx; height:27rpx; margin-right:8rpx; flex-shrink:0; }.address-row text { min-width:0; flex:1; color:#7E8490; font-size:21rpx; line-height:31rpx; max-height:62rpx; overflow:hidden; }.service-empty { display:flex; height:310rpx; justify-content:center; align-items:center; }.service-empty text { color:#A5A8AD; font-size:25rpx; }.error-state { flex-direction:column; }.error-state .retry-text { margin-top:22rpx; color:#F26A38; }.loading-row,.no-more { display:flex; height:90rpx; flex-direction:row; justify-content:center; align-items:center; }.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:shopping-loading 720ms linear infinite; }.loading-row text { color:#989DA5; font-size:22rpx; }.load-error text { color:#F26A38; }.no-more view { width:70rpx; height:1rpx; background-color:#E7E7E7; }.no-more text { margin:0 18rpx; color:#B1B3B7; font-size:21rpx; }.bottom-space { height:calc(30rpx + env(safe-area-inset-bottom)); }@keyframes shopping-loading { from { transform:rotate(0deg); } to { transform:rotate(360deg); } } |
| | | </style> |
| | |
| | | <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 class="discount-label">{{discount}}</text></view><view 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><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}}</text></view></view> |
| | | <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="detail-section"><view class="section-title"><text>权益详情</text><view></view></view><view class="detail-card"><text>{{benefitDetail == '' ? '权益详情暂未配置' : benefitDetail}}</text></view></view> |
| | | </view> |
| | | </view> |
| | | <view class="action-bar"><view class="contact-button" :class="!available || !advisorAvailable ? 'contact-button-disabled' : ''" hover-class="contact-button-active" @tap="contactSpecialist"><image src="/static/merchant-assets/headset-white.svg" mode="aspectFit"></image><text>{{!available ? '服务不可用' : (advisorAvailable ? '联系专员' : '暂未配置服务专员')}}</text></view></view> |
| | |
| | | import { isApiConfigured } from '../../config/api.uts' |
| | | import { getShoppingMerchantDetail } 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 merchantId = 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 discount = ref(''); 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 |
| | |
| | | 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 |
| | | if (options != null && options['merchantId'] != null) merchantId.value = decodeURIComponent(options['merchantId'] as string) |
| | | const session = getSession(); if (session != null && session.talentId != null) talentId.value = session.talentId as string |
| | | loadMerchantDetail() |
| | | }) |
| | | const replaceHeroImage = () => { heroImage.value = '' } |
| | | const loadMerchantDetail = () => { |
| | | detailLoading.value = true; detailError.value = false; available.value = false |
| | | if (merchantId.value == '' || !isApiConfigured()) { detailLoading.value = false; detailError.value = true; return } |
| | | getShoppingMerchantDetail(merchantId.value, (data: any) => { |
| | | 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 : '请咨询服务专员' |
| | | applicableCards.value = data.applicableCards != null ? data.applicableCards as string : '请咨询服务专员' |
| | | address.value = data.address != null ? data.address as string : '请咨询服务专员' |
| | | discount.value = data.discountText != null ? data.discountText as string : '' |
| | | 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) : '' |
| | | benefitDetail.value = data.benefitDetail != null ? data.benefitDetail as string : '以服务专员说明为准' |
| | | benefitDetail.value = data.benefitDetail != null ? data.benefitDetail as string : '' |
| | | merchantPhone.value = data.contactMobile != null ? data.contactMobile as string : '' |
| | | latitude.value = data.latitude != null ? Number(data.latitude) : 0; longitude.value = data.longitude != null ? Number(data.longitude) : 0 |
| | | available.value = data.available === true |
| | |
| | | <view v-else-if="!available" class="state-box"><text>{{unavailableReason}}</text></view> |
| | | <view v-else><text class="field-label">姓名</text><view class="input-box"><input v-model="form.name" maxlength="64" placeholder="请输入姓名" placeholder-class="input-placeholder" /></view> |
| | | <text class="field-label">手机号</text><view class="input-box"><input v-model="form.mobile" type="number" maxlength="11" placeholder="请输入手机号" placeholder-class="input-placeholder" /></view> |
| | | <text class="field-label">身份证号</text><view class="input-box"><input v-model="form.idCard" maxlength="18" placeholder="请输入身份证号" placeholder-class="input-placeholder" /></view> |
| | | <text class="field-label">身份证号</text><view class="input-box"><input :value="form.idCard" type="idcard" maxlength="18" placeholder="请输入身份证号" placeholder-class="input-placeholder" @input="handleIdCardInput" /></view> |
| | | <text class="field-label">政治面貌</text><view class="input-box"><input v-model="form.politicalStatus" maxlength="64" placeholder="请输入政治面貌" placeholder-class="input-placeholder" /></view> |
| | | <text class="field-label">学历</text><picker class="field-picker" :range="educationOptions" :value="educationIndex" @change="changeEducation"><view class="picker-box"><text :class="form.education == '' ? 'picker-placeholder' : ''">{{form.education == '' ? '请选择' : form.education}}</text><text class="picker-arrow">⌄</text></view></picker> |
| | | <text class="field-label">毕业院校及专业</text><view class="input-box"><input v-model="form.schoolMajor" maxlength="255" placeholder="请输入毕业院校及专业" placeholder-class="input-placeholder" /></view> |
| | |
| | | const serviceId = ref(''); const talentId = ref(''); const loading = ref(true); const loadError = ref(false); const available = ref(false); const unavailableReason = ref('该权益暂未开放'); const isSubmitting = ref(false); const submitKey = ref('') |
| | | const educationOptions = ref(['']); const institutionOptions = ref(['']); const educationIndex = ref(0); const institutionIndex = ref(0) |
| | | const form = ref({ name: '', mobile: '', idCard: '', politicalStatus: '', education: '', schoolMajor: '', workUnit: '', inInstitution: '', workExperience: '' }) |
| | | const normalizeIdCard = (value: string): string => { |
| | | const raw = value.toUpperCase().replace(/[^0-9X]/g, '') |
| | | const xIndex = raw.indexOf('X') |
| | | if (xIndex >= 17) return raw.substring(0, xIndex).replace(/[^0-9]/g, '').slice(0, 17) + 'X' |
| | | return raw.replace(/[^0-9]/g, '').slice(0, 18) |
| | | } |
| | | 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 |
| | |
| | | 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 } |
| | | const handleIdCardInput = (event: any) => { const normalized = normalizeIdCard(event.detail.value as string); const next = form.value; next.idCard = normalized; form.value = next; return normalized } |
| | | const required = (value: string, label: string): boolean => { if (value.trim() != '') return true; uni.showToast({ title: '请填写' + label, icon: 'none' }); return false } |
| | | const submitForm = () => { |
| | | if (isSubmitting.value) return |
| | | if (talentId.value == '') { uni.showToast({ title: '请登录后提交', icon: 'none' }); return } |
| | | if (!required(form.value.name, '姓名')) return |
| | | if (!(/^1\d{10}$/).test(form.value.mobile.trim())) { uni.showToast({ title: '请填写正确的手机号', icon: 'none' }); return } |
| | | if (!(/^(\d{15}|\d{17}[0-9Xx])$/).test(form.value.idCard.trim())) { uni.showToast({ title: '请填写正确的身份证号', icon: 'none' }); return } |
| | | if (!(/^[0-9]{17}[0-9X]$/).test(form.value.idCard.trim())) { uni.showToast({ title: '请填写正确的18位身份证号', 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 || '') }) }, (error) => { isSubmitting.value = false; uni.showToast({ title: error.message, icon: 'none' }) }) |
| | |
| | | <view v-else> |
| | | <view class="service-row"><text class="service-name">{{title}}</text><text class="free-label">{{freeLabel}}</text></view> |
| | | <view class="section-title"><text>权益详情</text><view></view></view> |
| | | <view class="detail-card"><text>{{rulesText}}</text><text class="contact-text" @tap="callConsultation">咨询电话:{{consultationPhone}}</text></view> |
| | | <view class="detail-card"><text>{{rulesText}}</text></view> |
| | | <view v-if="!available" class="unavailable"><text>{{unavailableReason}}</text></view> |
| | | </view> |
| | | </view> |
| | |
| | | |
| | | 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 consultationPhone = ref(''); const unavailableReason = ref('该权益暂未开放') |
| | | const fallbackHeroImage = '/static/stay-assets/hotel-vienna.jpg'; const heroImage = ref(fallbackHeroImage); const title = ref('配偶推荐就业'); const freeLabel = 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 |
| | |
| | | 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 || ''; consultationPhone.value = data.consultationPhone || ''; 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; freeLabel.value = data.freeLabel || freeLabel.value; 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 } |
| | | 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) }) } |
| | | </script> |
| | | |
| | |
| | | .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; } .detail-card .contact-text { margin-top:20rpx; color:#FF7A27; } .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; } .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> |
| | |
| | | export const getTransportTopic = (onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appTransportTopic'), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | export const getTransportTopicMerchants = (type: string, page: number, size: number, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appTransportTopicMerchants'), method: 'GET', skipAuth: true, data: { type: type, page: page, size: size } }, onSuccess, onError) |
| | | export const getShoppingTopic = (onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appShoppingTopic'), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | 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 getShoppingTopicMerchants = (categoryId: string, talentId: string, page: number, size: number, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => { |
| | | const data: any = { categoryId: categoryId, page: page, size: size } |
| | | if (talentId != '') data.talentId = Number(talentId) |
| | | return request<any>({ path: getApiEndpoint('appShoppingTopicMerchants'), method: 'GET', skipAuth: true, data: data }, 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, talentId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => { |
| | |
| | | return request<any>({ path: getApiEndpoint('appChildrenEntrustmentBenefit') + encodeURIComponent(serviceId), method: 'GET', skipAuth: true, data: data }, 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) |
| | | export const getChildrenEntrustmentSchool = (serviceId: string, schoolId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appChildrenEntrustmentBenefit') + encodeURIComponent(serviceId) + '/school-detail', method: 'GET', skipAuth: true, data: { schoolId: schoolId } }, onSuccess, onError) |
| | | export const getChildren = (talentId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appChildren'), method: 'GET', skipAuth: true, data: { talentId: talentId } }, onSuccess, onError) |
| | | export const getChild = (childId: string, talentId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appChildren') + '/' + encodeURIComponent(childId), method: 'GET', skipAuth: true, data: { talentId: talentId } }, onSuccess, onError) |
| | | export const createChild = (payload: any, idempotencyKey: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appChildren'), method: 'POST', skipAuth: true, data: payload, idempotencyKey: idempotencyKey }, onSuccess, onError) |
| | |
| | | const data: any = { talentId: Number(talentId), cardId: Number(cardId), proofFiles: [], description: '手机号:' + phone } |
| | | return request<any>({ path: getApiEndpoint('appCarWashMerchantDetail') + encodeURIComponent(merchantId) + '/credentials', method: 'POST', skipAuth: true, data: data, idempotencyKey: idempotencyKey }, onSuccess, onError) |
| | | } |
| | | export const getShoppingMerchantDetail = (merchantId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appShoppingMerchantDetail') + encodeURIComponent(merchantId), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | export const getShoppingMerchantDetail = (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('appShoppingMerchantDetail') + encodeURIComponent(merchantId), method: 'GET', skipAuth: true, data: data }, onSuccess, onError) |
| | | } |
| | | export const searchParkingLots = (merchantId: string, keyword: string, page: number, size: number, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appParkingMerchantDetail') + encodeURIComponent(merchantId) + '/parking-lots', method: 'GET', skipAuth: true, data: { keyword: keyword, page: page, size: size } }, onSuccess, onError) |
| | | export const submitParkingPlateRegistration = (merchantId: string, talentId: string, benefitId: string, parkingLotId: string, plateNumber: string, idempotencyKey: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => { |
| | | const data: any = { talentId: Number(talentId), benefitId: Number(benefitId), plateNumber: plateNumber } |
| | |
| | | || isApprovalMerchant.value |
| | | || merchantAppLoginOptionalTypes.includes(merchantTypeCode.value) |
| | | )) |
| | | const requiresMerchantOperator = computed(() => (!isEditMode.value || !merchantHasLoginAccount.value) && ( |
| | | const merchantOperatorEnabled = computed(() => ( |
| | | merchantTypeCode.value === 'scenic' |
| | | || (supportsMerchantAppLogin.value && Number(form.canLoginMerchantApp) === 1) |
| | | )) |
| | | const requiresMerchantOperator = computed(() => (!isEditMode.value || !merchantHasLoginAccount.value) && merchantOperatorEnabled.value) |
| | | function carWashJumpMeta() { |
| | | if (!isCarWashMerchant.value) return {} |
| | | if (form.miniProgramJumpType !== 'miniprogram') { |
| | |
| | | <el-form-item v-if="supportsMerchantAppLogin" class="merchant-account-login-toggle" label="登录商户端" label-width="100px"> |
| | | <el-switch v-model="form.canLoginMerchantApp" :active-value="1" :inactive-value="0" /> |
| | | </el-form-item> |
| | | <el-form-item class="merchant-account-field" label="登录账号" prop="loginAccount" :required="requiresMerchantOperator" :error="accountTaken ? '登录账号已存在,请更换' : ''"> |
| | | <el-form-item class="merchant-account-field" label="登录账号" prop="loginAccount" :required="merchantOperatorEnabled" :error="accountTaken ? '登录账号已存在,请更换' : ''"> |
| | | <el-input v-model="accountForm.loginAccount" maxlength="32" :disabled="isEditMode && merchantHasLoginAccount" :placeholder="isEditMode && !merchantHasLoginAccount ? '请输入登录账号' : (isEditMode ? '已配置登录账号' : '仅支持英文和数字')" @input="normalizeLoginAccount" @blur="checkLoginAccountAvailable" /> |
| | | </el-form-item> |
| | | <el-form-item class="merchant-account-field" label="登录密码" prop="loginPassword" :required="requiresMerchantOperator || (isEditMode && Boolean(accountForm.loginPassword))"> |
| | | <el-form-item class="merchant-account-field" label="登录密码" prop="loginPassword" :required="merchantOperatorEnabled"> |
| | | <el-input v-model="accountForm.loginPassword" type="password" show-password :placeholder="isEditMode ? '留空则不修改,6至64位' : ''" /> |
| | | </el-form-item> |
| | | <el-form-item class="merchant-account-field" label="确认密码" prop="confirmPassword" :required="requiresMerchantOperator || (isEditMode && Boolean(accountForm.loginPassword))"> |
| | | <el-form-item class="merchant-account-field" label="确认密码" prop="confirmPassword" :required="merchantOperatorEnabled"> |
| | | <el-input v-model="accountForm.confirmPassword" type="password" show-password :placeholder="isEditMode ? '再次输入新密码' : ''" /> |
| | | </el-form-item> |
| | | </el-form> |
| | |
| | | return row.statusName || row.codeStatusName || text(row.status) |
| | | } |
| | | |
| | | function isVoidedCode(row: Record<string, any>) { |
| | | const status = String(row.status || row.codeStatus || '').trim().toUpperCase() |
| | | const statusName = String(row.statusName || row.codeStatusName || '').trim() |
| | | return status === 'VOID' || statusName === '作废' || statusName === '已作废' |
| | | } |
| | | |
| | | function formatTime(value: unknown) { |
| | | return value ? String(value).replace('T', ' ') : '-' |
| | | } |
| | |
| | | } |
| | | |
| | | async function voidCode(row: Record<string, any>) { |
| | | if (isVoidedCode(row)) return |
| | | await voidTicketPackageCode(row.codeId || row.id, '后台作废') |
| | | ElMessage.success('已作废') |
| | | await loadData() |
| | |
| | | <el-button link type="primary" @click="toggleStatus(row)">{{ Number(row.status) === 1 ? '禁用' : '启用' }}</el-button> |
| | | <el-button link type="primary" @click="openEdit(row)">查看</el-button> |
| | | <el-button link type="primary" @click="setMode('codes', row)">码池</el-button> |
| | | <el-button link type="primary" @click="openGenerate(row)">生成码</el-button> |
| | | <el-button link type="primary" @click="setMode('claims', row)">领取记录</el-button> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | <el-table-column label="状态" min-width="100" align="center"><template #default="{ row }">{{ codeStatusText(row) }}</template></el-table-column> |
| | | <el-table-column prop="lockedTalentName" label="锁定/领取人" min-width="130" align="center" /> |
| | | <el-table-column label="更新时间" min-width="160" align="center"><template #default="{ row }">{{ formatTime(row.updatedAt) }}</template></el-table-column> |
| | | <el-table-column label="操作" width="100" fixed="right" align="center"><template #default="{ row }"><el-button link type="danger" @click="voidCode(row)">作废</el-button></template></el-table-column> |
| | | <el-table-column label="操作" width="100" fixed="right" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-button |
| | | link |
| | | :type="isVoidedCode(row) ? 'info' : 'danger'" |
| | | :disabled="isVoidedCode(row)" |
| | | @click="voidCode(row)" |
| | | >作废</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </template> |
| | | |
| | |
| | | return ApiResponse.success(service.schools(serviceId)); |
| | | } |
| | | |
| | | @GetMapping("/{serviceId}/school-detail") |
| | | public ApiResponse<Map<String, Object>> configuredSchool(@PathVariable Long serviceId, |
| | | @RequestParam String schoolId) { |
| | | return ApiResponse.success(service.school(serviceId, schoolId)); |
| | | } |
| | | |
| | | @GetMapping("/schools/{schoolId}") |
| | | public ApiResponse<Map<String, Object>> school(@PathVariable Long schoolId) { |
| | | return ApiResponse.success(service.school(schoolId)); |
| | |
| | | } |
| | | |
| | | private List<Map<String, Object>> configuredSchools(Object value) { |
| | | return configuredSchoolRows(value).stream().map(row -> { |
| | | Map<String, Object> school = new LinkedHashMap<>(); |
| | | school.put("id", row.get("id")); |
| | | school.put("name", row.get("name")); |
| | | school.put("address", row.get("address")); |
| | | school.put("latitude", row.get("latitude")); |
| | | school.put("longitude", row.get("longitude")); |
| | | school.put("phone", row.get("phone")); |
| | | school.put("detailAvailable", row.get("detailAvailable")); |
| | | return school; |
| | | }).toList(); |
| | | } |
| | | |
| | | private List<Map<String, Object>> configuredSchoolRows(Object value) { |
| | | String json = text(value, ""); |
| | | if (json.isBlank()) return List.of(); |
| | | try { |
| | |
| | | if (rows == null) return List.of(); |
| | | return rows.stream().map(row -> { |
| | | Map<String, Object> school = new LinkedHashMap<>(); |
| | | school.put("id", row.getOrDefault("id", row.get("name"))); |
| | | school.put("name", text(row.get("name"), "未命名学校")); |
| | | String name = text(row.get("name"), "未命名学校"); |
| | | school.put("id", text(row.get("id"), name)); |
| | | school.put("name", name); |
| | | school.put("address", text(row.get("address"), "暂未配置学校地址")); |
| | | school.put("latitude", row.get("latitude")); |
| | | school.put("longitude", row.get("longitude")); |
| | | school.put("phone", text(row.get("phone"), text(row.get("contactMobile"), ""))); |
| | | school.put("detailAvailable", !text(row.get("detail"), "").isBlank()); |
| | | String content = text(row.get("detail"), ""); |
| | | school.put("content", content); |
| | | school.put("detailAvailable", !content.isBlank()); |
| | | return school; |
| | | }).toList(); |
| | | } catch (RuntimeException ignored) { |
| | |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> school(Long serviceId, String schoolId) { |
| | | if (schoolId == null || schoolId.isBlank()) throw new BusinessException(ErrorCode.BAD_REQUEST, "学校信息不能为空"); |
| | | Map<String, Object> merchant = approvalService(serviceId); |
| | | List<Map<String, Object>> configuredSchools = configuredSchoolRows(merchant.get("usageInstructions")); |
| | | if (!configuredSchools.isEmpty()) { |
| | | Map<String, Object> matched = configuredSchools.stream() |
| | | .filter(row -> schoolId.equals(text(row.get("id"), "")) || schoolId.equals(text(row.get("name"), ""))) |
| | | .findFirst() |
| | | .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "学校不存在或暂未开放")); |
| | | if (!booleanValue(matched.get("detailAvailable"))) throw new BusinessException(ErrorCode.CONFLICT, "学校介绍暂未开放"); |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("id", matched.get("id")); |
| | | result.put("name", matched.get("name")); |
| | | result.put("address", matched.get("address")); |
| | | result.put("content", matched.get("content")); |
| | | return result; |
| | | } |
| | | try { |
| | | return school(Long.parseLong(schoolId)); |
| | | } catch (NumberFormatException exception) { |
| | | throw new BusinessException(ErrorCode.NOT_FOUND, "学校不存在或暂未开放"); |
| | | } |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> school(Long schoolId) { |
| | | if (schoolId == null || schoolId <= 0) throw new BusinessException(ErrorCode.BAD_REQUEST, "学校信息不能为空"); |
| | | List<Map<String, Object>> rows = query(""" |
| | |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "提交凭证不能为空"); |
| | | } |
| | | List<Map<String, Object>> existing = query(""" |
| | | select id, registration_no, status |
| | | select id, registration_no, merchant_id, talent_id, mobile, status, created_at |
| | | from transport_fuel_mobile_registration |
| | | where idempotency_key = ? and deleted = 0 |
| | | """, idempotencyKey); |
| | | if (!existing.isEmpty()) return registrationView(existing.get(0)); |
| | | if (!existing.isEmpty()) { |
| | | Map<String, Object> registration = existing.get(0); |
| | | ensureAdminApplication( |
| | | text(registration.get("registrationNo"), ""), |
| | | longNumber(registration.get("merchantId")), |
| | | longNumber(registration.get("talentId")), |
| | | text(registration.get("mobile"), request.mobile()), |
| | | idempotencyKey, |
| | | registration.get("createdAt") |
| | | ); |
| | | return registrationView(registration); |
| | | } |
| | | |
| | | Map<String, Object> merchant = fuelMerchant(merchantId); |
| | | if (number(merchant.get("status")) != 1) throw new BusinessException(ErrorCode.CONFLICT, "加油站当前不可用"); |
| | |
| | | insert into transport_fuel_mobile_registration(registration_no, merchant_id, benefit_id, talent_id, mobile, status, idempotency_key) |
| | | values (?, ?, ?, ?, ?, 'SUBMITTED', ?) |
| | | """, registrationNo, merchantId, request.benefitId(), request.talentId(), request.mobile(), idempotencyKey); |
| | | ensureAdminApplication(registrationNo, merchantId, request.talentId(), request.mobile(), idempotencyKey, null); |
| | | return registrationView(one(""" |
| | | select id, registration_no, status |
| | | from transport_fuel_mobile_registration |
| | | where idempotency_key = ? and deleted = 0 |
| | | """, idempotencyKey)); |
| | | } |
| | | |
| | | private void ensureAdminApplication(String registrationNo, Long merchantId, Long talentId, String mobile, |
| | | String idempotencyKey, Object submittedAt) { |
| | | Integer existing = jdbc.queryForObject(""" |
| | | select count(*) from service_application |
| | | where deleted = 0 and (application_no = ? or idempotency_key = ?) |
| | | """, Integer.class, registrationNo, idempotencyKey); |
| | | if (existing != null && existing > 0) return; |
| | | |
| | | Map<String, Object> card = activeCard(talentId); |
| | | Map<String, Object> merchant = fuelMerchant(merchantId); |
| | | Integer previous = jdbc.queryForObject(""" |
| | | select count(*) from transport_fuel_mobile_registration |
| | | where talent_id = ? and registration_no <> ? and deleted = 0 |
| | | """, Integer.class, talentId, registrationNo); |
| | | String submissionType = previous != null && previous > 0 ? "修改" : "新增"; |
| | | Object effectiveSubmittedAt = submittedAt == null ? java.time.LocalDateTime.now() : submittedAt; |
| | | |
| | | jdbc.update(""" |
| | | insert into service_application( |
| | | application_no, application_type, talent_id, card_id, merchant_id, |
| | | talent_name, talent_mobile, talent_level, card_no, merchant_name, contact_mobile, |
| | | applicant_name, applicant_mobile, alipay_mobile, submission_type, |
| | | submitted_at, status, idempotency_key, deleted |
| | | ) values (?, 'FUEL_SERVICE', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '待处理', ?, 0) |
| | | """, |
| | | registrationNo, |
| | | talentId, |
| | | card.get("cardId"), |
| | | merchantId, |
| | | card.get("talentName"), |
| | | card.get("talentMobile"), |
| | | card.get("talentLevel"), |
| | | card.get("cardNo"), |
| | | merchant.get("merchantName"), |
| | | merchant.get("contactMobile"), |
| | | card.get("talentName"), |
| | | mobile, |
| | | mobile, |
| | | submissionType, |
| | | effectiveSubmittedAt, |
| | | idempotencyKey |
| | | ); |
| | | Long applicationId = jdbc.queryForObject( |
| | | "select id from service_application where idempotency_key = ? and deleted = 0", |
| | | Long.class, |
| | | idempotencyKey |
| | | ); |
| | | jdbc.update(""" |
| | | insert into service_application_operation_log( |
| | | application_id, application_no, operation_type, after_status, |
| | | operator_type, operator_id, operator_name, remark, created_at |
| | | ) values (?, ?, 'SUBMIT', '待处理', 'TALENT', ?, ?, '提交加油服务登记', ?) |
| | | """, applicationId, registrationNo, talentId, card.get("talentName"), effectiveSubmittedAt); |
| | | } |
| | | |
| | | private Map<String, Object> activeCard(Long talentId) { |
| | | List<Map<String, Object>> cards = query(""" |
| | | select c.id as card_id, c.card_no, c.card_level as talent_level, |
| | | u.name as talent_name, u.mobile as talent_mobile |
| | | from talent_card c |
| | | join talent_user u on u.id = c.talent_id and u.deleted = 0 and u.status = 1 |
| | | where c.talent_id = ? and c.deleted = 0 and c.status = 1 |
| | | and (c.valid_start is null or c.valid_start <= current_date) |
| | | and (c.valid_end is null or c.valid_end >= current_date) |
| | | and (c.expire_time is null or c.expire_time > now()) |
| | | order by case when c.card_type = 'PERSONAL' then 0 else 1 end, c.id desc |
| | | limit 1 |
| | | """, talentId); |
| | | if (cards.isEmpty()) throw new BusinessException(ErrorCode.CONFLICT, "未找到有效人才卡"); |
| | | return cards.get(0); |
| | | } |
| | | |
| | | private Map<String, Object> fuelMerchant(Long merchantId) { |
| | |
| | | return value instanceof Number number ? number.intValue() : 0; |
| | | } |
| | | |
| | | private Long longNumber(Object value) { |
| | | return value instanceof Number number ? number.longValue() : null; |
| | | } |
| | | |
| | | private String text(Object value, String fallback) { |
| | | if (value == null) return fallback; |
| | | String text = String.valueOf(value).trim(); |
| | |
| | | @NotNull Long talentId, |
| | | @NotNull Long benefitId, |
| | | Long parkingLotId, |
| | | @NotBlank @Pattern(regexp = "^[\\u4E00-\\u9FFF][A-Z][A-Z0-9]{5}$", message = "请输入正确的车牌号") String plateNumber |
| | | @NotBlank @Pattern(regexp = "^[\\u4E00-\\u9FFF][A-Z][A-Z0-9]{5,6}$", message = "请输入正确的车牌号") String plateNumber |
| | | ) {} |
| | |
| | | @GetMapping("/topics/shopping/merchants") |
| | | public ApiResponse<PageResult<Map<String, Object>>> shoppingMerchants( |
| | | @RequestParam String categoryId, |
| | | @RequestParam(required = false) Long talentId, |
| | | @RequestParam(defaultValue = "1") int page, |
| | | @RequestParam(defaultValue = "4") int size |
| | | ) { |
| | | return ApiResponse.success(portalService.shoppingMerchants(categoryId, page, size)); |
| | | return ApiResponse.success(portalService.shoppingMerchants(categoryId, page, size, talentId)); |
| | | } |
| | | |
| | | @GetMapping("/topics/premium") |
| | |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> shoppingMerchants(String categoryId, int page, int size) { |
| | | return shoppingMerchants(categoryId, page, size, null); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> shoppingMerchants(String categoryId, int page, int size, Long talentId) { |
| | | if (categoryId == null || categoryId.isBlank()) throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择购物服务分类"); |
| | | int categoryCount = jdbcTemplate.queryForObject(""" |
| | | select count(*) from shopping_topic_category |
| | |
| | | args.addAll(merchantTypeCodes); |
| | | PageResult<Map<String, Object>> result = page(""" |
| | | select m.id, m.merchant_name, m.merchant_type_code as merchant_type, m.contact_name, m.contact_mobile, m.logo_url, m.cover_url, m.address, |
| | | m.service_label, m.status, m.sort_no, m.longitude, m.latitude, m.business_status |
| | | m.service_label, m.status, m.sort_no, m.longitude, m.latitude, m.business_status, m.merchant_rights_config |
| | | from merchant m |
| | | left join shopping_topic_merchant mapping on mapping.merchant_id = m.id and mapping.deleted = 0 and mapping.category_key = ? |
| | | """ + merchantWhere + " order by mapping.sort_no asc, m.sort_no asc, m.id asc", |
| | | "select count(*) from merchant m left join shopping_topic_merchant mapping on mapping.merchant_id = m.id and mapping.deleted = 0 and mapping.category_key = ? " + merchantWhere, |
| | | page, size, args.toArray()); |
| | | String cardLevel = activeProductCardLevel(talentId); |
| | | String sourceAreaType = activeTalentSourceAreaType(talentId); |
| | | List<Map<String, Object>> records = result.records().stream().map(row -> { |
| | | Map<String, Object> item = enrichMerchantSearchRow(row); |
| | | item.put("discountText", textOrDefault(item.get("serviceLabel"), "人才专享")); |
| | | List<Map<String, Object>> rights = merchantRightsConfigs(row.get("merchantRightsConfig")); |
| | | Map<String, Object> configuredRight = matchingMerchantRight(rights, cardLevel, sourceAreaType, true); |
| | | Map<String, Object> levelRight = matchingMerchantRight(rights, cardLevel, sourceAreaType, false); |
| | | if (configuredRight != null) { |
| | | item.put("discountText", textOrDefault(configuredRight.get("tag"), textOrDefault(item.get("serviceLabel"), ""))); |
| | | item.put("rightsAvailable", true); |
| | | } else if (levelRight != null || !rights.isEmpty()) { |
| | | item.put("discountText", levelRight == null ? "" : textOrDefault(levelRight.get("tag"), "")); |
| | | item.put("rightsAvailable", false); |
| | | item.put("available", false); |
| | | item.put("unavailableReason", levelRight == null ? "当前人才卡暂未配置该权益" : "当前人才卡暂未开通该权益"); |
| | | } else { |
| | | item.put("discountText", textOrDefault(item.get("serviceLabel"), "")); |
| | | item.put("rightsAvailable", true); |
| | | } |
| | | item.remove("merchantRightsConfig"); |
| | | return item; |
| | | }).toList(); |
| | | return new PageResult<>(records, result.page(), result.size(), result.total()); |
| | |
| | | result.put("institutionName", first(row.get("hospitalName"), row.get("merchantName"), row.get("parkingLot"), "人才服务机构")); |
| | | result.put("appointmentContent", first(row.get("productName"), row.get("packageName"), row.get("applicationType"), "服务预约")); |
| | | result.put("appointmentTime", first(row.get("expectedTime"), "以专员通知为准")); |
| | | result.put("advisor", advisor(string(row.get("cardLevelSnapshot")))); |
| | | result.put("advisor", advisor(talentId, string(row.get("cardLevelSnapshot")))); |
| | | return result; |
| | | } |
| | | |
| | | private Map<String, Object> advisor(String cardLevel) { |
| | | private Map<String, Object> advisor(Long talentId, String cardLevel) { |
| | | List<Map<String, Object>> assignedStaff = jdbc.queryForList(""" |
| | | select operator.display_name, operator.position_name, operator.mobile |
| | | from talent_user talent |
| | | join admin_operator operator on operator.id = talent.service_staff_id |
| | | and operator.deleted = 0 and operator.status = 1 |
| | | where talent.id = ? and talent.deleted = 0 |
| | | limit 1 |
| | | """, talentId).stream().map(DatabaseRows::normalize).toList(); |
| | | if (!assignedStaff.isEmpty()) { |
| | | Map<String, Object> row = assignedStaff.get(0); |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("name", row.get("displayName")); |
| | | result.put("department", row.get("positionName")); |
| | | result.put("mobile", row.get("mobile")); |
| | | result.put("consultUrl", ""); |
| | | return result; |
| | | } |
| | | |
| | | List<Map<String, Object>> rows = jdbc.queryForList(""" |
| | | select advisor_name, department, mobile, consult_url |
| | | from talent_card_advisor where card_level = ? and status = 1 limit 1 |
| | |
| | | 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)); |
| | | } |
| | | } |
| | |
| | | @Service |
| | | public class ShoppingMerchantPortalService { |
| | | private final JdbcTemplate jdbc; |
| | | private final PortalService portalService; |
| | | |
| | | public ShoppingMerchantPortalService(JdbcTemplate jdbc) { |
| | | public ShoppingMerchantPortalService(JdbcTemplate jdbc, PortalService portalService) { |
| | | this.jdbc = jdbc; |
| | | this.portalService = portalService; |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> detail(Long merchantId) { |
| | | return detail(merchantId, null); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> detail(Long merchantId, Long talentId) { |
| | | Map<String, Object> row = one(""" |
| | | select m.id, m.merchant_name, m.cover_url, m.service_label, m.address, m.contact_mobile, m.contact_name, m.rights_text, |
| | | m.longitude, m.latitude, m.status as merchant_status, |
| | | case m.merchant_type_code |
| | | when 'housing' then 'housing' when 'car' then 'car' |
| | | when 'appliance' then 'appliance' when 'digital' then 'digital' |
| | | when 'culture' then 'book' when 'telecom' then 'digital' end as category_key, |
| | | when 'culture' then 'book' when 'telecom' then 'telecom' end as category_key, |
| | | 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, |
| | |
| | | where m.id = ? and m.merchant_type_code in ('shopping', 'housing', 'car', 'appliance', 'digital', 'culture', 'telecom') and m.deleted = 0 |
| | | order by detail_config.id asc limit 1 |
| | | """, merchantId); |
| | | boolean available = number(row.get("merchantStatus")) == 1 && (row.get("detailStatus") == null || number(row.get("detailStatus")) == 1); |
| | | Map<String, Object> configuredMerchant = portalService.merchantDetail(merchantId, talentId); |
| | | String rightsStatus = text(configuredMerchant.get("rightsStatus"), "UNCONFIGURED"); |
| | | boolean rightsAvailable = !"DISABLED".equals(rightsStatus) && !"MISSING".equals(rightsStatus); |
| | | boolean available = number(row.get("merchantStatus")) == 1 |
| | | && (row.get("detailStatus") == null || number(row.get("detailStatus")) == 1) |
| | | && rightsAvailable; |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("merchantId", row.get("id")); |
| | | 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"), "请咨询服务专员")); |
| | | result.put("applicableCards", text(row.get("talentLevels"), "请咨询服务专员")); |
| | | result.put("address", text(row.get("address"), "请咨询服务专员")); |
| | | result.put("discountText", text(row.get("serviceLabel"), "")); |
| | | result.put("applicableCards", text(configuredMerchant.get("rightsTag"), 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"), text(row.get("rightsText"), "以服务专员说明为准"))); |
| | | result.put("benefitDetail", text(configuredMerchant.get("rightsText"), text(row.get("benefitDetail"), text(row.get("rightsText"), "")))); |
| | | result.put("rightsStatus", rightsStatus); |
| | | result.put("available", available); |
| | | result.put("unavailableReason", available ? "" : text(row.get("unavailableReason"), "该购物服务当前不可用")); |
| | | String unavailableReason = switch (rightsStatus) { |
| | | case "DISABLED" -> "当前人才卡暂未开通该权益"; |
| | | case "MISSING" -> "当前人才卡暂未配置该权益"; |
| | | default -> text(row.get("unavailableReason"), "该购物服务当前不可用"); |
| | | }; |
| | | result.put("unavailableReason", available ? "" : unavailableReason); |
| | | Map<String, Object> advisor = new LinkedHashMap<>(); |
| | | advisor.put("advisorId", row.get("advisorId")); |
| | | advisor.put("name", text(row.get("advisorName"), text(row.get("contactName"), ""))); |
| | |
| | | result.put("talentMobile", application.getTalentMobile()); |
| | | result.put("talentLevel", application.getTalentLevel()); |
| | | result.put("cardNo", application.getCardNo()); |
| | | result.put("merchantName", application.getMerchantName()); |
| | | result.put("hospitalName", application.getHospitalName()); |
| | | result.put("contactMobile", application.getContactMobile()); |
| | | result.put("packageName", application.getPackageName()); |
| | | result.put("productName", application.getProductName()); |
| | | result.put("expectedTime", application.getExpectedTime()); |
| | | result.put("submissionType", application.getSubmissionType()); |
| | | result.put("plateNo", application.getPlateNo()); |
| | | result.put("parkingLot", application.getParkingLot()); |
| | | result.put("alipayMobile", application.getAlipayMobile()); |
| | | result.put("serviceName", serviceName(application)); |
| | | result.put("appointmentContent", appointmentContent(application)); |
| | | result.put("appointmentTime", first(application.getExpectedTime(), "以专员通知为准")); |
| New file |
| | |
| | | insert into service_application( |
| | | application_no, application_type, talent_id, card_id, merchant_id, |
| | | talent_name, talent_mobile, talent_level, card_no, merchant_name, contact_mobile, |
| | | applicant_name, applicant_mobile, alipay_mobile, submission_type, |
| | | submitted_at, status, idempotency_key, deleted |
| | | ) |
| | | select r.registration_no, 'FUEL_SERVICE', r.talent_id, c.id, r.merchant_id, |
| | | u.name, u.mobile, c.card_level, c.card_no, m.merchant_name, m.contact_mobile, |
| | | u.name, r.mobile, r.mobile, |
| | | case when exists ( |
| | | select 1 from transport_fuel_mobile_registration previous |
| | | where previous.talent_id = r.talent_id |
| | | and previous.id < r.id |
| | | and previous.deleted = 0 |
| | | ) then '修改' else '新增' end, |
| | | r.created_at, |
| | | case r.status when 'PROCESSED' then '已处理' when 'REJECTED' then '已驳回' else '待处理' end, |
| | | r.idempotency_key, 0 |
| | | from transport_fuel_mobile_registration r |
| | | join talent_user u on u.id = r.talent_id and u.deleted = 0 |
| | | join merchant m on m.id = r.merchant_id and m.deleted = 0 |
| | | left join talent_card c on c.id = ( |
| | | select candidate.id |
| | | from talent_card candidate |
| | | where candidate.talent_id = r.talent_id and candidate.deleted = 0 |
| | | order by case |
| | | when candidate.status = 1 |
| | | and (candidate.valid_start is null or candidate.valid_start <= current_date) |
| | | and (candidate.valid_end is null or candidate.valid_end >= current_date) |
| | | and (candidate.expire_time is null or candidate.expire_time > now()) |
| | | then 0 else 1 end, |
| | | case when candidate.card_type = 'PERSONAL' then 0 else 1 end, |
| | | candidate.id desc |
| | | limit 1 |
| | | ) |
| | | where r.deleted = 0 |
| | | and not exists ( |
| | | select 1 from service_application application |
| | | where application.deleted = 0 |
| | | and (application.application_no = r.registration_no |
| | | or application.idempotency_key = r.idempotency_key) |
| | | ); |
| | | |
| | | insert into service_application_operation_log( |
| | | application_id, application_no, operation_type, after_status, |
| | | operator_type, operator_id, operator_name, remark, created_at |
| | | ) |
| | | select application.id, application.application_no, 'SUBMIT', application.status, |
| | | 'TALENT', application.talent_id, application.talent_name, '提交加油服务登记', application.submitted_at |
| | | from service_application application |
| | | where application.deleted = 0 |
| | | and application.application_type = 'FUEL_SERVICE' |
| | | and not exists ( |
| | | select 1 from service_application_operation_log operation_log |
| | | where operation_log.application_id = application.id |
| | | and operation_log.operation_type = 'SUBMIT' |
| | | ); |
| New file |
| | |
| | | package com.talentcard.server.business.portal; |
| | | |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | import static org.mockito.ArgumentMatchers.anyString; |
| | | import static org.mockito.ArgumentMatchers.eq; |
| | | import static org.mockito.Mockito.when; |
| | | |
| | | 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 ChildrenEntrustmentPortalServiceTests { |
| | | @Mock |
| | | private JdbcTemplate jdbc; |
| | | |
| | | @InjectMocks |
| | | private ChildrenEntrustmentPortalService service; |
| | | |
| | | @Test |
| | | void returnsSchoolDetailFromMerchantConfiguration() { |
| | | Long serviceId = 91L; |
| | | when(jdbc.queryForList(anyString(), eq(serviceId))).thenReturn(List.of(Map.of( |
| | | "id", serviceId, |
| | | "merchant_type_code", "children", |
| | | "usage_instructions", "[{\"name\":\"第一中学\",\"address\":\"学校地址\",\"detail\":\"<p>后台配置的学校详情</p>\"}]"))); |
| | | |
| | | Map<String, Object> detail = service.school(serviceId, "第一中学"); |
| | | |
| | | assertThat(detail) |
| | | .containsEntry("id", "第一中学") |
| | | .containsEntry("name", "第一中学") |
| | | .containsEntry("address", "学校地址") |
| | | .containsEntry("content", "<p>后台配置的学校详情</p>"); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.talentcard.server.business.portal; |
| | | |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | |
| | | import org.junit.jupiter.api.Test; |
| | | |
| | | import jakarta.validation.Validation; |
| | | import jakarta.validation.Validator; |
| | | |
| | | class ParkingPlateRegistrationRequestTests { |
| | | private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); |
| | | |
| | | @Test |
| | | void acceptsStandardAndNewEnergyPlateNumbers() { |
| | | assertThat(validator.validate(request("川A12345"))).isEmpty(); |
| | | assertThat(validator.validate(request("川AD12345"))).isEmpty(); |
| | | } |
| | | |
| | | @Test |
| | | void rejectsPlateSuffixOutsideFiveToSixCharacters() { |
| | | assertThat(validator.validate(request("川A1234"))).isNotEmpty(); |
| | | assertThat(validator.validate(request("川A1234567"))).isNotEmpty(); |
| | | } |
| | | |
| | | private ParkingPlateRegistrationRequest request(String plateNumber) { |
| | | return new ParkingPlateRegistrationRequest(1L, 2L, null, plateNumber); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.talentcard.server.business.portal; |
| | | |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | import static org.mockito.ArgumentMatchers.anyString; |
| | | import static org.mockito.ArgumentMatchers.eq; |
| | | import static org.mockito.Mockito.when; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | import org.junit.jupiter.api.extension.ExtendWith; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.jdbc.core.JdbcTemplate; |
| | | import org.mockito.InjectMocks; |
| | | import org.mockito.Mock; |
| | | import org.mockito.junit.jupiter.MockitoExtension; |
| | | |
| | | @ExtendWith(MockitoExtension.class) |
| | | class ProcessingApplicationPortalServiceTests { |
| | | @Mock |
| | | private JdbcTemplate jdbc; |
| | | |
| | | @InjectMocks |
| | | private ProcessingApplicationPortalService service; |
| | | |
| | | @Test |
| | | void usesServiceStaffAssignedToTalent() { |
| | | Long talentId = 7L; |
| | | String applicationNo = "SE-PROCESSING-1"; |
| | | when(jdbc.queryForList(anyString(), eq(talentId), eq(applicationNo), eq(talentId))).thenReturn(List.of(Map.of( |
| | | "application_no", applicationNo, |
| | | "application_type", "SPOUSE_EMPLOYMENT", |
| | | "status", "待处理", |
| | | "merchant_name", "配偶推荐就业", |
| | | "card_level_snapshot", "卓越人才卡"))); |
| | | when(jdbc.queryForList(anyString(), eq(talentId))).thenReturn(List.of(Map.of( |
| | | "display_name", "后台配置专员", |
| | | "position_name", "人才服务岗", |
| | | "mobile", "13911112222"))); |
| | | |
| | | Map<String, Object> detail = service.detail(applicationNo, talentId); |
| | | |
| | | assertThat(detail.get("advisor")).isInstanceOfSatisfying(Map.class, advisor -> assertThat(advisor) |
| | | .containsEntry("name", "后台配置专员") |
| | | .containsEntry("department", "人才服务岗") |
| | | .containsEntry("mobile", "13911112222")); |
| | | } |
| | | |
| | | @Test |
| | | void usesServiceStaffAssignedToTalentForHotelReservation() { |
| | | Long talentId = 1015L; |
| | | String applicationNo = "SA202608191626042390"; |
| | | when(jdbc.queryForList(anyString(), eq(talentId), eq(applicationNo), eq(talentId))).thenReturn(List.of(Map.of( |
| | | "application_no", applicationNo, |
| | | "application_type", "HOTEL_RESERVATION", |
| | | "status", "待处理", |
| | | "merchant_name", "喜望酒店1", |
| | | "card_level_snapshot", "菁英人才卡"))); |
| | | when(jdbc.queryForList(anyString(), eq(talentId))).thenReturn(List.of(Map.of( |
| | | "display_name", "酒店服务专员", |
| | | "position_name", "人才服务岗", |
| | | "mobile", "13540211212"))); |
| | | |
| | | Map<String, Object> detail = service.detail(applicationNo, talentId); |
| | | |
| | | assertThat(detail.get("advisor")).isInstanceOfSatisfying(Map.class, advisor -> assertThat(advisor) |
| | | .containsEntry("name", "酒店服务专员") |
| | | .containsEntry("department", "人才服务岗") |
| | | .containsEntry("mobile", "13540211212")); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.talentcard.server.business.portal; |
| | | |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | import static org.mockito.ArgumentMatchers.anyString; |
| | | import static org.mockito.ArgumentMatchers.eq; |
| | | import static org.mockito.Mockito.when; |
| | | |
| | | 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 ShoppingMerchantPortalServiceTests { |
| | | @Mock |
| | | private JdbcTemplate jdbc; |
| | | |
| | | @Mock |
| | | private PortalService portalService; |
| | | |
| | | @InjectMocks |
| | | private ShoppingMerchantPortalService service; |
| | | |
| | | @Test |
| | | void usesCardLevelRightTagAndDetailConfiguredForTalent() { |
| | | Long merchantId = 102L; |
| | | Long talentId = 1015L; |
| | | when(jdbc.queryForList(anyString(), eq(merchantId))).thenReturn(List.of(Map.ofEntries( |
| | | Map.entry("id", merchantId), |
| | | Map.entry("merchant_name", "喜望数码服务"), |
| | | Map.entry("merchant_status", 1), |
| | | Map.entry("category_key", "digital"), |
| | | Map.entry("service_label", ""), |
| | | Map.entry("address", "成都市天府新谷"), |
| | | Map.entry("contact_mobile", "13540211247"), |
| | | Map.entry("rights_text", "旧权益说明")))); |
| | | when(portalService.merchantDetail(merchantId, talentId)).thenReturn(Map.of( |
| | | "rightsStatus", "AVAILABLE", |
| | | "rightsTag", "卓越人才卡", |
| | | "rightsText", "后台配置的数码服务权益详情")); |
| | | |
| | | Map<String, Object> detail = service.detail(merchantId, talentId); |
| | | |
| | | assertThat(detail) |
| | | .containsEntry("applicableCards", "卓越人才卡") |
| | | .containsEntry("benefitDetail", "后台配置的数码服务权益详情") |
| | | .containsEntry("discountText", "") |
| | | .containsEntry("address", "成都市天府新谷") |
| | | .containsEntry("available", true); |
| | | } |
| | | } |