| New file |
| | |
| | | <template> |
| | | <view class="page"> |
| | | <view class="page-header"> |
| | | <view class="safe-space" :style="'height:' + navTop + 'px'"></view> |
| | | <view class="nav-row" :style="'height:' + navHeight + 'px;padding-right:' + navRight + 'px'"> |
| | | <view class="back-button" hover-class="back-button-active" @tap="goBack"><image src="/static/news-assets/arrow-left.svg" mode="aspectFit"></image></view> |
| | | <text class="nav-title">{{detailMode ? '政策详情' : '展示类服务政策'}}</text> |
| | | <view class="capsule-space" :style="'width:' + navWidth + 'px'"></view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="!detailMode" class="list-content"> |
| | | <view v-if="listLoading" class="list-state"><text>正在加载展示服务</text></view> |
| | | <view v-else-if="listError" class="list-state error-state"><text>展示服务加载失败</text><text class="retry-text" @tap="loadList">重新加载</text></view> |
| | | <view v-else-if="records.length == 0" class="list-state"><text>暂无展示类服务</text></view> |
| | | <view v-else class="policy-list"> |
| | | <view class="policy-item" v-for="item in records" :key="item.merchantId" @tap="openDetail(item)"> |
| | | <image class="policy-image" :src="item.coverUrl != '' ? resolveApiUrl(item.coverUrl) : ''" mode="aspectFill" @error="replaceImage"></image> |
| | | <view class="policy-info"> |
| | | <text class="policy-name">{{item.merchantName}}</text> |
| | | <text v-if="item.serviceLabel != ''" class="policy-label">{{item.serviceLabel}}</text> |
| | | <text class="policy-text">{{item.policyText}}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-else class="detail-content"> |
| | | <view v-if="detailLoading" class="list-state"><text>正在加载政策详情</text></view> |
| | | <view v-else-if="detailError" class="list-state error-state"><text>政策详情加载失败</text><text class="retry-text" @tap="reloadDetail">重新加载</text></view> |
| | | <view v-else class="detail-card"> |
| | | <view class="detail-hero"> |
| | | <image class="detail-image" :src="detail.coverUrl != '' ? resolveApiUrl(detail.coverUrl) : ''" mode="aspectFill" @error="replaceImage"></image> |
| | | <view class="detail-shade"></view> |
| | | <text class="detail-name">{{detail.merchantName}}</text> |
| | | </view> |
| | | <view class="detail-body"> |
| | | <view class="detail-section"> |
| | | <text class="detail-title">优惠政策</text> |
| | | <text class="detail-text">{{detail.policyText != '' ? detail.policyText : detail.rightsText}}</text> |
| | | </view> |
| | | <view v-if="detail.cardName != '' || detail.cardLevel != ''" class="detail-section"> |
| | | <text class="detail-title">适用人才卡</text> |
| | | <text class="detail-text">{{detail.cardName != '' ? detail.cardName : detail.cardLevel}}</text> |
| | | </view> |
| | | <view v-if="detail.handlingInstructions != ''" class="detail-section"> |
| | | <text class="detail-title">办理说明</text> |
| | | <text class="detail-text">{{detail.handlingInstructions}}</text> |
| | | </view> |
| | | <view v-if="detail.address != ''" class="detail-section"> |
| | | <text class="detail-title">商户地址</text> |
| | | <text class="detail-text">{{detail.address}}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="detailMode && !detailLoading && !detailError" class="action-bar"> |
| | | <view class="action-button" @tap="openMap"><text>导航</text></view> |
| | | <view class="action-button primary" @tap="consult"><text>电话咨询</text></view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup lang="uts"> |
| | | import { apiEnvironment } from '../../config/api.uts' |
| | | import { getDisplayMerchantPolicies, getDisplayMerchantPolicyDetail, recordDisplayMerchantPolicyEvent } from '../../services/portal.uts' |
| | | import { resolveApiUrl } from '../../services/http.uts' |
| | | import { getSession } from '../../services/session.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | const navTop = ref(28) |
| | | const navHeight = ref(32) |
| | | const navWidth = ref(88) |
| | | const navRight = ref(10) |
| | | const talentId = ref('') |
| | | const cardId = ref('') |
| | | const detailMode = ref(false) |
| | | const records = ref<any[]>([]) |
| | | const listLoading = ref(true) |
| | | const listError = ref(false) |
| | | const detailLoading = ref(false) |
| | | const detailError = ref(false) |
| | | const currentMerchantId = ref('') |
| | | const detail = ref({ merchantName: '', coverUrl: '', policyText: '', rightsText: '', handlingInstructions: '', address: '', contactMobile: '', cardName: '', cardLevel: '', serviceLabel: '', latitude: 0, longitude: 0 }) |
| | | |
| | | onLoad((options: any) => { |
| | | // #ifdef MP-WEIXIN |
| | | const capsule = uni.getMenuButtonBoundingClientRect() |
| | | const systemInfo = uni.getSystemInfoSync() |
| | | navTop.value = capsule.top |
| | | navHeight.value = capsule.height |
| | | navWidth.value = capsule.width |
| | | navRight.value = systemInfo.windowWidth - capsule.right |
| | | // #endif |
| | | const session = getSession() |
| | | if (session != null && session.talentId != null) talentId.value = session.talentId as string |
| | | if (talentId.value == '' && apiEnvironment == 'development') talentId.value = '2' |
| | | if (options != null && options['cardId'] != null) cardId.value = options['cardId'] as string |
| | | if (options != null && options['merchantId'] != null && options['merchantId'] != '') { |
| | | detailMode.value = true |
| | | loadDetail('' + options['merchantId']) |
| | | } else { |
| | | loadList() |
| | | } |
| | | }) |
| | | |
| | | const loadList = () => { |
| | | listLoading.value = true |
| | | listError.value = false |
| | | getDisplayMerchantPolicies(talentId.value, cardId.value, '', '', 1, 50, (data: any) => { |
| | | records.value = data != null && data.records instanceof Array ? data.records as any[] : [] |
| | | listLoading.value = false |
| | | }, (error) => { listLoading.value = false; listError.value = true }) |
| | | } |
| | | const openDetail = (item: any) => { |
| | | detailMode.value = true |
| | | loadDetail('' + item.merchantId) |
| | | } |
| | | const reloadDetail = () => loadDetail(currentMerchantId.value) |
| | | const loadDetail = (merchantId: string) => { |
| | | if (merchantId == '') return |
| | | currentMerchantId.value = merchantId |
| | | detailLoading.value = true |
| | | detailError.value = false |
| | | getDisplayMerchantPolicyDetail(merchantId, talentId.value, cardId.value, (data: any) => { |
| | | detailLoading.value = false |
| | | detail.value = { |
| | | merchantName: data != null && data.merchantName != null ? String(data.merchantName) : '', |
| | | coverUrl: data != null && data.coverUrl != null ? String(data.coverUrl) : '', |
| | | policyText: data != null && data.policyText != null ? String(data.policyText) : '', |
| | | rightsText: data != null && data.rightsText != null ? String(data.rightsText) : '', |
| | | handlingInstructions: data != null && data.handlingInstructions != null ? String(data.handlingInstructions) : '', |
| | | address: data != null && data.address != null ? String(data.address) : '', |
| | | contactMobile: data != null && data.contactMobile != null ? String(data.contactMobile) : '', |
| | | cardName: data != null && data.cardName != null ? String(data.cardName) : '', |
| | | cardLevel: data != null && data.cardLevel != null ? String(data.cardLevel) : '', |
| | | serviceLabel: data != null && data.serviceLabel != null ? String(data.serviceLabel) : '', |
| | | latitude: data != null && data.latitude != null ? Number(data.latitude) : 0, |
| | | longitude: data != null && data.longitude != null ? Number(data.longitude) : 0 |
| | | } |
| | | recordDisplayMerchantPolicyEvent(merchantId, talentId.value, cardId.value, 'VIEW', '', () => {}, () => {}) |
| | | }, (error) => { detailLoading.value = false; detailError.value = true }) |
| | | } |
| | | const consult = () => { |
| | | if (detail.value.contactMobile == '') { |
| | | uni.showToast({ title: '该商户暂未提供联系电话', icon: 'none' }) |
| | | return |
| | | } |
| | | recordDisplayMerchantPolicyEvent(currentMerchantId.value, talentId.value, cardId.value, 'CONSULT', '', () => {}, () => {}) |
| | | uni.makePhoneCall({ phoneNumber: detail.value.contactMobile, fail: () => uni.showToast({ title: '暂时无法拨打电话', icon: 'none' }) }) |
| | | } |
| | | const openMap = () => { |
| | | if (detail.value.latitude <= 0 || detail.value.longitude <= 0) { |
| | | uni.showToast({ title: '该商户暂未配置导航地址', icon: 'none' }) |
| | | return |
| | | } |
| | | uni.openLocation({ latitude: detail.value.latitude, longitude: detail.value.longitude, name: detail.value.merchantName, address: detail.value.address, scale: 18, fail: () => uni.showToast({ title: '暂时无法打开地图导航', icon: 'none' }) }) |
| | | } |
| | | const replaceImage = () => {} |
| | | const goBack = () => safeBack('/pages/benefits/benefits') |
| | | </script> |
| | | |
| | | <style> |
| | | .page { display: flex; width: 100%; height: 100vh; background-color: #F5F7FA; flex-direction: column; } |
| | | .page-header { position: relative; z-index: 10; flex-shrink: 0; } |
| | | .safe-space { height: 28px; } |
| | | .nav-row { display: flex; height: 32px; box-sizing: border-box; padding-left: 20rpx; padding-right: 10px; 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; } |
| | | .back-button-active { opacity: .55; } |
| | | .nav-title { position: absolute; left: 50%; top: 50%; color: #171B22; font-size: 37rpx; font-weight: 700; transform: translate(-50%, -50%); white-space: nowrap; } |
| | | .capsule-space { width: 88px; height: 100%; flex-shrink: 0; } |
| | | .list-content { min-height: 0; padding: 40rpx 40rpx 60rpx; flex: 1; overflow-y: auto; } |
| | | .list-state { display: flex; min-height: 240rpx; color: #8A94A1; font-size: 25rpx; flex-direction: column; justify-content: center; align-items: center; } |
| | | .error-state { flex-direction: column; } |
| | | .retry-text { margin-top: 18rpx; color: #F26A38; } |
| | | .policy-list { display: flex; flex-direction: column; } |
| | | .policy-item { display: flex; min-height: 150rpx; margin-bottom: 22rpx; box-sizing: border-box; padding: 22rpx; border-radius: 22rpx; background-color: #FFFFFF; flex-direction: row; align-items: center; box-shadow: 0 9rpx 25rpx rgba(68,78,91,.04); } |
| | | .policy-image { width: 116rpx; height: 116rpx; flex-shrink: 0; border-radius: 14rpx; background-color: #F0F2F5; } |
| | | .policy-info { min-width: 0; margin-left: 20rpx; flex: 1; } |
| | | .policy-name { display: block; color: #20252D; font-size: 29rpx; font-weight: 700; } |
| | | .policy-label { display: block; margin-top: 8rpx; color: #F26A38; font-size: 23rpx; } |
| | | .policy-text { display: block; margin-top: 10rpx; color: #7B8592; font-size: 23rpx; line-height: 34rpx; } |
| | | .detail-content { min-height: 0; padding: 0 0 130rpx; flex: 1; overflow-y: auto; } |
| | | .detail-card { display: flex; flex-direction: column; } |
| | | .detail-hero { position: relative; height: 300rpx; } |
| | | .detail-image { width: 100%; height: 100%; background-color: #E7EAEF; } |
| | | .detail-shade { position: absolute; left: 0; right: 0; bottom: 0; height: 130rpx; background-image: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,.5) 100%); } |
| | | .detail-name { position: absolute; left: 36rpx; bottom: 26rpx; color: #FFFFFF; font-size: 36rpx; font-weight: 700; } |
| | | .detail-body { margin-top: -26rpx; box-sizing: border-box; padding: 30rpx 36rpx 40rpx; border-radius: 26rpx 26rpx 0 0; background-color: #F5F7FA; } |
| | | .detail-section { margin-top: 26rpx; } |
| | | .detail-title { display: block; color: #242A33; font-size: 27rpx; font-weight: 700; } |
| | | .detail-text { display: block; margin-top: 14rpx; color: #697381; font-size: 25rpx; line-height: 38rpx; } |
| | | .action-bar { display: flex; position: fixed; z-index: 20; left: 0; right: 0; bottom: 0; box-sizing: border-box; padding: 18rpx 40rpx calc(18rpx + env(safe-area-inset-bottom)); background-color: #FFFFFF; flex-direction: row; } |
| | | .action-button { display: flex; height: 80rpx; margin-right: 20rpx; border-radius: 40rpx; background-color: #F3F5F8; flex: 1; justify-content: center; align-items: center; } |
| | | .action-button text { color: #46505E; font-size: 27rpx; font-weight: 600; } |
| | | .action-button.primary { margin-right: 0; background-image: linear-gradient(90deg, #FF4E35 0%, #FF8318 100%); } |
| | | .action-button.primary text { color: #FFFFFF; } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <view class="page"> |
| | | <view class="page-header"> |
| | | <view class="safe-space" :style="'height:' + navTop + 'px'"></view> |
| | | <view class="nav-row" :style="'height:' + navHeight + 'px;padding-right:' + navRight + 'px'"> |
| | | <view class="back-button" hover-class="back-button-active" @tap="goBack"><image src="/static/news-assets/arrow-left.svg" mode="aspectFit"></image></view> |
| | | <text class="nav-title">电影票兑换券</text> |
| | | <view class="capsule-space" :style="'width:' + navWidth + 'px'"></view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="content"> |
| | | <view v-if="loading" class="list-state"><text>正在加载可领取兑换券</text></view> |
| | | <view v-else-if="loadError" class="list-state error-state"><text>兑换券加载失败</text><text class="retry-text" @tap="loadPackages">重新加载</text></view> |
| | | <view v-else-if="packages.length == 0" class="list-state"><text>暂无可领取的电影票兑换券</text></view> |
| | | <view v-else class="package-list"> |
| | | <view class="package-card" v-for="item in packages" :key="item.packageId"> |
| | | <view class="package-head"> |
| | | <image class="package-image" :src="item.coverUrl != '' ? resolveApiUrl(item.coverUrl) : ''" mode="aspectFill" @error="replaceImage"></image> |
| | | <view class="package-title-area"> |
| | | <text class="package-name">{{item.packageName}}</text> |
| | | <text class="package-merchant">{{item.merchantName}}</text> |
| | | </view> |
| | | </view> |
| | | <view class="package-row"><text>可兑换额度</text><text class="quota-value">{{item.quotaRemaining}} / {{item.quotaTotal}}</text></view> |
| | | <view v-if="item.validEnd != null && item.validEnd != ''" class="package-row"><text>有效期至</text><text>{{item.validEnd}}</text></view> |
| | | <view v-if="item.appName != ''" class="package-row"><text>兑换渠道</text><text>{{item.appName}}APP</text></view> |
| | | <view v-if="item.usageText != ''" class="package-usage">{{item.usageText}}</view> |
| | | <view class="claim-button" :class="item.available === false ? 'is-disabled' : ''" @tap="claimPackage(item)"> |
| | | <text>{{item.available === false ? (item.unavailableReason != '' ? item.unavailableReason : '不可领取') : '领取兑换码'}}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="showResult" class="result-mask" @tap="closeResult"> |
| | | <view class="result-card" @tap.stop> |
| | | <text class="result-title">领取成功</text> |
| | | <text class="result-label">兑换码</text> |
| | | <text class="result-code">{{result.exchangeCode}}</text> |
| | | <text class="result-tip">复制兑换码后前往{{result.appName != '' ? result.appName : '指定商家'}}APP兑换电影票{{result.validEnd != '' ? ',有效期至' + result.validEnd : ''}}</text> |
| | | <view class="copy-button" @tap="copyCode"><text>复制兑换码</text></view> |
| | | <view class="close-button" @tap="closeResult"><text>完成</text></view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup lang="uts"> |
| | | import { apiEnvironment } from '../../config/api.uts' |
| | | import { getTicketPackages, claimTicketPackage } from '../../services/portal.uts' |
| | | import { resolveApiUrl } from '../../services/http.uts' |
| | | import { getSession } from '../../services/session.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | const navTop = ref(28) |
| | | const navHeight = ref(32) |
| | | const navWidth = ref(88) |
| | | const navRight = ref(10) |
| | | const talentId = ref('') |
| | | const cardId = ref('') |
| | | const packages = ref<any[]>([]) |
| | | const loading = ref(true) |
| | | const loadError = ref(false) |
| | | const isClaiming = ref(false) |
| | | const showResult = ref(false) |
| | | const result = ref({ exchangeCode: '', appName: '', validEnd: '', statusName: '' }) |
| | | |
| | | onLoad((options: any) => { |
| | | // #ifdef MP-WEIXIN |
| | | const capsule = uni.getMenuButtonBoundingClientRect() |
| | | const systemInfo = uni.getSystemInfoSync() |
| | | navTop.value = capsule.top |
| | | navHeight.value = capsule.height |
| | | navWidth.value = capsule.width |
| | | navRight.value = systemInfo.windowWidth - capsule.right |
| | | // #endif |
| | | const session = getSession() |
| | | if (session != null && session.talentId != null) talentId.value = session.talentId as string |
| | | if (talentId.value == '' && apiEnvironment == 'development') talentId.value = '2' |
| | | if (options != null && options['cardId'] != null) cardId.value = options['cardId'] as string |
| | | loadPackages() |
| | | }) |
| | | |
| | | const loadPackages = () => { |
| | | loading.value = true |
| | | loadError.value = false |
| | | getTicketPackages(talentId.value, cardId.value, '', (data: any) => { |
| | | const records: any[] = data instanceof Array ? data as any[] : [] |
| | | const next: any[] = [] |
| | | for (let index = 0; index < records.length; index++) { |
| | | const item = records[index] |
| | | item.quotaTotal = item.packageQuotaTotal != null ? Number(item.packageQuotaTotal) : (item.quotaTotal != null ? Number(item.quotaTotal) : 0) |
| | | item.quotaRemaining = item.packageQuotaRemaining != null ? Number(item.packageQuotaRemaining) : (item.quotaRemaining != null ? Number(item.quotaRemaining) : 0) |
| | | next.push(item) |
| | | } |
| | | packages.value = next |
| | | loading.value = false |
| | | }, (error) => { loading.value = false; loadError.value = true }) |
| | | } |
| | | const claimPackage = (item: any) => { |
| | | if (item.available === false) return |
| | | if (isClaiming.value) return |
| | | uni.showModal({ |
| | | title: '领取电影票兑换券', |
| | | content: '确认领取【' + item.packageName + '】兑换码吗?领取后请在有效期内前往商家APP兑换。', |
| | | confirmText: '领取', |
| | | confirmColor: '#FF5935', |
| | | success: (res: any) => { |
| | | if (res == null || res.confirm !== true) return |
| | | if (isClaiming.value) return |
| | | isClaiming.value = true |
| | | claimTicketPackage('' + item.packageId, talentId.value, cardId.value, (data: any) => { |
| | | isClaiming.value = false |
| | | result.value = { |
| | | exchangeCode: data != null && data.exchangeCode != null ? String(data.exchangeCode) : '', |
| | | appName: item.appName != null ? String(item.appName) : '', |
| | | validEnd: data != null && data.validEnd != null ? String(data.validEnd) : '', |
| | | statusName: data != null && data.statusName != null ? String(data.statusName) : '' |
| | | } |
| | | showResult.value = true |
| | | loadPackages() |
| | | }, (error) => { |
| | | isClaiming.value = false |
| | | uni.showToast({ title: error.message, icon: 'none' }) |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | const copyCode = () => { |
| | | if (result.value.exchangeCode == '') return |
| | | uni.setClipboardData({ data: result.value.exchangeCode, success: () => uni.showToast({ title: '兑换码已复制', icon: 'success' }) }) |
| | | } |
| | | const closeResult = () => { showResult.value = false } |
| | | const replaceImage = () => {} |
| | | const goBack = () => safeBack('/pages/benefits/benefits') |
| | | </script> |
| | | |
| | | <style> |
| | | .page { display: flex; width: 100%; height: 100vh; background-color: #F5F7FA; flex-direction: column; } |
| | | .page-header { position: relative; z-index: 10; flex-shrink: 0; } |
| | | .safe-space { height: 28px; } |
| | | .nav-row { display: flex; height: 32px; box-sizing: border-box; padding-left: 20rpx; padding-right: 10px; 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; } |
| | | .back-button-active { opacity: .55; } |
| | | .nav-title { position: absolute; left: 50%; top: 50%; color: #171B22; font-size: 37rpx; font-weight: 700; transform: translate(-50%, -50%); white-space: nowrap; } |
| | | .capsule-space { width: 88px; height: 100%; flex-shrink: 0; } |
| | | .content { min-height: 0; padding: 40rpx 40rpx 60rpx; flex: 1; overflow-y: auto; } |
| | | .list-state { display: flex; min-height: 240rpx; color: #8A94A1; font-size: 25rpx; flex-direction: column; justify-content: center; align-items: center; } |
| | | .error-state { flex-direction: column; } |
| | | .retry-text { margin-top: 18rpx; color: #F26A38; } |
| | | .package-list { display: flex; flex-direction: column; } |
| | | .package-card { margin-bottom: 24rpx; box-sizing: border-box; padding: 26rpx; border-radius: 24rpx; background-color: #FFFFFF; box-shadow: 0 9rpx 25rpx rgba(68,78,91,.04); } |
| | | .package-head { display: flex; flex-direction: row; align-items: center; } |
| | | .package-image { width: 120rpx; height: 120rpx; flex-shrink: 0; border-radius: 16rpx; background-color: #F0F2F5; } |
| | | .package-title-area { min-width: 0; margin-left: 22rpx; flex: 1; } |
| | | .package-name { display: block; color: #20252D; font-size: 30rpx; font-weight: 700; } |
| | | .package-merchant { display: block; margin-top: 10rpx; color: #697381; font-size: 24rpx; } |
| | | .package-row { display: flex; height: 46rpx; margin-top: 14rpx; flex-direction: row; align-items: center; justify-content: space-between; } |
| | | .package-row text { color: #7B8592; font-size: 24rpx; } |
| | | .package-row .quota-value { color: #F26A38; font-weight: 700; } |
| | | .package-usage { margin-top: 12rpx; color: #9AA3AE; font-size: 22rpx; line-height: 34rpx; } |
| | | .claim-button { display: flex; height: 78rpx; margin-top: 22rpx; border-radius: 40rpx; background-image: linear-gradient(90deg, #FF4E35 0%, #FF8318 100%); justify-content: center; align-items: center; } |
| | | .claim-button text { color: #FFFFFF; font-size: 27rpx; font-weight: 600; } |
| | | .claim-button.is-disabled { background-image: none; background-color: #C9CED6; } |
| | | .result-mask { display: flex; position: fixed; z-index: 50; left: 0; right: 0; top: 0; bottom: 0; background-color: rgba(0,0,0,.45); justify-content: center; align-items: center; } |
| | | .result-card { width: 590rpx; box-sizing: border-box; padding: 48rpx 44rpx 40rpx; border-radius: 28rpx; background-color: #FFFFFF; flex-direction: column; align-items: center; } |
| | | .result-title { color: #232933; font-size: 34rpx; font-weight: 700; text-align: center; } |
| | | .result-label { margin-top: 40rpx; color: #8B95A2; font-size: 24rpx; text-align: center; } |
| | | .result-code { margin-top: 14rpx; color: #F26A38; font-size: 42rpx; font-weight: 700; letter-spacing: 3rpx; text-align: center; } |
| | | .result-tip { margin-top: 22rpx; color: #75808F; font-size: 24rpx; line-height: 36rpx; text-align: center; } |
| | | .copy-button { display: flex; height: 80rpx; margin-top: 40rpx; border-radius: 40rpx; background-image: linear-gradient(90deg, #FF4E35 0%, #FF8318 100%); justify-content: center; align-items: center; } |
| | | .copy-button text { color: #FFFFFF; font-size: 27rpx; font-weight: 600; } |
| | | .close-button { display: flex; height: 72rpx; margin-top: 18rpx; border-radius: 36rpx; background-color: #F3F5F8; justify-content: center; align-items: center; } |
| | | .close-button text { color: #697381; font-size: 26rpx; } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <view class="page"> |
| | | <view class="navigation-bar"> |
| | | <view class="back-button" @tap="goBack"><image class="back-icon" src="/static/icons/lucide/back-left.svg" mode="aspectFit"></image></view> |
| | | <text class="page-title">结算管理</text> |
| | | <view class="export-button" :class="exporting ? 'exporting' : ''" @tap="handleExport"> |
| | | <image class="export-icon" src="/static/icons/lucide/file-text.svg" mode="aspectFit"></image> |
| | | </view> |
| | | </view> |
| | | |
| | | <scroll-view class="content-scroll" scroll-y="true" :show-scrollbar="false" :enhanced="false" lower-threshold="80" @scrolltolower="loadMore"> |
| | | <view class="top-card"> |
| | | <view class="period-switch"> |
| | | <view class="period-tab" :class="periodType == 'DAY' ? 'active' : ''" @tap="switchPeriod('DAY')"><text>按日</text></view> |
| | | <view class="period-tab" :class="periodType == 'MONTH' ? 'active' : ''" @tap="switchPeriod('MONTH')"><text>按月</text></view> |
| | | </view> |
| | | <view class="range-row"> |
| | | <view class="range-box"> |
| | | <text class="range-label">开始</text> |
| | | <text class="range-value">{{startDateText}}</text> |
| | | </view> |
| | | <view class="range-divider"></view> |
| | | <view class="range-box"> |
| | | <text class="range-label">结束</text> |
| | | <text class="range-value">{{endDateText}}</text> |
| | | </view> |
| | | </view> |
| | | <view class="summary-grid"> |
| | | <view class="summary-item"> |
| | | <text class="summary-label">交易总额</text> |
| | | <text class="summary-value">¥{{formatAmount(overviewTotals.transactionAmountCents)}}</text> |
| | | </view> |
| | | <view class="summary-item"> |
| | | <text class="summary-label">补贴总额</text> |
| | | <text class="summary-value">¥{{formatAmount(overviewTotals.subsidyAmountCents)}}</text> |
| | | </view> |
| | | <view class="summary-item"> |
| | | <text class="summary-label">应结算额</text> |
| | | <text class="summary-value highlight">¥{{formatAmount(overviewTotals.settlementAmountCents)}}</text> |
| | | </view> |
| | | <view class="summary-item"> |
| | | <text class="summary-label">订单数</text> |
| | | <text class="summary-value">{{overviewTotals.orderCount}} 笔</text> |
| | | </view> |
| | | </view> |
| | | <text v-if="overviewErrorText.length > 0" class="basis-error">{{overviewErrorText}}</text> |
| | | <text class="basis-text">{{statisticsBasis}}</text> |
| | | </view> |
| | | |
| | | <page-feedback v-if="listLoadState != 'success'" :state="listLoadState" loading-text="结算数据加载中" error-text="结算数据加载失败,点击重试" :trace-id="traceId" @retry="reloadSummaries"></page-feedback> |
| | | |
| | | <view v-else class="record-list"> |
| | | <view class="record-card" v-for="item in records" :key="item.periodKey"> |
| | | <view class="record-head"> |
| | | <view class="record-title-wrap"> |
| | | <text class="record-title">{{periodType == 'MONTH' ? item.periodKey + '月' : item.periodKey}}</text> |
| | | <text class="record-subtitle">{{item.merchantName}}</text> |
| | | </view> |
| | | <text class="record-count">{{item.orderCount}} 笔</text> |
| | | </view> |
| | | <view class="record-grid"> |
| | | <view class="record-cell"><text class="record-cell-label">交易额</text><text class="record-cell-value">¥{{formatAmount(item.transactionAmountCents)}}</text></view> |
| | | <view class="record-cell"><text class="record-cell-label">补贴额</text><text class="record-cell-value">¥{{formatAmount(item.subsidyAmountCents)}}</text></view> |
| | | <view class="record-cell"><text class="record-cell-label">应结算额</text><text class="record-cell-value highlight">¥{{formatAmount(item.settlementAmountCents)}}</text></view> |
| | | <view class="record-cell"><text class="record-cell-label">券金额</text><text class="record-cell-value">¥{{formatAmount(item.couponAmountCents)}}</text></view> |
| | | </view> |
| | | <text class="record-time">统计区间:{{formatTimeRange(item.periodStartTime, item.periodEndTime)}}</text> |
| | | </view> |
| | | |
| | | <view class="footer-state" @tap="retryLoadMore"> |
| | | <text v-if="loadMoreState == 'loading'" class="footer-state-text">正在加载更多…</text> |
| | | <text v-else-if="loadMoreState == 'error'" class="footer-state-text footer-state-retry">加载失败,点击重试</text> |
| | | <text v-else-if="loadMoreState == 'no-more'" class="footer-state-text">没有更多结算数据了</text> |
| | | <text v-else class="footer-state-text">上滑加载更多</text> |
| | | </view> |
| | | </view> |
| | | </scroll-view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup lang="uts"> |
| | | import { hasActiveSession } from '../../services/session.uts' |
| | | import { exportSettlementDetails, loadSettlementOverview, loadSettlementSummaries, SettlementStatisticsRow } from '../../services/settlement-statistics.uts' |
| | | |
| | | const periodType = ref('DAY') |
| | | const listLoadState = ref('loading') |
| | | const loadMoreState = ref('more') |
| | | const traceId = ref('') |
| | | const exporting = ref(false) |
| | | const records = ref<Array<SettlementStatisticsRow>>([]) |
| | | const overviewErrorText = ref('') |
| | | const overviewTotals = ref<SettlementStatisticsRow>({ |
| | | periodKey: '', |
| | | periodType: '', |
| | | merchantId: '', |
| | | merchantName: '', |
| | | merchantType: '', |
| | | orderCount: 0, |
| | | transactionAmountCents: 0, |
| | | transactionAmount: '', |
| | | subsidyAmountCents: 0, |
| | | subsidyAmount: '', |
| | | settlementAmountCents: 0, |
| | | settlementAmount: '', |
| | | originalAmountCents: 0, |
| | | originalAmount: '', |
| | | couponAmountCents: 0, |
| | | couponAmount: '', |
| | | governmentSubsidyOrderCount: 0, |
| | | periodStartTime: '', |
| | | periodEndTime: '', |
| | | statisticsBasis: '' |
| | | }) |
| | | const statisticsBasis = ref('') |
| | | const page = ref(1) |
| | | const pageSize = 8 |
| | | const currentDate = new Date() |
| | | const defaultStartDate = currentDate.getFullYear().toString() + '-' + (currentDate.getMonth() + 1 < 10 ? '0' : '') + (currentDate.getMonth() + 1).toString() + '-01' |
| | | const defaultEndDate = currentDate.getFullYear().toString() + '-' + (currentDate.getMonth() + 1 < 10 ? '0' : '') + (currentDate.getMonth() + 1).toString() + '-' + (currentDate.getDate() < 10 ? '0' : '') + currentDate.getDate().toString() |
| | | const startDate = ref(defaultStartDate) |
| | | const endDate = ref(defaultEndDate) |
| | | let cancelListRequest: (() => void) | null = null |
| | | let cancelOverviewRequest: (() => void) | null = null |
| | | let cancelExportRequest: (() => void) | null = null |
| | | |
| | | const startDateText = computed(() => startDate.value.length > 0 ? startDate.value : '未设置') |
| | | const endDateText = computed(() => endDate.value.length > 0 ? endDate.value : '未设置') |
| | | |
| | | const goBack = () => uni.navigateBack() |
| | | |
| | | const formatAmount = (value: number): string => (Number(value || 0) / 100).toFixed(2) |
| | | const formatDate = (value: string): string => value.length > 0 ? value.replace('T', ' ').substring(0, 10) : '-' |
| | | const formatTimeRange = (start: string, end: string): string => { |
| | | if (start.length == 0 && end.length == 0) return '—' |
| | | return formatDate(start) + ' 至 ' + formatDate(end) |
| | | } |
| | | |
| | | const clearRequests = () => { |
| | | if (cancelListRequest != null) { |
| | | cancelListRequest() |
| | | cancelListRequest = null |
| | | } |
| | | if (cancelOverviewRequest != null) { |
| | | cancelOverviewRequest() |
| | | cancelOverviewRequest = null |
| | | } |
| | | } |
| | | |
| | | const applyOverview = (value: any) => { |
| | | overviewErrorText.value = '' |
| | | const total = value != null && value.total != null ? value.total : null |
| | | if (total != null) { |
| | | overviewTotals.value = total as SettlementStatisticsRow |
| | | } |
| | | statisticsBasis.value = value != null && value.total != null && value.total.statisticsBasis != null ? value.total.statisticsBasis : (value != null && value.records instanceof Array && value.records.length > 0 ? value.records[0].statisticsBasis : '') |
| | | if (value != null && value.startDate != null && value.startDate.length > 0) startDate.value = value.startDate |
| | | if (value != null && value.endDate != null && value.endDate.length > 0) endDate.value = value.endDate |
| | | } |
| | | |
| | | const loadOverview = () => { |
| | | if (cancelOverviewRequest != null) { |
| | | cancelOverviewRequest() |
| | | cancelOverviewRequest = null |
| | | } |
| | | if (!hasActiveSession()) return |
| | | overviewErrorText.value = '' |
| | | cancelOverviewRequest = loadSettlementOverview(startDate.value, endDate.value, (value: any, _traceId: string) => { |
| | | cancelOverviewRequest = null |
| | | applyOverview(value) |
| | | }, (error: any) => { |
| | | cancelOverviewRequest = null |
| | | traceId.value = error != null && error.traceId != null ? error.traceId : '' |
| | | overviewErrorText.value = error != null && error.message != null ? error.message as string : '结算总览加载失败' |
| | | }) |
| | | } |
| | | |
| | | const applyPage = (result: any, append: boolean) => { |
| | | const nextRecords = append ? records.value.concat(result.records) : result.records |
| | | records.value = nextRecords |
| | | traceId.value = '' |
| | | listLoadState.value = nextRecords.length == 0 ? 'empty' : 'success' |
| | | loadMoreState.value = result.page * result.size >= result.total ? 'no-more' : 'more' |
| | | page.value = result.page |
| | | } |
| | | |
| | | const showListError = (error: any, append: boolean) => { |
| | | const message = error != null && error.message != null ? error.message as string : '结算数据加载失败,点击重试' |
| | | if (append && records.value.length > 0) { |
| | | loadMoreState.value = 'error' |
| | | traceId.value = error != null && error.traceId != null ? error.traceId : '' |
| | | return |
| | | } |
| | | listLoadState.value = 'error' |
| | | traceId.value = error != null && error.traceId != null ? error.traceId : '' |
| | | uni.showToast({ title: message, icon: 'none' }) |
| | | } |
| | | |
| | | const requestSummaries = (targetPage: number, append: boolean) => { |
| | | if (cancelListRequest != null) { |
| | | cancelListRequest() |
| | | cancelListRequest = null |
| | | } |
| | | if (!append) { |
| | | records.value = [] |
| | | listLoadState.value = 'loading' |
| | | loadMoreState.value = 'more' |
| | | } else { |
| | | loadMoreState.value = 'loading' |
| | | } |
| | | if (!hasActiveSession()) { |
| | | listLoadState.value = 'error' |
| | | traceId.value = '' |
| | | return |
| | | } |
| | | cancelListRequest = loadSettlementSummaries(periodType.value, startDate.value, endDate.value, targetPage, pageSize, (result: any, _traceId: string) => { |
| | | cancelListRequest = null |
| | | applyPage(result, append) |
| | | }, (error: any) => { |
| | | cancelListRequest = null |
| | | showListError(error, append) |
| | | }) |
| | | } |
| | | |
| | | const reloadSummaries = () => { |
| | | loadOverview() |
| | | requestSummaries(1, false) |
| | | } |
| | | |
| | | const loadMore = () => { |
| | | if (listLoadState.value != 'success' || loadMoreState.value != 'more') return |
| | | requestSummaries(page.value + 1, true) |
| | | } |
| | | |
| | | const retryLoadMore = () => { |
| | | if (loadMoreState.value == 'error') requestSummaries(page.value + 1, true) |
| | | } |
| | | |
| | | const switchPeriod = (value: string) => { |
| | | if (periodType.value == value) return |
| | | periodType.value = value |
| | | reloadSummaries() |
| | | } |
| | | |
| | | const handleExport = () => { |
| | | if (exporting.value) return |
| | | if (!hasActiveSession()) { |
| | | uni.showToast({ title: '请先登录后再导出', icon: 'none' }) |
| | | return |
| | | } |
| | | exporting.value = true |
| | | if (cancelExportRequest != null) { |
| | | cancelExportRequest() |
| | | cancelExportRequest = null |
| | | } |
| | | cancelExportRequest = exportSettlementDetails('', startDate.value, endDate.value, (tempPath: string) => { |
| | | exporting.value = false |
| | | cancelExportRequest = null |
| | | uni.openDocument({ |
| | | filePath: tempPath, |
| | | showMenu: true, |
| | | success: () => {}, |
| | | fail: () => { |
| | | uni.showToast({ title: '文件已下载,但无法直接打开', icon: 'none' }) |
| | | } |
| | | }) |
| | | }, (error: any) => { |
| | | exporting.value = false |
| | | cancelExportRequest = null |
| | | uni.showToast({ title: error != null && error.message != null ? error.message : '导出失败,请重试', icon: 'none' }) |
| | | }) |
| | | } |
| | | |
| | | onShow(() => { |
| | | reloadSummaries() |
| | | }) |
| | | |
| | | onUnload(() => { |
| | | clearRequests() |
| | | if (cancelExportRequest != null) { |
| | | cancelExportRequest() |
| | | cancelExportRequest = null |
| | | } |
| | | }) |
| | | </script> |
| | | |
| | | <style> |
| | | page { background-color: #F4F6F9; } |
| | | .page { display: flex; width: 100%; height: 100vh; flex-direction: column; overflow: hidden; background-color: #F4F6F9; } |
| | | .navigation-bar { display: flex; height: 174rpx; padding: 68rpx 38rpx 0; flex: none; flex-direction: row; align-items: center; justify-content: space-between; position: relative; box-sizing: border-box; } |
| | | .back-button, .export-button { display: flex; width: 64rpx; height: 64rpx; justify-content: center; align-items: center; } |
| | | .back-icon, .export-icon { width: 38rpx; height: 38rpx; } |
| | | .page-title { color: #202B3E; font-size: 34rpx; line-height: 48rpx; text-align: center; font-weight: 700; flex: 1; } |
| | | .content-scroll { flex: 1; height: 0; padding: 0 34rpx; box-sizing: border-box; } |
| | | .top-card { padding: 26rpx 28rpx 30rpx; border-radius: 20rpx; background-color: #FFFFFF; box-shadow: 0 8rpx 24rpx rgba(49,55,66,.05); } |
| | | .period-switch { display: flex; width: 250rpx; padding: 6rpx; border-radius: 16rpx; background-color: #F3F5F8; box-sizing: border-box; } |
| | | .period-tab { display: flex; flex: 1; height: 60rpx; justify-content: center; align-items: center; border-radius: 12rpx; color: #6D7786; font-size: 25rpx; font-weight: 600; } |
| | | .period-tab.active { background-color: #FF7625; color: #FFFFFF; } |
| | | .range-row { display: flex; align-items: center; margin-top: 22rpx; } |
| | | .range-box { display: flex; flex: 1; flex-direction: column; } |
| | | .range-label { color: #8B94A2; font-size: 22rpx; line-height: 32rpx; } |
| | | .range-value { margin-top: 6rpx; color: #293547; font-size: 26rpx; line-height: 36rpx; font-weight: 600; } |
| | | .range-divider { width: 24rpx; height: 2rpx; margin: 0 18rpx; background-color: #DDE3EA; } |
| | | .summary-grid { display: flex; flex-wrap: wrap; margin-top: 20rpx; } |
| | | .summary-item { width: calc(50% - 10rpx); padding: 18rpx 18rpx 16rpx; margin-top: 18rpx; border-radius: 16rpx; background-color: #F8FAFC; box-sizing: border-box; } |
| | | .summary-item:nth-child(2n) { margin-left: 20rpx; } |
| | | .summary-label { color: #7A8595; font-size: 22rpx; line-height: 32rpx; } |
| | | .summary-value { display: block; margin-top: 10rpx; color: #1F2A3A; font-size: 30rpx; line-height: 42rpx; font-weight: 700; } |
| | | .summary-value.highlight { color: #FF7625; } |
| | | .basis-text { display: block; margin-top: 18rpx; color: #98A2B1; font-size: 21rpx; line-height: 30rpx; } |
| | | .basis-error { display: block; margin-top: 18rpx; color: #E95B55; font-size: 21rpx; line-height: 30rpx; } |
| | | .record-list { padding-bottom: 34rpx; } |
| | | .record-card { margin-top: 18rpx; padding: 24rpx 26rpx 22rpx; border-radius: 20rpx; background-color: #FFFFFF; box-shadow: 0 8rpx 24rpx rgba(49,55,66,.05); } |
| | | .record-head { display: flex; justify-content: space-between; align-items: flex-start; } |
| | | .record-title-wrap { display: flex; flex-direction: column; } |
| | | .record-title { color: #1F2A3A; font-size: 30rpx; line-height: 42rpx; font-weight: 700; } |
| | | .record-subtitle { margin-top: 4rpx; color: #6E7786; font-size: 22rpx; line-height: 32rpx; } |
| | | .record-count { color: #FF7625; font-size: 24rpx; line-height: 34rpx; font-weight: 700; } |
| | | .record-grid { display: flex; flex-wrap: wrap; margin-top: 16rpx; } |
| | | .record-cell { width: calc(50% - 10rpx); padding: 14rpx 16rpx; margin-top: 14rpx; border-radius: 14rpx; background-color: #F8FAFC; box-sizing: border-box; } |
| | | .record-cell:nth-child(2n) { margin-left: 20rpx; } |
| | | .record-cell-label { color: #7A8595; font-size: 20rpx; line-height: 30rpx; } |
| | | .record-cell-value { display: block; margin-top: 6rpx; color: #1F2A3A; font-size: 27rpx; line-height: 38rpx; font-weight: 700; } |
| | | .record-cell-value.highlight { color: #FF7625; } |
| | | .record-time { display: block; margin-top: 14rpx; color: #96A0AF; font-size: 21rpx; line-height: 30rpx; } |
| | | .footer-state { display: block; width: 100%; padding: 16rpx 0 30rpx; box-sizing: border-box; } |
| | | .footer-state-text { display: block; width: 100%; color: #A4AFBE; font-size: 23rpx; line-height: 34rpx; text-align: center; } |
| | | .footer-state-retry { color: #FF7625; } |
| | | .exporting { opacity: 0.55; } |
| | | </style> |
| New file |
| | |
| | | import { getApiBaseUrl, getApiEndpoint, isApiConfigured } from '../config/api.uts' |
| | | import { HttpError, request } from './http.uts' |
| | | import { refreshMerchantSession } from './session-refresh.uts' |
| | | import { getAccessToken, getCurrentStoreId } from './session.uts' |
| | | |
| | | export type SettlementStatisticsRow = { |
| | | periodKey: string |
| | | periodType: string |
| | | merchantId: string |
| | | merchantName: string |
| | | merchantType: string |
| | | orderCount: number |
| | | transactionAmountCents: number |
| | | transactionAmount: string |
| | | subsidyAmountCents: number |
| | | subsidyAmount: string |
| | | settlementAmountCents: number |
| | | settlementAmount: string |
| | | originalAmountCents: number |
| | | originalAmount: string |
| | | couponAmountCents: number |
| | | couponAmount: string |
| | | governmentSubsidyOrderCount: number |
| | | periodStartTime: string |
| | | periodEndTime: string |
| | | statisticsBasis: string |
| | | } |
| | | |
| | | export type SettlementStatisticsPage = { |
| | | records: Array<SettlementStatisticsRow> |
| | | page: number |
| | | size: number |
| | | total: number |
| | | } |
| | | |
| | | export type SettlementStatisticsOverview = { |
| | | startDate: string |
| | | endDate: string |
| | | records: Array<SettlementStatisticsRow> |
| | | total: SettlementStatisticsRow | null |
| | | } |
| | | |
| | | export type SettlementStatisticsDashboardCard = { |
| | | title: string |
| | | amountCents?: number |
| | | amount?: string |
| | | count?: number |
| | | unit: string |
| | | } |
| | | |
| | | export type SettlementStatisticsDashboard = { |
| | | merchantId: string |
| | | statisticsDate: string |
| | | weekStartDate: string |
| | | transactionTotal: SettlementStatisticsDashboardCard |
| | | todayVerification: SettlementStatisticsDashboardCard |
| | | weekVerification: SettlementStatisticsDashboardCard |
| | | subsidyTotal: SettlementStatisticsDashboardCard |
| | | transactionAmountCents: number |
| | | transactionAmount: string |
| | | todayVerificationCount: number |
| | | weekVerificationCount: number |
| | | subsidyAmountCents: number |
| | | subsidyAmount: string |
| | | hasData: boolean |
| | | statisticsBasis: string |
| | | } |
| | | |
| | | const stringValue = (value: any): string => value == null ? '' : '' + value |
| | | const numberValue = (value: any): number => value == null ? 0 : Number(value) |
| | | |
| | | const toRow = (value: any): SettlementStatisticsRow => ({ |
| | | periodKey: stringValue(value != null ? value.periodKey : ''), |
| | | periodType: stringValue(value != null ? value.periodType : ''), |
| | | merchantId: stringValue(value != null ? value.merchantId : ''), |
| | | merchantName: stringValue(value != null ? value.merchantName : ''), |
| | | merchantType: stringValue(value != null ? value.merchantType : ''), |
| | | orderCount: numberValue(value != null ? value.orderCount : 0), |
| | | transactionAmountCents: numberValue(value != null ? value.transactionAmountCents : 0), |
| | | transactionAmount: stringValue(value != null ? value.transactionAmount : ''), |
| | | subsidyAmountCents: numberValue(value != null ? value.subsidyAmountCents : 0), |
| | | subsidyAmount: stringValue(value != null ? value.subsidyAmount : ''), |
| | | settlementAmountCents: numberValue(value != null ? value.settlementAmountCents : 0), |
| | | settlementAmount: stringValue(value != null ? value.settlementAmount : ''), |
| | | originalAmountCents: numberValue(value != null ? value.originalAmountCents : 0), |
| | | originalAmount: stringValue(value != null ? value.originalAmount : ''), |
| | | couponAmountCents: numberValue(value != null ? value.couponAmountCents : 0), |
| | | couponAmount: stringValue(value != null ? value.couponAmount : ''), |
| | | governmentSubsidyOrderCount: numberValue(value != null ? value.governmentSubsidyOrderCount : 0), |
| | | periodStartTime: stringValue(value != null ? value.periodStartTime : ''), |
| | | periodEndTime: stringValue(value != null ? value.periodEndTime : ''), |
| | | statisticsBasis: stringValue(value != null ? value.statisticsBasis : '') |
| | | }) |
| | | |
| | | const toPage = (value: any): SettlementStatisticsPage => { |
| | | const source = value != null ? value : {} |
| | | const records = source.records instanceof Array ? source.records : [] |
| | | return { |
| | | records: records.map((item: any) => toRow(item)), |
| | | page: numberValue(source.page), |
| | | size: numberValue(source.size), |
| | | total: numberValue(source.total) |
| | | } |
| | | } |
| | | |
| | | const toOverview = (value: any): SettlementStatisticsOverview => { |
| | | const source = value != null ? value : {} |
| | | const records = source.records instanceof Array ? source.records : [] |
| | | return { |
| | | startDate: stringValue(source.startDate), |
| | | endDate: stringValue(source.endDate), |
| | | records: records.map((item: any) => toRow(item)), |
| | | total: source.total != null ? toRow(source.total) : null |
| | | } |
| | | } |
| | | |
| | | const toDashboard = (value: any): SettlementStatisticsDashboard => ({ |
| | | merchantId: stringValue(value != null ? value.merchantId : ''), |
| | | statisticsDate: stringValue(value != null ? value.statisticsDate : ''), |
| | | weekStartDate: stringValue(value != null ? value.weekStartDate : ''), |
| | | transactionTotal: value != null && value.transactionTotal != null ? value.transactionTotal as SettlementStatisticsDashboardCard : { title: '交易总额', amountCents: 0, amount: '0.00', unit: '元' }, |
| | | todayVerification: value != null && value.todayVerification != null ? value.todayVerification as SettlementStatisticsDashboardCard : { title: '今日核销', count: 0, unit: '笔' }, |
| | | weekVerification: value != null && value.weekVerification != null ? value.weekVerification as SettlementStatisticsDashboardCard : { title: '本周核销', count: 0, unit: '笔' }, |
| | | subsidyTotal: value != null && value.subsidyTotal != null ? value.subsidyTotal as SettlementStatisticsDashboardCard : { title: '补贴总额', amountCents: 0, amount: '0.00', unit: '元' }, |
| | | transactionAmountCents: numberValue(value != null ? value.transactionAmountCents : 0), |
| | | transactionAmount: stringValue(value != null ? value.transactionAmount : ''), |
| | | todayVerificationCount: numberValue(value != null ? value.todayVerificationCount : 0), |
| | | weekVerificationCount: numberValue(value != null ? value.weekVerificationCount : 0), |
| | | subsidyAmountCents: numberValue(value != null ? value.subsidyAmountCents : 0), |
| | | subsidyAmount: stringValue(value != null ? value.subsidyAmount : ''), |
| | | hasData: value != null && value.hasData == true, |
| | | statisticsBasis: stringValue(value != null ? value.statisticsBasis : '') |
| | | }) |
| | | |
| | | export const loadSettlementSummaries = ( |
| | | periodType: string, |
| | | startDate: string, |
| | | endDate: string, |
| | | page: number, |
| | | size: number, |
| | | onSuccess: (value: SettlementStatisticsPage, traceId: string) => void, |
| | | onError: (error: HttpError) => void |
| | | ): (() => void) => request<any>({ |
| | | path: getApiEndpoint('merchantSettlementStatistics') + '/summaries', |
| | | method: 'GET', |
| | | data: { periodType: periodType, startDate: startDate, endDate: endDate, page: page, size: size } |
| | | }, (data: any, traceId: string) => onSuccess(toPage(data), traceId), onError) |
| | | |
| | | export const loadSettlementOverview = ( |
| | | startDate: string, |
| | | endDate: string, |
| | | onSuccess: (value: SettlementStatisticsOverview, traceId: string) => void, |
| | | onError: (error: HttpError) => void |
| | | ): (() => void) => request<any>({ |
| | | path: getApiEndpoint('merchantSettlementStatistics') + '/overview', |
| | | method: 'GET', |
| | | data: { startDate: startDate, endDate: endDate } |
| | | }, (data: any, traceId: string) => onSuccess(toOverview(data), traceId), onError) |
| | | |
| | | export const loadSettlementCurrentMonth = ( |
| | | onSuccess: (value: SettlementStatisticsOverview, traceId: string) => void, |
| | | onError: (error: HttpError) => void |
| | | ): (() => void) => request<any>({ |
| | | path: getApiEndpoint('merchantSettlementStatistics') + '/current-month', |
| | | method: 'GET' |
| | | }, (data: any, traceId: string) => onSuccess(toOverview(data), traceId), onError) |
| | | |
| | | export const loadSettlementDashboardCards = ( |
| | | onSuccess: (value: SettlementStatisticsDashboard, traceId: string) => void, |
| | | onError: (error: HttpError) => void |
| | | ): (() => void) => request<any>({ |
| | | path: getApiEndpoint('merchantSettlementStatistics') + '/dashboard-cards', |
| | | method: 'GET' |
| | | }, (data: any, traceId: string) => onSuccess(toDashboard(data), traceId), onError) |
| | | |
| | | export const exportSettlementDetails = ( |
| | | status: string, |
| | | startDate: string, |
| | | endDate: string, |
| | | onSuccess: (path: string) => void, |
| | | onError: (error: HttpError) => void |
| | | ): (() => void) => { |
| | | if (!isApiConfigured()) { |
| | | onError({ code: 'API_NOT_CONFIGURED', message: '后台服务尚未配置,请联系管理员', statusCode: 0, traceId: '' }) |
| | | return () => {} |
| | | } |
| | | const baseUrl = getApiBaseUrl().replace(/\/$/, '') |
| | | const query: Array<string> = [] |
| | | if (status != null && status.trim().length > 0) query.push('status=' + encodeURIComponent(status)) |
| | | if (startDate != null && startDate.trim().length > 0) query.push('startDate=' + encodeURIComponent(startDate)) |
| | | if (endDate != null && endDate.trim().length > 0) query.push('endDate=' + encodeURIComponent(endDate)) |
| | | const url = baseUrl + getApiEndpoint('merchantSettlementStatistics') + '/export' + (query.length > 0 ? '?' + query.join('&') : '') |
| | | let currentTask: any = null |
| | | const headers: any = { |
| | | 'X-Request-Id': 'merchant-export-' + Date.now(), |
| | | 'X-Client-Platform': 'mp-weixin', |
| | | 'X-Client-Version': '1.0.0' |
| | | } |
| | | const token = getAccessToken() |
| | | const storeId = getCurrentStoreId() |
| | | if (token.length > 0) headers.Authorization = 'Bearer ' + token |
| | | if (storeId.length > 0) headers['X-Merchant-Store-Id'] = storeId |
| | | const run = (retried: boolean) => { |
| | | currentTask = uni.downloadFile({ |
| | | url: url, |
| | | header: headers, |
| | | success: (response: any) => { |
| | | if (response != null && response.statusCode == 401 && !retried) { |
| | | refreshMerchantSession((success: boolean) => { |
| | | if (!success) { |
| | | onError({ code: 'UNAUTHORIZED', message: '登录已失效,请重新登录', statusCode: 401, traceId: '' }) |
| | | return |
| | | } |
| | | const refreshedToken = getAccessToken() |
| | | if (refreshedToken.length > 0) headers.Authorization = 'Bearer ' + refreshedToken |
| | | run(true) |
| | | }) |
| | | return |
| | | } |
| | | if (response == null || response.statusCode < 200 || response.statusCode >= 300 || response.tempFilePath == null || response.tempFilePath.length == 0) { |
| | | onError({ code: 'EXPORT_FAILED', message: '结算明细导出失败', statusCode: response != null ? response.statusCode as number : 0, traceId: '' }) |
| | | return |
| | | } |
| | | onSuccess(response.tempFilePath as string) |
| | | }, |
| | | fail: (error: any) => onError({ code: 'NETWORK_ERROR', message: error != null && error.errMsg != null ? error.errMsg as string : '结算明细导出失败', statusCode: 0, traceId: '' }) |
| | | }) |
| | | } |
| | | run(false) |
| | | return () => { |
| | | if (currentTask != null && currentTask.abort != null) currentTask.abort() |
| | | } |
| | | } |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { onMounted, reactive, ref } from 'vue' |
| | | import { Download, Refresh, Search } from '@element-plus/icons-vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { exportServiceApplications, getServiceApplications, updateServiceApplicationStatus } from '../api/admin' |
| | | import { saveBlob } from '../utils/excel' |
| | | |
| | | const loading = ref(false) |
| | | const processingId = ref<number | string | null>(null) |
| | | const rows = ref<Record<string, any>[]>([]) |
| | | const filters = reactive({ applicationType: '', keyword: '', serviceKeyword: '', cardLevel: '', status: '', submittedTime: [] as string[] }) |
| | | const pagination = reactive({ page: 1, size: 10, total: 0 }) |
| | | |
| | | const appointmentTypes = [ |
| | | { label: '体检预约', value: 'MEDICAL_RESERVATION' }, |
| | | { label: '医院快捷通道', value: 'FAST_TRACK' }, |
| | | { label: '酒店预约', value: 'HOTEL_RESERVATION' }, |
| | | { label: '配偶就业', value: 'SPOUSE_EMPLOYMENT' }, |
| | | { label: '学生托管', value: 'CHILD_ENTRUSTMENT' }, |
| | | ] |
| | | const cardLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '青年人才卡', 'EXCELLENT', 'OUTSTANDING', 'ELITE'] |
| | | const statusOptions = ['待处理', '已处理', '已驳回', '待审核', '审核通过', '审核不通过', '已取消'] |
| | | |
| | | function queryParams(size = pagination.size) { |
| | | return { page: pagination.page, size, applicationType: filters.applicationType || undefined, keyword: filters.keyword || undefined, talentKeyword: filters.keyword || undefined, serviceKeyword: filters.serviceKeyword || undefined, merchantName: filters.serviceKeyword || undefined, hospitalName: filters.serviceKeyword || undefined, cardLevel: filters.cardLevel || undefined, status: filters.status || undefined, submittedStart: filters.submittedTime?.[0], submittedEnd: filters.submittedTime?.[1] } |
| | | } |
| | | async function loadData() { loading.value = true; try { const result = await getServiceApplications(queryParams() as any); rows.value = (result.records || []) as Record<string, any>[]; pagination.total = result.total || 0 } finally { loading.value = false } } |
| | | function search() { pagination.page = 1; loadData() } |
| | | function resetFilters() { Object.assign(filters, { applicationType: '', keyword: '', serviceKeyword: '', cardLevel: '', status: '', submittedTime: [] }); search() } |
| | | function text(value: unknown) { return value === undefined || value === null || value === '' ? '-' : String(value) } |
| | | async function handle(row: Record<string, any>, status = '已处理') { processingId.value = row.id; try { await updateServiceApplicationStatus(row.id, status, '管理员', status === '已驳回' ? '后台驳回' : ''); ElMessage.success('状态已更新'); await loadData() } finally { processingId.value = null } } |
| | | async function download() { const blob = await exportServiceApplications(queryParams() as any); saveBlob(blob, '预约申请列表.xlsx') } |
| | | onMounted(loadData) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="page-stack prototype-page"> |
| | | <div class="toolbar"><div><h2>预约服务管理</h2><p>查看、处理、导出体检、快捷通道、酒店、配偶就业、学生托管预约申请。</p></div><el-button :icon="Refresh" :loading="loading" @click="loadData">刷新</el-button></div> |
| | | <section class="panel"> |
| | | <el-form class="filter-form" :model="filters" label-width="84px"> |
| | | <el-form-item label="预约类型"><el-select v-model="filters.applicationType" clearable placeholder="全部"><el-option v-for="item in appointmentTypes" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item> |
| | | <el-form-item label="关键词"><el-input v-model="filters.keyword" clearable placeholder="姓名、手机号、卡号、申请编号" /></el-form-item> |
| | | <el-form-item label="服务机构"><el-input v-model="filters.serviceKeyword" clearable placeholder="医院、酒店或机构名称" /></el-form-item> |
| | | <el-form-item label="卡片等级"><el-select v-model="filters.cardLevel" clearable placeholder="全部"><el-option v-for="level in cardLevels" :key="level" :label="level" :value="level" /></el-select></el-form-item> |
| | | <el-form-item label="状态"><el-select v-model="filters.status" clearable placeholder="全部"><el-option v-for="status in statusOptions" :key="status" :label="status" :value="status" /></el-select></el-form-item> |
| | | <el-form-item label="提交时间"><el-date-picker v-model="filters.submittedTime" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" /></el-form-item> |
| | | <div class="toolbar-actions"><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="resetFilters">重置</el-button><el-button :icon="Download" @click="download">导出</el-button></div> |
| | | </el-form> |
| | | <el-table v-loading="loading" :data="rows" border empty-text="暂无预约申请数据"> |
| | | <el-table-column prop="applicationNo" label="申请单号" min-width="130" /> |
| | | <el-table-column label="类型" min-width="120"><template #default="{ row }">{{ text(row.applicationTypeName || row.applicationType) }}</template></el-table-column> |
| | | <el-table-column prop="talentName" label="持卡人" min-width="110" /> |
| | | <el-table-column prop="talentMobile" label="手机号" min-width="120" /> |
| | | <el-table-column prop="talentLevel" label="等级" min-width="110" /> |
| | | <el-table-column label="服务机构" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ text(row.serviceName || row.merchantName || row.hospitalName) }}</template></el-table-column> |
| | | <el-table-column label="预约内容" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ text(row.appointmentContent || row.productName || row.packageName) }}</template></el-table-column> |
| | | <el-table-column label="预约时间" min-width="150"><template #default="{ row }">{{ text(row.appointmentTime || row.expectedTime) }}</template></el-table-column> |
| | | <el-table-column prop="submittedAt" label="提交时间" min-width="170" /> |
| | | <el-table-column prop="status" label="状态" min-width="110" /> |
| | | <el-table-column label="操作" width="170" fixed="right"><template #default="{ row }"><el-button link type="primary" :loading="processingId === row.id" @click="handle(row)">处理</el-button><el-button link type="danger" :loading="processingId === row.id" @click="handle(row, '已驳回')">驳回</el-button></template></el-table-column> |
| | | </el-table> |
| | | <div class="pagination-row"><el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.size" background layout="total, prev, pager, next" :total="pagination.total" @change="loadData" /></div> |
| | | </section> |
| | | </div> |
| | | </template> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { onMounted, reactive, ref } from 'vue' |
| | | import { Plus, Refresh, Search } from '@element-plus/icons-vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { createBenefitServiceCoupon, getBenefitServiceCoupons, issueBenefitServiceCoupon, updateBenefitServiceCoupon, updateBenefitServiceCouponStatus } from '../api/admin' |
| | | |
| | | const loading = ref(false) |
| | | const rows = ref<Record<string, any>[]>([]) |
| | | const dialogVisible = ref(false) |
| | | const issueVisible = ref(false) |
| | | const editingId = ref<number | string | null>(null) |
| | | const activeCoupon = ref<Record<string, any> | null>(null) |
| | | const filters = reactive({ keyword: '', couponType: '', batchNo: '', status: undefined as number | undefined, validOn: '' }) |
| | | const pagination = reactive({ page: 1, size: 10, total: 0 }) |
| | | const form = reactive({ merchantId: '', couponName: '', couponType: 'CASH', couponAmount: 0, conditionType: 'NONE', conditionAmount: 0, talentLevels: [] as string[], validStart: '', validEnd: '', batchNo: '', totalQuantity: 100, governmentSubsidy: false, status: 1 }) |
| | | const issueForm = reactive({ talentId: '', cardId: '', expireTime: '', remark: '' }) |
| | | const couponTypes = [{ label: '代金券', value: 'CASH' }, { label: '满减券', value: 'FULL_REDUCTION' }, { label: '折扣券', value: 'DISCOUNT' }, { label: '兑换券', value: 'EXCHANGE' }] |
| | | const talentLevels = ['EXCELLENT', 'OUTSTANDING', 'ELITE', 'YOUTH'] |
| | | function text(value: unknown) { return value === undefined || value === null || value === '' ? '-' : String(value) } |
| | | async function loadData() { loading.value = true; try { const result = await getBenefitServiceCoupons({ page: pagination.page, size: pagination.size, keyword: filters.keyword || undefined, couponType: filters.couponType || undefined, batchNo: filters.batchNo || undefined, status: filters.status, validOn: filters.validOn || undefined }); rows.value = result.records || []; pagination.total = result.total || 0 } finally { loading.value = false } } |
| | | function search() { pagination.page = 1; loadData() } |
| | | function resetFilters() { Object.assign(filters, { keyword: '', couponType: '', batchNo: '', status: undefined, validOn: '' }); search() } |
| | | function openCreate() { editingId.value = null; Object.assign(form, { merchantId: '', couponName: '', couponType: 'CASH', couponAmount: 0, conditionType: 'NONE', conditionAmount: 0, talentLevels: [], validStart: '', validEnd: '', batchNo: '', totalQuantity: 100, governmentSubsidy: false, status: 1 }); dialogVisible.value = true } |
| | | function openEdit(row: Record<string, any>) { editingId.value = row.id; Object.assign(form, { merchantId: row.merchantId || '', couponName: row.couponName || row.name || '', couponType: row.couponType || 'CASH', couponAmount: Number(row.couponAmount || row.amount || 0), conditionType: row.conditionType || 'NONE', conditionAmount: Number(row.conditionAmount || 0), talentLevels: row.talentLevels || [], validStart: row.validStart || '', validEnd: row.validEnd || '', batchNo: row.batchNo || '', totalQuantity: Number(row.totalQuantity ?? 100), governmentSubsidy: Boolean(row.governmentSubsidy), status: Number(row.status ?? 1) }); dialogVisible.value = true } |
| | | async function save() { const payload = { ...form, merchantId: Number(form.merchantId), totalQuantity: Number(form.totalQuantity), couponAmount: Number(form.couponAmount), conditionAmount: Number(form.conditionAmount || 0) }; if (editingId.value) await updateBenefitServiceCoupon(editingId.value, payload); else await createBenefitServiceCoupon(payload); ElMessage.success('保存成功'); dialogVisible.value = false; await loadData() } |
| | | async function toggleStatus(row: Record<string, any>) { await updateBenefitServiceCouponStatus(row.id, Number(row.status) === 1 ? 0 : 1); ElMessage.success('状态已更新'); await loadData() } |
| | | function openIssue(row: Record<string, any>) { activeCoupon.value = row; Object.assign(issueForm, { talentId: '', cardId: '', expireTime: '', remark: '' }); issueVisible.value = true } |
| | | async function issue() { if (!activeCoupon.value) return; await issueBenefitServiceCoupon(activeCoupon.value.id, { talentId: Number(issueForm.talentId), cardId: issueForm.cardId ? Number(issueForm.cardId) : undefined, expireTime: issueForm.expireTime || undefined, remark: issueForm.remark }); ElMessage.success('发放成功'); issueVisible.value = false } |
| | | onMounted(loadData) |
| | | </script> |
| | | <template> |
| | | <div class="page-stack"><div class="toolbar"><div><h2>优惠券管理</h2><p>管理优惠券类型、面额、适用商户、有效期、批次和领取发放。</p></div><div class="toolbar-actions"><el-button :icon="Refresh" :loading="loading" @click="loadData">刷新</el-button><el-button type="primary" :icon="Plus" @click="openCreate">新增优惠券</el-button></div></div> |
| | | <section class="panel"><el-form class="filter-form" :model="filters" label-width="84px"><el-form-item label="关键词"><el-input v-model="filters.keyword" clearable placeholder="优惠券或商户名称" /></el-form-item><el-form-item label="券类型"><el-select v-model="filters.couponType" clearable placeholder="全部"><el-option v-for="item in couponTypes" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item><el-form-item label="批次号"><el-input v-model="filters.batchNo" clearable /></el-form-item><el-form-item label="状态"><el-select v-model="filters.status" clearable placeholder="全部"><el-option label="上架" :value="1" /><el-option label="下架" :value="0" /></el-select></el-form-item><el-form-item label="有效日期"><el-date-picker v-model="filters.validOn" value-format="YYYY-MM-DD" /></el-form-item><div class="toolbar-actions"><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="resetFilters">重置</el-button></div></el-form> |
| | | <el-table v-loading="loading" :data="rows" border empty-text="暂无优惠券数据"><el-table-column label="优惠券名称" min-width="160"><template #default="{ row }">{{ text(row.couponName || row.name) }}</template></el-table-column><el-table-column prop="merchantName" label="适用商户" min-width="150" show-overflow-tooltip /><el-table-column prop="couponType" label="类型" width="110" /><el-table-column label="面额" width="100"><template #default="{ row }">{{ text(row.couponAmount || row.amount) }}</template></el-table-column><el-table-column prop="batchNo" label="批次号" min-width="130" /><el-table-column label="有效期" min-width="190"><template #default="{ row }">{{ text(row.validStart) }} ~ {{ text(row.validEnd) }}</template></el-table-column><el-table-column label="状态" width="90"><template #default="{ row }">{{ Number(row.status) === 1 ? '上架' : '下架' }}</template></el-table-column><el-table-column label="操作" width="220" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openEdit(row)">编辑</el-button><el-button link type="primary" @click="toggleStatus(row)">{{ Number(row.status) === 1 ? '下架' : '上架' }}</el-button><el-button link type="primary" @click="openIssue(row)">发放</el-button></template></el-table-column></el-table><div class="pagination-row"><el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.size" background layout="total, prev, pager, next" :total="pagination.total" @change="loadData" /></div></section> |
| | | <el-dialog v-model="dialogVisible" :title="editingId ? '编辑优惠券' : '新增优惠券'" width="680px"><el-form :model="form" label-width="110px"><el-form-item label="商户ID" required><el-input v-model="form.merchantId" /></el-form-item><el-form-item label="券名称" required><el-input v-model="form.couponName" /></el-form-item><el-form-item label="券类型"><el-select v-model="form.couponType"><el-option v-for="item in couponTypes" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item><el-form-item label="面额"><el-input-number v-model="form.couponAmount" :min="0" /></el-form-item><el-form-item label="适用卡等级"><el-select v-model="form.talentLevels" multiple clearable><el-option v-for="level in talentLevels" :key="level" :label="level" :value="level" /></el-select></el-form-item><el-form-item label="有效期"><el-date-picker v-model="form.validStart" value-format="YYYY-MM-DD" placeholder="开始日期" /><el-date-picker v-model="form.validEnd" value-format="YYYY-MM-DD" placeholder="结束日期" /></el-form-item><el-form-item label="批次号"><el-input v-model="form.batchNo" /></el-form-item><el-form-item label="发放总量"><el-input-number v-model="form.totalQuantity" :min="-1" /></el-form-item><el-form-item label="政府补贴"><el-switch v-model="form.governmentSubsidy" /></el-form-item><el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" /></el-form-item></el-form><template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" @click="save">保存</el-button></template></el-dialog> |
| | | <el-dialog v-model="issueVisible" title="发放优惠券" width="520px"><el-form :model="issueForm" label-width="100px"><el-form-item label="持卡人ID" required><el-input v-model="issueForm.talentId" /></el-form-item><el-form-item label="人才卡ID"><el-input v-model="issueForm.cardId" /></el-form-item><el-form-item label="过期时间"><el-date-picker v-model="issueForm.expireTime" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" /></el-form-item><el-form-item label="备注"><el-input v-model="issueForm.remark" type="textarea" /></el-form-item></el-form><template #footer><el-button @click="issueVisible = false">取消</el-button><el-button type="primary" @click="issue">确认发放</el-button></template></el-dialog> |
| | | </div> |
| | | </template> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { onMounted, reactive, ref } from 'vue' |
| | | import { Refresh, Search } from '@element-plus/icons-vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { auditCarWashCredential, getCarWashCredentials, getCarWashJumpConfigs, rollbackCarWashCredential, saveCarWashJumpConfig } from '../api/admin' |
| | | |
| | | const activeTab = ref('configs') |
| | | const loading = ref(false) |
| | | const rows = ref<Record<string, any>[]>([]) |
| | | const filters = reactive({ keyword: '', status: undefined as number | undefined, dateRange: [] as string[] }) |
| | | const pagination = reactive({ page: 1, size: 10, total: 0 }) |
| | | const configDialogVisible = ref(false) |
| | | const configForm = reactive({ merchantId: '', externalLinkType: 'miniProgram', externalMiniProgramAppId: '', externalMiniProgramPath: '', externalWebUrl: '', status: 1 }) |
| | | const statusOptions = [{ label: '待审核', value: 0 }, { label: '已通过', value: 1 }, { label: '已驳回', value: 2 }, { label: '已回滚', value: 3 }] |
| | | async function loadData() { loading.value = true; try { if (activeTab.value === 'configs') { const result = await getCarWashJumpConfigs({ page: pagination.page, size: pagination.size, keyword: filters.keyword || undefined }); rows.value = result.records || []; pagination.total = result.total || 0 } else { const result = await getCarWashCredentials({ page: pagination.page, size: pagination.size, keyword: filters.keyword || undefined, status: filters.status, startDate: filters.dateRange?.[0], endDate: filters.dateRange?.[1] }); rows.value = result.records || []; pagination.total = result.total || 0 } } finally { loading.value = false } } |
| | | function search() { pagination.page = 1; loadData() } |
| | | function resetFilters() { Object.assign(filters, { keyword: '', status: undefined, dateRange: [] }); search() } |
| | | function switchTab() { pagination.page = 1; rows.value = []; loadData() } |
| | | function openConfig(row: Record<string, any>) { Object.assign(configForm, { merchantId: row.merchantId || row.id, externalLinkType: row.externalLinkType || 'none', externalMiniProgramAppId: row.externalMiniProgramAppId || '', externalMiniProgramPath: row.externalMiniProgramPath || '', externalWebUrl: row.externalWebUrl || '', status: Number(row.serviceStatus ?? row.status ?? 1) }); configDialogVisible.value = true } |
| | | async function saveConfig() { await saveCarWashJumpConfig(configForm.merchantId, { externalLinkType: configForm.externalLinkType, externalMiniProgramAppId: configForm.externalMiniProgramAppId, externalMiniProgramPath: configForm.externalMiniProgramPath, externalWebUrl: configForm.externalWebUrl, status: configForm.status }); ElMessage.success('保存成功'); configDialogVisible.value = false; await loadData() } |
| | | async function audit(row: Record<string, any>, status: number) { await auditCarWashCredential(row.id, status, status === 1 ? '后台审核通过' : '后台审核驳回'); ElMessage.success('审核完成'); await loadData() } |
| | | async function rollback(row: Record<string, any>) { await rollbackCarWashCredential(row.id, '后台回滚权益次数'); ElMessage.success('已回滚'); await loadData() } |
| | | onMounted(loadData) |
| | | </script> |
| | | <template> |
| | | <div class="page-stack"> |
| | | <div class="toolbar"><div><h2>洗车权益管理</h2><p>维护洗车第三方跳转配置,查看凭证并人工审核/回滚权益次数。</p></div><el-button :icon="Refresh" :loading="loading" @click="loadData">刷新</el-button></div> |
| | | <section class="panel"> |
| | | <el-tabs v-model="activeTab" @tab-change="switchTab"><el-tab-pane label="跳转配置" name="configs" /><el-tab-pane label="凭证审核" name="credentials" /></el-tabs> |
| | | <el-form class="filter-form" :model="filters" label-width="84px"><el-form-item label="关键词"><el-input v-model="filters.keyword" clearable placeholder="商家、地址、手机号、凭证编号" /></el-form-item><el-form-item v-if="activeTab === 'credentials'" label="状态"><el-select v-model="filters.status" clearable placeholder="全部"><el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item><el-form-item v-if="activeTab === 'credentials'" label="日期"><el-date-picker v-model="filters.dateRange" type="daterange" value-format="YYYY-MM-DD" /></el-form-item><div class="toolbar-actions"><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="resetFilters">重置</el-button></div></el-form> |
| | | <el-table v-if="activeTab === 'configs'" v-loading="loading" :data="rows" border empty-text="暂无跳转配置"><el-table-column prop="merchantId" label="商户ID" width="100" /><el-table-column prop="merchantName" label="商户名称" min-width="160" /><el-table-column prop="externalLinkType" label="跳转类型" min-width="110" /><el-table-column prop="externalMiniProgramAppId" label="小程序AppId" min-width="160" /><el-table-column prop="externalMiniProgramPath" label="小程序路径" min-width="200" show-overflow-tooltip /><el-table-column prop="externalWebUrl" label="网页地址" min-width="200" show-overflow-tooltip /><el-table-column label="状态" width="90"><template #default="{ row }">{{ Number(row.serviceStatus ?? row.status) === 1 ? '启用' : '停用' }}</template></el-table-column><el-table-column label="操作" width="120" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openConfig(row)">配置</el-button></template></el-table-column></el-table> |
| | | <el-table v-else v-loading="loading" :data="rows" border empty-text="暂无洗车凭证"><el-table-column prop="credentialNo" label="凭证编号" min-width="130" /><el-table-column prop="talentName" label="持卡人" min-width="110" /><el-table-column prop="talentMobile" label="手机号" min-width="120" /><el-table-column prop="merchantName" label="商户" min-width="150" /><el-table-column prop="submitTime" label="提交时间" min-width="170" /><el-table-column prop="statusText" label="状态" min-width="110" /><el-table-column label="操作" width="210" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="audit(row, 1)">通过</el-button><el-button link type="danger" @click="audit(row, 2)">驳回</el-button><el-button link type="warning" @click="rollback(row)">回滚</el-button></template></el-table-column></el-table> |
| | | <div class="pagination-row"><el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.size" background layout="total, prev, pager, next" :total="pagination.total" @change="loadData" /></div> |
| | | </section> |
| | | <el-dialog v-model="configDialogVisible" title="洗车跳转配置" width="620px"><el-form :model="configForm" label-width="130px"><el-form-item label="商户ID"><el-input v-model="configForm.merchantId" disabled /></el-form-item><el-form-item label="跳转类型"><el-select v-model="configForm.externalLinkType"><el-option label="第三方小程序" value="miniProgram" /><el-option label="网页" value="web" /><el-option label="不跳转" value="none" /></el-select></el-form-item><el-form-item label="小程序AppId"><el-input v-model="configForm.externalMiniProgramAppId" /></el-form-item><el-form-item label="小程序路径"><el-input v-model="configForm.externalMiniProgramPath" /></el-form-item><el-form-item label="网页地址"><el-input v-model="configForm.externalWebUrl" /></el-form-item><el-form-item label="状态"><el-switch v-model="configForm.status" :active-value="1" :inactive-value="0" /></el-form-item></el-form><template #footer><el-button @click="configDialogVisible = false">取消</el-button><el-button type="primary" @click="saveConfig">保存</el-button></template></el-dialog> |
| | | </div> |
| | | </template> |
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import { onMounted, reactive, ref } from 'vue' |
| | | import { Plus, Refresh, Search } from '@element-plus/icons-vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { createDisplayService, getDisplayServiceCategories, getDisplayServiceDetail, getDisplayServices, updateDisplayService, updateDisplayServiceSort, updateDisplayServiceStatus } from '../api/admin' |
| | | |
| | | const loading = ref(false) |
| | | const rows = ref<Record<string, any>[]>([]) |
| | | const categories = ref<Record<string, any>[]>([]) |
| | | const dialogVisible = ref(false) |
| | | const editingId = ref<number | string | null>(null) |
| | | const filters = reactive({ keyword: '', category: '', status: undefined as number | undefined }) |
| | | const pagination = reactive({ page: 1, size: 10, total: 0 }) |
| | | const form = reactive({ merchantName: '', category: '', contactName: '', contactMobile: '', address: '', logoUrl: '', coverUrl: '', serviceLabel: '', rightsText: '', handlingInstructions: '', sortNo: 0, status: 1, recommend: 0, cardPolicies: [] as any[] }) |
| | | async function loadCategories() { try { categories.value = await getDisplayServiceCategories() } catch { categories.value = [] } } |
| | | async function loadData() { loading.value = true; try { const result = await getDisplayServices({ page: pagination.page, size: pagination.size, keyword: filters.keyword || undefined, category: filters.category || undefined, status: filters.status }); rows.value = result.records || []; pagination.total = result.total || 0 } finally { loading.value = false } } |
| | | function search() { pagination.page = 1; loadData() } |
| | | function resetFilters() { Object.assign(filters, { keyword: '', category: '', status: undefined }); search() } |
| | | function resetForm(row?: Record<string, any>) { editingId.value = row?.serviceId || row?.id || null; Object.assign(form, { merchantName: row?.merchantName || '', category: row?.category || '', contactName: row?.contactName || '', contactMobile: row?.contactMobile || '', address: row?.address || '', logoUrl: row?.logoUrl || '', coverUrl: row?.coverUrl || '', serviceLabel: row?.serviceLabel || '', rightsText: row?.rightsText || '', handlingInstructions: row?.handlingInstructions || '', sortNo: Number(row?.sortNo || 0), status: Number(row?.status ?? 1), recommend: Number(row?.recommend ?? 0), cardPolicies: row?.cardPolicies || [] }) } |
| | | function openCreate() { resetForm(); dialogVisible.value = true } |
| | | async function openEdit(row: Record<string, any>) { const detail = await getDisplayServiceDetail(row.serviceId || row.id); resetForm(detail); dialogVisible.value = true } |
| | | async function save() { if (editingId.value) await updateDisplayService(editingId.value, form); else await createDisplayService(form); ElMessage.success('保存成功'); dialogVisible.value = false; await loadData() } |
| | | async function toggleStatus(row: Record<string, any>) { await updateDisplayServiceStatus(row.serviceId || row.id, Number(row.status) === 1 ? 0 : 1); ElMessage.success('状态已更新'); await loadData() } |
| | | async function changeSort(row: Record<string, any>) { await updateDisplayServiceSort(row.serviceId || row.id, Number(row.sortNo || 0)); ElMessage.success('排序已更新'); await loadData() } |
| | | onMounted(async () => { await loadCategories(); await loadData() }) |
| | | </script> |
| | | <template> |
| | | <div class="page-stack"> |
| | | <div class="toolbar"><div><h2>展示类服务管理</h2><p>维护停车、体检、购房、购车、家电、公寓租赁等展示服务。</p></div><div class="toolbar-actions"><el-button :icon="Refresh" :loading="loading" @click="loadData">刷新</el-button><el-button type="primary" :icon="Plus" @click="openCreate">新增服务</el-button></div></div> |
| | | <section class="panel"> |
| | | <el-form class="filter-form" :model="filters" label-width="84px"><el-form-item label="关键词"><el-input v-model="filters.keyword" clearable placeholder="商户名称、联系人、地址" /></el-form-item><el-form-item label="分类"><el-select v-model="filters.category" clearable placeholder="全部"><el-option label="全部" value="" /><el-option v-for="item in categories" :key="item.key || item.categoryKey" :label="item.name || item.categoryLabel" :value="item.key || item.categoryKey" /></el-select></el-form-item><el-form-item label="状态"><el-select v-model="filters.status" clearable placeholder="全部"><el-option label="上架" :value="1" /><el-option label="下架" :value="0" /></el-select></el-form-item><div class="toolbar-actions"><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="resetFilters">重置</el-button></div></el-form> |
| | | <el-table v-loading="loading" :data="rows" border empty-text="暂无展示服务数据"><el-table-column prop="merchantName" label="服务名称" min-width="150" /><el-table-column prop="categoryName" label="分类" min-width="120" /><el-table-column prop="contactName" label="联系人" min-width="100" /><el-table-column prop="contactMobile" label="联系电话" min-width="130" /><el-table-column prop="address" label="地址" min-width="170" show-overflow-tooltip /><el-table-column prop="sortNo" label="排序" min-width="80" /><el-table-column label="状态" min-width="90"><template #default="{ row }">{{ Number(row.status) === 1 ? '上架' : '下架' }}</template></el-table-column><el-table-column label="操作" width="240" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openEdit(row)">编辑</el-button><el-button link type="primary" @click="toggleStatus(row)">{{ Number(row.status) === 1 ? '下架' : '上架' }}</el-button><el-button link type="primary" @click="changeSort(row)">更新排序</el-button></template></el-table-column></el-table> |
| | | <div class="pagination-row"><el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.size" background layout="total, prev, pager, next" :total="pagination.total" @change="loadData" /></div> |
| | | </section> |
| | | <el-dialog v-model="dialogVisible" title="展示类服务" width="760px"><el-form :model="form" label-width="110px"><el-form-item label="服务名称"><el-input v-model="form.merchantName" /></el-form-item><el-form-item label="分类"><el-select v-model="form.category"><el-option v-for="item in categories" :key="item.key || item.categoryKey" :label="item.name || item.categoryLabel" :value="item.key || item.categoryKey" /></el-select></el-form-item><el-form-item label="联系人"><el-input v-model="form.contactName" /></el-form-item><el-form-item label="联系电话"><el-input v-model="form.contactMobile" /></el-form-item><el-form-item label="地址"><el-input v-model="form.address" /></el-form-item><el-form-item label="封面图"><el-input v-model="form.coverUrl" /></el-form-item><el-form-item label="服务标签"><el-input v-model="form.serviceLabel" /></el-form-item><el-form-item label="权益说明"><el-input v-model="form.rightsText" type="textarea" :rows="4" /></el-form-item><el-form-item label="办理说明"><el-input v-model="form.handlingInstructions" type="textarea" :rows="4" /></el-form-item><el-form-item label="排序"><el-input-number v-model="form.sortNo" :min="0" /></el-form-item><el-form-item label="推荐"><el-switch v-model="form.recommend" :active-value="1" :inactive-value="0" /></el-form-item><el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" /></el-form-item></el-form><template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" @click="save">保存</el-button></template></el-dialog> |
| | | </div> |
| | | </template> |