| | |
| | | <image class="merchant-image" :src="item.image" mode="aspectFill" @error="replaceImage(item)"></image> |
| | | <view class="benefit-info"> |
| | | <text class="benefit-name">{{item.name}}</text> |
| | | <view class="tag-row"><text class="coupon-tag">券</text><text class="discount-tag">{{item.discount}}</text></view> |
| | | <view v-if="item.hasCoupon || item.discount != ''" class="tag-row"><text v-if="item.hasCoupon" class="coupon-tag">券</text><text v-if="item.discount != ''" :class="item.hasCoupon ? 'discount-tag' : 'discount-tag no-coupon'">{{item.discount}}</text></view> |
| | | <view class="address-row"><image src="/static/benefits-assets/map-pin.svg"></image><text>{{item.address}}</text></view> |
| | | </view> |
| | | </view> |
| | |
| | | import { isApiConfigured } from '../../config/api.uts' |
| | | import { getMerchants } from '../../services/portal.uts' |
| | | import { resolveApiUrl } from '../../services/http.uts' |
| | | import { getSession } from '../../services/session.uts' |
| | | |
| | | const navTop = ref(28) |
| | | const navHeight = ref(32) |
| | |
| | | const navRight = ref(10) |
| | | const isSolidNav = ref(false) |
| | | const fallbackImage = '' |
| | | const talentId = ref('') |
| | | const replaceImage = (item: any) => { if (item.image != '') item.image = '' } |
| | | |
| | | onLoad((options: any) => { |
| | |
| | | failInitialOnce.value = previewState == 'initialError' |
| | | failLoadMoreOnce.value = previewState == 'loadMoreError' |
| | | } |
| | | const session = getSession() |
| | | if (session != null && session.talentId != null) talentId.value = '' + session.talentId |
| | | loadInitialBenefits() |
| | | }) |
| | | |
| | |
| | | const loadMoreError = ref(false) |
| | | const failInitialOnce = ref(false) |
| | | const failLoadMoreOnce = ref(false) |
| | | const formatDiscount = (item: any): string => { |
| | | const sale = item.salePrice != null ? Number(item.salePrice) : 0 |
| | | const original = item.originalPrice != null ? Number(item.originalPrice) : 0 |
| | | if (sale > 0 && original > 0 && sale < original) return '' + Math.round(sale / original * 100) / 10 + '折' |
| | | return '人才专享' |
| | | } |
| | | const mapMerchants = (records: any[]): any[] => { |
| | | const mapped: any[] = [] |
| | | for (let index = 0; index < records.length; index++) { |
| | |
| | | id: '' + item.id, |
| | | name: item.merchantName != null ? item.merchantName as string : '人才权益商户', |
| | | merchantType: merchantType, |
| | | discount: formatDiscount(item), |
| | | hasCoupon: item.couponAvailable === true || Number(item.couponAvailable) == 1, |
| | | 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 |
| | | }) |
| | |
| | | hasLoaded.value = false |
| | | currentPage.value = 0 |
| | | if (isApiConfigured()) { |
| | | getMerchants(1, 4, '', (data: any) => { |
| | | getMerchants(1, 4, '', talentId.value, (data: any) => { |
| | | const records: any[] = data != null && data.records instanceof Array ? data.records as any[] : [] |
| | | visibleBenefits.value = mapMerchants(records) |
| | | loadedBenefitCount.value = records.length |
| | |
| | | loadMoreError.value = false |
| | | loadingMore.value = true |
| | | if (isApiConfigured()) { |
| | | getMerchants(currentPage.value + 1, 4, '', (data: any) => { |
| | | getMerchants(currentPage.value + 1, 4, '', talentId.value, (data: any) => { |
| | | const records: any[] = data != null && data.records instanceof Array ? data.records as any[] : [] |
| | | const next = visibleBenefits.value.slice() |
| | | const mapped = mapMerchants(records) |
| | |
| | | .benefit-name { color: #292929; font-size: 29rpx; line-height: 40rpx; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| | | .tag-row { flex-direction: row; align-items: center; } |
| | | .coupon-tag { padding: 6rpx 8rpx; border-radius: 6rpx; background-color: #FFF0EC; color: #FF5D37; font-size: 21rpx; } |
| | | .discount-tag { margin-left: 11rpx; padding: 6rpx 9rpx; border-radius: 6rpx; background-color: #FFF5E7; color: #FF7C2B; font-size: 21rpx; } |
| | | .discount-tag { margin-left: 11rpx; padding: 6rpx 9rpx; border-radius: 6rpx; background-color: #FFF5E7; color: #FF7C2B; font-size: 21rpx; }.discount-tag.no-coupon { margin-left: 0; } |
| | | .address-row { display: flex; min-width: 0; box-sizing: border-box; padding: 7rpx 10rpx; border-radius: 7rpx; background-image: linear-gradient(90deg, rgba(239,240,242,.98) 0%, rgba(242,243,245,.76) 58%, rgba(255,255,255,0) 100%); flex-direction: row; align-items: center; } |
| | | .address-row image { width: 27rpx; height: 27rpx; margin-right: 9rpx; flex-shrink: 0; } |
| | | .address-row text { flex: 1; color: #7E8490; font-size: 21rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| | |
| | | if (line == '卓越人才卡' || line == '杰出人才卡' || line == '菁英人才卡') return 'condition-line card-heading' |
| | | if (line.indexOf('发放对象:') == 0) return 'condition-line recipient-heading' |
| | | if (/^[一二三四五六七]类人才/.test(line)) return 'condition-line category-heading' |
| | | if (/^\d+\.\s*[^((]+[。.]$/.test(line)) return 'condition-line numbered-heading' |
| | | if (/^\d+\.\s*/.test(line)) return 'condition-line numbered-heading' |
| | | return 'condition-line' |
| | | } |
| | | const goBack = () => safeBack('/pages/card/card') |
| | |
| | | .page { display: flex; width: 100%; height: 100vh; background-color: #FFF9F3; flex-direction: column; } |
| | | .custom-nav { flex-shrink: 0; padding-bottom: 24rpx; background-image: linear-gradient(180deg, #FFE5AB 0%, #FFF4DC 100%); } |
| | | .safe-space { height: 28px; } .nav-row { display: flex; height: 32px; box-sizing: border-box; padding-left: 26rpx; padding-right: 10px; position: relative; align-items: center; justify-content: space-between; } |
| | | .back-button { display: flex; width: 60rpx; height: 60rpx; margin-left: -8rpx; justify-content: center; align-items: center; } .back-button image { width: 38rpx; height: 38rpx; } .back-button-active { opacity: .55; } |
| | | .back-button { display: flex; width: 60rpx; height: 60rpx; position: absolute; left: 18rpx; top: 50%; margin: 0; transform: translateY(-50%); justify-content: center; align-items: center; z-index: 2; } .back-button image { width: 38rpx; height: 38rpx; } .back-button-active { opacity: .55; } |
| | | .nav-title { position: absolute; left: 50%; top: 50%; color: #222326; font-size: 36rpx; font-weight: 700; transform: translate(-50%, -50%); white-space: nowrap; } .capsule-space { width: 88px; height: 100%; flex-shrink: 0; } |
| | | .content { min-height: 0; flex: 1; } .hero { padding: 58rpx 58rpx 42rpx; background-image: linear-gradient(135deg, #F26B38 0%, #FA9534 100%); } .hero-title { display: block; color: #FFFFFF; font-size: 43rpx; font-weight: 700; } .hero-subtitle { display: block; margin-top: 15rpx; color: rgba(255,255,255,.87); font-size: 26rpx; } |
| | | .summary-card, .conditions-card { margin: 30rpx 30rpx 0; padding: 34rpx; border-radius: 22rpx; background-color: #FFFFFF; box-shadow: 0 9rpx 28rpx rgba(116,76,46,.08); } .summary-title, .conditions-title { display: block; color: #24262A; font-size: 31rpx; font-weight: 700; } .summary-text { display: block; margin-top: 20rpx; color: #6F7782; font-size: 28rpx; line-height: 46rpx; } |
| | |
| | | .conditions-content .card-heading:first-child { margin-top: 0; } |
| | | .recipient-heading { margin: 15rpx 0 22rpx; color: #E84B35; font-weight: 700; text-align: center; } |
| | | .category-heading { margin-top: 19rpx; color: #34383E; font-weight: 700; text-align: center; } |
| | | .numbered-heading { margin-top: 19rpx; color: #34383E; font-weight: 700; } |
| | | .numbered-heading { margin-top: 19rpx; color: #222222; font-weight: 700; } |
| | | .content-bottom { height: 52rpx; height: calc(52rpx + env(safe-area-inset-bottom)); } |
| | | </style> |
| | |
| | | </view> |
| | | |
| | | <view class="content-sheet"> |
| | | <view class="title-block"><text class="page-title">{{benefitTitle}}</text><view class="talent-tag"><text>{{applicableCards}}</text></view></view> |
| | | <view class="title-block"><text class="page-title">{{benefitTitle}}</text><view v-if="applicableCards != ''" class="talent-tag"><text>{{applicableCards}}</text></view></view> |
| | | <view class="tab-bar"> |
| | | <view class="tab" :class="activeTab == 0 ? 'active' : ''" @tap="switchTab(0)"> |
| | | <text>使用说明</text><view class="tab-line"></view> |
| | |
| | | <view v-if="schoolLoaded && schools.length > 0" class="school-panel"> |
| | | <view class="school-table-header"><view class="school-head-name"><text>学校名称</text></view><view class="school-head-action"></view></view> |
| | | <view class="school-row" v-for="item in schools" :key="item.id"> |
| | | <view :class="item.detailAvailable ? 'school-name-cell' : 'school-name-cell is-disabled'" hover-class="school-name-cell-active" @tap="openSchoolDetail(item)"><text>{{item.name}}</text></view> |
| | | <view :class="item.detailAvailable ? 'school-name-cell' : 'school-name-cell is-disabled'" hover-class="school-name-cell-active" @tap="openSchoolDetail(item)"><text class="school-name-text">{{item.name}}</text></view> |
| | | <view class="school-actions"> |
| | | <view :class="canNavigate(item) ? 'school-action' : 'school-action is-disabled'" @tap="openLocation(item)"><image src="/static/premium-assets/school-location.svg" mode="aspectFit"></image></view> |
| | | <view v-if="item.phone != ''" class="school-action" @tap="callSchool(item)"><image src="/static/premium-assets/school-phone.svg" mode="aspectFit"></image></view> |
| | |
| | | benefitError.value = false |
| | | getChildrenEntrustmentBenefit(serviceId.value, talentId.value, (data: any) => { |
| | | benefitTitle.value = data.title || '子女托管' |
| | | applicableCards.value = data.applicableCards || '以服务说明为准' |
| | | applicableCards.value = data.applicableCards != null ? data.applicableCards as string : '' |
| | | heroImage.value = data.heroImage != null && data.heroImage != '' ? resolveApiUrl(data.heroImage as string) : '' |
| | | description.value = data.rightsText || '暂无使用说明' |
| | | benefitLoading.value = false |
| | |
| | | .school-head-name text { width: 100%; color: #26374F; font-size: 23rpx; font-weight: 700; text-align: left; } |
| | | .school-row { display: flex; height: 92rpx; min-height: 92rpx; border-top-width: 1rpx; border-top-style: solid; border-top-color: #E7ECF4; flex-direction: row; align-items: center; } |
| | | .school-name-cell { display: flex; min-width: 0; height: 92rpx; flex: 1; box-sizing: border-box; padding: 0 22rpx; align-items: center; justify-content: flex-start; } |
| | | .school-name-cell > text:first-child { min-width:0; flex:1; color:#48566A; font-size:21rpx; line-height:31rpx; text-align:left; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } |
| | | .school-name-text { display:block; width:100%; min-width:0; height:92rpx; flex:1; color:#48566A; font-size:21rpx; line-height:92rpx; text-align:left; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } |
| | | .school-name-cell-active { background-color:#F5F8FD; } |
| | | .school-actions { display: flex; width: 126rpx; height: 92rpx; flex-direction: row; align-items: center; justify-content: space-evenly; flex-shrink: 0; } |
| | | .school-action { display: flex; width: 46rpx; height: 46rpx; box-sizing: border-box; border-width: 2rpx; border-style: solid; border-color: #B7D6FF; border-radius: 50%; background-color: #F1F7FF; justify-content: center; align-items: center; } |
| | |
| | | </view> |
| | | <view v-else class="merchant-title-row"> |
| | | <text class="merchant-title">{{merchantName}}</text> |
| | | <view class="merchant-tags"><text v-if="merchantHasCoupon" class="coupon-tag">券</text><text v-if="merchantDiscount != ''" class="discount-tag">{{merchantDiscount}}</text><text v-if="isHotelMerchant && merchantConsumeDiscount != ''" class="discount-tag">{{merchantConsumeDiscount}}</text></view> |
| | | <view class="merchant-tags"><text v-if="merchantHasCoupon && !isFuelMerchant" class="coupon-tag">券</text><text v-if="merchantDiscount != '' && !isFuelMerchant" class="discount-tag">{{merchantDiscount}}</text><text v-if="isHotelMerchant && merchantConsumeDiscount != ''" class="discount-tag">{{merchantConsumeDiscount}}</text></view> |
| | | </view> |
| | | <view v-if="merchantServiceLabel != '' || applicableCards != ''" class="service-tags"><text v-if="!isParkingMerchant && !isMedicalMerchant && merchantServiceLabel != ''">{{merchantServiceLabel}}</text><text v-if="applicableCards != ''">{{applicableCards}}</text></view> |
| | | <view v-if="(!isFuelMerchant && !isHotelMerchant && merchantServiceLabel != '') || applicableCards != ''" class="service-tags"><text v-if="!isParkingMerchant && !isMedicalMerchant && !isFuelMerchant && !isHotelMerchant && merchantServiceLabel != ''">{{merchantServiceLabel}}</text><text v-if="applicableCards != ''">{{applicableCards}}</text></view> |
| | | <view v-if="!isParkingMerchant && merchantType != 'apartment'" class="merchant-address-row"> |
| | | <image class="address-icon" src="/static/benefits-assets/map-pin.svg"></image> |
| | | <text>{{merchantAddress}}</text> |
| | |
| | | <view v-if="listLoading" class="list-state"><view class="loading-spinner"></view><text>正在加载适用门店</text></view> |
| | | <view v-else-if="listError" class="list-state error-state"><text>适用门店加载失败</text><text class="retry-text" @tap="loadMerchantLists">重新加载</text></view> |
| | | <view v-else-if="listLoaded && applicableStores.length == 0" class="list-state"><text>暂无适用门店</text></view> |
| | | <view v-if="listLoaded && applicableStores.length > 0" class="store-table-header"><view class="store-head-name"><text>适用门店名称</text></view><view class="store-head-address"><text>详细地址</text></view><view class="store-head-action"></view></view> |
| | | <view v-if="listLoaded && applicableStores.length > 0" class="store-table-header"><view class="store-head-name"><text>{{isParkingMerchant ? '停车场名称' : '适用门店名称'}}</text></view><view class="store-head-address"><text>详细地址</text></view><view class="store-head-action"></view></view> |
| | | <view class="store-row" v-for="item in applicableStores" :key="item.id"> |
| | | <view class="store-name-cell"><text>{{item.name}}</text></view> |
| | | <view class="store-address-cell"><text>{{item.address}}</text></view> |
| | | <view class="store-name-cell"><view class="store-cell-content"><text>{{item.name}}</text></view></view> |
| | | <view class="store-address-cell"><view class="store-cell-content"><text>{{item.address}}</text></view></view> |
| | | <view :class="item.canNavigate === false ? 'store-location is-disabled' : 'store-location'" @tap="openStoreLocation(item)"><image src="/static/merchant-assets/store-pin-blue.svg" mode="aspectFit"></image></view> |
| | | </view> |
| | | </view> |
| | |
| | | .store-head-action { width: 72rpx; height: 67rpx; flex-shrink: 0; } |
| | | .store-head-name text, .store-head-address text { width: 100%; color: #26374F; font-size: 23rpx; font-weight: 700; text-align: left; } |
| | | .store-row { display: flex; min-height: 92rpx; border-top-width: 1rpx; border-top-style: solid; border-top-color: #E7ECF4; flex-direction: row; align-items: stretch; } |
| | | .store-name-cell { display: flex; width: 250rpx; box-sizing: border-box; padding: 17rpx 13rpx; border-right-width: 1rpx; border-right-style: solid; border-right-color: #E7ECF4; align-items: center; justify-content: flex-start; } |
| | | .store-address-cell { display: flex; min-width: 0; flex: 1; box-sizing: border-box; padding: 17rpx 13rpx; align-items: center; justify-content: flex-start; } |
| | | .store-name-cell text, .store-address-cell text { width: 100%; color: #48566A; font-size: 21rpx; line-height: 31rpx; text-align: left; } |
| | | .store-name-cell { display: flex; width: 250rpx; min-height: 92rpx; box-sizing: border-box; border-right-width: 1rpx; border-right-style: solid; border-right-color: #E7ECF4; align-items: stretch; justify-content: flex-start; } |
| | | .store-address-cell { display: flex; min-width: 0; min-height: 92rpx; flex: 1; box-sizing: border-box; align-items: stretch; justify-content: flex-start; } |
| | | .store-cell-content { display: flex; width: 100%; min-width: 0; min-height: 92rpx; box-sizing: border-box; padding: 13rpx; flex-direction: row; align-items: center; } |
| | | .store-cell-content text { width: 100%; color: #48566A; font-size: 21rpx; line-height: 31rpx; text-align: left; } |
| | | .store-location { display: flex; width: 72rpx; flex-shrink: 0; justify-content: center; align-items: center; } |
| | | .store-location image { width: 34rpx; height: 34rpx; box-sizing: border-box; padding: 4rpx; border-width: 2rpx; border-style: solid; border-color: #8BB9FF; border-radius: 50%; background-color: #EEF6FF; } |
| | | .store-location.is-disabled { opacity: .4; } |
| | |
| | | <view class="hero"><image class="hero-image" :src="activeScene.image" mode="aspectFill" @error="replaceImage(activeScene)"></image><view class="hero-top-shade"></view><view class="hero-bottom-shade"></view><text class="scene-name">{{activeScene.name}}</text><scroll-view class="scene-scroll" scroll-x :show-scrollbar="false" scroll-with-animation :scroll-left="sceneScrollLeft" @scrollend="handleSceneScrollEnd"><view class="scene-row" :style="'width:' + sceneTrackWidth + 'rpx'"><view class="scene-pad"></view><view v-for="(item, index) in scenes" :key="item.id" class="scene-item" :class="index == activeIndex ? 'active' : ''" @tap="selectScene(index)"><image :src="item.image" mode="aspectFill" @error="replaceImage(item)"></image></view><view class="scene-pad"></view></view></scroll-view></view> |
| | | <view v-if="topicError" class="topic-error"><text>专题配置加载失败</text><text @tap="loadPremiumTopic">重新加载</text></view> |
| | | <view v-if="topicDescription != ''" class="topic-summary"><view class="intro-card"><text>{{topicDescription}}</text></view></view> |
| | | <view class="service-section"><scroll-view class="tab-scroll" scroll-x :show-scrollbar="false"><view class="service-tabs" :style="'width:' + Math.max(750, serviceTabs.length * 190) + 'rpx'"><view v-for="tab in serviceTabs" :key="tab.id" class="service-tab" :class="tab.id == activeTab ? 'active' : ''" @tap="selectTab(tab.id)"><text>{{tab.name}}</text><view class="tab-line"></view></view></view></scroll-view> |
| | | <view class="service-section"><scroll-view class="tab-scroll" scroll-x :show-scrollbar="false"><view class="service-tabs" :style="'width:' + Math.max(750, serviceTabs.length * 234) + 'rpx'"><view v-for="tab in serviceTabs" :key="tab.id" class="service-tab" :class="tab.id == activeTab ? 'active' : ''" @tap="selectTab(tab.id)"><text>{{tab.name}}</text><view class="tab-line"></view></view></view></scroll-view> |
| | | <view v-if="initialLoading" class="service-empty"><view class="loading-spinner"></view><text>正在加载臻享服务</text></view><view v-else-if="initialError" class="service-empty error-state"><text>臻享服务加载失败</text><text class="retry-text" @tap="retryInitialLoad">重新加载</text></view><view v-else-if="hasLoaded && visibleServices.length == 0" class="service-empty"><text>暂无相关臻享服务</text></view><view v-else-if="hasLoaded" class="service-list"><view v-for="item in visibleServices" :key="item.id" class="service-item" :class="item.available ? '' : 'is-unavailable'" @tap="openService(item)"><image class="service-image" :src="item.image" mode="aspectFill" @error="replaceImage(item)"></image><view class="service-info"><text class="service-name">{{item.name}}</text><text class="service-benefit">{{item.available ? item.benefit : '已停用'}}</text><view class="address-row"><image src="/static/benefits-assets/map-pin.svg" mode="aspectFit"></image><text>{{item.address}}</text></view></view></view></view> |
| | | <view v-if="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> |
| | |
| | | const loadServices = (append: boolean) => { |
| | | if (!isApiConfigured() || activeTab.value == '') { initialLoading.value = false; loadingMore.value = false; initialError.value = true; return }; const requestVersion = listRequestVersion.value |
| | | if (!append && failInitialOnce.value) { failInitialOnce.value = false; initialLoading.value = false; initialError.value = true; return }; if (append && failLoadMoreOnce.value) { failLoadMoreOnce.value = false; loadingMore.value = false; loadMoreError.value = true; return } |
| | | getPremiumTopicServices(activeTab.value, currentPage.value + 1, 4, (data: any) => { if (requestVersion != listRequestVersion.value) return; const records: any[] = data != null && data.records instanceof Array ? data.records as any[] : []; const next = append ? visibleServices.value.slice() : []; for (let index = 0; index < records.length; index++) { const item = records[index]; next.push({ id: '' + item.id, businessType: item.businessType != null ? item.businessType as string : 'MERCHANT', merchantType: item.merchantType != null ? item.merchantType as string : 'service', name: item.merchantName != null ? item.merchantName as string : '臻享服务', benefit: activeTab.value == '免费体检' || activeTab.value == '配偶推荐就业' ? '免费' : (item.benefitText != null ? item.benefitText as string : '人才专享'), address: item.address != null ? item.address as string : '', image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage, available: item.available !== false }) }; visibleServices.value = next; currentPage.value++; hasMore.value = data != null && data.total != null ? next.length < data.total : false; initialLoading.value = false; loadingMore.value = false; hasLoaded.value = true }, () => { if (requestVersion != listRequestVersion.value) return; initialLoading.value = false; loadingMore.value = false; if (append) loadMoreError.value = true; else initialError.value = true }) |
| | | getPremiumTopicServices(activeTab.value, currentPage.value + 1, 4, (data: any) => { if (requestVersion != listRequestVersion.value) return; const records: any[] = data != null && data.records instanceof Array ? data.records as any[] : []; const next = append ? visibleServices.value.slice() : []; for (let index = 0; index < records.length; index++) { const item = records[index]; next.push({ id: '' + item.id, businessType: item.businessType != null ? item.businessType as string : 'MERCHANT', merchantType: item.merchantType != null ? item.merchantType as string : 'service', name: item.merchantName != null ? item.merchantName as string : '臻享服务', benefit: activeTab.value == '免费体检' || activeTab.value == '配偶推荐就业' || item.businessType == 'ADMIN_APPROVAL' ? '免费' : (item.benefitText != null ? item.benefitText as string : '人才专享'), address: item.address != null ? item.address as string : '', image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage, available: item.available !== false }) }; visibleServices.value = next; currentPage.value++; hasMore.value = data != null && data.total != null ? next.length < data.total : false; initialLoading.value = false; loadingMore.value = false; hasLoaded.value = true }, () => { if (requestVersion != listRequestVersion.value) return; initialLoading.value = false; loadingMore.value = false; if (append) loadMoreError.value = true; else initialError.value = true }) |
| | | } |
| | | const loadMore = () => { if (initialLoading.value || loadingMore.value || !hasMore.value) return; loadMoreError.value = false; loadingMore.value = true; loadServices(true) } |
| | | const goBack = () => safeBack('/pages/benefits/benefits') |
| | |
| | | <style> |
| | | @font-face { font-family:'FoodCalligraphy'; src:url('/static/fonts/food-calligraphy-subset.ttf') format('truetype'); }.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:#7296B5; }.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,.48),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,.40)); }.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:48rpx 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:148rpx; margin-right:42rpx; flex-direction:column; align-items:center; flex-shrink:0; }.service-tab text { color:#8A8D92; font-size:28rpx; 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; } |
| | | .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:48rpx 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:210rpx; margin-right:24rpx; flex-direction:column; align-items:center; flex-shrink:0; }.service-tab text { color:#8A8D92; font-size:28rpx; font-weight:600; white-space:nowrap; }.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-benefit { 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; max-height:62rpx; overflow:hidden; color:#7E8490; font-size:21rpx; line-height:31rpx; }.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:premium-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 premium-loading { from { transform:rotate(0deg); } to { transform:rotate(360deg); } } |
| | | </style> |
| | |
| | | |
| | | <view v-if="detailText.length > 0" class="detail-card"> |
| | | <text class="card-title">商品详情</text> |
| | | <text class="detail-description">{{detailText}}</text> |
| | | <rich-text class="detail-description" :nodes="detailText"></rich-text> |
| | | </view> |
| | | |
| | | <view class="notice-card"> |
| | |
| | | <script setup lang="uts"> |
| | | import { isApiConfigured } from '../../config/api.uts' |
| | | import { getProductDetail } from '../../services/portal.uts' |
| | | import { sanitizeRichText } from '../../services/business.uts' |
| | | import { resolveApiUrl } from '../../services/http.uts' |
| | | import { getSession } from '../../services/session.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | |
| | | const productAvailable = ref(true) |
| | | const productSoldOut = ref(false) |
| | | |
| | | // 后台分别返回图片数组与文字结构,不使用富文本 HTML。 |
| | | // Sanitize and render backend-configured product details as rich text. |
| | | const detailText = ref('') |
| | | |
| | | // 购买须知固定只有四项,后台只返回四项各自的内容文本。 |
| | |
| | | productAvailable.value = data == null || data.available !== false |
| | | productSoldOut.value = data != null && (data.soldOut === true || Number(data.stock) == 0) |
| | | const rawDetailText = data != null && data.detailText != null ? data.detailText as string : '' |
| | | detailText.value = stripDetailMarkup(rawDetailText) |
| | | detailText.value = sanitizeRichText(rawDetailText) |
| | | purchaseNotices.value = buildPurchaseNotices(data != null ? data.purchaseNotice : null) |
| | | }, () => {}) |
| | | } |
| | |
| | | return next |
| | | } |
| | | |
| | | const stripDetailMarkup = (value: string): string => { |
| | | if (value == '') return '' |
| | | let text = value.replace(/<img[^>]*>/gi, '') |
| | | text = text.replace(/<br\s*\/?>/gi, '\n').replace(/<\/p>/gi, '\n').replace(/<[^>]+>/g, '') |
| | | return text.replace(/ /gi, ' ').replace(/&/gi, '&').replace(/</gi, '<').replace(/>/gi, '>').trim() |
| | | } |
| | | const goBack = () => safeBack('/pages/benefits/benefits') |
| | | const handleScroll = (event: any) => { |
| | | const shouldShow = event.detail.scrollTop > 64 |
| | |
| | | <view v-if="topicError" class="topic-error"><text>专题配置加载失败</text><text @tap="loadShoppingTopic">重新加载</text></view> |
| | | <view v-if="topicDescription != ''" class="topic-summary"><view class="intro-card"><text>{{topicDescription}}</text></view></view> |
| | | <view class="service-section"> |
| | | <scroll-view class="tab-scroll" scroll-x :show-scrollbar="false"><view class="service-tabs" :style="'width:' + Math.max(750, serviceTabs.length * 186) + 'rpx'"><view v-for="tab in serviceTabs" :key="tab.id" class="service-tab" :class="tab.id == activeTab ? 'active' : ''" @tap="selectTab(tab.id)"><text>{{tab.name}}</text><view class="tab-line"></view></view></view></scroll-view> |
| | | <scroll-view class="tab-scroll" scroll-x :show-scrollbar="false"><view class="service-tabs" :style="'width:' + Math.max(750, serviceTabs.length * 210) + 'rpx'"><view v-for="tab in serviceTabs" :key="tab.id" class="service-tab" :class="tab.id == activeTab ? 'active' : ''" @tap="selectTab(tab.id)"><text>{{tab.name}}</text><view class="tab-line"></view></view></view></scroll-view> |
| | | <view v-if="initialLoading" class="service-empty"><view class="loading-spinner"></view><text>正在加载购物服务</text></view> |
| | | <view v-else-if="initialError" class="service-empty error-state"><text>购物服务加载失败</text><text class="retry-text" @tap="retryInitialLoad">重新加载</text></view> |
| | | <view v-else-if="hasLoaded && visibleServices.length == 0" class="service-empty"><text>暂无相关购物服务</text></view> |
| | |
| | | @font-face { font-family:'FoodCalligraphy'; src:url('/static/fonts/food-calligraphy-subset.ttf') format('truetype'); } |
| | | .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; } |
| | | .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:180rpx; margin-right:30rpx; flex-direction:column; align-items:center; flex-shrink:0; }.service-tab text { color:#8A8D92; font-size:29rpx; line-height:40rpx; font-weight:600; white-space:nowrap; }.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; }.service-tags { display:flex; min-height:38rpx; flex-direction:row; align-items:center; }.service-tags text { margin-right:9rpx; padding:5rpx 9rpx; border-radius:5rpx; font-size:21rpx; line-height:28rpx; white-space:nowrap; }.coupon-tag { background-color:#FFF0EC; color:#FF5D37; }.service-tag { background-color:#FFF5E7; color:#FF7C2B; }.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> |
| | |
| | | <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 :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><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><view class="picker-arrow"></view></view></picker> |
| | | <text class="field-label">毕业院校及专业</text><view class="input-box"><input v-model="form.schoolMajor" maxlength="255" placeholder="请输入毕业院校及专业" placeholder-class="input-placeholder" /></view> |
| | | <text class="field-label">现工作单位及职务</text><view class="input-box"><input v-model="form.workUnit" maxlength="255" placeholder="请输入现工作单位及职务" placeholder-class="input-placeholder" /></view> |
| | | <text class="field-label">是否为机关事业单位在编人员</text><picker class="field-picker" :range="institutionOptions" :value="institutionIndex" @change="changeInstitution"><view class="picker-box"><text :class="form.inInstitution == '' ? 'picker-placeholder' : ''">{{form.inInstitution == '' ? '请选择' : form.inInstitution}}</text><text class="picker-arrow">⌄</text></view></picker> |
| | | <text class="field-label">是否为机关事业单位在编人员</text><picker class="field-picker" :range="institutionOptions" :value="institutionIndex" @change="changeInstitution"><view class="picker-box"><text :class="form.inInstitution == '' ? 'picker-placeholder' : ''">{{form.inInstitution == '' ? '请选择' : form.inInstitution}}</text><view class="picker-arrow"></view></view></picker> |
| | | <text class="field-label">工作经历</text><view class="textarea-box"><textarea v-model="form.workExperience" maxlength="4000" placeholder="请输入工作经历" placeholder-class="input-placeholder"></textarea></view> |
| | | <view :class="isSubmitting ? 'submit-button submit-button-disabled' : 'submit-button'" hover-class="submit-button-active" @tap="submitForm"><text>{{isSubmitting ? '提交中' : '提交配偶信息'}}</text></view><view class="safe-bottom"></view> |
| | | </view> |
| | |
| | | </script> |
| | | |
| | | <style> |
| | | .page { display:flex; width:100%; height:100vh; background-color:#F4F7FB; background-image:linear-gradient(180deg,#FFF0D7 0%,#FFF2DE 250rpx,#F4F7FB 510rpx); flex-direction:column; }.page-header { position:relative; z-index:10; flex-shrink:0; }.safe-space { height:28px; }.nav-row { display:flex; box-sizing:border-box; padding-left:20rpx; position:relative; flex-direction:row; align-items:center; justify-content:space-between; }.back-button { display:flex; width:70rpx; height:64rpx; margin-left:-4rpx; justify-content:center; align-items:center; }.back-button image { width:36rpx; height:36rpx; }.nav-title { position:absolute; left:50%; top:50%; color:#171B22; font-size:37rpx; line-height:48rpx; font-weight:700; transform:translate(-50%,-50%); white-space:nowrap; }.capsule-space { height:100%; flex-shrink:0; }.form-scroll { min-height:0; flex:1; }.form-content { padding:55rpx 34rpx 0; }.field-label { display:block; margin-left:5rpx; color:#74808F; font-size:25rpx; line-height:36rpx; font-weight:600; }.field-label:not(:first-child) { margin-top:44rpx; }.input-box,.picker-box { display:flex; height:112rpx; margin-top:14rpx; box-sizing:border-box; padding:0 25rpx; border-radius:28rpx; background-color:#FFFFFF; flex-direction:row; align-items:center; }.input-box input { width:100%; color:#4E5867; font-size:28rpx; }.input-placeholder,.picker-placeholder { color:#C1C8D1; }.field-picker { display:block; }.picker-box { color:#4E5867; font-size:28rpx; justify-content:space-between; }.picker-arrow { color:#8793A3; font-size:39rpx; }.textarea-box { height:272rpx; margin-top:14rpx; box-sizing:border-box; padding:22rpx 25rpx; border-radius:28rpx; background-color:#FFFFFF; }.textarea-box textarea { width:100%; height:100%; color:#4E5867; font-size:28rpx; line-height:42rpx; }.submit-button { display:flex; height:100rpx; margin:45rpx 4rpx 0; border-radius:51rpx; background-image:linear-gradient(90deg,#FF4E35,#FF8318); justify-content:center; align-items:center; }.submit-button-disabled { opacity:.62; }.submit-button text { color:#FFFFFF; font-size:29rpx; font-weight:700; }.submit-button-active { opacity:.8; transform:scale(.99); }.state-box { display:flex; min-height:400rpx; color:#8D96A5; font-size:28rpx; flex-direction:column; align-items:center; justify-content:center; }.retry { margin-top:24rpx; color:#FF7A27; }.safe-bottom { height:70rpx; height:calc(70rpx + env(safe-area-inset-bottom)); } |
| | | .page { display:flex; width:100%; height:100vh; background-color:#F4F7FB; background-image:linear-gradient(180deg,#FFF0D7 0%,#FFF2DE 250rpx,#F4F7FB 510rpx); flex-direction:column; }.page-header { position:relative; z-index:10; flex-shrink:0; }.safe-space { height:28px; }.nav-row { display:flex; box-sizing:border-box; padding-left:20rpx; position:relative; flex-direction:row; align-items:center; justify-content:space-between; }.back-button { display:flex; width:70rpx; height:64rpx; margin-left:-4rpx; justify-content:center; align-items:center; }.back-button image { width:36rpx; height:36rpx; }.nav-title { position:absolute; left:50%; top:50%; color:#171B22; font-size:37rpx; line-height:48rpx; font-weight:700; transform:translate(-50%,-50%); white-space:nowrap; }.capsule-space { height:100%; flex-shrink:0; }.form-scroll { min-height:0; flex:1; }.form-content { padding:55rpx 34rpx 0; }.field-label { display:block; margin-left:5rpx; color:#74808F; font-size:25rpx; line-height:36rpx; font-weight:600; }.field-label:not(:first-child) { margin-top:44rpx; }.input-box,.picker-box { display:flex; height:112rpx; margin-top:14rpx; box-sizing:border-box; padding:0 25rpx; border-radius:28rpx; background-color:#FFFFFF; flex-direction:row; align-items:center; }.input-box input { width:100%; color:#4E5867; font-size:28rpx; }.input-placeholder,.picker-placeholder { color:#C1C8D1; }.field-picker { display:block; }.picker-box { color:#4E5867; font-size:28rpx; justify-content:space-between; }.picker-arrow { width:14rpx; height:14rpx; margin-right:5rpx; border-right:3rpx solid #8E9AAA; border-bottom:3rpx solid #8E9AAA; flex-shrink:0; transform:rotate(45deg) translateY(-3rpx); }.textarea-box { height:272rpx; margin-top:14rpx; box-sizing:border-box; padding:22rpx 25rpx; border-radius:28rpx; background-color:#FFFFFF; }.textarea-box textarea { width:100%; height:100%; color:#4E5867; font-size:28rpx; line-height:42rpx; }.submit-button { display:flex; height:100rpx; margin:45rpx 4rpx 0; border-radius:51rpx; background-image:linear-gradient(90deg,#FF4E35,#FF8318); justify-content:center; align-items:center; }.submit-button-disabled { opacity:.62; }.submit-button text { color:#FFFFFF; font-size:29rpx; font-weight:700; }.submit-button-active { opacity:.8; transform:scale(.99); }.state-box { display:flex; min-height:400rpx; color:#8D96A5; font-size:28rpx; flex-direction:column; align-items:center; justify-content:center; }.retry { margin-top:24rpx; color:#FF7A27; }.safe-bottom { height:70rpx; height:calc(70rpx + env(safe-area-inset-bottom)); } |
| | | </style> |
| | |
| | | return request<any>({ path: getApiEndpoint('appPolicyDetail') + encodeURIComponent(policyId), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | } |
| | | |
| | | export const getMerchants = (page: number, size: number, keyword: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => { |
| | | return request<any>({ path: getApiEndpoint('appMerchants'), method: 'GET', skipAuth: true, data: { page: page, size: size, keyword: keyword } }, onSuccess, onError) |
| | | export const getMerchants = (page: number, size: number, keyword: string, talentId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => { |
| | | const data: any = { page: page, size: size, keyword: keyword } |
| | | if (talentId != '') data.talentId = Number(talentId) |
| | | return request<any>({ path: getApiEndpoint('appMerchants'), method: 'GET', skipAuth: true, data: data }, onSuccess, onError) |
| | | } |
| | | |
| | | export const getFoodTopic = (onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => { |
| | |
| | | <template> |
| | | <view class="page"><view class="nav"><view class="nav-back" @tap="goBack"><image class="back-icon" src="/static/icons/lucide/back-left.svg" mode="aspectFit"></image></view><text class="nav-title">选择银行卡</text><view class="nav-right-placeholder"></view></view><view class="add" @tap="addCard">添加银行卡</view><scroll-view class="scroll" scroll-y="true"><view v-if="loading" class="state">正在加载银行卡</view><view v-else-if="cards.length == 0" class="state">暂无银行卡</view><view v-for="card in cards" :key="card.id" class="card" @tap="selectCard(card)"><view class="row"><text class="name">{{card.bankName}}</text><text :class="'status ' + card.status">{{statusText(card.status)}}</text></view><text class="number">{{card.cardMasked}}</text><text class="branch">{{card.branchName}}</text><text v-if="card.reason.length > 0" class="reason">{{card.reason}}</text></view></scroll-view></view> |
| | | <view class="page"> |
| | | <view class="nav" :style="'height:' + navBarHeight + 'px'"> |
| | | <view class="nav-back" :style="'top:' + navTop + 'px;height:' + navHeight + 'px'" @tap="goBack"><image class="back-icon" src="/static/icons/lucide/back-left.svg" mode="aspectFit"></image></view> |
| | | <text class="nav-title" :style="'top:' + navTop + 'px;height:' + navHeight + 'px'">选择银行卡</text> |
| | | </view> |
| | | <view class="add" @tap="addCard">添加银行卡</view> |
| | | <scroll-view class="scroll" scroll-y="true"><view v-if="loading" class="state">正在加载银行卡</view><view v-else-if="cards.length == 0" class="state">暂无银行卡</view><view v-for="card in cards" :key="card.id" class="card" @tap="selectCard(card)"><view class="row"><text class="name">{{card.bankName}}</text><text :class="'status ' + card.status">{{statusText(card.status)}}</text></view><text class="number">{{card.cardMasked}}</text><text class="branch">{{card.branchName}}</text><text v-if="card.reason.length > 0" class="reason">{{card.reason}}</text></view></scroll-view> |
| | | </view> |
| | | </template> |
| | | <script setup lang="uts"> |
| | | import { loadBankCards, SettlementBankCard } from '../../services/settlement.uts' |
| | | const storageKey = 'merchant-withdrawal-selected-bank-id' |
| | | const cards = ref<Array<SettlementBankCard>>([]); const loading = ref(true) |
| | | const navTop = ref(28); const navHeight = ref(32); const navBarHeight = ref(76) |
| | | const goBack = () => uni.navigateBack() |
| | | const addCard = () => uni.navigateTo({ url: '/pages/bank-card-add/bank-card-add' }) |
| | | const statusText = (value: string): string => value == 'approved' ? '已通过' : value == 'rejected' ? '未通过' : '审核中' |
| | | const selectCard = (card: SettlementBankCard) => { if (card.status != 'approved') { uni.showToast({ title: card.reason.length > 0 ? card.reason : '该银行卡尚未审核通过', icon: 'none' }); return }; uni.setStorageSync(storageKey, card.id); uni.navigateBack() } |
| | | const load = () => { loading.value = true; loadBankCards((data) => { cards.value = data; loading.value = false }, (error: any) => { loading.value = false; uni.showToast({ title: error.message || '银行卡加载失败', icon: 'none' }) }) } |
| | | onLoad(() => { |
| | | // Align every custom navigation element to the native capsule's center line. |
| | | // #ifdef MP-WEIXIN |
| | | const capsule = uni.getMenuButtonBoundingClientRect() |
| | | navTop.value = capsule.top |
| | | navHeight.value = capsule.height |
| | | navBarHeight.value = capsule.top + capsule.height + 16 |
| | | // #endif |
| | | }) |
| | | onShow(() => load()) |
| | | </script> |
| | | <style>.page{height:100vh;background:#f4f6f9}.nav{height:174rpx;padding:68rpx 38rpx 0;display:flex;position:relative;justify-content:center;align-items:center;box-sizing:border-box;color:#202b3e}.nav-back{display:flex;width:64rpx;height:64rpx;position:absolute;left:38rpx;top:89rpx;align-items:center;justify-content:center;z-index:2}.back-icon{width:38rpx;height:38rpx}.nav-title{width:100%;font-size:34rpx;line-height:48rpx;text-align:center;font-weight:700}.nav-right-placeholder{width:64rpx;height:64rpx;position:absolute;right:38rpx;top:89rpx}.add{height:88rpx;margin:0 34rpx 28rpx;border-radius:44rpx;background:#ff7625;color:#fff;text-align:center;line-height:88rpx;font-weight:700}.scroll{height:calc(100vh - 290rpx);padding:0 34rpx;box-sizing:border-box}.card{padding:28rpx;margin-bottom:22rpx;background:#fff;border-radius:28rpx}.row{display:flex;justify-content:space-between}.name{font-size:30rpx;font-weight:700;color:#2c3748}.number{display:block;margin-top:20rpx;font-size:28rpx;color:#3a4556}.branch,.reason{display:block;margin-top:10rpx;color:#8993a1;font-size:23rpx}.reason{color:#e95b5b}.status{font-size:23rpx}.approved{color:#249765}.pending{color:#e58b28}.rejected{color:#e95b5b}.state{padding:150rpx 0;text-align:center;color:#8993a1}</style> |
| | | <style> |
| | | /* Match the shared merchant navigation: one-line title centered on the capsule. */ |
| | | .nav-title { |
| | | position: absolute; |
| | | left: 112rpx; |
| | | right: 112rpx; |
| | | width: auto; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | line-height: 1; |
| | | white-space: nowrap; |
| | | } |
| | | .nav-back { |
| | | left: 16px; |
| | | width: 48px; |
| | | justify-content: flex-start; |
| | | } |
| | | .back-icon { width: 44rpx; height: 44rpx; transform: translateY(5px); } |
| | | .nav-right-placeholder { display: none; } |
| | | </style> |
| | |
| | | .hero { height: 382rpx; position: relative; overflow: hidden; } |
| | | .hero-image { position: absolute; top: 0; left: 0; width: 100%; height: 382rpx; } |
| | | .hero-mask { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-image: linear-gradient(180deg, rgba(27, 25, 23, 0.22) 0%, rgba(27, 25, 23, 0.58) 100%); } |
| | | .hero-nav { display: flex; height: 154rpx; padding: 82rpx 38rpx 0; position: relative; box-sizing: border-box; } |
| | | .hero-back { display: flex; width: 58rpx; height: 58rpx; align-items: center; justify-content: center; } |
| | | .hero-nav { height: 154rpx; position: absolute; top: 0; left: 0; right: 0; z-index: 3; box-sizing: border-box; } |
| | | .hero-back { display: flex; position: absolute; width: 58rpx; height: 58rpx; align-items: center; justify-content: center; } |
| | | .hero-back-icon { width: 34rpx; height: 34rpx; } |
| | | .hero-title-wrap { display: flex; flex-direction: column; align-items: center; position: relative; margin-top: 50rpx; } |
| | | .hero-title-wrap { display: flex; flex-direction: column; align-items: center; position: absolute; top: 204rpx; left: 0; right: 0; z-index: 2; } |
| | | .hero-title { color: #FFFFFF; font-size: 42rpx; line-height: 59rpx; font-weight: 700; } |
| | | .hero-subtitle { color: rgba(255, 255, 255, 0.90); font-size: 25rpx; line-height: 35rpx; margin-top: 4rpx; } |
| | | |
| | |
| | | <text class="hours-text" @tap="showEditSheet(index)">{{item.start}}-{{item.end}}</text> |
| | | <view class="period-tag"><text>时段 {{index + 1}}</text></view> |
| | | </view> |
| | | <view class="add-hour" @tap="showAddSheet"><text class="add-plus">+</text><text>添加营业时间段</text></view> |
| | | <view class="add-hour" @tap="showAddSheet"><text class="add-plus">+</text><text class="add-hour-label">添加营业时间段</text></view> |
| | | </view> |
| | | </view> |
| | | <view class="save-button" :class="saving ? 'save-button-disabled' : ''" @tap="saveHours"><text>{{saving ? '保存中' : '保存'}}</text></view> |
| | |
| | | .minus-button { display: flex; width: 34rpx; height: 34rpx; margin-right: 20rpx; justify-content: center; align-items: center; border-radius: 17rpx; color: #FFFFFF; font-size: 27rpx; line-height: 32rpx; font-weight: 700; background-color: #1D2939; } |
| | | .hours-text { flex: 1; color: #202B3E; font-size: 30rpx; line-height: 42rpx; font-weight: 700; } |
| | | .period-tag { padding: 9rpx 17rpx; border-radius: 23rpx; color: #657184; font-size: 22rpx; line-height: 31rpx; background-color: #FFFFFF; } |
| | | .add-hour { display: flex; height: 72rpx; padding: 0 24rpx; margin: 26rpx auto 0; justify-content: center; align-items: center; border-width: 1rpx; border-style: solid; border-color: #FFD1BD; border-radius: 36rpx; color: #FF5A42; font-size: 25rpx; line-height: 35rpx; font-weight: 600; background-color: #FFFCF9; box-sizing: border-box; } |
| | | .add-plus { font-size: 31rpx; line-height: 35rpx; margin-right: 8rpx; font-weight: 400; } |
| | | .add-hour { display: flex; height: 72rpx; padding: 0 24rpx; margin: 26rpx auto 0; flex-direction: row; flex-wrap: nowrap; justify-content: center; align-items: center; border-width: 1rpx; border-style: solid; border-color: #FFD1BD; border-radius: 36rpx; color: #FF5A42; font-size: 25rpx; line-height: 35rpx; font-weight: 600; background-color: #FFFCF9; box-sizing: border-box; } |
| | | .add-plus { display: flex; width: 31rpx; height: 35rpx; margin-right: 8rpx; flex-shrink: 0; align-items: center; justify-content: center; font-size: 31rpx; line-height: 35rpx; font-weight: 400; } |
| | | .add-hour-label { flex-shrink: 0; white-space: nowrap; } |
| | | .save-button { display: flex; height: 104rpx; margin-top: 30rpx; justify-content: center; align-items: center; border-radius: 52rpx; color: #FFFFFF; font-size: 32rpx; line-height: 45rpx; font-weight: 700; background-image: linear-gradient(90deg, #FF6A22 0%, #FF8F20 100%); box-shadow: 0 16rpx 28rpx rgba(255, 110, 35, 0.18); } |
| | | .save-button-disabled { opacity: 0.62; } |
| | | .bottom-space { height: 110rpx; } |
| | |
| | | .setting-chevron { width: 28rpx; height: 28rpx; } |
| | | .business-state { display: flex; flex-direction: row; align-items: center; } |
| | | .business-state-disabled { opacity: 0.58; } |
| | | .business-state-text { color: #FF5A42; font-size: 25rpx; line-height: 35rpx; font-weight: 600; } |
| | | .state-switch { display: flex; width: 68rpx; height: 40rpx; padding: 3rpx; margin-left: 12rpx; border-radius: 22rpx; align-items: center; background-color: #CDD5E0; box-sizing: border-box; } |
| | | .state-switch.open { justify-content: flex-end; background-color: #FF5A42; } |
| | | .state-thumb { width: 34rpx; height: 34rpx; border-radius: 17rpx; background-color: #FFFFFF; box-shadow: 0 2rpx 7rpx rgba(36, 41, 49, 0.18); } |
| | | .business-state-text { color: #FF5545; font-size: 30rpx; line-height: 42rpx; font-weight: 700; } |
| | | .state-switch { position: relative; width: 82rpx; height: 52rpx; margin-left: 18rpx; border-radius: 28rpx; background-color: #CDD5E0; box-sizing: border-box; } |
| | | .state-switch.open { background-color: #FF5A42; } |
| | | .state-thumb { position: absolute; top: 4rpx; left: 4rpx; width: 44rpx; height: 44rpx; border-radius: 22rpx; background-color: #FFFFFF; box-shadow: 0 2rpx 7rpx rgba(36, 41, 49, 0.18); } |
| | | .state-switch.open .state-thumb { left: 34rpx; } |
| | | |
| | | </style> |
| | |
| | | <camera class="scan-camera" device-position="back" flash="off" mode="scanCode" @scancode="onScanCode" @error="onCameraError"></camera> |
| | | <cover-view class="camera-shade"></cover-view> |
| | | <cover-view class="back-button" @tap.stop="goBack"><cover-image class="back-icon" src="/static/icons/lucide/back-left.svg"></cover-image></cover-view> |
| | | <cover-view class="scan-frame" :class="scanState != 'scanning' ? 'scan-paused' : ''"> |
| | | <cover-view class="frame-top"></cover-view><cover-view class="frame-bottom"></cover-view> |
| | | <cover-view class="scan-frame"> |
| | | <cover-view class="scan-window"> |
| | | <cover-view class="frame-top"></cover-view><cover-view class="frame-bottom"></cover-view> |
| | | <cover-view v-if="scanState == 'scanning'" class="scan-line"></cover-view> |
| | | </cover-view> |
| | | <cover-view class="frame-corner corner-tl"></cover-view><cover-view class="frame-corner corner-tr"></cover-view><cover-view class="frame-corner corner-bl"></cover-view><cover-view class="frame-corner corner-br"></cover-view> |
| | | <cover-view v-if="scanState == 'scanning'" class="scan-line"></cover-view> |
| | | </cover-view> |
| | | <cover-view v-if="scanState == 'scanning'" class="scan-tip">将取景框对准二维码,即可自动扫描</cover-view> |
| | | |
| | |
| | | .scan-camera { width: 100%; height: 100vh; background-color: #000000; position: absolute; top: 0; right: 0; bottom: 0; left: 0; } |
| | | .camera-shade { position: absolute; top: 0; right: 0; bottom: 0; left: 0; background-color: rgba(0, 0, 0, 0.32); } |
| | | .back-button { display: flex; width: 72rpx; height: 72rpx; justify-content: center; align-items: center; position: absolute; top: 82rpx; left: 28rpx; z-index: 20; } |
| | | .back-icon { width: 40rpx; height: 40rpx; } |
| | | .scan-frame { width: 570rpx; height: 570rpx; position: absolute; top: 382rpx; left: 90rpx; z-index: 10; background-color: rgba(40, 137, 91, 0.22); box-sizing: border-box; } |
| | | .scan-frame.scan-paused { background-color: rgba(24, 36, 31, 0.42); } |
| | | .frame-top, .frame-bottom { width: 100%; height: 4rpx; position: absolute; left: 0; background-color: #6BE2AD; opacity: 0.85; } |
| | | .back-icon { width: 40rpx; height: 40rpx; filter: invert(1); } |
| | | .scan-frame { width: 460rpx; height: 460rpx; position: absolute; top: 430rpx; left: 145rpx; z-index: 10; box-sizing: border-box; } |
| | | .scan-window { width: 460rpx; height: 286rpx; position: absolute; top: 0; left: 0; background-color: rgba(40, 137, 91, 0.22); } |
| | | .frame-top, .frame-bottom { height: 4rpx; position: absolute; background-color: #6BE2AD; opacity: 0.85; } |
| | | .frame-top { width: 382rpx; left: 78rpx; } |
| | | .frame-bottom { width: 100%; left: 0; } |
| | | .frame-top { top: 0; } |
| | | .frame-bottom { bottom: 0; } |
| | | .frame-corner { width: 42rpx; height: 42rpx; position: absolute; border-color: #69E6B1; box-sizing: border-box; } |
| | |
| | | .corner-bl { bottom: 0; left: 0; border-bottom-width: 7rpx; border-left-width: 7rpx; } |
| | | .corner-br { right: 0; bottom: 0; border-right-width: 7rpx; border-bottom-width: 7rpx; } |
| | | .scan-line { width: 100%; height: 3rpx; position: absolute; top: 0; left: 0; background-color: rgba(133, 255, 192, 0.9); box-shadow: 0 0 15rpx rgba(111, 241, 170, 0.8); animation: scan-line-move 1800ms ease-in-out infinite alternate; } |
| | | .scan-tip { width: 100%; position: absolute; top: 1116rpx; left: 0; z-index: 10; color: #FFFFFF; font-size: 27rpx; line-height: 38rpx; text-align: center; } |
| | | .status-panel { display: flex; width: 620rpx; min-height: 330rpx; padding: 42rpx 38rpx 34rpx; flex-direction: column; align-items: center; position: absolute; left: 65rpx; bottom: 120rpx; z-index: 30; border-radius: 36rpx; background-color: rgba(255, 255, 255, 0.96); box-sizing: border-box; } |
| | | .scan-tip { width: 100%; position: absolute; top: 975rpx; left: 0; z-index: 10; color: #FFFFFF; font-size: 27rpx; line-height: 38rpx; text-align: center; } |
| | | .status-panel { display: none; } |
| | | .loading-spinner { width: 58rpx; height: 58rpx; border-width: 6rpx; border-style: solid; border-color: #FFD5BF; border-top-color: #FF6B2C; border-radius: 50%; animation: loading-rotate 800ms linear infinite; } |
| | | .status-mark { display: flex; width: 64rpx; height: 64rpx; justify-content: center; align-items: center; border-radius: 32rpx; color: #FFFFFF; font-size: 38rpx; line-height: 64rpx; font-weight: 700; text-align: center; } |
| | | .success-mark { background-color: #35B879; } |
| | |
| | | .half-action { width: 48%; } |
| | | .status-actions .primary-action.half-action { margin-left: 4%; } |
| | | .full-action { width: 100%; } |
| | | @keyframes scan-line-move { from { transform: translateY(0); } to { transform: translateY(566rpx); } } |
| | | @keyframes scan-line-move { from { transform: translateY(0); } to { transform: translateY(282rpx); } } |
| | | @keyframes loading-rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } |
| | | </style> |
| | |
| | | |
| | | .merchant-form-row.address-row { |
| | | grid-template-columns: 68px minmax(0, 1fr) 88px; |
| | | align-items: start; |
| | | } |
| | | |
| | | .merchant-form-row.address-row > .el-form-item__label { |
| | | line-height: 32px; |
| | | } |
| | | |
| | | .merchant-form-row.three-columns { |
| | |
| | | grid-template-columns: 58px minmax(0, 1fr); |
| | | align-items: center; |
| | | margin: 84px 0 18px; |
| | | } |
| | | |
| | | .merchant-create-right .merchant-form-row, |
| | | .merchant-create-right .merchant-hours-block { |
| | | column-gap: 18px; |
| | | } |
| | | |
| | | .merchant-hours-list { |
| | |
| | | box-shadow: 0 0 0 1px #d9dee8 inset !important; |
| | | } |
| | | |
| | | /* Keep every editable product control visually consistent with the merchant form. */ |
| | | .merchant-product-form-panel { |
| | | --el-fill-color-blank: #ffffff; |
| | | --el-fill-color-light: #ffffff; |
| | | --el-disabled-bg-color: #ffffff; |
| | | --el-disabled-text-color: #334155; |
| | | } |
| | | |
| | | .merchant-product-form-panel .el-input__wrapper, |
| | | .merchant-product-form-panel .el-input-number .el-input__wrapper, |
| | | .merchant-product-form-panel .el-select__wrapper, |
| | | .merchant-product-form-panel .el-date-editor.el-input__wrapper, |
| | | .merchant-product-form-panel .el-time-editor.el-input__wrapper { |
| | | background: #ffffff !important; |
| | | box-shadow: 0 0 0 1px #d9dee8 inset !important; |
| | | } |
| | | |
| | | .merchant-product-form-panel .el-input__wrapper:hover, |
| | | .merchant-product-form-panel .el-input__wrapper.is-focus, |
| | | .merchant-product-form-panel .el-select__wrapper:hover, |
| | | .merchant-product-form-panel .el-select__wrapper.is-focused, |
| | | .merchant-product-form-panel .el-date-editor.el-input__wrapper:hover, |
| | | .merchant-product-form-panel .el-date-editor.el-input__wrapper.is-focus, |
| | | .merchant-product-form-panel .el-time-editor.el-input__wrapper:hover, |
| | | .merchant-product-form-panel .el-time-editor.el-input__wrapper.is-focus { |
| | | box-shadow: 0 0 0 1px #b8c2d1 inset !important; |
| | | } |
| | | |
| | | .merchant-product-form-panel .el-button, |
| | | .product-form-actions .el-button { |
| | | height: 28px; |
| | |
| | | |
| | | .coupon-requirement-row .coupon-full-amount-control > span { |
| | | color: #334155; |
| | | font-size: 12px; |
| | | font-size: 14px; |
| | | line-height: 28px; |
| | | white-space: nowrap; |
| | | } |
| | |
| | | |
| | | .detail-money-field { |
| | | grid-template-columns: 56px 160px auto; |
| | | } |
| | | |
| | | .detail-period-field { |
| | | grid-template-columns: 84px minmax(0, 1fr); |
| | | } |
| | | |
| | | .detail-period-field label { |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .detail-field label, |
| | |
| | | |
| | | /* 角色管理筛选项与标题左边缘对齐。 */ |
| | | .role-management-filter-form .el-form-item__label { |
| | | flex: 0 0 42px; |
| | | width: 42px !important; |
| | | flex: 0 0 86px; |
| | | width: 86px !important; |
| | | justify-content: flex-start; |
| | | } |
| | | |
| | | .role-management-filter-form .business-fill-filter-actions { |
| | | grid-column: -2 / -1; |
| | | grid-row: 1; |
| | | justify-self: end; |
| | | } |
| | | |
| | | .upgrade-management-filter-form .el-form-item__label { |
| | | justify-content: flex-end; |
| | | } |
| | | |
| | | .upgrade-management-filter-form { |
| | | grid-template-columns: minmax(220px, 1fr) minmax(280px, 1.15fr) minmax(220px, 0.9fr) auto; |
| | | gap: 14px 34px; |
| | | } |
| | | |
| | | .upgrade-management-filter-form .upgrade-management-filter-date { |
| | | grid-column: 1; |
| | | } |
| | | |
| | | .upgrade-management-filter-form .business-fill-filter-actions { |
| | | grid-row: 1 / span 2; |
| | | grid-column: 4; |
| | | align-content: start; |
| | | } |
| | | |
| | | .upgrade-management-filter-form .el-date-editor { |
| | | width: 100% !important; |
| | | } |
| | | |
| | | .business-fill-filter-form .el-input, |
| | |
| | | padding: 18px 0 16px; |
| | | } |
| | | |
| | | .movie-ticket-code-pool-tools { |
| | | justify-content: flex-start; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .movie-ticket-package-table { |
| | | color: #111827; |
| | | font-size: 12px; |
| | |
| | | |
| | | .movie-ticket-create-row { |
| | | display: grid; |
| | | grid-template-columns: 58px minmax(180px, 600px) 36px 180px 86px; |
| | | grid-template-columns: 58px 544px 36px 180px 86px; |
| | | gap: 12px; |
| | | align-items: center; |
| | | color: #111827; |
| | |
| | | merchantId: number | string |
| | | merchantName?: string |
| | | merchantType?: string |
| | | merchantTypeCode?: string |
| | | contactMobile?: string |
| | | productNo?: string |
| | | productType: string |
| | |
| | | productImages?: string[] | string |
| | | rightsConfig?: Array<Record<string, unknown>> | string |
| | | auditStatus?: number |
| | | auditRecordId?: number | string |
| | | auditRemark?: string |
| | | status?: number |
| | | sortNo?: number |
| | |
| | | <div class="page-stack prototype-page business-fill-page"> |
| | | <section class="panel"> |
| | | <div class="panel-header"><h3>{{ title }}</h3></div> |
| | | <el-form class="business-fill-filter-form" :class="{ 'role-management-filter-form': isRole }" label-width="86px"> |
| | | <el-form class="business-fill-filter-form" :class="{ 'role-management-filter-form': isRole, 'upgrade-management-filter-form': isUpgrade }" label-width="86px"> |
| | | <el-form-item :label="isUpgrade ? '持卡人' : isRole ? '角色' : '员工'"> |
| | | <el-input v-model="filters.keyword" :placeholder="isUpgrade ? '持卡人姓名、手机号、卡号' : '请输入关键词'" clearable /> |
| | | </el-form-item> |
| | |
| | | <el-option v-if="isUpgrade" label="已驳回" value="已驳回" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item v-if="isUpgrade" label="申请时间"> |
| | | <el-form-item v-if="isUpgrade" label="申请时间" class="upgrade-management-filter-date"> |
| | | <el-date-picker v-model="filters.timeRange" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" /> |
| | | </el-form-item> |
| | | <div class="business-fill-filter-actions"> |
| | |
| | | return String(Math.round(value)) |
| | | } |
| | | |
| | | function compareTrend(value: unknown) { |
| | | const number = Number(value ?? 0) |
| | | if (!Number.isFinite(number) || number === 0) return 'flat' |
| | | return number > 0 ? 'up' : 'down' |
| | | } |
| | | |
| | | function trendIcon(value: unknown) { |
| | | const trend = compareTrend(value) |
| | | return trend === 'up' ? '↑' : trend === 'down' ? '↓' : '—' |
| | | } |
| | | |
| | | function barHeight(value: unknown) { |
| | | return `${Math.max(4, (Number(value || 0) / maxBarValue.value) * 168)}px` |
| | | } |
| | |
| | | <div v-for="card in summaryCards" :key="card.label" class="subsidy-card" :class="card.tone"> |
| | | <div class="subsidy-label">{{ card.label }}</div> |
| | | <div class="subsidy-value">{{ formatNumber(card.value) }}</div> |
| | | <div class="subsidy-compare">{{ card.unit }} <span>{{ formatNumber(card.compare) }}</span></div> |
| | | <div class="subsidy-compare" :class="`trend-${compareTrend(card.compare)}`"> |
| | | {{ card.unit }} <span>{{ formatNumber(card.compare) }}</span><b>{{ trendIcon(card.compare) }}</b> |
| | | </div> |
| | | <div class="sparkline" :class="card.tone"> |
| | | <i v-for="(point, index) in card.spark" :key="index" :style="{ height: `${point}%` }" /> |
| | | </div> |
| | |
| | | .subsidy-card.green { background: #f0fff2; } |
| | | .subsidy-label { font-size: 14px; color: #111827; } |
| | | .subsidy-value { margin-top: 18px; font-size: 25px; line-height: 1; color: #07142c; } |
| | | .subsidy-compare { margin-top: 8px; color: #8d98a7; font-size: 12px; } |
| | | .subsidy-compare { display: flex; align-items: center; gap: 4px; margin-top: 8px; color: #8d98a7; font-size: 12px; } |
| | | .subsidy-compare span { color: inherit; } |
| | | .subsidy-compare b { font-size: 15px; line-height: 12px; font-weight: 700; } |
| | | .subsidy-compare.trend-up { color: #18a957; } |
| | | .subsidy-compare.trend-down { color: #f04444; } |
| | | .subsidy-compare.trend-flat { color: #8d98a7; } |
| | | .subsidy-compare span { color: #ff3b30; } |
| | | .sparkline { position: absolute; right: 14px; bottom: 16px; display: flex; align-items: end; width: 80px; height: 44px; gap: 4px; } |
| | | .sparkline i { flex: 1; min-height: 5px; border-radius: 999px 999px 0 0; background: #1677ff; } |
| | |
| | | .donut-labels { display: flex; gap: 14px; margin-top: 18px; color: #697589; font-size: 12px; } |
| | | .donut-labels i { display: inline-block; width: 22px; height: 9px; margin-right: 4px; border-radius: 2px; vertical-align: middle; } |
| | | @media (max-width: 1100px) { .top-grid, .bottom-grid { grid-template-columns: 1fr; } .subsidy-cards { grid-template-columns: 1fr; max-width: none; } } |
| | | </style> |
| | | </style> |
| | |
| | | return value === undefined || value === null || value === '' ? '-' : String(value) |
| | | } |
| | | |
| | | function dateTimeText(value?: string | number) { |
| | | return text(value).replace('T', ' ') |
| | | } |
| | | |
| | | const cardholderInfo = computed(() => [ |
| | | { label: '持卡人姓名:', value: text(detail.value?.talentName) }, |
| | | { label: '手机号:', value: text(detail.value?.talentMobile) }, |
| | |
| | | |
| | | const submissionInfo = computed(() => [ |
| | | { label: '支付宝绑定手机号:', value: text(detail.value?.alipayMobile) }, |
| | | { label: '提交时间:', value: text(detail.value?.submittedAt) }, |
| | | { label: '提交时间:', value: dateTimeText(detail.value?.submittedAt) }, |
| | | ]) |
| | | |
| | | const statusInfo = computed(() => [ |
| | | { label: '状态:', value: text(detail.value?.status) }, |
| | | { label: '处理时间:', value: text(detail.value?.handledAt) }, |
| | | { label: '处理时间:', value: dateTimeText(detail.value?.handledAt) }, |
| | | { label: '处理人:', value: text(detail.value?.handlerName) }, |
| | | ]) |
| | | |
| | |
| | | </div> |
| | | </section> |
| | | |
| | | <el-button v-if="!handled" type="primary" class="reservation-detail-action" @click="markHandled">已处理</el-button> |
| | | <el-button v-if="!handled" type="primary" class="reservation-detail-action" @click="markHandled">处理</el-button> |
| | | </div> |
| | | </template> |
| | |
| | | total: 0, |
| | | }) |
| | | |
| | | function formatDateTime(value?: string) { |
| | | return value ? value.replace('T', ' ') : '' |
| | | } |
| | | |
| | | function normalizeRecord(row: ServiceApplication): ServiceApplication { |
| | | return { ...row, submittedAt: formatDateTime(row.submittedAt) } |
| | | } |
| | | |
| | | async function loadData() { |
| | | loading.value = true |
| | | try { |
| | |
| | | submittedStart: filters.submittedTime?.[0], |
| | | submittedEnd: filters.submittedTime?.[1], |
| | | }) |
| | | rows.value = result.records |
| | | rows.value = result.records.map(normalizeRecord) |
| | | pagination.total = result.total |
| | | } catch { |
| | | rows.value = [] |
| | |
| | | |
| | | async function download() { |
| | | const result = await getServiceApplications({ page: 1, size: 1000, applicationType: 'FUEL_SERVICE', talentKeyword: filters.talentKeyword || undefined, cardLevel: filters.cardLevel || undefined, status: filters.status || undefined, submissionType: filters.submissionType || undefined, submittedStart: filters.submittedTime?.[0], submittedEnd: filters.submittedTime?.[1] }) |
| | | exportApplications('加油服务登记', result.records) |
| | | exportApplications('加油服务登记', result.records.map(normalizeRecord)) |
| | | } |
| | | |
| | | function exportApplications(filename: string, data: ServiceApplication[]) { |
| | |
| | | <el-option v-for="level in cardLevels" :key="level" :label="level" :value="level" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <div class="fuel-filter-spacer" /> |
| | | <el-form-item label="提交类型"> |
| | | <el-select v-model="filters.submissionType" placeholder="全部" clearable> |
| | | <el-option label="全部" value="" /> |
| | | <el-option v-for="type in submissionTypes" :key="type" :label="type" :value="type" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="提交时间"> |
| | | <el-date-picker |
| | | v-model="filters.submittedTime" |
| | |
| | | <el-select v-model="filters.status" placeholder="全部" clearable> |
| | | <el-option label="全部" value="" /> |
| | | <el-option v-for="status in statusOptions" :key="status" :label="status" :value="status" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="提交类型"> |
| | | <el-select v-model="filters.submissionType" placeholder="全部" clearable> |
| | | <el-option label="全部" value="" /> |
| | | <el-option v-for="type in submissionTypes" :key="type" :label="type" :value="type" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <div class="fuel-service-filter-actions"> |
| | |
| | | return value === undefined || value === null || value === '' ? '-' : String(value) |
| | | } |
| | | |
| | | function dateTimeText(value?: string | number) { |
| | | return text(value).replace('T', ' ') |
| | | } |
| | | |
| | | const cardholderInfo = computed(() => [ |
| | | { label: '持卡人姓名:', value: text(detail.value?.talentName) }, |
| | | { label: '手机号:', value: text(detail.value?.talentMobile) }, |
| | |
| | | { label: '房型:', value: text(detail.value?.productName) }, |
| | | { label: '期望入住时间:', value: text(detail.value?.expectedTime) }, |
| | | { label: '入住人数:', value: detail.value?.guestCount ? `${detail.value.guestCount}人` : '-' }, |
| | | { label: '提交时间:', value: text(detail.value?.submittedAt) }, |
| | | { label: '提交时间:', value: dateTimeText(detail.value?.submittedAt) }, |
| | | ]) |
| | | |
| | | const statusInfo = computed(() => [ |
| | | { label: '状态:', value: text(detail.value?.status) }, |
| | | { label: '处理时间:', value: text(detail.value?.handledAt) }, |
| | | { label: '处理时间:', value: dateTimeText(detail.value?.handledAt) }, |
| | | { label: '处理人:', value: text(detail.value?.handlerName) }, |
| | | ]) |
| | | |
| | |
| | | total: 0, |
| | | }) |
| | | |
| | | function formatDateTime(value?: string) { |
| | | return value ? value.replace('T', ' ') : '' |
| | | } |
| | | |
| | | function normalizeRecord(row: ServiceApplication): ServiceApplication { |
| | | return { ...row, submittedAt: formatDateTime(row.submittedAt) } |
| | | } |
| | | |
| | | async function loadData() { |
| | | loading.value = true |
| | | try { |
| | |
| | | submittedStart: filters.submittedTime?.[0], |
| | | submittedEnd: filters.submittedTime?.[1], |
| | | }) |
| | | rows.value = result.records |
| | | rows.value = result.records.map(normalizeRecord) |
| | | pagination.total = result.total |
| | | } catch { |
| | | rows.value = [] |
| | |
| | | |
| | | async function download() { |
| | | const result = await getServiceApplications({ page: 1, size: 1000, applicationType: 'HOTEL_RESERVATION', talentKeyword: filters.talentKeyword || undefined, merchantName: filters.merchantName || undefined, cardLevel: filters.cardLevel || undefined, subsidy: filters.subsidy || undefined, status: filters.status || undefined, submittedStart: filters.submittedTime?.[0], submittedEnd: filters.submittedTime?.[1] }) |
| | | downloadExcel('酒店预约', [{ title: '预约单号', value: row => row.applicationNo }, { title: '持卡人', value: row => row.talentName }, { title: '手机号', value: row => row.talentMobile }, { title: '人才等级', value: row => row.talentLevel }, { title: '酒店', value: row => row.merchantName }, { title: '入住时间', value: row => row.expectedTime }, { title: '入住人数', value: row => row.guestCount }, { title: '补贴', value: row => row.subsidy }, { title: '提交时间', value: row => row.submittedAt }, { title: '状态', value: row => row.status }], result.records) |
| | | downloadExcel('酒店预约', [{ title: '预约单号', value: row => row.applicationNo }, { title: '持卡人', value: row => row.talentName }, { title: '手机号', value: row => row.talentMobile }, { title: '人才等级', value: row => row.talentLevel }, { title: '酒店', value: row => row.merchantName }, { title: '入住时间', value: row => row.expectedTime }, { title: '入住人数', value: row => row.guestCount }, { title: '补贴', value: row => row.subsidy }, { title: '提交时间', value: row => formatDateTime(row.submittedAt) }, { title: '状态', value: row => row.status }], result.records) |
| | | ElMessage.success(`已导出 ${result.records.length} 条预约`) |
| | | } |
| | | |
| | |
| | | return value === undefined || value === null || value === '' ? '-' : String(value) |
| | | } |
| | | |
| | | function dateTimeText(value?: string | number) { |
| | | return text(value).replace('T', ' ') |
| | | } |
| | | |
| | | const cardholderInfo = computed(() => [ |
| | | { label: '家属姓名:', value: text(detail.value?.applicantName) }, |
| | | { label: '手机号:', value: text(detail.value?.applicantMobile) }, |
| | |
| | | { label: '体检人姓名:', value: text(detail.value?.applicantName) }, |
| | | { label: '手机号:', value: text(detail.value?.applicantMobile) }, |
| | | { label: '期望体检时间:', value: text(detail.value?.expectedTime) }, |
| | | { label: '提交时间:', value: text(detail.value?.submittedAt) }, |
| | | { label: '提交时间:', value: dateTimeText(detail.value?.submittedAt) }, |
| | | ]) |
| | | |
| | | const handled = computed(() => detail.value?.status === '已处理') |
| | | |
| | | const statusInfo = computed(() => [ |
| | | { label: '状态:', value: text(detail.value?.status) }, |
| | | { label: '处理时间:', value: text(detail.value?.handledAt) }, |
| | | { label: '处理时间:', value: dateTimeText(detail.value?.handledAt) }, |
| | | { label: '处理人:', value: text(detail.value?.handlerName) }, |
| | | ]) |
| | | |
| | |
| | | </div> |
| | | </section> |
| | | |
| | | <el-button v-if="!handled" type="primary" class="reservation-detail-action" @click="markHandled">已处理</el-button> |
| | | <el-button v-if="!handled" type="primary" class="reservation-detail-action" @click="markHandled">处理</el-button> |
| | | </div> |
| | | </template> |
| | |
| | | <div class="detail-field"><span>证照有效期起始日期</span><strong>{{ dateText(detail.licenseBeginDate) }}</strong></div> |
| | | <div class="detail-field"><span>证照有效期结束日期</span><strong>{{ dateText(detail.licenseEndDate) }}</strong></div> |
| | | <div class="detail-field"><span>注册地址(省、市、区)</span><strong>{{ text(detail.registeredRegion) }}</strong></div> |
| | | <div class="detail-field"><span>详细注册地址</span><strong>{{ text(detail.registeredAddress) }}</strong></div> |
| | | <div class="detail-field detail-field--auto-height"><span>详细注册地址</span><strong>{{ text(detail.registeredAddress) }}</strong></div> |
| | | <div class="detail-field"><span>法人姓名</span><strong>{{ text(detail.subjectName) }}</strong></div> |
| | | <div class="detail-field"><span>法人证件类型</span><strong>身份证</strong></div> |
| | | <div class="detail-field"><span>法人证件号码</span><strong>{{ text(detail.subjectIdNo || detail.subjectIdNoMasked) }}</strong></div> |
| | |
| | | .common-grid { margin-bottom: -2px; } |
| | | .detail-field { display: grid; grid-template-columns: 144px minmax(0, 1fr); align-items: center; gap: 12px; min-height: 32px; } |
| | | .detail-field > span { color: #303133; font-size: 13px; font-weight: 500; white-space: nowrap; } |
| | | .detail-field > strong { min-height: 32px; box-sizing: border-box; padding: 7px 10px; color: #606266; font-size: 13px; font-weight: 400; line-height: 18px; word-break: break-all; background: #f5f7fa; } |
| | | .detail-field > strong { min-width: 0; min-height: 32px; box-sizing: border-box; padding: 7px 10px; color: #606266; font-size: 13px; font-weight: 400; line-height: 18px; word-break: break-all; background: #f5f7fa; } |
| | | .detail-field--auto-height { align-items: stretch; } |
| | | .detail-field--auto-height > strong { height: auto; white-space: pre-wrap; overflow-wrap: anywhere; } |
| | | .placeholder-field { visibility: hidden; } |
| | | </style> |
| | |
| | | |
| | | const form = reactive({ |
| | | packageName: '', |
| | | totalQuantity: 0, |
| | | totalQuantity: 1, |
| | | merchantIds: [] as Array<number | string>, |
| | | validStart: '', |
| | | validEnd: '', |
| | |
| | | usageText: '', |
| | | status: 1, |
| | | sortNo: 0, |
| | | levelRules: ticketRuleLevels.map(level => ({ talentLevel: level, enabled: 1, claimableCount: undefined as number | undefined, resetCycle: '每天' })), |
| | | levelRules: ticketRuleLevels.map(level => ({ talentLevel: level, enabled: 1, claimableCount: 1, resetCycle: '每天' })), |
| | | }) |
| | | |
| | | const codeForm = reactive({ batchNo: '', codesText: '', remark: '' }) |
| | |
| | | editingId.value = row ? rowId(row) : null |
| | | Object.assign(form, { |
| | | packageName: row?.packageName || '', |
| | | totalQuantity: Number(row?.totalQuantity || row?.totalCount || row?.totalCodes || 0), |
| | | totalQuantity: Number(row?.totalQuantity || row?.totalCount || row?.totalCodes || 1), |
| | | merchantIds: Array.isArray(row?.merchantIds) && row.merchantIds.length ? row.merchantIds.map(String) : row?.merchantId ? [String(row.merchantId)] : [], |
| | | validStart: row?.validStart || '', |
| | | validEnd: row?.validEnd || '', |
| | |
| | | return { |
| | | talentLevel: level, |
| | | enabled: Number(found?.enabled ?? 1), |
| | | claimableCount: found?.claimableCount === undefined ? undefined : Number(found.claimableCount), |
| | | claimableCount: Number(found?.claimableCount) > 0 ? Number(found?.claimableCount) : 1, |
| | | resetCycle: found?.resetCycle || '每天', |
| | | } |
| | | }) |
| | |
| | | ElMessage.warning('请输入票包名称') |
| | | return |
| | | } |
| | | const totalQuantity = Number(form.totalQuantity) |
| | | if (!Number.isInteger(totalQuantity) || totalQuantity < 1) { |
| | | ElMessage.warning('数量必须为正整数') |
| | | return |
| | | } |
| | | const invalidLevelRule = form.levelRules.find(rule => { |
| | | const claimableCount = Number(rule.claimableCount) |
| | | return !Number.isInteger(claimableCount) || claimableCount < 1 |
| | | }) |
| | | if (invalidLevelRule) { |
| | | ElMessage.warning(`${invalidLevelRule.talentLevel}可领数量必须为正整数`) |
| | | return |
| | | } |
| | | saving.value = true |
| | | try { |
| | | const levelRules = form.levelRules.map(rule => ({ |
| | | talentLevel: rule.talentLevel, |
| | | enabled: rule.enabled, |
| | | claimableCount: Number(rule.claimableCount || 1), |
| | | claimableCount: Number(rule.claimableCount), |
| | | resetCycle: rule.resetCycle || '每天', |
| | | })) |
| | | const payload = { |
| | | packageName: form.packageName.trim(), |
| | | totalQuantity: Number(form.totalQuantity || 0), |
| | | totalQuantity, |
| | | merchantId: form.merchantIds[0] ? Number(form.merchantIds[0]) : undefined, |
| | | merchantIds: form.merchantIds.map(item => Number(item)).filter(Boolean), |
| | | talentLevels: levelRules.filter(rule => Number(rule.enabled) === 1).map(rule => rule.talentLevel), |
| | |
| | | <el-button :icon="Refresh" @click="resetCodeFilters">重置</el-button> |
| | | </div> |
| | | </el-form> |
| | | <div class="movie-ticket-package-tools"> |
| | | <div class="movie-ticket-package-tools movie-ticket-code-pool-tools"> |
| | | <el-button v-permission="'admin.movie-tickets.import'" type="primary" @click="openImport(selectedPackage || {})">导入码</el-button> |
| | | <el-button @click="openGenerate(selectedPackage || {})">批量生成</el-button> |
| | | <span /> |
| | | </div> |
| | | <el-table v-loading="loading" :data="rows" border class="movie-ticket-package-table" empty-text="暂无兑换码"> |
| | | <el-table-column label="码ID" min-width="90" align="center"><template #default="{ row }">{{ text(row.codeId || row.id) }}</template></el-table-column> |
| | |
| | | <span>票包名称</span> |
| | | <el-input v-model="form.packageName" /> |
| | | <span>数量</span> |
| | | <el-input-number v-model="form.totalQuantity" :min="0" controls-position="right" /> |
| | | <el-input-number v-model="form.totalQuantity" :min="1" :step="1" :precision="0" controls-position="right" /> |
| | | <el-button type="primary" @click="merchantDialogVisible = true">可用商家</el-button> |
| | | </div> |
| | | <div v-if="selectedMerchantNames" class="movie-ticket-selected-merchants">已选商家:{{ selectedMerchantNames }}</div> |
| | |
| | | <strong>{{ rule.talentLevel }}</strong> |
| | | <el-switch v-model="rule.enabled" :active-value="1" :inactive-value="0" /> |
| | | <span>可领数量</span> |
| | | <el-input-number v-model="rule.claimableCount" :min="1" controls-position="right" placeholder="请输入" /> |
| | | <el-input-number v-model="rule.claimableCount" :min="1" :step="1" :precision="0" controls-position="right" placeholder="请输入" /> |
| | | <span>张</span> |
| | | <span>重置周期</span> |
| | | <el-select v-model="rule.resetCycle"> |
| | |
| | | |
| | | <section class="detail-section"> |
| | | <h3>订单信息</h3> |
| | | <div class="info-card info-grid order-grid"> |
| | | <div class="info-card info-grid order-grid" :class="{ 'store-consume-order-grid': detail.orderType === 'STORE_CONSUME' }"> |
| | | <div><span>订单编号</span><strong>{{ detail.orderNo || '-' }}</strong></div> |
| | | <div><span>商品金额</span><strong>{{ money(detail.orderAmount) }}</strong></div> |
| | | <div><span>商品数量</span><strong>{{ items.length }}</strong></div> |
| | |
| | | <strong class="discount-amount">{{ yuan(detail.subsidyAmount) }}</strong> |
| | | </section> |
| | | </div> |
| | | <div><span>实际应付</span><strong class="amount-strong">{{ money(detail.payAmount) }}</strong></div> |
| | | <div class="actual-pay-row"><span>实际应付</span><strong class="amount-strong">{{ money(detail.payAmount) }}</strong></div> |
| | | <div><span>下单时间</span><strong>{{ dateTime(detail.createdAt) }}</strong></div> |
| | | <div><span>核销时间</span><strong>{{ dateTime(detail.verifyTime) }}</strong></div> |
| | | <div><span>订单状态</span><strong>{{ orderStatus(detail) }}</strong></div> |
| | |
| | | .order-preview-image { display: block; width: 100%; max-height: 520px; } |
| | | .empty-cover { color: #86909c; } |
| | | .order-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } |
| | | .store-consume-order-grid .actual-pay-row { grid-column: 1 / -1; } |
| | | .info-grid .discount-summary-row { grid-column: 1 / -1; display: block !important; align-items: initial !important; gap: 0 !important; } |
| | | .discount-prototype-grid { |
| | | display: grid !important; |
| | |
| | | } as Record<string, string>)[action] || 'view'}` |
| | | const pageCodes = (page: Page) => page.actions.map((action) => code(page.code, action)) |
| | | const moduleCodes = (module: Module) => module.pages.flatMap(pageCodes) |
| | | const allPermissionCodes = () => modules.flatMap(moduleCodes) |
| | | const checked = (values: string[]) => values.every((value) => form.permissionCodes.includes(value)) |
| | | const partial = (values: string[]) => values.some((value) => form.permissionCodes.includes(value)) && !checked(values) |
| | | const toggle = (values: string[], enabled: boolean) => { const selected = new Set(form.permissionCodes); values.forEach((value) => enabled ? selected.add(value) : selected.delete(value)); form.permissionCodes = [...selected] } |
| | | const toggleModule = (module: Module, enabled: boolean) => toggle(moduleCodes(module), enabled) |
| | | const togglePage = (page: Page, enabled: boolean) => toggle(pageCodes(page), enabled) |
| | | const parse = (value: AdminRole['permissionCodes']) => { if (Array.isArray(value)) return value; try { const result = JSON.parse(value || '[]'); return Array.isArray(result) ? result : [] } catch { return [] } } |
| | | async function load() { if (!id.value) return; const role = (await getAdminRoles()).find((item) => String(item.id) === id.value); if (!role) { ElMessage.error('角色不存在'); router.replace('/roles'); return }; Object.assign(form, { roleName: role.roleName, remark: role.remark || '', status: role.status, permissionCodes: parse(role.permissionCodes) }) } |
| | | async function load() { if (!id.value) return; const role = (await getAdminRoles()).find((item) => String(item.id) === id.value); if (!role) { ElMessage.error('角色不存在'); router.replace('/roles'); return }; Object.assign(form, { roleName: role.roleName, remark: role.remark || '', status: role.status, permissionCodes: role.roleCode === 'platform_admin' ? allPermissionCodes() : parse(role.permissionCodes) }) } |
| | | async function save() { if (isViewMode.value) return; const valid = await formRef.value?.validate().catch(() => false); if (!valid) return; saving.value = true; try { const payload = { roleName: form.roleName.trim(), roleCode: '', remark: form.remark.trim(), status: form.status, permissionCodes: form.permissionCodes }; if (id.value) await updateAdminRole(id.value, payload); else await createAdminRole(payload); ElMessage.success('保存成功'); router.replace('/roles') } finally { saving.value = false } } |
| | | watch(() => route.fullPath, load, { immediate: true }) |
| | | </script> |
| | |
| | | return value === undefined || value === null || value === '' ? '-' : String(value) |
| | | } |
| | | |
| | | function formatDateTime(value?: string | number) { |
| | | return text(value).replace('T', ' ') |
| | | } |
| | | |
| | | const cardholderInfo = computed(() => [ |
| | | { label: '持卡人姓名:', value: text(detail.value?.talentName) }, |
| | | { label: '手机号:', value: text(detail.value?.talentMobile) }, |
| | |
| | | |
| | | const statusInfo = computed(() => [ |
| | | { label: '状态:', value: text(detail.value?.status) }, |
| | | { label: '处理时间:', value: text(detail.value?.handledAt) }, |
| | | { label: '处理时间:', value: formatDateTime(detail.value?.handledAt) }, |
| | | { label: '处理人:', value: text(detail.value?.handlerName) }, |
| | | ]) |
| | | |
| | |
| | | total: 0, |
| | | }) |
| | | |
| | | function formatDateTime(value?: string) { |
| | | return value ? value.replace('T', ' ') : '' |
| | | } |
| | | |
| | | function normalizeRecord(row: ServiceApplication): ServiceApplication { |
| | | return { ...row, submittedAt: formatDateTime(row.submittedAt) } |
| | | } |
| | | |
| | | async function loadData() { |
| | | loading.value = true |
| | | try { |
| | |
| | | submittedStart: filters.submittedTime?.[0], |
| | | submittedEnd: filters.submittedTime?.[1], |
| | | }) |
| | | rows.value = result.records |
| | | rows.value = result.records.map(normalizeRecord) |
| | | pagination.total = result.total |
| | | } catch { |
| | | rows.value = [] |
| | |
| | | |
| | | async function download() { |
| | | const result = await getServiceApplications({ page: 1, size: 1000, applicationType: 'SPOUSE_EMPLOYMENT', talentKeyword: filters.talentKeyword || undefined, cardLevel: filters.cardLevel || undefined, status: filters.status || undefined, submittedStart: filters.submittedTime?.[0], submittedEnd: filters.submittedTime?.[1] }) |
| | | downloadExcel('配偶就业申请', [{ title: '申请单号', value: row => row.applicationNo }, { title: '持卡人', value: row => row.talentName }, { title: '手机号', value: row => row.talentMobile }, { title: '人才等级', value: row => row.talentLevel }, { title: '配偶姓名', value: row => row.spouseName }, { title: '配偶手机号', value: row => row.spouseMobile }, { title: '学历', value: row => row.spouseEducation }, { title: '工作单位', value: row => row.spouseWorkUnit }, { title: '提交时间', value: row => row.submittedAt }, { title: '状态', value: row => row.status }], result.records) |
| | | downloadExcel('配偶就业申请', [{ title: '申请单号', value: row => row.applicationNo }, { title: '持卡人', value: row => row.talentName }, { title: '手机号', value: row => row.talentMobile }, { title: '人才等级', value: row => row.talentLevel }, { title: '配偶姓名', value: row => row.spouseName }, { title: '配偶手机号', value: row => row.spouseMobile }, { title: '学历', value: row => row.spouseEducation }, { title: '工作单位', value: row => row.spouseWorkUnit }, { title: '提交时间', value: row => formatDateTime(row.submittedAt) }, { title: '状态', value: row => row.status }], result.records) |
| | | ElMessage.success(`已导出 ${result.records.length} 条申请`) |
| | | } |
| | | |
| | |
| | | <el-option v-for="level in cardLevels" :key="level" :label="level" :value="level" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <div class="spouse-filter-spacer" /> |
| | | <el-form-item label="提交时间"> |
| | | <el-date-picker |
| | | v-model="filters.submittedTime" |
| | |
| | | <el-option v-for="status in statusOptions" :key="status" :label="status" :value="status" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <div class="spouse-filter-spacer" /> |
| | | <div class="spouse-employment-filter-actions"> |
| | | <el-button type="primary" :icon="Search" @click="search">查询</el-button> |
| | | <el-button :icon="Refresh" @click="resetFilters">重置</el-button> |
| | |
| | | <div class="page-stack"> |
| | | <section class="panel"> |
| | | <div class="panel-header"><h3>账号</h3></div> |
| | | <el-form class="filters" inline |
| | | <el-form class="filters" inline label-position="left" |
| | | ><el-form-item label="用户名称" |
| | | ><el-input v-model="filters.name" /></el-form-item |
| | | ><el-form-item label="用户电话" |
| | |
| | | ><el-option label="启用" value="1" /><el-option |
| | | label="禁用" |
| | | value="0" /></el-select></el-form-item |
| | | ><el-form-item |
| | | ><el-button :icon="Refresh" @click="reset">重置</el-button |
| | | ><el-button type="primary" :icon="Search" |
| | | >搜索</el-button |
| | | ></el-form-item |
| | | ><el-form-item class="staff-filter-actions" |
| | | ><div class="staff-filter-button-stack" |
| | | ><el-button type="primary" :icon="Search" |
| | | >查询</el-button |
| | | ><el-button :icon="Refresh" @click="reset">重置</el-button |
| | | ></div></el-form-item |
| | | ></el-form |
| | | > |
| | | <div class="tools"> |
| | |
| | | v-model="form.status" |
| | | :disabled="editingDefaultAdmin" |
| | | :active-value="1" |
| | | :inactive-value="0" |
| | | active-text="启用" |
| | | inactive-text="禁用" /></el-form-item></el-form |
| | | :inactive-value="0" /></el-form-item></el-form |
| | | ><template #footer |
| | | ><el-button @click="visible = false">取消</el-button |
| | | ><el-button type="primary" :loading="saving" @click="save" |
| | |
| | | </template> |
| | | <style scoped> |
| | | .filters { |
| | | margin: 14px 20px 0; |
| | | padding: 16px; |
| | | display: grid; |
| | | grid-template-columns: max-content max-content max-content max-content minmax(0, 1fr) 74px; |
| | | gap: 12px 28px; |
| | | align-items: start; |
| | | margin: 14px 0 0; |
| | | padding: 16px 0; |
| | | border-bottom: 1px solid #d9dee8; |
| | | } |
| | | .filters :deep(.el-form-item) { |
| | | margin: 0; |
| | | } |
| | | .filters :deep(.el-form-item__label) { |
| | | text-align: left !important; |
| | | justify-content: flex-start !important; |
| | | } |
| | | .filters :deep(.el-input), |
| | | .filters :deep(.el-select) { |
| | | width: 130px; |
| | | } |
| | | .filters :deep(.staff-filter-actions) { |
| | | grid-column: -2 / -1; |
| | | grid-row: 1; |
| | | margin: 0; |
| | | } |
| | | .staff-filter-button-stack { |
| | | display: grid; |
| | | grid-template-columns: 74px; |
| | | gap: 10px; |
| | | } |
| | | .staff-filter-button-stack :deep(.el-button + .el-button) { |
| | | margin-left: 0; |
| | | } |
| | | .tools { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | padding: 18px 20px 14px; |
| | | justify-content: flex-start; |
| | | padding: 18px 0 14px; |
| | | } |
| | | .table { |
| | | margin: 0 20px; |
| | | width: calc(100% - 40px); |
| | | margin: 0; |
| | | width: 100%; |
| | | } |
| | | .full { |
| | | width: 100%; |
| | |
| | | |
| | | @GetMapping("/products/{id}") |
| | | public ApiResponse<Map<String, Object>> product(@PathVariable Long id) { |
| | | return ApiResponse.success(adminBusinessService.product(id)); |
| | | return ApiResponse.success(adminBusinessService.productAuditRecord(id)); |
| | | } |
| | | |
| | | @PostMapping("/products") |
| | |
| | | @PathVariable Long id, |
| | | @Valid @RequestBody ProductAuditRequest request |
| | | ) { |
| | | return ApiResponse.success(adminBusinessService.auditProduct(id, request)); |
| | | return ApiResponse.success(adminBusinessService.auditProductAuditRecord(id, request)); |
| | | } |
| | | |
| | | @GetMapping("/coupons") |
| | |
| | | result.put("cardCount", count("select count(*) from talent_card where deleted = 0")); |
| | | result.put("merchantCount", count("select count(*) from merchant where deleted = 0")); |
| | | result.put("orderCount", count("select count(*) from benefit_order where deleted = 0")); |
| | | result.put("pendingProductCount", count("select count(*) from merchant_product where deleted = 0 and audit_status = 1")); |
| | | result.put("pendingProductCount", count("select count(*) from merchant_product_audit_record where deleted = 0 and audit_status = 1")); |
| | | result.put("todayVerifyCount", count("select count(*) from verify_record where date(verify_time) = current_date")); |
| | | return result; |
| | | } |
| | |
| | | countSql.append(" and date(m.created_at) <= ?"); |
| | | args.add(createdEnd); |
| | | } |
| | | sql.append(" order by m.is_recommend desc, m.sort_no desc, m.id desc"); |
| | | sql.append(" order by m.sort_no desc, m.created_at desc, m.id desc"); |
| | | PageResult<Map<String, Object>> result = page(sql.toString(), countSql.toString(), page, size, args.toArray()); |
| | | applyBusinessStatus(result.records()); |
| | | return result; |
| | |
| | | public List<Map<String, Object>> pendingNotices() { |
| | | List<Map<String, Object>> notices = new ArrayList<>(); |
| | | addPendingNotice(notices, "product-audit", "团单审核", "您有 %d 条团单待审核", "/products", |
| | | "select count(*) as pending_count, max(coalesce(submit_time, created_at)) as latest_time from merchant_product where deleted = 0 and audit_status = 1"); |
| | | "select count(*) as pending_count, max(coalesce(submit_time, created_at)) as latest_time from merchant_product_audit_record where deleted = 0 and audit_status = 1"); |
| | | addPendingNotice(notices, "family-card", "家属卡申请", "您有 %d 条家属卡申请待审核", "/family-card-applications", |
| | | "select count(*) as pending_count, max(apply_time) as latest_time from family_card_application where deleted = 0 and status = '待审核'"); |
| | | addPendingNotice(notices, "upgrade", "升级申请", "您有 %d 条升级申请待审核", "/upgrade-applications", |
| | |
| | | int size |
| | | ) { |
| | | StringBuilder sql = new StringBuilder(""" |
| | | select p.id, p.merchant_id, m.merchant_name, m.merchant_type, m.contact_mobile, |
| | | p.product_no, p.product_type, p.product_name, |
| | | p.subtitle, p.cover_url, p.original_price, p.sale_price, p.subsidy_amount, |
| | | p.is_government_subsidy, p.stock, p.sales_count, p.detail_text, p.benefit_desc, |
| | | p.purchase_notice, p.valid_start_time, p.valid_end_time, p.product_images, p.rights_config, |
| | | p.audit_status, p.audit_remark, p.status, p.sort_no, p.submit_time, p.created_at |
| | | from merchant_product p |
| | | left join merchant m on m.id = p.merchant_id |
| | | where p.deleted = 0 |
| | | select r.id, r.product_id, r.merchant_id, m.merchant_name, m.merchant_type, m.merchant_type_code, m.contact_mobile, |
| | | r.product_no, r.product_type, r.product_name, |
| | | r.subtitle, r.cover_url, r.original_price, r.sale_price, r.subsidy_amount, |
| | | r.is_government_subsidy, r.stock, r.sales_count, r.detail_text, r.benefit_desc, |
| | | r.purchase_notice, r.valid_start_time, r.valid_end_time, r.product_images, r.rights_config, |
| | | r.audit_status, r.audit_remark, r.audit_by, r.audit_time, r.submit_time, |
| | | r.source_created_at as created_at, p.status |
| | | from merchant_product_audit_record r |
| | | left join merchant_product p on p.id = r.product_id |
| | | left join merchant m on m.id = r.merchant_id |
| | | where r.deleted = 0 |
| | | """); |
| | | StringBuilder countSql = new StringBuilder(""" |
| | | select count(*) |
| | | from merchant_product p |
| | | left join merchant m on m.id = p.merchant_id |
| | | where p.deleted = 0 |
| | | from merchant_product_audit_record r |
| | | left join merchant m on m.id = r.merchant_id |
| | | where r.deleted = 0 |
| | | """); |
| | | List<Object> args = new ArrayList<>(); |
| | | if (merchantId != null) { |
| | | sql.append(" and p.merchant_id = ?"); |
| | | countSql.append(" and p.merchant_id = ?"); |
| | | sql.append(" and r.merchant_id = ?"); |
| | | countSql.append(" and r.merchant_id = ?"); |
| | | args.add(merchantId); |
| | | } |
| | | if (merchantName != null && !merchantName.isBlank()) { |
| | |
| | | args.add("%" + contactMobile.trim() + "%"); |
| | | } |
| | | if (keyword != null && !keyword.isBlank()) { |
| | | sql.append(" and p.product_name like ?"); |
| | | countSql.append(" and p.product_name like ?"); |
| | | sql.append(" and r.product_name like ?"); |
| | | countSql.append(" and r.product_name like ?"); |
| | | String like = "%" + keyword.trim() + "%"; |
| | | args.add(like); |
| | | } |
| | |
| | | args.add(MerchantTypeCatalog.canonicalCode(type)); |
| | | } |
| | | if (status != null) { |
| | | sql.append(" and p.audit_status = ?"); |
| | | countSql.append(" and p.audit_status = ?"); |
| | | sql.append(" and r.audit_status = ?"); |
| | | countSql.append(" and r.audit_status = ?"); |
| | | args.add(status); |
| | | } |
| | | if (createdStart != null && !createdStart.isBlank()) { |
| | | sql.append(" and date(p.created_at) >= ?"); |
| | | countSql.append(" and date(p.created_at) >= ?"); |
| | | sql.append(" and date(coalesce(r.submit_time, r.created_at)) >= ?"); |
| | | countSql.append(" and date(coalesce(r.submit_time, r.created_at)) >= ?"); |
| | | args.add(createdStart); |
| | | } |
| | | if (createdEnd != null && !createdEnd.isBlank()) { |
| | | sql.append(" and date(p.created_at) <= ?"); |
| | | countSql.append(" and date(p.created_at) <= ?"); |
| | | sql.append(" and date(coalesce(r.submit_time, r.created_at)) <= ?"); |
| | | countSql.append(" and date(coalesce(r.submit_time, r.created_at)) <= ?"); |
| | | args.add(createdEnd); |
| | | } |
| | | sql.append(" order by p.submit_time desc, p.id desc"); |
| | | sql.append(" order by coalesce(r.submit_time, r.created_at) desc, r.id desc"); |
| | | return page(sql.toString(), countSql.toString(), page, size, args.toArray()); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> product(Long id) { |
| | | return one(""" |
| | | select p.id, p.merchant_id, m.merchant_name, m.merchant_type, m.contact_mobile, |
| | | select p.id, p.merchant_id, m.merchant_name, m.merchant_type, m.merchant_type_code, m.contact_mobile, |
| | | p.product_no, p.product_type, p.product_name, |
| | | p.subtitle, p.cover_url, p.original_price, p.sale_price, p.subsidy_amount, |
| | | p.is_government_subsidy, p.stock, p.sales_count, p.detail_text, p.benefit_desc, |
| | |
| | | left join merchant m on m.id = p.merchant_id |
| | | where p.deleted = 0 and p.id = ? |
| | | """, id); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> productAuditRecord(Long auditRecordId) { |
| | | return one(""" |
| | | select r.id, r.product_id, r.merchant_id, m.merchant_name, m.merchant_type, m.merchant_type_code, m.contact_mobile, |
| | | r.product_no, r.product_type, r.product_name, r.subtitle, r.cover_url, |
| | | r.original_price, r.sale_price, r.subsidy_amount, r.is_government_subsidy, |
| | | r.stock, r.sales_count, r.detail_text, r.benefit_desc, r.purchase_notice, |
| | | r.valid_start_time, r.valid_end_time, r.product_images, r.rights_config, |
| | | r.audit_status, r.audit_remark, r.audit_by, r.audit_time, r.submit_time, |
| | | r.source_created_at as created_at, p.status |
| | | from merchant_product_audit_record r |
| | | left join merchant_product p on p.id = r.product_id |
| | | left join merchant m on m.id = r.merchant_id |
| | | where r.deleted = 0 and r.id = ? |
| | | """, auditRecordId); |
| | | } |
| | | |
| | | @Transactional |
| | |
| | | blankToNull(request.validEndTime()), |
| | | toJson(request.productImages()), |
| | | toJson(request.rightsConfig())); |
| | | return one("select * from merchant_product where product_no = ?", productNo); |
| | | Long productId = jdbcTemplate.queryForObject("select id from merchant_product where product_no = ?", Long.class, productNo); |
| | | appendProductAuditRecord(productId); |
| | | return product(productId); |
| | | } |
| | | |
| | | @Transactional |
| | |
| | | request.sortNo() == null ? 0 : request.sortNo(), |
| | | id); |
| | | ensureUpdated(updated); |
| | | appendProductAuditRecord(id); |
| | | return product(id); |
| | | } |
| | | |
| | |
| | | |
| | | @Transactional |
| | | public Map<String, Object> auditProduct(Long id, ProductAuditRequest request) { |
| | | List<Map<String, Object>> pendingRecords = DatabaseRows.normalize(jdbcTemplate.queryForList(""" |
| | | select id from merchant_product_audit_record |
| | | where product_id = ? and deleted = 0 and audit_status = 1 |
| | | order by id desc limit 1 |
| | | """, id)); |
| | | if (!pendingRecords.isEmpty()) { |
| | | return auditProductAuditRecord(Long.valueOf(String.valueOf(pendingRecords.get(0).get("id"))), request); |
| | | } |
| | | if (request.auditStatus() != 2 && request.auditStatus() != 3) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "Invalid audit status"); |
| | | } |
| | |
| | | """, request.auditStatus(), request.auditRemark(), request.auditStatus() == 2 ? 1 : 0, id); |
| | | ensureUpdated(updated); |
| | | return product(id); |
| | | } |
| | | |
| | | @Transactional |
| | | public Map<String, Object> auditProductAuditRecord(Long auditRecordId, ProductAuditRequest request) { |
| | | if (request.auditStatus() != 2 && request.auditStatus() != 3) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "Invalid audit status"); |
| | | } |
| | | Map<String, Object> record = productAuditRecord(auditRecordId); |
| | | Number currentAuditStatus = (Number) record.get("auditStatus"); |
| | | if (currentAuditStatus == null || currentAuditStatus.intValue() != 1) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "Product audit record is not pending"); |
| | | } |
| | | int updated = jdbcTemplate.update(""" |
| | | update merchant_product_audit_record |
| | | set audit_status = ?, audit_remark = ?, audit_time = now() |
| | | where deleted = 0 and id = ? and audit_status = 1 |
| | | """, request.auditStatus(), request.auditRemark(), auditRecordId); |
| | | ensureUpdated(updated); |
| | | Long productId = Long.valueOf(String.valueOf(record.get("productId"))); |
| | | jdbcTemplate.update(""" |
| | | update merchant_product |
| | | set audit_status = ?, audit_remark = ?, audit_time = now(), status = ? |
| | | where deleted = 0 and id = ? and audit_status = 1 |
| | | """, request.auditStatus(), request.auditRemark(), request.auditStatus() == 2 ? 1 : 0, productId); |
| | | return productAuditRecord(auditRecordId); |
| | | } |
| | | |
| | | private void appendProductAuditRecord(Long productId) { |
| | | jdbcTemplate.update(""" |
| | | update merchant_product_audit_record |
| | | set audit_status = 4, audit_remark = coalesce(audit_remark, 'superseded by a new submission') |
| | | where product_id = ? and deleted = 0 and audit_status = 1 |
| | | """, productId); |
| | | jdbcTemplate.update(""" |
| | | insert into merchant_product_audit_record( |
| | | product_id, merchant_id, product_no, product_type, product_name, subtitle, cover_url, |
| | | original_price, sale_price, subsidy_amount, is_government_subsidy, stock, sales_count, |
| | | detail_text, benefit_desc, purchase_notice, valid_start_time, valid_end_time, |
| | | product_images, rights_config, audit_status, audit_remark, audit_by, submit_time, |
| | | audit_time, source_created_at |
| | | ) |
| | | select id, merchant_id, product_no, product_type, product_name, subtitle, cover_url, |
| | | original_price, sale_price, subsidy_amount, is_government_subsidy, stock, sales_count, |
| | | detail_text, benefit_desc, purchase_notice, valid_start_time, valid_end_time, |
| | | product_images, rights_config, audit_status, audit_remark, audit_by, submit_time, |
| | | audit_time, created_at |
| | | from merchant_product |
| | | where id = ? and deleted = 0 |
| | | """, productId); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | |
| | | countSql.append(" and date(c.created_at) <= ?"); |
| | | args.add(createdEnd); |
| | | } |
| | | sql.append(" order by c.id desc"); |
| | | sql.append(" order by c.created_at desc, c.id desc"); |
| | | return page(sql.toString(), countSql.toString(), page, size, args.toArray()); |
| | | } |
| | | |
| | |
| | | public Map<String, Object> settings() { Map<String, Object> result = new LinkedHashMap<>(); for (Map<String, Object> row : rows("select setting_key, setting_value from admin_setting")) result.put(String.valueOf(row.get("settingKey")), row.get("settingValue")); return result; } |
| | | @Transactional public Map<String, Object> saveSettings(Map<String, String> body) { for (String key : List.of("bookstoreDiscount", "hotelSubsidy", "homeVideoUrl", "miniProgramUrl")) jdbc.update("insert into admin_setting(setting_key, setting_value) values (?, ?) on duplicate key update setting_value = values(setting_value)", key, body.getOrDefault(key, "")); return settings(); } |
| | | |
| | | public List<Map<String, Object>> roles() { return rows("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role order by id"); } |
| | | public List<Map<String, Object>> roles() { return rows("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role order by created_at desc, id desc"); } |
| | | @Transactional public Map<String, Object> createRole(AdminRoleRequest request) { String code = roleCode(request.roleCode()); jdbc.update("insert into admin_role(role_name,role_code,remark,permission_codes,status) values (?,?,?,?,?)", request.roleName().trim(), code, request.remark(), permissions(request.permissionCodes()), request.status()); return one("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role where id=last_insert_id()"); } |
| | | @Transactional public Map<String, Object> updateRole(Long id, AdminRoleRequest request) { if ("platform_admin".equals(existingRoleCode(id))) throw new BusinessException(ErrorCode.CONFLICT, "超级管理员角色不可编辑"); String oldCode = existingRoleCode(id); String code = request.roleCode() == null || request.roleCode().isBlank() ? oldCode : request.roleCode().trim(); if (jdbc.update("update admin_role set role_name=?,role_code=?,remark=?,permission_codes=?,status=? where id=?", request.roleName().trim(), code, request.remark(), permissions(request.permissionCodes()), request.status(), id) == 0) throw new BusinessException(ErrorCode.NOT_FOUND, "Role does not exist"); jdbc.update("update admin_session s join admin_operator o on o.id=s.operator_id set s.revoked_at=now() where s.revoked_at is null and o.role_code in (?, ?)", oldCode, code); return one("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role where id=?", id); } |
| | | @Transactional public Map<String, Object> updateRoleStatus(Long id, Byte status) { String code = existingRoleCode(id); if (status == 0 && "platform_admin".equals(code)) throw new BusinessException(ErrorCode.CONFLICT, "平台管理员角色不可禁用"); jdbc.update("update admin_role set status=? where id=?", status, id); return one("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role where id=?", id); } |
| | | @Transactional public Map<String, Object> deleteRole(Long id) { String code = existingRoleCode(id); if ("platform_admin".equals(code)) throw new BusinessException(ErrorCode.CONFLICT, "平台管理员角色不可删除"); if (Boolean.TRUE.equals(jdbc.queryForObject("select exists(select 1 from admin_operator where role_code=? and deleted=0)", Boolean.class, code))) throw new BusinessException(ErrorCode.CONFLICT, "该角色仍有员工使用,不能删除"); jdbc.update("delete from admin_role where id=?", id); return Map.of("id", id, "deleted", true); } |
| | | |
| | | public List<Map<String, Object>> staff() { return rows("select id,account,display_name,mobile,role_code,position_name,customer_wechat,status,created_at from admin_operator where deleted=0 order by id"); } |
| | | public List<Map<String, Object>> staff() { return rows("select id,account,display_name,mobile,role_code,position_name,customer_wechat,status,created_at from admin_operator where deleted=0 order by created_at desc, id desc"); } |
| | | public Map<String, Object> staffAccountAvailable(String account) { String normalized = account == null ? "" : account.trim(); if (!normalized.matches("^[A-Za-z0-9]{1,32}$")) return Map.of("available", false); Long count = jdbc.queryForObject("select count(*) from admin_operator where account=?", Long.class, normalized); return Map.of("available", count == null || count == 0); } |
| | | @Transactional public Map<String, Object> createStaff(AdminStaffRequest request) { if (request.password() == null || request.password().isBlank()) throw new BusinessException(ErrorCode.BAD_REQUEST, "A password is required for a new staff account"); if (!Boolean.TRUE.equals(staffAccountAvailable(request.account()).get("available"))) throw new BusinessException(ErrorCode.CONFLICT, "登录账号已存在,请更换"); jdbc.update("insert into admin_operator(account,password_hash,display_name,mobile,role_code,position_name,customer_wechat,status) values (?,?,?,?,?,?,?,?)", request.account().trim(), MerchantPasswordHasher.encode(request.password()), request.displayName().trim(), blankToNull(request.mobile()), request.roleCode(), request.positionName().trim(), blankToNull(request.customerWechat()), request.status()); return staffOne("last_insert_id()"); } |
| | | @Transactional public Map<String, Object> updateStaff(Long id, AdminStaffRequest request) { if (isDefaultAdmin(id)) throw new BusinessException(ErrorCode.CONFLICT, "超级管理员账号不可编辑"); if (!matchesExistingAccount(id, request.account())) throw new BusinessException(ErrorCode.CONFLICT, "登录账号不可修改"); String sql = request.password() == null || request.password().isBlank() ? "update admin_operator set account=?,display_name=?,mobile=?,role_code=?,position_name=?,customer_wechat=?,status=? where id=?" : "update admin_operator set account=?,password_hash=?,display_name=?,mobile=?,role_code=?,position_name=?,customer_wechat=?,status=? where id=?"; int updated = request.password() == null || request.password().isBlank() ? jdbc.update(sql, request.account().trim(), request.displayName().trim(), blankToNull(request.mobile()), request.roleCode(), request.positionName().trim(), blankToNull(request.customerWechat()), request.status(), id) : jdbc.update(sql, request.account().trim(), MerchantPasswordHasher.encode(request.password()), request.displayName().trim(), blankToNull(request.mobile()), request.roleCode(), request.positionName().trim(), blankToNull(request.customerWechat()), request.status(), id); if (updated == 0) throw new BusinessException(ErrorCode.NOT_FOUND, "Staff account does not exist"); return staffOne("?", id); } |
| | |
| | | List<Object> rowArgs = new ArrayList<>(args); |
| | | rowArgs.add(limit); |
| | | rowArgs.add((current - 1) * limit); |
| | | List<Map<String, Object>> values = rows(query + where + " order by " + tableAlias + ".id desc limit ? offset ?", rowArgs.toArray()); |
| | | List<Map<String, Object>> values = rows(query + where + " order by " + tableAlias + ".created_at desc, " + tableAlias + ".id desc limit ? offset ?", rowArgs.toArray()); |
| | | Long total = jdbc.queryForObject(count + where, Long.class, args.toArray()); |
| | | return new PageResult<>(values, current, limit, total == null ? 0 : total); |
| | | } |
| | |
| | | public void run(ApplicationArguments args) { |
| | | try { |
| | | ensureBankCardBranchCodeColumn(); |
| | | ensureBankCardHistoryIndex(); |
| | | ensureDirectoryTable(); |
| | | importDirectoryIfEmpty(); |
| | | } catch (Exception exception) { |
| | |
| | | jdbc.execute("alter table merchant_bank_card add column branch_code varchar(12) default null after branch_name"); |
| | | } |
| | | |
| | | private void ensureBankCardHistoryIndex() { |
| | | Integer nonUnique = jdbc.query(""" |
| | | select non_unique from information_schema.statistics |
| | | where table_schema = database() and table_name = 'merchant_bank_card' |
| | | and index_name = 'uk_merchant_bank_card_hash' |
| | | limit 1 |
| | | """, result -> result.next() ? result.getInt("non_unique") : null); |
| | | if (nonUnique != null && nonUnique == 0) { |
| | | jdbc.execute("alter table merchant_bank_card drop index uk_merchant_bank_card_hash"); |
| | | } |
| | | Integer historyIndex = jdbc.query(""" |
| | | select 1 from information_schema.statistics |
| | | where table_schema = database() and table_name = 'merchant_bank_card' |
| | | and index_name = 'idx_merchant_bank_card_hash' |
| | | limit 1 |
| | | """, result -> result.next() ? 1 : null); |
| | | if (historyIndex == null) { |
| | | jdbc.execute("create index idx_merchant_bank_card_hash on merchant_bank_card(merchant_id, card_hash)"); |
| | | } |
| | | } |
| | | |
| | | private void ensureDirectoryTable() { |
| | | jdbc.execute(""" |
| | | create table if not exists bank_branch_directory ( |
| | |
| | | where id = ? and merchant_id = ? and deleted = 0 |
| | | """, request.productName().trim(), request.salePrice(), coverUrl, images, |
| | | value(request.detailText(), ""), jsonObject(notice), productId, principal.merchantId()); |
| | | appendProductAuditRecord(productId); |
| | | return product(principal, productId); |
| | | } |
| | | |
| | | private void appendProductAuditRecord(Long productId) { |
| | | jdbc.update(""" |
| | | update merchant_product_audit_record |
| | | set audit_status = 4, audit_remark = coalesce(audit_remark, 'superseded by a new submission') |
| | | where product_id = ? and deleted = 0 and audit_status = 1 |
| | | """, productId); |
| | | jdbc.update(""" |
| | | insert into merchant_product_audit_record( |
| | | product_id, merchant_id, product_no, product_type, product_name, subtitle, cover_url, |
| | | original_price, sale_price, subsidy_amount, is_government_subsidy, stock, sales_count, |
| | | detail_text, benefit_desc, purchase_notice, valid_start_time, valid_end_time, |
| | | product_images, rights_config, audit_status, audit_remark, audit_by, submit_time, |
| | | audit_time, source_created_at |
| | | ) |
| | | select id, merchant_id, product_no, product_type, product_name, subtitle, cover_url, |
| | | original_price, sale_price, subsidy_amount, is_government_subsidy, stock, sales_count, |
| | | detail_text, benefit_desc, purchase_notice, valid_start_time, valid_end_time, |
| | | product_images, rights_config, audit_status, audit_remark, audit_by, submit_time, |
| | | audit_time, created_at |
| | | from merchant_product |
| | | where id = ? and deleted = 0 |
| | | """, productId); |
| | | } |
| | | |
| | | @Transactional |
| | | public Map<String, Object> updateProductStatus(MerchantPrincipal principal, Long productId, MerchantProductStatusRequest request) { |
| | | store(principal); |
| | |
| | | List<Map<String, Object>> branchRecords = rows("select clearing_code, branch_name, bank_name, head_bank_code from bank_branch_directory where clearing_code = ? limit 1", request.branchCode().trim()); |
| | | if (branchRecords.isEmpty()) throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择真实开户行"); |
| | | Map<String, Object> branch = branchRecords.get(0); |
| | | List<Map<String, Object>> same = rows("select * from merchant_bank_card where merchant_id = ? and card_hash = ? limit 1", principal.merchantId(), hash); |
| | | if (!same.isEmpty()) { |
| | | Map<String, Object> existing = same.get(0); |
| | | if (!"rejected".equals(clean(existing.get("status")))) return bankCard(existing); |
| | | jdbc.update("update merchant_bank_card set status='pending', card_no_encrypted=?, reject_reason=null, provider_token_no=null, provider_apply_no=null, provider_request_no=null, provider_response_code=null, provider_response_message=null where id=?", sensitiveDataCrypto.encrypt(number), existing.get("id")); |
| | | submitBankCardToHuifu(principal, existing, number, branch); |
| | | return bankCard(rows("select * from merchant_bank_card where id = ?", existing.get("id")).get(0)); |
| | | } |
| | | List<Map<String, Object>> active = rows("select id from merchant_bank_card where merchant_id = ? and card_hash = ? and status in ('pending', 'approved') order by id desc limit 1", principal.merchantId(), hash); |
| | | if (!active.isEmpty()) throw new BusinessException(ErrorCode.CONFLICT, "该银行卡已有审核中或已通过的申请,请勿重复添加"); |
| | | jdbc.update("insert into merchant_bank_card(merchant_id, store_id, bank_name, branch_name, branch_code, city_code, card_tail, card_masked, card_hash, card_no_encrypted, idempotency_key) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", principal.merchantId(), principal.storeId(), clean(branch.get("bankName")), clean(branch.get("branchName")), clean(branch.get("clearingCode")), request.cityCode().trim(), number.substring(number.length() - 4), "**** **** **** " + number.substring(number.length() - 4), hash, sensitiveDataCrypto.encrypt(number), key.trim()); |
| | | Map<String, Object> card = rows("select * from merchant_bank_card where merchant_id = ? and card_hash = ?", principal.merchantId(), hash).get(0); |
| | | Map<String, Object> card = rows("select * from merchant_bank_card where merchant_id = ? and idempotency_key = ? limit 1", principal.merchantId(), key.trim()).get(0); |
| | | submitBankCardToHuifu(principal, card, number, branch); |
| | | return bankCard(rows("select * from merchant_bank_card where id = ?", card.get("id")).get(0)); |
| | | } |
| | |
| | | left join talent_card tc on tc.id = o.card_id |
| | | left join benefit b on b.id = o.benefit_id |
| | | left join (select order_id, max(verify_time) verify_time from verify_record where status = 1 group by order_id) verified on verified.order_id = o.id |
| | | """ + conditions + " order by o.id desc limit ? offset ?"; |
| | | """ + conditions + " order by o.created_at desc, o.id desc limit ? offset ?"; |
| | | List<Object> recordArguments = new ArrayList<>(arguments); |
| | | recordArguments.add(safeSize); |
| | | recordArguments.add((safePage - 1) * safeSize); |
| | |
| | | left join benefit b on b.id = r.benefit_id |
| | | left join merchant_product p on p.id = o.product_id and p.deleted = 0 |
| | | left join merchant m on m.id = r.merchant_id |
| | | """.formatted(ORDER_CARD_LEVEL_SQL) + where + " order by r.id desc limit ? offset ?", pageArgs.toArray()); |
| | | """.formatted(ORDER_CARD_LEVEL_SQL) + where + " order by r.created_at desc, r.id desc limit ? offset ?", pageArgs.toArray()); |
| | | Long total = jdbcTemplate.queryForObject(""" |
| | | select count(*) |
| | | from order_refund r |
| | |
| | | left join talent_card tc on tc.id = r.card_id |
| | | left join benefit b on b.id = r.benefit_id |
| | | left join merchant m on m.id = r.merchant_id |
| | | """.formatted(ORDER_CARD_LEVEL_SQL) + where + " order by r.id desc limit ? offset ?", pageArgs.toArray()); |
| | | """.formatted(ORDER_CARD_LEVEL_SQL) + where + " order by r.verify_time desc, r.id desc limit ? offset ?", pageArgs.toArray()); |
| | | Long total = jdbcTemplate.queryForObject(""" |
| | | select count(*) |
| | | from verify_record r |
| | |
| | | left join benefit b on b.id = o.benefit_id |
| | | left join merchant_product p on p.id = o.product_id and p.deleted = 0 |
| | | left join merchant m on m.id = o.merchant_id |
| | | """.formatted(ORDER_CARD_LEVEL_SQL) + where + " order by o.id desc limit ? offset ?", pageArgs.toArray()); |
| | | """.formatted(ORDER_CARD_LEVEL_SQL) + where + " order by o.created_at desc, o.id desc limit ? offset ?", pageArgs.toArray()); |
| | | Long total = jdbcTemplate.queryForObject(""" |
| | | select count(*) |
| | | from benefit_order o |
| | |
| | | result.put("benefitId", benefit.get("id")); |
| | | result.put("serviceId", serviceId); |
| | | result.put("title", benefit.get("title")); |
| | | result.put("applicableCards", benefit.get("applicableCards")); |
| | | result.put("applicableCards", text(matchedRight.get("tag"), "")); |
| | | result.put("heroImage", benefit.get("heroImage")); |
| | | result.put("rightsText", text(matchedRight.get("detail"), text(benefit.get("rightsText"), "暂无使用说明"))); |
| | | result.put("available", available); |
| | |
| | | @RequestParam(required = false) String mode, |
| | | @RequestParam(required = false) String sortField, |
| | | @RequestParam(required = false) String sortOrder, |
| | | @RequestParam(required = false) Long talentId, |
| | | @RequestParam(defaultValue = "1") int page, |
| | | @RequestParam(defaultValue = "10") int size |
| | | ) { |
| | | return ApiResponse.success(portalService.merchants(type, keyword, categoryId, mode, sortField, sortOrder, page, size)); |
| | | return ApiResponse.success(portalService.merchants(type, keyword, categoryId, mode, sortField, sortOrder, talentId, page, size)); |
| | | } |
| | | |
| | | @GetMapping("/topics/food") |
| | |
| | | String mode, |
| | | String sortField, |
| | | String sortOrder, |
| | | Long talentId, |
| | | int page, |
| | | int size |
| | | ) { |
| | |
| | | from merchant |
| | | """ + merchantWhere + " order by sort_no asc, id asc", |
| | | "select count(*) from merchant" + merchantWhere, page, size, args.toArray()); |
| | | List<Map<String, Object>> records = result.records().stream().map(this::enrichMerchantSearchRow).toList(); |
| | | List<Map<String, Object>> records = result.records().stream().map(row -> { |
| | | Map<String, Object> item = enrichMerchantSearchRow(row); |
| | | Long merchantId = item.get("id") instanceof Number id ? id.longValue() : null; |
| | | item.put("couponAvailable", merchantId != null && hasActiveMerchantCoupon(merchantId)); |
| | | item.put("discountText", merchantId == null ? "" : merchantLowestTalentDiscount(merchantId, talentId)); |
| | | return item; |
| | | }).toList(); |
| | | return new PageResult<>(records, result.page(), result.size(), result.total()); |
| | | } |
| | | |
| | |
| | | card.put("name", value(row.get("name"), "-")); |
| | | card.put("gender", genderText(row.get("gender"))); |
| | | card.put("cardNo", value(row.get("cardNo"), "-")); |
| | | card.put("workUnit", maskWorkUnit((String) row.get("workUnit"))); |
| | | card.put("workUnit", value(row.get("workUnit"), "未填写")); |
| | | card.put("status", status); |
| | | card.put("statusText", cardStatusText(status)); |
| | | card.put("canShowCode", "ACTIVE".equals(status)); |
| | |
| | | private String maskCardNo(String cardNo) { |
| | | if (cardNo == null || cardNo.isBlank()) return "-"; |
| | | return cardNo.length() <= 6 ? cardNo : cardNo.substring(0, 2) + "****" + cardNo.substring(cardNo.length() - 4); |
| | | } |
| | | |
| | | private String maskWorkUnit(String workUnit) { |
| | | if (workUnit == null || workUnit.isBlank()) return "未填写"; |
| | | return workUnit.length() <= 4 ? workUnit.substring(0, 1) + "***" : workUnit.substring(0, 2) + "****" + workUnit.substring(workUnit.length() - 2); |
| | | } |
| | | |
| | | private String genderText(Object gender) { |
| | |
| | | countSql.append(" and register_time < ?"); |
| | | args.add(registerEnd.plusDays(1).atStartOfDay()); |
| | | } |
| | | sql.append(" order by id desc limit ? offset ?"); |
| | | sql.append(" order by register_time desc, id desc limit ? offset ?"); |
| | | Object[] pageArgs = Arrays.copyOf(args.toArray(), args.size() + 2); |
| | | pageArgs[args.size()] = safeSize; |
| | | pageArgs[args.size() + 1] = (safePage - 1) * safeSize; |
| | |
| | | or m.contact_mobile like concat('%', #{keyword}, '%') |
| | | or m.address like concat('%', #{keyword}, '%')) |
| | | </if> |
| | | order by m.sort_no desc, m.id desc |
| | | order by m.sort_no desc, m.created_at desc, m.id desc |
| | | limit #{size} offset #{offset} |
| | | </script> |
| | | """) |
| | |
| | | </foreach> |
| | | ) |
| | | </if> |
| | | order by m.sort_no desc, m.id desc |
| | | order by m.sort_no desc, m.created_at desc, m.id desc |
| | | limit #{size} offset #{offset} |
| | | </script> |
| | | """) |
| | |
| | | or m.contact_mobile like concat('%', #{keyword}, '%') |
| | | or m.address like concat('%', #{keyword}, '%')) |
| | | </if> |
| | | order by b.sort_no desc, m.sort_no desc, m.id desc |
| | | order by b.sort_no desc, m.sort_no desc, m.created_at desc, m.id desc |
| | | limit #{size} offset #{offset} |
| | | </script> |
| | | """) |
| | |
| | | join merchant mm on mm.id = pm.merchant_id and mm.deleted = 0 |
| | | where pm.package_id = p.id and pm.deleted = 0 and mm.merchant_name like concat('%', #{merchantKeyword}, '%') |
| | | )) |
| | | order by c.id desc |
| | | order by c.created_at desc, c.id desc |
| | | limit #{size} offset #{offset} |
| | | """) |
| | | List<Map<String, Object>> selectAdminPage(@Param("packageId") Long packageId, |
| | |
| | | where mm.id = p.merchant_id and mm.deleted = 0 and mm.merchant_name like concat('%', #{merchantKeyword}, '%') |
| | | )) |
| | | group by p.id |
| | | order by p.sort_no desc, p.id desc |
| | | order by p.sort_no desc, p.created_at desc, p.id desc |
| | | limit #{size} offset #{offset} |
| | | """) |
| | | List<Map<String, Object>> selectAdminPage(@Param("merchantId") Long merchantId, |
| | |
| | | where pm.package_id = p.id and pm.merchant_id = #{merchantId} and pm.deleted = 0 |
| | | )) |
| | | group by p.id |
| | | order by p.sort_no desc, p.id desc |
| | | order by p.sort_no desc, p.created_at desc, p.id desc |
| | | """) |
| | | List<Map<String, Object>> selectPortalPackages(@Param("merchantId") Long merchantId); |
| | | |
| | |
| | | and (#{keyword} is null or #{keyword} = '' or m.merchant_name like concat('%', #{keyword}, '%') |
| | | or m.address like concat('%', #{keyword}, '%') or m.contact_mobile like concat('%', #{keyword}, '%')) |
| | | and (#{merchantId} is null or m.id = #{merchantId}) |
| | | order by m.sort_no desc, m.id desc |
| | | order by m.sort_no desc, m.created_at desc, m.id desc |
| | | limit #{size} offset #{offset} |
| | | """) |
| | | List<Map<String, Object>> selectConfigPage(@Param("merchantId") Long merchantId, |
| | |
| | | and (#{cardLevel} is null or #{cardLevel} = '' or r.card_level = #{cardLevel}) |
| | | and (#{enabled} is null or r.enabled = #{enabled}) |
| | | and (#{keyword} is null or #{keyword} = '' or m.merchant_name like concat('%', #{keyword}, '%') or m.address like concat('%', #{keyword}, '%')) |
| | | order by m.sort_no desc, m.id desc, |
| | | order by m.sort_no desc, m.created_at desc, m.id desc, |
| | | case r.card_level when '卓越人才卡' then 1 when '杰出人才卡' then 2 when '菁英人才卡' then 3 else 99 end |
| | | limit #{size} offset #{offset} |
| | | """) |
| | |
| | | and r.status = 1 |
| | | and r.card_level = #{cardLevel} |
| | | and (#{merchantId} is null or r.merchant_id = #{merchantId}) |
| | | order by m.sort_no desc, m.id desc |
| | | order by m.sort_no desc, m.created_at desc, m.id desc |
| | | """) |
| | | List<Map<String, Object>> selectAvailableForCard(@Param("cardLevel") String cardLevel, @Param("merchantId") Long merchantId); |
| | | } |
| | |
| | | import java.util.List; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import jakarta.validation.Valid; |
| | | import jakarta.validation.constraints.DecimalMin; |
| | | import jakarta.validation.constraints.Min; |
| | | import jakarta.validation.constraints.NotBlank; |
| | | import jakarta.validation.constraints.NotNull; |
| | | import jakarta.validation.constraints.Size; |
| | | |
| | | @Schema(name = "后台票包保存请求", description = "后台录入或修改票包信息") |
| | |
| | | String packageName, |
| | | |
| | | @Schema(description = "票包数量", example = "888") |
| | | @Min(value = 0, message = "票包数量不能小于0") |
| | | @NotNull(message = "票包数量不能为空") |
| | | @Min(value = 1, message = "票包数量必须为正整数") |
| | | Integer totalQuantity, |
| | | |
| | | @Schema(description = "指定兑换商家ID", example = "1") |
| | |
| | | Integer sortNo, |
| | | |
| | | @Schema(description = "每个票包按人才等级配置的可领数量和重置周期") |
| | | List<TicketPackageLevelRuleRequest> levelRules |
| | | List<@Valid TicketPackageLevelRuleRequest> levelRules |
| | | ) { |
| | | public record TicketPackageLevelRuleRequest( |
| | | @Schema(description = "人才卡等级", example = "卓越人才卡") |
| | |
| | | Integer enabled, |
| | | |
| | | @Schema(description = "可领数量", example = "2") |
| | | @NotNull(message = "可领数量不能为空") |
| | | @Min(value = 1, message = "可领数量必须大于0") |
| | | Integer claimableCount, |
| | | |
| | |
| | | # Keep disabled until the Huifu test/production product, merchant and keys are provisioned. |
| | | enabled: true |
| | | environment: prod |
| | | system-id: 6666000212191071 |
| | | system-id: 6666000226048687 |
| | | product-id: KAZX |
| | | merchant-id: 6666000212191071 |
| | | merchant-id: 6666000226048687 |
| | | mini-program-app-id: wxcfee477acb54d179 |
| | | private-key: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcsR3R3nUFCv/4l6vczGk6j4UgZhuMm/fTs2MQQqFkIuoISd38TOiIMaLS3KLYL+D+qhUCps56svOK/3M/kdyXfPsBphJyXB5Ar7il8pJ6BDa38StzTrlRm+IXX+4diJvqxUimetjpx0OfnqRkNYQWc/ZVeoEdQbITqLyxogy0R3rkR/wnOTwxHBbkCYsDCxSon4W0SoM5fLs3o947INbPUbUsf0vL5l6NiqlV9YsZ3qAWvvPoPX4HI1s5maMiVKfUwe27P3ARW9n6n3tadBzbB6lrmAEcFoihXpZjNN9UaRKjYfYjTS9JRkDqRyYGhz5cHp9GthEksMvhLP84oEanAgMBAAECggEAKV6d5+Ya8TzpPiIVTxNtCPgRhu9OgL5BHVS0Z/ufjselEJEoGc/YaPTp71AvcnKZrcMMDwopY73RjTZKoFMLcbeesd296xGlLyD12GKGB88K/1+eQDBdv00v0UoTnRqPR9nUh5fRo09n7EG/V3HFsQPm2sSgSDArKwOW5jNJtWLsGIVCW8gg3RQ9YlZ5HW85neBnjBFnrMA9S/hxwZD336M8CiViIa0v9o5CS7qxAqXMzzoOC7Vr3hZ/X0i/5GrSH9DRO7ougSmP4F/zCEc2dViA0rOJ6m3OuS0osh5FulihOBOjI5uLETEt4K/7PuV9xOd4oAz0OBpJ2bhPf3YpGQKBgQDQdEcmnlULa3jTSrbr0vYA3iMpcls/8F8+I/QymMXHAUeVyc8XKuGzblx+/n3x8ZoAvdVg1ZAO5wgLh/P0BEL74iWhfHedjt8xcdk2R5U0RuOnx1TUTKrmLKRZ5gStNFnGlK4yVwpiNqHn/cr+gYYeZSPARFVi00WdXbqVbEEf2wKBgQDAbmo4uHrmHN5GYvt8lVmT9aha1IpKOOLenjd+Y/vKGJcvHzOVCHnOh8OGO/4p7CxOq0sshER713jW2cFGO3xpFEFpIc45pDQ8Ut0XuSoOjKtwPzid8vKX7A1If6FDLMNy7bYR1qqnIJNJnN/f3lsksX+es7QTuRShhA0X0mnEJQKBgBE5OlPHi1FZPO+AYeLUWH0z3YXkLAP5cXUp9Vhk5RqPUYHf3duVMHbp7HyVvIRMKSRciw/oNXH1xuVlA1QrMfmr8t4H8b4z+t8UABWLDpn2LYNEZg7wksJX9/Bxn/tLswtEZ6xDItmAmBoaDza+Lv2975lITCHRQRRClVMZUbw3AoGAVFrSs6SjM2aqRZ60ccuipgrFT3PmX10Kjt46o2w+/zvCYoTB/u3lP5UiMCDybPw7/Ax4+r3Mq6EsNrrkT95ZzlJzzhtkpL4Hq/4DL5gbCJ+/s4fzUMIkeFahCAvy7FieIS51ZuJ8kqDkEBv1w9RcjfxFXpOWOCMlGRyjBLNXbAUCgYEAjcTEuXctlGEjtO1MBaHc4hkkpiPG7gxV/Bq1XXLszv4LTaOfDEpjGGBpVYtb3URYrif/HzX8T1KHELvPziuqP2ghaJQzbztep1TGQ48A5VpOvHdA/w/+k4JJl0PyKBPrFXiHIw1if7rnvOLt27TymwXSaJquSz7Sz2+wXFxzKJk= |
| | | platform-public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgxOuXt1UEdZNPk6sntqiIlcs7e+0fZ3CVgKMV2Qv4ixwKmha5etL5WyIONmjoGEHePHpanYkXvm0kHkbbDkjpyHDFdEshuP1eGx5tRUTISRvXY/iFuD3xyeWSsvvjW6hS29OvqVCPcnBUC7jcRj15dJS2IpYR0iDGNS+/M0TyOFwRQME07At+qO+MSjKu4aRIjlglukjD7YbE2s35uV066nsBUCZUNpGPuju1pieUW4zyoIW6K70dyXCQy61oRI6JmA4JRCOJV1BE80N3GrLI4Pbq1C3ZBTgOPi5RWYNq1hfu807BDkPLc3dJKYt22Ea1jPJsg6e/B9Xbzh6JaUNAQIDAQAB |
| | | private-key: MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCzPfipd3gxqHSr/ZiZG4hGXXulhXY9PCdf644SiE8IpH2Gf8pmvfTgs77FoyWVAPmYyF3JrQGQTlKjtHzkUGW20FETwopDwh7KP9zUvrHRHsAhCEi+kkD+4OO4BgP6I4sxJRANcVQM/G0woScxdHJB74gxVVnlxr3RpiR2aKyaEX6jCIsYiCXcuNJ7tpjX5H6pnFEldjWQpJgd1QnLrybgf66mEzQ/ym7SvaQ8oNWiILcpmUsAOUMwMgEr4J9MWTW/XH78++GtvrjEH311lmOk7+cM9qhGzvguXUFQkLnckseYnYAoEUaXmioBXu5qrqifMGZYCThLX9pzbJsFgswxAgMBAAECggEAbVdxmJert8dGnSDPdR8Ji7s9UWTQE1zdvDLq8T070U7e0CymoPRiLK0ZxGDY7UOx/1TDeeLTIF/XI8xXn8O3uMTtoLs9M0Z5F/HLWfCWcrZqh/wQvEhl49X2tGzfAjmqkKlGWJz4pA9c4Jx+Vzns9XIUx9uEQppxczIB7kW+fSEHGGIrilMbFPDWlqkjMuMBN7pDNUCMh9zEYCweLAzB1ZM0W3DVrRntCHJj1nLEFmBAsVDDy5J/m1Ew1SQ0252Ky8arYF6FjeOpnzw8WbCAXk6Cb3hjz83KWyoLXMzrHvkzEaR8eSaB5rmDP2Ukz5FIclqK91aMFVL8OQzq7Vh2fQKBgQDkWQIFK8s+YasyqcM+goXeYh+89anwbSbFB3yWrkIJrfh2aCjV0i/71tqh3aGKIZXokjoBR6ASWlHHtYJtAoms5wdvp6W/8phWlpM4clEsb8BHlQhWy+XCvH+KVVZAQrNMsAZPtUp/FhURnXJm7c7A9z3a0O10pSRu0ZuDcWc9OwKBgQDI8qS85p5Z5uy9WWcU676wS7dXyXK6Ev6Z3gMQpj2sGwCDOj0eZr3QZnLkFzLqV87pp5hsqX2xjphOrlVFKQDyWES5VF0hkX6jRMyodRw72FNEBAtEHXTaLY8QuBS6Sdxktywa50KZzbyJJ3RZgdFCpMwLxMakRG090nzhsu/1gwKBgQDA6pHSXJVxNoK0YsgY35zJrIyzz7iq7wx6GUEg7EfOdbQcFROq0dDIK1S6u8vmL0m/Gwr2WdTarIO6/ZYbErHN6cFWi4R1Dje1r7DhV3NM33zK/RX2fny2cRovfLlJwRBKEczDOp6dj8JtB1/HpdGvJTKU0z5J9ds55MxF2GEoUQKBgQCFp44W7rVISqYP0xUS/zIYBGRwCVc7mEKriNU1D71ZaaRPpD7ep6t8n4evWhbNc5X2JzjknV8y6As52g5xL7KnfjpHomtHVoSKh8owCaS70UigpqfvqD/+lergTe/LcDEc9BBr3IGZ1xOdjhZ/Ki9LSl1FPp54x+J5CjVx0Rh1jQKBgGfyr/mG9BMoXu6tHUoxxY5xw0Q8IA2N0kcSoilp233QQRBnxnO4zm7FITpnovaGvTj9LT6K+kDWxDTGG/Da+qzxtAMquJ5ZRpWv3fGEu6jzsJWcxTLMTiNroezq8oynNQbVqLlst1hl0LP6hX60b4jjEKHcQQKCerGav19Ay2Hj |
| | | platform-public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhRV2nN2xnEGw4giX8O/K3j3UdD80QKeigZqkPaVPULBc8D/Ov9bzioz/eMsprbI1grU8M6LCcGC0eFgBpA5YHxKzL4bjiAxzYbbzK/l+LaqD1Xzg4/k5qYiPe7I8BUU5FcpR37sRgO+14dwUYtFyjuIDENcsiUcadEpt45xxfFDKm+xaKtEBGDYS4fRQoVd/QvXTf6dZwWyK15uB84gqyjQFwJF+r4FtmSwe11bLf83D9M5nHpfZ8xiRNPGUXWaoeX6TT/QMY94Ep5hdkqGiNYJsk8Fdvdqkpelqqk4EGeGNBoHcvEZ6pIorFNUTZ0L99BI8poOGia+71W82xd20vwIDAQAB |
| | | notify-url: ${HUIFU_NOTIFY_URL:http://221.182.45.100:11451/api/payments/huifu/notify} |
| | | withdrawal-product-id: ${HUIFU_WITHDRAWAL_PRODUCT_ID:KAZX} |
| | | withdrawal-notify-url: ${HUIFU_WITHDRAWAL_NOTIFY_URL:http://221.182.45.100:11451/api/payments/huifu/withdrawal-notify} |
| New file |
| | |
| | | alter table merchant_bank_card |
| | | drop index uk_merchant_bank_card_hash; |
| | | |
| | | create index idx_merchant_bank_card_hash |
| | | on merchant_bank_card(merchant_id, card_hash); |
| New file |
| | |
| | | create table if not exists merchant_product_audit_record ( |
| | | id bigint unsigned not null auto_increment, |
| | | product_id bigint unsigned not null, |
| | | merchant_id bigint unsigned not null, |
| | | product_no varchar(64) not null, |
| | | product_type varchar(64) not null, |
| | | product_name varchar(128) not null, |
| | | subtitle varchar(255) default null, |
| | | cover_url varchar(500) default null, |
| | | original_price decimal(10,2) not null default 0.00, |
| | | sale_price decimal(10,2) not null default 0.00, |
| | | subsidy_amount decimal(10,2) not null default 0.00, |
| | | is_government_subsidy tinyint not null default 0, |
| | | stock int not null default -1, |
| | | sales_count int not null default 0, |
| | | detail_text text, |
| | | benefit_desc text, |
| | | purchase_notice json default null, |
| | | valid_start_time datetime default null, |
| | | valid_end_time datetime default null, |
| | | product_images json default null, |
| | | rights_config json default null, |
| | | audit_status tinyint not null default 0, |
| | | audit_remark varchar(500) default null, |
| | | audit_by bigint unsigned default null, |
| | | submit_time datetime default null, |
| | | audit_time datetime default null, |
| | | source_created_at datetime default null, |
| | | created_at datetime not null default current_timestamp, |
| | | deleted tinyint not null default 0, |
| | | primary key (id), |
| | | key idx_product_audit_record_product (product_id, id), |
| | | key idx_product_audit_record_status (audit_status), |
| | | key idx_product_audit_record_submit_time (submit_time), |
| | | key idx_product_audit_record_merchant (merchant_id) |
| | | ) engine=InnoDB default charset=utf8mb4 comment='merchant product audit history'; |
| | | |
| | | insert into merchant_product_audit_record( |
| | | product_id, merchant_id, product_no, product_type, product_name, subtitle, cover_url, |
| | | original_price, sale_price, subsidy_amount, is_government_subsidy, stock, sales_count, |
| | | detail_text, benefit_desc, purchase_notice, valid_start_time, valid_end_time, |
| | | product_images, rights_config, audit_status, audit_remark, audit_by, submit_time, |
| | | audit_time, source_created_at |
| | | ) |
| | | select p.id, p.merchant_id, p.product_no, p.product_type, p.product_name, p.subtitle, p.cover_url, |
| | | p.original_price, p.sale_price, p.subsidy_amount, p.is_government_subsidy, p.stock, p.sales_count, |
| | | p.detail_text, p.benefit_desc, p.purchase_notice, p.valid_start_time, p.valid_end_time, |
| | | p.product_images, p.rights_config, p.audit_status, p.audit_remark, p.audit_by, p.submit_time, |
| | | p.audit_time, p.created_at |
| | | from merchant_product p |
| | | where not exists ( |
| | | select 1 from merchant_product_audit_record r where r.product_id = p.id |
| | | ); |