fix(ui): 移除返回图标旋转样式并优化商户页面功能
- 移除多个页面中的 back-icon transform: rotate(180deg) 样式
- 将开发环境API基础URL从 192.168.110.13 改为 127.0.0.1
- 为商户详情页添加轮播图支持多张封面图片
- 优化商户页面 tabs 显示逻辑,增加使用说明选项
- 修复营业时间页面可编辑时间段功能,支持修改已有时间段
- 优化申请条件页面条件文本展示格式
- 修复后端人才码验证逻辑,移除过期检查
- 更新商户仪表板数据显示逻辑,始终显示统计数据
- 优化商户信用代码输入验证,自动过滤非字母数字字符
1 文件已重命名
22个文件已添加
39个文件已修改
| | |
| | | export const developmentLoginBypassEnabled: boolean = false |
| | | |
| | | // 后台提供合法域名后仅在此处填写,业务页面不得硬编码域名。 |
| | | const developmentBaseUrl = 'http://192.168.110.13:8080' |
| | | // 微信开发者工具运行在本机,使用回环地址可避免公共网络防火墙拦截局域网转发请求。 |
| | | const developmentBaseUrl = 'http://127.0.0.1:8080' |
| | | const stagingBaseUrl = '' |
| | | const productionBaseUrl = '' |
| | | |
| | |
| | | <scroll-view class="content" scroll-y="true" show-scrollbar="false"> |
| | | <view class="hero"><text class="hero-title">{{cardType}}</text><text class="hero-subtitle">东区人才卡申请条件</text></view> |
| | | <view class="summary-card"><text class="summary-title">发放对象</text><text class="summary-text">{{recipient}}</text></view> |
| | | <view class="conditions-card"><text class="conditions-title">具体申请条件</text><text class="conditions-text">{{conditions}}</text></view> |
| | | <view class="conditions-card"> |
| | | <text class="conditions-title">具体申请条件</text> |
| | | <view class="conditions-content"> |
| | | <text v-for="(line, index) in conditionLines" :key="index" :class="conditionLineClass(line)">{{line}}</text> |
| | | </view> |
| | | </view> |
| | | <view class="content-bottom"></view> |
| | | </scroll-view> |
| | | </view> |
| | |
| | | const cardType = ref('东区人才卡') |
| | | const recipient = ref('') |
| | | const conditions = ref('') |
| | | const conditionLines = ref([] as string[]) |
| | | |
| | | const excellentConditions = `一类人才(国内外顶尖人才) |
| | | 1. 国家最高科学技术奖获得者。 |
| | |
| | | // #endif |
| | | recipient.value = '经区人才服务专窗认定的高层次人才,根据人才层次发放卓越人才卡、杰出人才卡或菁英人才卡。' |
| | | conditions.value = '卓越人才卡\n发放对象:经区人才服务专窗认定的一至三类高层次人才。\n\n' + excellentConditions + '\n\n杰出人才卡\n发放对象:经区人才服务专窗认定的四至五类高层次人才。\n\n' + outstandingConditions + '\n\n菁英人才卡\n发放对象:经区人才服务专窗认定的六至七类高层次人才。\n\n' + eliteConditions |
| | | conditionLines.value = conditions.value |
| | | .replace(/(([0-9]+))/g, '\n($1)') |
| | | .split('\n') |
| | | .map((line: string): string => line.trim()) |
| | | .filter((line: string): boolean => line != '') |
| | | }) |
| | | const conditionLineClass = (line: string): string => { |
| | | 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' |
| | | return 'condition-line' |
| | | } |
| | | const goBack = () => safeBack('/pages/card/card') |
| | | </script> |
| | | |
| | |
| | | .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-text { display: block; margin-top: 24rpx; color: #535B66; font-size: 27rpx; line-height: 48rpx; white-space: pre-wrap; } .content-bottom { height: 52rpx; height: calc(52rpx + env(safe-area-inset-bottom)); } |
| | | .conditions-content { margin-top: 24rpx; } |
| | | .condition-line { display: block; color: #535B66; font-size: 27rpx; line-height: 48rpx; } |
| | | .card-heading { margin-top: 34rpx; color: #E84B35; font-size: 32rpx; font-weight: 700; } |
| | | .conditions-content .card-heading:first-child { margin-top: 0; } |
| | | .recipient-heading { margin: 15rpx 0 22rpx; color: #34383E; font-weight: 700; text-align: center; } |
| | | .category-heading { margin-top: 19rpx; color: #34383E; font-weight: 700; } |
| | | .content-bottom { height: 52rpx; height: calc(52rpx + env(safe-area-inset-bottom)); } |
| | | </style> |
| | |
| | | .dialog-title { display: block; color: #386BB0; font-size: 43rpx; line-height: 60rpx; font-weight: 700; text-align: center; } |
| | | .dialog-copy { display: block; margin-top: 44rpx; color: #737F91; font-size: 27rpx; line-height: 42rpx; font-weight: 600; } |
| | | .dialog-phone { display: block; margin-top: 16rpx; color: #FF8957; font-size: 27rpx; line-height: 39rpx; font-weight: 700; } |
| | | .dialog-confirm { display: flex; height: 80rpx; margin: 39rpx 69rpx 0; border-radius: 41rpx; background-image: linear-gradient(90deg, #FF4A32 0%, #FF8A1B 100%); justify-content: center; align-items: center; } |
| | | .dialog-confirm { display: flex; width: 240rpx; height: 68rpx; margin: 39rpx auto 0; border-radius: 35rpx; background-image: linear-gradient(90deg, #FF4A32 0%, #FF8A1B 100%); justify-content: center; align-items: center; } |
| | | .dialog-confirm text { color: #FFFFFF; font-size: 28rpx; font-weight: 700; } |
| | | .dialog-confirm-active { opacity: .78; } |
| | | @keyframes card-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } |
| | |
| | | id: '' + item.id, |
| | | name: item.merchantName != null ? item.merchantName as string : '餐饮商户', |
| | | merchantType: item.merchantType != null ? item.merchantType as string : 'food', |
| | | discount: item.discountText != null ? item.discountText as string : '', |
| | | discount: item.discountText != null && item.discountText != '人才专享' ? item.discountText as string : '', |
| | | hasCoupon: item.couponAvailable === true || Number(item.couponAvailable) == 1, |
| | | address: item.address != null ? item.address as string : '', |
| | | image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackFoodImage, |
| | |
| | | |
| | | <scroll-view :class="'content' + (isMedicalMerchant ? ' content-full' : '')" scroll-y @scroll="handleScroll"> |
| | | <view class="hero"> |
| | | <image class="hero-image" :src="merchantHero" mode="aspectFill" @error="replaceHeroImage"></image> |
| | | <swiper class="hero-swiper" :indicator-dots="displayedHeroImages.length > 1" indicator-color="rgba(255,255,255,.55)" indicator-active-color="#FFFFFF" circular="true"> |
| | | <swiper-item v-for="(imageUrl, index) in displayedHeroImages" :key="index"><image class="hero-image" :src="imageUrl" mode="aspectFill" @error="replaceHeroImage(index)"></image></swiper-item> |
| | | </swiper> |
| | | <view class="hero-shade"></view> |
| | | </view> |
| | | |
| | |
| | | </view> |
| | | |
| | | <view v-if="!merchantError" class="tabs"> |
| | | <view class="tab-item" :class="currentTab == 'primary' ? 'active' : ''" @tap="changeTab('primary')"><text>{{isMedicalMerchant ? '预约' : (groupBuyingEnabled ? '消费购买' : '使用说明')}}</text><view class="tab-line"></view></view> |
| | | <view class="tab-item" :class="currentTab == 'primary' ? 'active' : ''" @tap="changeTab('primary')"><text>{{isMedicalMerchant ? '预约' : (showUsageInstructions ? '使用说明' : '消费购买')}}</text><view class="tab-line"></view></view> |
| | | <view class="tab-item" :class="currentTab == 'rights' ? 'active' : ''" @tap="changeTab('rights')"><text>{{isParkingMerchant ? '停车场地址' : (isTransportMerchant ? '适用门店' : '权益详情')}}</text><view class="tab-line"></view></view> |
| | | </view> |
| | | |
| | |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else-if="showUsageInstructions" class="usage-panel"><text>{{usageInstructions}}</text></view> |
| | | <view v-else-if="groupBuyingEnabled" class="package-list"> |
| | | <view v-if="listLoading" class="list-state"><view class="loading-spinner"></view><text>正在加载可用套餐</text></view> |
| | | <view v-else-if="listError" class="list-state error-state"><text>套餐列表加载失败</text><text class="retry-text" @tap="loadMerchantLists">重新加载</text></view> |
| | |
| | | const merchantType = ref('food') |
| | | const merchantName = ref('百杏厨房(中环天地店)') |
| | | const merchantHero = ref('') |
| | | const merchantHeroImages = ref<string[]>([]) |
| | | const displayedHeroImages = computed((): string[] => merchantHeroImages.value.length > 0 ? merchantHeroImages.value : [merchantHero.value]) |
| | | const fallbackImage = '' |
| | | const replaceHeroImage = () => { merchantHero.value = '' } |
| | | const replaceHeroImage = (index: number) => { |
| | | if (merchantHeroImages.value.length == 0) { merchantHero.value = ''; return } |
| | | if (index >= 0 && index < merchantHeroImages.value.length) merchantHeroImages.value[index] = '' |
| | | } |
| | | const replaceImage = (item: any) => { if (item.image != '') item.image = '' } |
| | | // 非团购服务仍沿用各自的静态标签;餐饮团购标签由接口的券与最低折扣决定。 |
| | | const merchantPrimaryTag = ref('') |
| | |
| | | const listLoading = ref(true) |
| | | const listLoaded = ref(false) |
| | | const listError = ref(false) |
| | | // 餐饮商家没有上架商品时,改为展示当前人才卡等级可用的到店消费说明。 |
| | | const showUsageInstructions = computed(() => !isMedicalMerchant.value && (!groupBuyingEnabled.value || (merchantType.value == 'food' && listLoaded.value && visiblePackages.value.length == 0))) |
| | | const failOnce = ref(false) |
| | | const forceEmpty = ref(false) |
| | | let parkingDetailLoaded = false |
| | |
| | | getMerchantDetail(merchantId.value, talentId.value, (data: any) => { |
| | | merchantName.value = data != null && data.merchantName != null ? data.merchantName as string : merchantName.value |
| | | merchantHero.value = data != null && data.coverUrl != null && data.coverUrl != '' ? resolveApiUrl(data.coverUrl as string) : merchantHero.value |
| | | const heroRecords: any[] = data != null && data.heroImages instanceof Array ? data.heroImages as any[] : [] |
| | | const nextHeroes: string[] = [] |
| | | for (let index = 0; index < heroRecords.length; index++) { |
| | | const value = '' + heroRecords[index] |
| | | if (value != '') nextHeroes.push(resolveApiUrl(value)) |
| | | } |
| | | if (nextHeroes.length == 0) nextHeroes.push(merchantHero.value) |
| | | merchantHeroImages.value = nextHeroes |
| | | merchantAddress.value = data != null && data.address != null ? data.address as string : merchantAddress.value |
| | | const rawType = data != null && data.merchantType != null ? data.merchantType as string : merchantType.value |
| | | merchantType.value = rawType == '餐饮服务' ? 'food' : rawType |
| | | merchantAvailable.value = data == null || data.available !== false |
| | | merchantPhone.value = data != null && data.contactMobile != null ? data.contactMobile as string : '' |
| | | merchantHasCoupon.value = data != null && (data.couponAvailable === true || Number(data.couponAvailable) == 1) |
| | | merchantDiscount.value = data != null && data.discountText != null ? data.discountText as string : merchantDiscount.value |
| | | const discountText = data != null && data.discountText != null ? data.discountText as string : '' |
| | | merchantDiscount.value = discountText == '人才专享' ? '' : discountText |
| | | merchantServiceLabel.value = data != null && data.serviceLabel != null && data.serviceLabel != '' ? data.serviceLabel as string : merchantServiceLabel.value |
| | | applicableCards.value = data != null && data.applicableCards != null && data.applicableCards != '' ? data.applicableCards as string : applicableCards.value |
| | | merchantRightsText.value = data != null && data.rightsText != null && data.rightsText != '' ? data.rightsText as string : merchantRightsText.value |
| | | usageInstructions.value = data != null && data.usageInstructions != null && data.usageInstructions != '' ? data.usageInstructions as string : usageInstructions.value |
| | | groupBuyingEnabled.value = data == null || data.groupBuyingEnabled == null ? groupBuyingEnabled.value : data.groupBuyingEnabled === true || Number(data.groupBuyingEnabled) == 1 |
| | |
| | | .page { width: 100%; height: 100vh; background-color: #F6F6F6; } |
| | | .content { height: calc(100% - 150rpx); height: calc(100% - 150rpx - env(safe-area-inset-bottom)); background-color: #F6F6F6; } |
| | | .hero { height: 445rpx; position: relative; overflow: hidden; background-color: #3B3028; } |
| | | .hero-swiper { width: 100%; height: 445rpx; } |
| | | .hero-image { position: absolute; left: 0; top: 0; width: 100%; height: 445rpx; } |
| | | .hero-shade { position: absolute; left: 0; right: 0; top: 0; height: 190rpx; background-image: linear-gradient(180deg, rgba(0,0,0,.58) 0%, rgba(0,0,0,0) 100%); } |
| | | .custom-nav { position: fixed; z-index: 20; left: 0; right: 0; top: 0; padding-bottom: 20rpx; } |
| | |
| | | <view class="coupon-value"><text>{{couponText}}</text><text class="chevron">›</text></view> |
| | | </view> |
| | | <view class="price-item"><text class="price-label">原价</text><text class="price-value">¥ {{originalAmountText}}</text></view> |
| | | <view class="price-item"><text class="price-label">总优惠金额</text><text class="price-value">-¥ {{discountAmountText}}</text></view> |
| | | <view class="price-item"><text class="price-label">优惠金额</text><text class="price-value">-¥ {{couponAmountText}}</text></view> |
| | | <view class="price-item"><text class="price-label">用户实付</text><text class="price-value strong">¥ {{actualPayAmountText}}</text></view> |
| | | <view class="price-item"><text class="price-label">补贴金额</text><text class="price-value">¥ {{subsidyAmountText}}</text></view> |
| | | </view> |
| | |
| | | const productDiscount = ref('6折') |
| | | const salePriceText = computed(() => salePrice.value.toFixed(2)) |
| | | const originalAmountText = computed(() => originalAmount.value.toFixed(2)) |
| | | const discountAmountText = ref('0.00') |
| | | const couponAmountText = computed(() => couponAmount.value.toFixed(2)) |
| | | const subsidyAmountText = ref('0.00') |
| | | const actualPayAmountText = ref('0.00') |
| | | const createOrderIdempotencyKey = ref('') |
| | |
| | | couponAmount.value = toAmount(data.couponAmount) |
| | | couponText.value = data.couponLabel != null ? data.couponLabel as string : '无' |
| | | productDiscount.value = data.discountText != null ? data.discountText as string : productDiscount.value |
| | | discountAmountText.value = toAmount(data.discountAmount).toFixed(2) |
| | | subsidyAmountText.value = toAmount(data.subsidyAmount).toFixed(2) |
| | | actualPayAmountText.value = toAmount(data.payAmount).toFixed(2) |
| | | }, (error) => { |
| | |
| | | <view class="amount-card"> |
| | | <text class="amount-title">金额明细</text> |
| | | <view class="amount-row"><text>原价</text><text>¥ {{originalAmountText}}</text></view> |
| | | <view class="amount-row"><text>总优惠金额</text><text>- ¥ {{discountAmountText}}</text></view> |
| | | <view class="amount-row"><text>优惠金额</text><text>- ¥ {{discountAmountText}}</text></view> |
| | | <view class="amount-row"><text>用户实付</text><text class="amount-strong">¥ {{actualPayAmountText}}</text></view> |
| | | <view class="amount-row"><text>补贴金额</text><text>¥ {{subsidyAmountText}}</text></view> |
| | | </view> |
| | |
| | | const initialLoading = ref(true) |
| | | const initialError = ref(false) |
| | | const policyUnavailable = ref(false) |
| | | const policyContentText = (data: any): string => { |
| | | const extracted = data != null && data.contentText != null ? (data.contentText as string).trim() : '' |
| | | if (extracted != '') return extracted |
| | | const raw = data != null && data.content != null ? data.content as string : '' |
| | | return raw.replace(/<[^>]+>/g, '').replace(/ /g, ' ').trim() |
| | | } |
| | | |
| | | onLoad((options: any) => { |
| | | if (options != null && options.id != null) policyId.value = decodeURIComponent(options.id as string) |
| | |
| | | getPolicyDetail(policyId.value, (data: any) => { |
| | | policyTitle.value = data != null && data.title != null ? data.title as string : '人才政策' |
| | | policySummary.value = data != null && data.subtitle != null ? data.subtitle as string : '' |
| | | policyCategory.value = data != null && data.categoryName != null ? data.categoryName as string : '东区政策' |
| | | policyContent.value = data != null && data.contentText != null ? data.contentText as string : '' |
| | | policyCategory.value = data != null && data.subtitle != null && (data.subtitle as string).trim() != '' ? data.subtitle as string : '人才政策' |
| | | // 正文直接取后台政策内容;contentText 为空时兼容后台仅返回富文本 content 的情况。 |
| | | policyContent.value = policyContentText(data) |
| | | policyCover.value = '/static/home-assets/home-banner-v2.jpg' |
| | | const heroImage = data != null && data.heroImage != null ? data.heroImage : (data != null ? data.coverUrl : null) |
| | | if (heroImage != null && (heroImage as string).length > 0) policyCover.value = resolveApiUrl(heroImage as string) |
| | |
| | | <view v-else-if="hasLoaded" class="policy-list"> |
| | | <view class="policy-card" v-for="item in visiblePolicies" :key="item.id" hover-class="policy-card-active" @tap="openPolicy(item)"> |
| | | <text class="policy-title">{{item.title}}</text><text class="policy-desc">{{item.description}}</text> |
| | | <view class="policy-foot"><text class="policy-tag">东区政策</text><text class="policy-detail">查看详情</text></view> |
| | | <view class="policy-foot"><text class="policy-tag">{{item.tag}}</text><text class="policy-detail">查看详情</text></view> |
| | | </view> |
| | | </view> |
| | | <view v-if="loadingMore" class="loading-row"><view class="loading-spinner"></view><text>正在加载</text></view> |
| | |
| | | mapped.push({ |
| | | id: '' + item.id, |
| | | title: item.title != null ? item.title as string : '人才政策', |
| | | description: item.subtitle != null ? item.subtitle as string : '' |
| | | description: item.contentPreview != null ? item.contentPreview as string : '', |
| | | tag: item.subtitle != null && (item.subtitle as string).trim() != '' ? item.subtitle as string : '人才政策' |
| | | }) |
| | | } |
| | | return mapped |
| | |
| | | </view> |
| | | |
| | | <view class="product-summary"> |
| | | <view class="price-line"><text class="discount-label">折扣价</text><text class="price-symbol">¥</text><text class="price">{{productPrice}}</text><text class="old-price">¥{{productOriginalPrice}}</text><view class="summary-tags"><text class="coupon-tag">券</text><text class="discount-tag">{{productDiscount}}</text></view></view> |
| | | <view class="price-line"><text class="discount-label">折扣价</text><text class="price-symbol">¥</text><text class="price">{{productPrice}}</text><text class="old-price">¥{{productOriginalPrice}}</text><view class="summary-tags"><text v-if="productHasCoupon" class="coupon-tag">券</text><text v-if="productDiscount != ''" class="discount-tag">{{productDiscount}}</text></view></view> |
| | | <text class="product-name">{{productName}}</text> |
| | | <text v-if="!productAvailable" class="product-status">商品已下架</text> |
| | | <text v-else-if="productSoldOut" class="product-status">已售罄</text> |
| | |
| | | const productPrice = ref('138') |
| | | const productOriginalPrice = ref('266') |
| | | const productDiscount = ref('6折') |
| | | const productHasCoupon = ref(false) |
| | | const productAvailable = ref(true) |
| | | const productSoldOut = ref(false) |
| | | |
| | |
| | | const original = hasOriginal ? Number(data.originalPrice) : 0 |
| | | if (hasSale && !isNaN(sale)) productPrice.value = sale.toFixed(2).replace(/\.00$/, '') |
| | | if (hasOriginal && !isNaN(original) && original > 0) productOriginalPrice.value = original.toFixed(2).replace(/\.00$/, '') |
| | | if (data != null && data.discountText != null && data.discountText != '') productDiscount.value = data.discountText as string |
| | | productHasCoupon.value = data != null && (data.couponAvailable === true || Number(data.couponAvailable) == 1) |
| | | if (data != null && data.discountText != null) productDiscount.value = data.discountText as string |
| | | else if (original > 0 && sale < original) productDiscount.value = '' + Math.round(sale / original * 100) / 10 + '折' |
| | | productAvailable.value = data == null || data.available !== false |
| | | productSoldOut.value = data != null && (data.soldOut === true || Number(data.stock) == 0) |
| | |
| | | const hasTalentCard = ref(false) |
| | | const familyAccessLoading = ref(false) |
| | | const userName = ref('') |
| | | const avatarText = ref('人') |
| | | const avatarText = ref('普') |
| | | const cardLevel = ref('') |
| | | const cardNo = ref('') |
| | | const applicationMessage = ref('请完成认证后开通人才卡。') |
| | |
| | | profileLoading.value = false |
| | | hasTalentCard.value = data != null && data.hasTalentCard === true && data.card != null |
| | | applicationMessage.value = data != null && data.applicationMessage != null ? data.applicationMessage as string : '请完成认证后开通人才卡。' |
| | | if (!hasTalentCard.value) { avatarText.value = '人'; userName.value = '普通游客'; return } |
| | | if (!hasTalentCard.value) { |
| | | userName.value = '普通游客' |
| | | avatarText.value = userName.value.substring(0, 1) |
| | | return |
| | | } |
| | | const card: any = data.card |
| | | userName.value = card.name != null ? card.name as string : '人才用户' |
| | | avatarText.value = userName.value.length > 0 ? userName.value.substring(0, 1) : '人' |
| New file |
| | |
| | | <svg xmlns="http://www.w3.org/2000/svg" width="320" height="200" viewBox="0 0 320 200"> |
| | | <rect width="320" height="200" fill="#eef1f5"/> |
| | | <path d="M92 132l38-42 28 29 21-24 49 37H92z" fill="#c7d0dc"/> |
| | | <circle cx="119" cy="69" r="15" fill="#c7d0dc"/> |
| | | <text x="160" y="174" text-anchor="middle" fill="#8d98a8" font-family="sans-serif" font-size="14">图片加载失败</text> |
| | | </svg> |
| | |
| | | .page { display: flex; flex: 1; flex-direction: column; width: 100%; height: 100vh; overflow: hidden; background-color: #F4F6F9; } |
| | | .navigation-bar { display: flex; height: 174rpx; padding: 68rpx 38rpx 0; flex-direction: row; align-items: center; background-color: #F4F6F9; box-sizing: border-box; position: relative; flex: none; } |
| | | .back-button { display: flex; width: 64rpx; height: 64rpx; justify-content: center; align-items: center; position: absolute; left: 38rpx; top: 89rpx; z-index: 2; } |
| | | .back-icon { width: 38rpx; height: 38rpx; transform: rotate(180deg); } |
| | | .back-icon { width: 38rpx; height: 38rpx; } |
| | | .page-title { width: 100%; color: #202B3E; font-size: 34rpx; line-height: 48rpx; text-align: center; font-weight: 700; } |
| | | .content-scroll { flex: 1; height: 0; padding: 0 34rpx; background-color: #F4F6F9; box-sizing: border-box; } |
| | | .rich-card { padding: 36rpx 32rpx; border-radius: 32rpx; background-color: #FFFFFF; box-shadow: 0 14rpx 34rpx rgba(42, 53, 69, 0.05); box-sizing: border-box; } |
| | |
| | | .page { display: flex; flex: 1; flex-direction: column; width: 100%; height: 100vh; overflow: hidden; background-color: #F4F6F9; } |
| | | .navigation-bar { display: flex; height: 174rpx; padding: 68rpx 38rpx 0; flex-direction: row; align-items: center; background-color: #F4F6F9; box-sizing: border-box; position: relative; flex: none; } |
| | | .back-button { display: flex; width: 64rpx; height: 64rpx; justify-content: center; align-items: center; position: absolute; left: 38rpx; top: 89rpx; z-index: 2; } |
| | | .back-icon { width: 38rpx; height: 38rpx; transform: rotate(180deg); } |
| | | .back-icon { width: 38rpx; height: 38rpx; } |
| | | .page-title { width: 100%; color: #202B3E; font-size: 34rpx; line-height: 48rpx; text-align: center; font-weight: 700; } |
| | | .content { flex: 1; padding: 0 34rpx; background-color: #F4F6F9; box-sizing: border-box; } |
| | | .agreement-card { overflow: hidden; border-radius: 32rpx; background-color: #FFFFFF; box-shadow: 0 14rpx 34rpx rgba(42, 53, 69, 0.05); } |
| | |
| | | .page { display: flex; flex: 1; 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; position: relative; background-color: #F4F6F9; box-sizing: border-box; } |
| | | .back-button { display: flex; width: 64rpx; height: 64rpx; justify-content: center; align-items: center; position: absolute; left: 38rpx; top: 89rpx; z-index: 2; } |
| | | .back-icon { width: 38rpx; height: 38rpx; transform: rotate(180deg); } |
| | | .back-icon { width: 38rpx; height: 38rpx; } |
| | | .page-title { width: 100%; color: #202B3E; font-size: 34rpx; line-height: 48rpx; text-align: center; font-weight: 700; } |
| | | .search-section { padding: 0 34rpx 24rpx; flex: none; box-sizing: border-box; } |
| | | .search-box { display: flex; height: 88rpx; padding: 0 24rpx; flex-direction: row; align-items: center; border-width: 1rpx; border-style: solid; border-color: #E0E5EC; border-radius: 24rpx; background-color: #FFFFFF; box-sizing: border-box; } |
| | |
| | | .page { display: flex; flex: 1; 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; position: relative; background-color: #F4F6F9; box-sizing: border-box; } |
| | | .back-button { display: flex; width: 64rpx; height: 64rpx; justify-content: center; align-items: center; position: absolute; left: 38rpx; top: 89rpx; z-index: 2; } |
| | | .back-icon { width: 38rpx; height: 38rpx; transform: rotate(180deg); } |
| | | .back-icon { width: 38rpx; height: 38rpx; } |
| | | .page-title { width: 100%; color: #202B3E; font-size: 34rpx; line-height: 48rpx; text-align: center; font-weight: 700; } |
| | | .form-scroll { flex: 1; height: 0; padding: 0 34rpx; box-sizing: border-box; } |
| | | .form-card { padding: 8rpx 30rpx; border-radius: 32rpx; background-color: #FFFFFF; box-shadow: 0 14rpx 34rpx rgba(49, 55, 66, 0.05); box-sizing: border-box; } |
| | |
| | | .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-back-icon { width: 34rpx; height: 34rpx; transform: rotate(180deg); } |
| | | .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 { 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; } |
| | |
| | | <view class="hours-list"> |
| | | <view v-for="(item, index) in businessHours" :key="index" class="hours-row"> |
| | | <view class="minus-button" @tap="removeHour(index)"><text>−</text></view> |
| | | <text class="hours-text">{{item.start}}–{{item.end}}</text> |
| | | <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="sheet-mask" @tap="closeAddSheet"></view> |
| | | <view class="time-sheet"> |
| | | <view class="sheet-handle"></view> |
| | | <view class="sheet-header"><text class="sheet-title">添加营业时间段</text><view class="close-sheet" @tap="closeAddSheet"><text>×</text></view></view> |
| | | <view class="sheet-header"><text class="sheet-title">{{editingHourIndex >= 0 ? '编辑营业时间段' : '添加营业时间段'}}</text><view class="close-sheet" @tap="closeAddSheet"><text>×</text></view></view> |
| | | <view class="time-fields"> |
| | | <view class="time-field" :class="activeTimeField == 'start' ? 'active' : ''" @tap="selectTimeField('start')"><text class="time-label">开始时间</text><text class="time-value">{{startTime}}</text></view> |
| | | <view class="time-field" :class="activeTimeField == 'end' ? 'active' : ''" @tap="selectTimeField('end')"><text class="time-label">结束时间</text><text class="time-value">{{endTime}}</text></view> |
| | |
| | | <picker-view-column><view v-for="(hour, index) in hours" :key="hour" class="picker-item" :class="timePickerIndexes[0] == index ? 'selected' : ''">{{hour}} 时</view></picker-view-column> |
| | | <picker-view-column><view v-for="(minute, index) in minutes" :key="minute" class="picker-item" :class="timePickerIndexes[1] == index ? 'selected' : ''">{{minute}} 分</view></picker-view-column> |
| | | </picker-view> |
| | | <view class="sheet-actions"><view class="sheet-cancel" @tap="closeAddSheet"><text>取消</text></view><view class="sheet-confirm" @tap="confirmAddHour"><text>确认添加</text></view></view> |
| | | <view class="sheet-actions"><view class="sheet-cancel" @tap="closeAddSheet"><text>取消</text></view><view class="sheet-confirm" @tap="confirmAddHour"><text>{{editingHourIndex >= 0 ? '确认修改' : '确认添加'}}</text></view></view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | |
| | | const saving = ref(false) |
| | | let cancelHoursRequest: (() => void) | null = null |
| | | const addSheetVisible = ref(false) |
| | | const editingHourIndex = ref(-1) |
| | | const activeTimeField = ref('start') |
| | | const startTime = ref('08:00') |
| | | const endTime = ref('12:00') |
| | |
| | | return |
| | | } |
| | | activeTimeField.value = 'start' |
| | | editingHourIndex.value = -1 |
| | | startTime.value = '08:00' |
| | | endTime.value = '12:00' |
| | | setPickerTime(startTime.value) |
| | | addSheetVisible.value = true |
| | | } |
| | | |
| | | const showEditSheet = (index: number) => { |
| | | const period = businessHours.value[index] |
| | | if (period == null) return |
| | | editingHourIndex.value = index |
| | | activeTimeField.value = 'start' |
| | | startTime.value = period.start |
| | | endTime.value = period.end |
| | | setPickerTime(startTime.value) |
| | | addSheetVisible.value = true |
| | | } |
| | | |
| | | const closeAddSheet = () => { |
| | | addSheetVisible.value = false |
| | | editingHourIndex.value = -1 |
| | | } |
| | | |
| | | const selectTimeField = (field: string) => { |
| | |
| | | uni.showToast({ title: '结束时间需晚于开始时间', icon: 'none' }) |
| | | return |
| | | } |
| | | const overlapHour = businessHours.value.find((item) => { |
| | | return startMinutes < getTimeMinutes(item.end) && endMinutes > getTimeMinutes(item.start) |
| | | const overlapHour = businessHours.value.find((item, index) => { |
| | | return index != editingHourIndex.value && startMinutes < getTimeMinutes(item.end) && endMinutes > getTimeMinutes(item.start) |
| | | }) |
| | | if (overlapHour != null) { |
| | | uni.showToast({ title: '时间段不能与已有时段重叠', icon: 'none' }) |
| | | return |
| | | } |
| | | businessHours.value.push({ start: startTime.value, end: endTime.value }) |
| | | if (editingHourIndex.value >= 0) { |
| | | businessHours.value.splice(editingHourIndex.value, 1, { start: startTime.value, end: endTime.value }) |
| | | } else { |
| | | businessHours.value.push({ start: startTime.value, end: endTime.value }) |
| | | } |
| | | addSheetVisible.value = false |
| | | } |
| | | |
| | |
| | | .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-back-icon { width: 34rpx; height: 34rpx; transform: rotate(180deg); } |
| | | .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 { 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; } |
| | |
| | | .navigation-bar { display: flex; height: 176rpx; flex: none; justify-content: center; align-items: flex-end; padding-bottom: 26rpx; box-sizing: border-box; background-color: #FFFFFF; position: relative; } |
| | | .navigation-title { color: #293344; font-size: 34rpx; line-height: 48rpx; font-weight: 700; } |
| | | .back-button { display: flex; width: 72rpx; height: 72rpx; justify-content: center; align-items: center; position: absolute; left: 24rpx; bottom: 14rpx; } |
| | | .back-icon { width: 38rpx; height: 38rpx; transform: rotate(180deg); } |
| | | .back-icon { width: 38rpx; height: 38rpx; } |
| | | .content { flex: 1; height: 0; padding: 32rpx 30rpx 0; box-sizing: border-box; } |
| | | .talent-card, .amount-card, .method-card, .summary-card { border-radius: 14rpx; background-color: #FFFFFF; box-shadow: 0 4rpx 18rpx rgba(49, 62, 83, 0.06); box-sizing: border-box; } |
| | | .talent-card { display: flex; min-height: 204rpx; padding: 28rpx 30rpx; flex-direction: column; border-width: 1rpx; border-style: solid; border-color: #E4BE59; background-image: linear-gradient(135deg, #C99D32 0%, #F7E69D 55%, #E5BD4E 100%); position: relative; overflow: hidden; } |
| | |
| | | <view class="dashboard"> |
| | | <view v-if="dashboardState == 'loading'" class="dashboard-state"><text class="dashboard-state-text">经营数据加载中</text></view> |
| | | <view v-else-if="dashboardState == 'error'" class="dashboard-state"><text class="dashboard-state-text">{{dashboardError}}</text><text class="dashboard-retry" @tap="loadDashboard">重新加载</text></view> |
| | | <view v-else-if="dashboardState == 'empty'" class="dashboard-state"><text class="dashboard-state-text">暂无经营数据</text><text class="dashboard-state-tip">完成核销后将在这里展示经营统计</text></view> |
| | | <view v-else> |
| | | <view class="amount-card"> |
| | | <view class="card-copy"> |
| | |
| | | <text class="amount-unit">元</text> |
| | | </view> |
| | | </view> |
| | | <view class="round-icon wallet-icon"><image class="metric-svg" src="/static/icons/lucide/wallet-cards.svg" mode="aspectFit"></image></view> |
| | | </view> |
| | | |
| | | <view class="metric-row"> |
| | | <view class="small-card daily-card"> |
| | | <view class="small-card-top"> |
| | | <text class="metric-label">今日核销</text> |
| | | <view class="mini-icon verify-icon"><image class="mini-svg" src="/static/icons/lucide/scan-line-orange.svg" mode="aspectFit"></image></view> |
| | | </view> |
| | | <view class="small-number-line"><text class="small-number">{{todayVerificationCount}}</text><text class="small-unit">笔</text></view> |
| | | </view> |
| | | <view class="small-card weekly-card"> |
| | | <view class="small-card-top"> |
| | | <text class="metric-label">本周核销</text> |
| | | <view class="mini-icon calendar-icon"><image class="mini-svg" src="/static/icons/lucide/calendar-days.svg" mode="aspectFit"></image></view> |
| | | </view> |
| | | <view class="small-number-line"><text class="small-number">{{weekVerificationCount}}</text><text class="small-unit">笔</text></view> |
| | | </view> |
| | |
| | | <view class="subsidy-card"> |
| | | <view class="small-card-top"> |
| | | <text class="metric-label">补贴总额</text> |
| | | <view class="mini-icon subsidy-icon"><image class="mini-svg" src="/static/icons/lucide/hand-coins.svg" mode="aspectFit"></image></view> |
| | | </view> |
| | | <view class="small-number-line"><text class="subsidy-value">{{subsidyAmount}}</text><text class="amount-unit">元</text></view> |
| | | </view> |
| | |
| | | |
| | | const dashboardState = ref('loading') |
| | | const dashboardError = ref('') |
| | | const transactionAmount = ref('') |
| | | const todayVerificationCount = ref('') |
| | | const weekVerificationCount = ref('') |
| | | const subsidyAmount = ref('') |
| | | // 即使商户尚无核销记录,也始终展示统计卡片并以 0 补齐数据。 |
| | | const transactionAmount = ref('0.00') |
| | | const todayVerificationCount = ref('0') |
| | | const weekVerificationCount = ref('0') |
| | | const subsidyAmount = ref('0.00') |
| | | |
| | | const formatAmount = (value: any): string => { |
| | | const amount = Number(value) / 100 |
| | |
| | | return |
| | | } |
| | | if (!hasActiveSession()) { |
| | | dashboardState.value = 'empty' |
| | | dashboardState.value = 'success' |
| | | dashboardError.value = '' |
| | | return |
| | | } |
| | |
| | | .page { display: flex; flex: 1; 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; position: relative; background-color: #F4F6F9; box-sizing: border-box; } |
| | | .back-button { display: flex; width: 64rpx; height: 64rpx; justify-content: center; align-items: center; position: absolute; left: 38rpx; top: 89rpx; z-index: 2; } |
| | | .back-icon { width: 38rpx; height: 38rpx; transform: rotate(180deg); } |
| | | .back-icon { width: 38rpx; height: 38rpx; } |
| | | .page-title { width: 100%; color: #202B3E; font-size: 34rpx; line-height: 48rpx; text-align: center; font-weight: 700; } |
| | | .result-content { display: flex; padding: 70rpx 44rpx 0; flex: 1; flex-direction: column; align-items: center; box-sizing: border-box; } |
| | | .status-icon-wrap { display: flex; width: 176rpx; height: 176rpx; justify-content: center; align-items: center; border-radius: 88rpx; background-image: linear-gradient(145deg, #FFF0E6 0%, #FFF8F3 100%); box-shadow: 0 18rpx 38rpx rgba(255, 107, 44, 0.12); } |
| | |
| | | } |
| | | |
| | | const normalizeCreditCode = () => { |
| | | creditCode.value = creditCode.value.toUpperCase() |
| | | creditCode.value = creditCode.value.toUpperCase().replace(/[^0-9A-Z]/g, '') |
| | | clearFieldError('creditCode') |
| | | } |
| | | |
| | |
| | | .page { display: flex; flex: 1; 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; position: relative; background-color: #F4F6F9; box-sizing: border-box; } |
| | | .back-button { display: flex; width: 64rpx; height: 64rpx; justify-content: center; align-items: center; position: absolute; left: 38rpx; top: 89rpx; z-index: 2; } |
| | | .back-icon { width: 38rpx; height: 38rpx; transform: rotate(180deg); } |
| | | .back-icon { width: 38rpx; height: 38rpx; } |
| | | .page-title { width: 100%; color: #202B3E; font-size: 34rpx; line-height: 48rpx; text-align: center; font-weight: 700; } |
| | | .form-scroll { flex: 1; height: 0; padding: 0 34rpx; box-sizing: border-box; } |
| | | .form-card { margin-top: 26rpx; padding: 30rpx; border-radius: 30rpx; background-color: #FFFFFF; box-shadow: 0 14rpx 34rpx rgba(49, 55, 66, 0.05); box-sizing: border-box; } |
| | |
| | | <text class="section-title benefit-section-title">权益信息</text> |
| | | <view class="benefit-card"> |
| | | <view class="benefit-glow"></view> |
| | | <view class="benefit-head"><text class="customer-name">{{customerName}}</text><text class="talent-tag">卓越人才卡</text></view> |
| | | <view class="benefit-head"><text class="customer-name">{{customerName}}</text><text class="talent-tag">{{cardLevel}}</text></view> |
| | | <text class="customer-phone">{{customerPhone}}</text> |
| | | <view class="benefit-line"><text class="benefit-label">当前权益</text><text class="benefit-content">免费5次/年,超出次数按市价9折</text></view> |
| | | <view class="benefit-line"><text class="benefit-label">当前权益</text><text class="benefit-content">{{benefitName}}</text></view> |
| | | </view> |
| | | <view class="bottom-space"></view> |
| | | </scroll-view> |
| | | <view v-if="isCompleted && !isStoreCompleted" class="bottom-actions"><view class="cancel-verification-button" :class="revokeSubmitting ? 'cancel-verification-submitting' : ''" @tap="handleRevokeVerification"><text>{{revokeSubmitting ? '撤销中…' : '撤销核销'}}</text></view></view> |
| | | <view v-if="isCompleted && !isStoreCompleted" class="bottom-actions"><view class="cancel-verification-button" :class="revokeSubmitting ? 'cancel-verification-submitting' : (!canRevokeVerification ? 'cancel-verification-disabled' : '')" @tap="handleRevokeVerification"><text>{{revokeSubmitting ? '撤销中…' : '撤销核销'}}</text></view></view> |
| | | </view> |
| | | </template> |
| | | |
| | |
| | | const verificationAt = ref('') |
| | | const customerName = ref('') |
| | | const customerPhone = ref('') |
| | | const cardLevel = ref('人才卡') |
| | | const benefitName = ref('以当前订单权益规则为准') |
| | | const formatCents = (value: number): string => '¥' + (value / 100).toFixed(2) |
| | | |
| | | const applyRemoteOrder = (detail: MerchantOrderDetail) => { |
| | | orderNo.value = detail.orderNo |
| | | productAmount.value = formatCents(detail.orderAmount) |
| | | talentDiscount.value = '-¥0.00' |
| | | productDiscount.value = '-¥0.00' |
| | | talentDiscount.value = '-¥' + (detail.talentDiscountAmount / 100).toFixed(2) |
| | | productDiscount.value = '-¥' + (detail.couponAmount / 100).toFixed(2) |
| | | paidAmount.value = formatCents(detail.payAmount) |
| | | subsidyAmount.value = formatCents(detail.subsidyAmount) |
| | | createdAt.value = detail.createdAt.replace('T', ' ') |
| | | verificationAt.value = detail.verifyTime.length > 0 ? detail.verifyTime.replace('T', ' ') : '待核销' |
| | | customerName.value = detail.customerName |
| | | customerPhone.value = detail.customerMobile |
| | | cardLevel.value = detail.cardLevel.length > 0 ? detail.cardLevel : '人才卡' |
| | | benefitName.value = detail.benefitName.length > 0 ? detail.benefitName : '以当前订单权益规则为准' |
| | | isCompleted.value = detail.status == 'completed' |
| | | isStoreCompleted.value = detail.category == 'store' && detail.status == 'completed' |
| | | canRevokeVerification.value = detail.canRevoke |
| | |
| | | const handleRevokeVerification = () => { |
| | | if (revokeSubmitting.value) return |
| | | if (!canRevokeVerification.value) { |
| | | showRevokeExpired() |
| | | return |
| | | } |
| | | uni.showModal({ |
| | |
| | | .paid-page { background-image: linear-gradient(180deg, #E8EDF4 0%, #F3F5F8 390rpx, #F6F5F2 760rpx); } |
| | | .navigation-bar { display: flex; height: 174rpx; flex: none; position: relative; box-sizing: border-box; } |
| | | .back-button { display: flex; width: 72rpx; height: 72rpx; justify-content: center; align-items: center; position: absolute; top: 82rpx; left: 28rpx; z-index: 2; } |
| | | .back-icon { width: 40rpx; height: 40rpx; transform: rotate(180deg); } |
| | | .back-icon { width: 40rpx; height: 40rpx; } |
| | | .detail-scroll { flex: 1; height: 0; padding: 10rpx 34rpx 0; box-sizing: border-box; } |
| | | .status-panel { display: flex; min-height: 126rpx; flex-direction: row; justify-content: space-between; align-items: flex-start; } |
| | | .status-copy { display: flex; flex: 1; flex-direction: column; } |
| | |
| | | .bottom-actions { display: flex; height: 154rpx; padding: 20rpx 96rpx 34rpx; flex: none; background-color: #F6F5F2; box-sizing: border-box; } |
| | | .cancel-verification-button { display: flex; flex: 1; height: 94rpx; justify-content: center; align-items: center; border-radius: 48rpx; color: #FFFFFF; font-size: 29rpx; line-height: 41rpx; font-weight: 600; background-image: linear-gradient(90deg, #FF7625 0%, #FF9630 100%); box-shadow: 0 12rpx 24rpx rgba(255, 107, 36, 0.20); } |
| | | .cancel-verification-submitting { opacity: 0.62; box-shadow: none; } |
| | | .cancel-verification-disabled { background-image: none; background-color: #C8CDD5; box-shadow: none; } |
| | | </style> |
| | |
| | | const currentYear = ref(deviceDate.getFullYear()) |
| | | const currentMonth = ref(deviceDate.getMonth() + 1) |
| | | const currentDay = ref(deviceDate.getDate()) |
| | | const years = ref([(currentYear.value - 2).toString() + '年', (currentYear.value - 1).toString() + '年', currentYear.value.toString() + '年']) |
| | | const months = ref(allMonths.slice(0, currentMonth.value)) |
| | | const days = ref(allDays.slice(0, currentDay.value)) |
| | | const buildYears = (fromYear: number): Array<string> => { |
| | | const values: Array<string> = [] |
| | | for (let year = fromYear; year <= 2099; year++) values.push(year.toString() + '年') |
| | | return values |
| | | } |
| | | const years = ref(buildYears(currentYear.value - 2)) |
| | | const months = ref(allMonths.slice()) |
| | | const days = ref(allDays.slice(0, new Date(currentYear.value, currentMonth.value, 0).getDate())) |
| | | const defaultDate = ref(currentYear.value.toString() + '年' + currentMonth.value.toString() + '月' + currentDay.value.toString() + '日') |
| | | const defaultDateValue = ref(currentYear.value * 10000 + currentMonth.value * 100 + currentDay.value) |
| | | const startDate = ref(datePlaceholder) |
| | |
| | | const year = Number(parts[0]); const month = Number(parts[1]); const day = Number(parts[2]) |
| | | if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day) || month < 1 || month > 12 || day < 1 || day > 31) return |
| | | currentYear.value = year; currentMonth.value = month; currentDay.value = day |
| | | years.value = [(year - 2).toString() + '年', (year - 1).toString() + '年', year.toString() + '年'] |
| | | months.value = allMonths.slice(0, month); days.value = allDays.slice(0, day) |
| | | years.value = buildYears(year - 2) |
| | | months.value = allMonths.slice(); days.value = allDays.slice(0, new Date(year, month, 0).getDate()) |
| | | defaultDate.value = year.toString() + '年' + month.toString() + '月' + day.toString() + '日' |
| | | defaultDateValue.value = year * 10000 + month * 100 + day |
| | | if (!dateFilterApplied.value) { startDate.value = datePlaceholder; startDateValue.value = 0; endDate.value = datePlaceholder; endDateValue.value = 0; pickerIndexes.value = [2, month - 1, day - 1] } |
| | |
| | | } |
| | | |
| | | const getMaxDay = (year: number, month: number) => { |
| | | if (year == currentYear.value && month == currentMonth.value) { |
| | | return currentDay.value |
| | | } |
| | | if (month == 2) { |
| | | return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) ? 29 : 28 |
| | | } |
| | |
| | | const month = Math.floor(dateValue / 100) % 100 |
| | | const day = dateValue % 100 |
| | | const yearIndex = year - (currentYear.value - 2) |
| | | months.value = year == currentYear.value ? allMonths.slice(0, currentMonth.value) : allMonths |
| | | months.value = allMonths.slice() |
| | | days.value = allDays.slice(0, getMaxDay(year, month)) |
| | | pickerIndexes.value = [yearIndex, month - 1, day - 1] |
| | | } |
| | |
| | | let monthIndex = indexes[1] |
| | | let dayIndex = indexes[2] |
| | | const selectedYear = currentYear.value - 2 + yearIndex |
| | | months.value = selectedYear == currentYear.value ? allMonths.slice(0, currentMonth.value) : allMonths |
| | | months.value = allMonths.slice() |
| | | if (monthIndex >= months.value.length) { |
| | | monthIndex = months.value.length - 1 |
| | | } |
| | |
| | | endDate.value = datePlaceholder |
| | | startDateValue.value = 0 |
| | | endDateValue.value = 0 |
| | | months.value = allMonths.slice(0, currentMonth.value) |
| | | days.value = allDays.slice(0, currentDay.value) |
| | | months.value = allMonths.slice() |
| | | days.value = allDays.slice(0, getMaxDay(currentYear.value, currentMonth.value)) |
| | | pickerIndexes.value = [2, currentMonth.value - 1, currentDay.value - 1] |
| | | dateFilterText.value = '时间筛选' |
| | | dateFilterApplied.value = false |
| | |
| | | .page { display: flex; flex: 1; flex-direction: column; width: 100%; height: 100vh; overflow: hidden; background-color: #F4F6F9; } |
| | | .navigation-bar { display: flex; height: 174rpx; padding: 68rpx 38rpx 0; flex-direction: row; align-items: center; background-color: #F4F6F9; box-sizing: border-box; position: relative; flex: none; } |
| | | .back-button { display: flex; width: 64rpx; height: 64rpx; justify-content: center; align-items: center; position: absolute; left: 38rpx; top: 89rpx; z-index: 2; } |
| | | .back-icon { width: 38rpx; height: 38rpx; transform: rotate(180deg); } |
| | | .back-icon { width: 38rpx; height: 38rpx; } |
| | | .page-title { width: 100%; color: #202B3E; font-size: 34rpx; line-height: 48rpx; text-align: center; font-weight: 700; } |
| | | |
| | | .detail-scroll { flex: 1; height: 0; padding: 0 34rpx 164rpx; background-color: #F4F6F9; box-sizing: border-box; } |
| | |
| | | .page { display: flex; flex: 1; flex-direction: column; width: 100%; height: 100vh; overflow: hidden; background-color: #F4F6F9; } |
| | | .navigation-bar { display: flex; height: 174rpx; padding: 68rpx 38rpx 0; flex-direction: row; align-items: center; background-color: #F4F6F9; box-sizing: border-box; position: relative; flex: none; } |
| | | .back-button { display: flex; width: 64rpx; height: 64rpx; justify-content: center; align-items: center; position: absolute; left: 38rpx; top: 89rpx; z-index: 2; } |
| | | .back-icon { width: 38rpx; height: 38rpx; transform: rotate(180deg); } |
| | | .back-icon { width: 38rpx; height: 38rpx; } |
| | | .page-title { width: 100%; color: #202B3E; font-size: 34rpx; line-height: 48rpx; text-align: center; font-weight: 700; } |
| | | |
| | | .tabs { display: flex; height: 96rpx; padding: 0 42rpx; flex-direction: row; background-color: #F4F6F9; box-sizing: border-box; flex: none; } |
| | |
| | | <cover-view v-else class="status-mark" :class="scanState == 'success' ? 'success-mark' : 'error-mark'">{{scanState == 'success' ? '✓' : '!'}}</cover-view> |
| | | <cover-view class="status-title">{{statusTitle}}</cover-view> |
| | | <cover-view class="status-message">{{statusMessage}}</cover-view> |
| | | <cover-view v-if="scanState == 'success' && !verificationConfirmed" class="credential-summary"> |
| | | <cover-view class="credential-type">{{credentialLabel}}</cover-view> |
| | | <cover-view class="credential-person">{{customerName}} {{cardLevel}}</cover-view> |
| | | <cover-view v-if="customerMobile != ''" class="credential-mobile">{{customerMobile}}</cover-view> |
| | | <cover-view v-if="orderNo != ''" class="credential-order">订单号:{{orderNo}}</cover-view> |
| | | </cover-view> |
| | | <cover-view v-if="scanState == 'success' && !verificationConfirmed" class="status-actions"><cover-view class="secondary-action half-action" @tap="resetScanning">重新扫码</cover-view><cover-view class="primary-action half-action" @tap="confirmVerification">确认核销</cover-view></cover-view> |
| | | <cover-view v-else-if="scanState == 'success'" class="primary-action" @tap="resetScanning">继续扫码</cover-view> |
| | | <cover-view v-else-if="scanState == 'camera-error'" class="primary-action" @tap="openCameraSettings">打开相机设置</cover-view> |
| | | <cover-view v-if="scanState == 'camera-error'" class="primary-action" @tap="openCameraSettings">打开相机设置</cover-view> |
| | | <cover-view v-else-if="scanState == 'error'" class="status-actions"> |
| | | <cover-view class="secondary-action" :class="canRetryVerification ? 'half-action' : 'full-action'" @tap="resetScanning">重新扫描</cover-view> |
| | | <cover-view v-if="canRetryVerification" class="primary-action half-action" @tap="retryVerification">重试核验</cover-view> |
| | |
| | | </template> |
| | | |
| | | <script setup lang="uts"> |
| | | import { confirmVerificationCode, scanVerificationCode, VerificationScanResult } from '../../services/verification.uts' |
| | | import { scanVerificationCode, VerificationScanResult } from '../../services/verification.uts' |
| | | import { HttpError } from '../../services/http.uts' |
| | | |
| | | const hasScanned = ref(false) |
| | |
| | | const statusTitle = ref('') |
| | | const statusMessage = ref('') |
| | | const canRetryVerification = ref(false) |
| | | const verificationConfirmed = ref(false) |
| | | const confirming = ref(false) |
| | | const credentialLabel = ref('') |
| | | const customerName = ref('') |
| | | const customerMobile = ref('') |
| | | const cardLevel = ref('') |
| | | const orderNo = ref('') |
| | | let lastScanResult = '' |
| | | let currentVerificationId = '' |
| | | let openedManualConsumption = false |
| | | let openedConfirmation = false |
| | | let cancelVerificationRequest: (() => void) | null = null |
| | |
| | | verifyScanResult(lastScanResult) |
| | | } |
| | | |
| | | const submitConfirmation = () => { |
| | | if (confirming.value || currentVerificationId.length == 0) return |
| | | confirming.value = true |
| | | statusTitle.value = '正在确认核销' |
| | | statusMessage.value = '请稍候,不要重复提交' |
| | | cancelVerificationRequest = confirmVerificationCode(currentVerificationId, 'merchant-verify-' + currentVerificationId + '-' + Date.now(), (result: any, _traceId: string) => { |
| | | cancelVerificationRequest = null |
| | | confirming.value = false |
| | | verificationConfirmed.value = true |
| | | scanState.value = 'success' |
| | | statusTitle.value = '核销成功' |
| | | statusMessage.value = result.message != null ? result.message : '本次核销已完成' |
| | | }, (error: HttpError) => { |
| | | cancelVerificationRequest = null |
| | | confirming.value = false |
| | | scanState.value = 'success' |
| | | statusTitle.value = '确认核销失败' |
| | | statusMessage.value = error.message |
| | | }) |
| | | } |
| | | |
| | | const confirmVerification = () => { |
| | | if (confirming.value || currentVerificationId.length == 0) return |
| | | uni.showModal({ |
| | | title: '确认核销', |
| | | content: '确认后将消耗本次核销凭证,是否继续?', |
| | | confirmText: '确认核销', |
| | | confirmColor: '#FF6B2C', |
| | | success: (result: any) => { |
| | | if (result.confirm) submitConfirmation() |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const resetScanning = () => { |
| | | cancelPendingRequest() |
| | | hasScanned.value = false |
| | | lastScanResult = '' |
| | | currentVerificationId = '' |
| | | verificationConfirmed.value = false |
| | | confirming.value = false |
| | | scanState.value = 'scanning' |
| | | statusTitle.value = '' |
| | | statusMessage.value = '' |
| | | credentialLabel.value = '' |
| | | customerName.value = '' |
| | | customerMobile.value = '' |
| | | cardLevel.value = '' |
| | | orderNo.value = '' |
| | | canRetryVerification.value = false |
| | | } |
| | | |
| | | const credentialTypeLabel = (type: string): string => { |
| | | if (type == 'ORDER_CODE') return '订单核销码' |
| | | if (type == 'FREE_STAY_CODE') return '免费入住核销码' |
| | | return '人才码' |
| | | } |
| | | |
| | | const onCameraError = (event: any) => { |
| | |
| | | .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; transform: rotate(180deg); } |
| | | .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; } |
| | |
| | | .page { display: flex; flex: 1; flex-direction: column; width: 100%; height: 100vh; overflow: hidden; background-color: #F4F6F9; } |
| | | .navigation-bar { display: flex; height: 174rpx; padding: 68rpx 38rpx 0; flex-direction: row; align-items: center; background-color: #F4F6F9; box-sizing: border-box; position: relative; flex: none; } |
| | | .back-button { display: flex; width: 64rpx; height: 64rpx; justify-content: center; align-items: center; position: absolute; left: 38rpx; top: 89rpx; z-index: 2; } |
| | | .back-icon { width: 38rpx; height: 38rpx; transform: rotate(180deg); } |
| | | .back-icon { width: 38rpx; height: 38rpx; } |
| | | .page-title { width: 100%; color: #202B3E; font-size: 34rpx; line-height: 48rpx; text-align: center; font-weight: 700; } |
| | | |
| | | .content { flex: 1; padding: 0 34rpx 164rpx; background-color: #F4F6F9; box-sizing: border-box; } |
| | |
| | | <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="navigation-title">确认核销</text></view> |
| | | <view class="content"> |
| | | <view class="notice-card"><text class="notice-title">请确认核销信息</text><text class="notice-desc">确认后将完成本次权益核销,请仔细核对。</text></view> |
| | | <view class="credential-card"><view class="credential-head"><text class="credential-title">{{credentialLabel}}</text><text class="card-level">{{cardLevel}}</text></view><text class="customer-name">{{customerName}}</text><text class="customer-mobile">{{displayMobile(customerMobile)}}</text><view v-if="orderNo.length > 0" class="info-row"><text>订单号</text><text>{{orderNo}}</text></view></view> |
| | | <view class="navigation-bar"> |
| | | <view class="back-button" @tap.stop="goBack"><image class="back-icon" src="/static/icons/lucide/back-left.svg" mode="aspectFit"></image></view> |
| | | </view> |
| | | <view class="bottom-bar"><view class="confirm-button" :class="submitting ? 'button-disabled' : ''" @tap="confirm"><text>{{submitting ? '核销中…' : '确认核销'}}</text></view></view> |
| | | <scroll-view class="detail-scroll" scroll-y="true" :show-scrollbar="false"> |
| | | <view class="status-panel"> |
| | | <view class="status-copy"><text class="status-title">待核销</text><text class="status-desc">订单已完成支付,请确认后进行核销。</text></view> |
| | | <view class="status-icon-wrap"><image class="status-icon" src="/static/icons/lucide/badge-check.svg" mode="aspectFit"></image></view> |
| | | </view> |
| | | <view class="product-card"> |
| | | <view class="product-cover"></view> |
| | | <view class="product-copy"><text class="product-name">{{title}}</text><view class="product-price-row"><text class="unit-price">¥{{formatAmount(orderAmount)}}</text><view class="actual-price"><text class="actual-label">实付</text><text class="actual-amount">¥{{formatAmount(payAmount)}}</text></view></view></view> |
| | | </view> |
| | | <text class="section-title">订单信息</text> |
| | | <view class="info-card"> |
| | | <view class="info-row"><text class="info-label">订单号</text><text class="info-value">{{orderNo}}</text></view> |
| | | <view class="info-row"><text class="info-label">商品金额</text><text class="info-value">¥{{formatAmount(orderAmount)}}</text></view> |
| | | <view class="info-row"><text class="info-label">人才折扣</text><text class="info-value">-¥{{formatAmount(talentDiscountAmount)}}</text></view> |
| | | <view class="info-row"><text class="info-label">商品优惠</text><text class="info-value">-¥{{formatAmount(couponAmount)}}</text></view> |
| | | <view class="info-row"><text class="info-label">实际支付</text><text class="info-value pay-value">¥{{formatAmount(payAmount)}}</text></view> |
| | | <view class="info-row"><text class="info-label">补贴金额</text><text class="info-value">¥{{formatAmount(subsidyAmount)}}</text></view> |
| | | <view class="info-row"><text class="info-label">下单时间</text><text class="info-value time-value">{{formatDateTime(createdAt)}}</text></view> |
| | | <view class="info-row last-row"><text class="info-label">核销时间</text><text class="info-value muted-value">待核销</text></view> |
| | | </view> |
| | | <text class="section-title benefit-section-title">权益信息</text> |
| | | <view class="benefit-card"><view class="benefit-glow"></view><view class="benefit-head"><text class="customer-name">{{customerName}}</text><text class="talent-tag">{{cardLevel}}</text></view><text class="customer-phone">{{displayMobile(customerMobile)}}</text><view class="benefit-line"><text class="benefit-label">当前权益</text><text class="benefit-content">{{benefitName}}</text></view></view> |
| | | <view class="bottom-space"></view> |
| | | </scroll-view> |
| | | <view class="bottom-actions"><view class="confirm-button" :class="submitting ? 'button-disabled' : ''" @tap="confirm"><text>{{submitting ? '核销中...' : '确认核销'}}</text></view></view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup lang="uts"> |
| | | import { loadMerchantOrderDetail, MerchantOrderDetail } from '../../services/orders.uts' |
| | | import { confirmVerificationCode } from '../../services/verification.uts' |
| | | |
| | | const verificationId = ref('') |
| | | const credentialType = ref('') |
| | | const customerName = ref('持卡人') |
| | | const customerMobile = ref('') |
| | | const cardLevel = ref('人才卡') |
| | | const orderNo = ref('') |
| | | const title = ref('订单') |
| | | const benefitName = ref('人才专属权益') |
| | | const orderAmount = ref(0) |
| | | const talentDiscountAmount = ref(0) |
| | | const couponAmount = ref(0) |
| | | const payAmount = ref(0) |
| | | const subsidyAmount = ref(0) |
| | | const createdAt = ref('') |
| | | const submitting = ref(false) |
| | | const credentialLabel = computed((): string => { |
| | | if (credentialType.value == 'ORDER_CODE') return '订单核销码' |
| | | if (credentialType.value == 'FREE_STAY_CODE') return '免费入住核销码' |
| | | return '人才码' |
| | | }) |
| | | const displayMobile = (mobile: string): string => mobile.length < 7 ? mobile : mobile.substring(0, 3) + '***' + mobile.substring(mobile.length - 4) |
| | | const goBack = () => uni.navigateBack() |
| | | |
| | | const formatAmount = (amount: number): string => (amount / 100).toFixed(2) |
| | | const formatDateTime = (value: string): string => value.length > 0 ? value.replace('T', ' ') : '-' |
| | | const displayMobile = (mobile: string): string => mobile.length < 7 ? mobile : mobile.substring(0, 3) + '****' + mobile.substring(mobile.length - 4) |
| | | const textOption = (options: any, name: string, fallback: string): string => options != null && options[name] != null && ('' + options[name]).length > 0 ? decodeURIComponent('' + options[name]) : fallback |
| | | const goBack = () => uni.navigateBack() |
| | | |
| | | const applyOrder = (order: MerchantOrderDetail) => { |
| | | title.value = order.title.length > 0 ? order.title : title.value |
| | | benefitName.value = order.benefitName.length > 0 ? order.benefitName : benefitName.value |
| | | orderAmount.value = order.orderAmount |
| | | talentDiscountAmount.value = order.talentDiscountAmount |
| | | couponAmount.value = order.couponAmount |
| | | payAmount.value = order.payAmount |
| | | subsidyAmount.value = order.subsidyAmount |
| | | createdAt.value = order.createdAt |
| | | customerName.value = order.customerName.length > 0 ? order.customerName : customerName.value |
| | | customerMobile.value = order.customerMobile.length > 0 ? order.customerMobile : customerMobile.value |
| | | cardLevel.value = order.cardLevel.length > 0 ? order.cardLevel : cardLevel.value |
| | | } |
| | | |
| | | const loadOrder = () => { |
| | | if (orderNo.value.length == 0) return |
| | | loadMerchantOrderDetail(orderNo.value, (order: MerchantOrderDetail, _traceId: string) => applyOrder(order), () => {}) |
| | | } |
| | | |
| | | const confirm = () => { |
| | | if (submitting.value || verificationId.value.length == 0) return |
| | | submitting.value = true |
| | | confirmVerificationCode(verificationId.value, 'merchant-verify-' + verificationId.value + '-' + Date.now(), (result: any, _traceId: string) => { |
| | | submitting.value = false |
| | | uni.redirectTo({ url: '/pages/verification-success/verification-success?message=' + encodeURIComponent(result.message != null ? '' + result.message : '本次核销已完成') }) |
| | | }, (error: any) => { |
| | | submitting.value = false |
| | | uni.showToast({ title: error.message != null ? error.message : '核销失败,请重试', icon: 'none' }) |
| | | uni.showModal({ |
| | | title: '确认核销', |
| | | content: '确认后将完成本次订单核销,是否继续?', |
| | | confirmText: '确认核销', |
| | | confirmColor: '#FF6B2C', |
| | | success: (modal: any) => { |
| | | if (!modal.confirm) return |
| | | submitting.value = true |
| | | confirmVerificationCode(verificationId.value, 'merchant-verify-' + verificationId.value + '-' + Date.now(), (result: any, _traceId: string) => { |
| | | submitting.value = false |
| | | uni.redirectTo({ url: '/pages/verification-success/verification-success?orderNo=' + encodeURIComponent(orderNo.value) }) |
| | | }, (error: any) => { |
| | | submitting.value = false |
| | | uni.showToast({ title: error.message != null ? error.message : '核销失败,请重试', icon: 'none' }) |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | onLoad((options: any) => { |
| | | verificationId.value = textOption(options, 'verificationId', '') |
| | | credentialType.value = textOption(options, 'credentialType', '') |
| | | customerName.value = textOption(options, 'customerName', '持卡人') |
| | | customerMobile.value = textOption(options, 'customerMobile', '') |
| | | cardLevel.value = textOption(options, 'cardLevel', '人才卡') |
| | | orderNo.value = textOption(options, 'orderNo', '') |
| | | loadOrder() |
| | | }) |
| | | </script> |
| | | |
| | | <style> |
| | | page { background-color: #F6F5F2; } |
| | | .page { display: flex; width: 100%; height: 100vh; flex-direction: column; background-color: #F6F5F2; } |
| | | .navigation-bar { display: flex; height: 176rpx; flex: none; justify-content: center; align-items: flex-end; padding-bottom: 26rpx; box-sizing: border-box; background-color: #FFFFFF; position: relative; } |
| | | .navigation-title { color: #293344; font-size: 34rpx; line-height: 48rpx; font-weight: 700; } |
| | | .back-button { display: flex; width: 72rpx; height: 72rpx; justify-content: center; align-items: center; position: absolute; left: 24rpx; bottom: 14rpx; } |
| | | .back-icon { width: 38rpx; height: 38rpx; transform: rotate(180deg); } |
| | | .content { flex: 1; padding: 34rpx 32rpx; box-sizing: border-box; } |
| | | .notice-card, .credential-card { border-radius: 28rpx; background-color: #FFFFFF; box-shadow: 0 12rpx 28rpx rgba(62, 69, 80, 0.06); box-sizing: border-box; } |
| | | .notice-card { padding: 30rpx; } |
| | | .notice-title { display: block; color: #263043; font-size: 32rpx; line-height: 45rpx; font-weight: 700; } |
| | | .notice-desc { display: block; margin-top: 12rpx; color: #87909E; font-size: 25rpx; line-height: 36rpx; } |
| | | .credential-card { padding: 32rpx; margin-top: 26rpx; border-width: 1rpx; border-style: solid; border-color: #E4BE59; background-image: linear-gradient(135deg, #C99D32 0%, #F7E69D 55%, #E5BD4E 100%); } |
| | | .credential-head { display: flex; flex-direction: row; justify-content: space-between; align-items: center; } |
| | | .credential-title, .customer-name, .customer-mobile { color: #FFFFFF; } |
| | | .credential-title { font-size: 27rpx; line-height: 38rpx; font-weight: 600; } |
| | | .card-level { padding: 8rpx 18rpx; border-radius: 22rpx; color: #FFFFFF; font-size: 21rpx; line-height: 30rpx; background-color: rgba(149, 104, 22, 0.46); } |
| | | .customer-name { display: block; margin-top: 34rpx; font-size: 40rpx; line-height: 56rpx; font-weight: 700; } |
| | | .customer-mobile { display: block; margin-top: 11rpx; font-size: 28rpx; line-height: 40rpx; } |
| | | .info-row { display: flex; padding-top: 25rpx; margin-top: 25rpx; border-top-width: 1rpx; border-top-style: solid; border-top-color: rgba(255, 255, 255, 0.35); flex-direction: row; justify-content: space-between; color: #FFFFFF; font-size: 24rpx; line-height: 34rpx; } |
| | | .bottom-bar { display: flex; height: 148rpx; padding: 20rpx 84rpx 30rpx; flex: none; background-color: #FFFFFF; box-sizing: border-box; } |
| | | .confirm-button { display: flex; flex: 1; height: 92rpx; justify-content: center; align-items: center; border-radius: 46rpx; color: #FFFFFF; font-size: 29rpx; line-height: 41rpx; font-weight: 700; background-image: linear-gradient(90deg, #FF7428 0%, #FF9630 100%); } |
| | | .button-disabled { opacity: 0.62; } |
| | | .page { display: flex; flex: 1; flex-direction: column; width: 100%; height: 100vh; overflow: hidden; background-image: linear-gradient(180deg, #FFE3C4 0%, #FFF5EA 390rpx, #F6F5F2 760rpx); } |
| | | .navigation-bar { display: flex; height: 174rpx; flex: none; position: relative; box-sizing: border-box; } |
| | | .back-button { display: flex; width: 72rpx; height: 72rpx; justify-content: center; align-items: center; position: absolute; top: 82rpx; left: 28rpx; z-index: 2; } |
| | | .back-icon { width: 40rpx; height: 40rpx; } |
| | | .detail-scroll { flex: 1; height: 0; padding: 10rpx 34rpx 0; box-sizing: border-box; } |
| | | .status-panel { display: flex; min-height: 126rpx; flex-direction: row; justify-content: space-between; align-items: flex-start; } |
| | | .status-copy { display: flex; flex: 1; flex-direction: column; } |
| | | .status-title { color: #FF633A; font-size: 50rpx; line-height: 70rpx; font-weight: 700; } |
| | | .status-desc { margin-top: 11rpx; color: #97A0AE; font-size: 25rpx; line-height: 36rpx; } |
| | | .status-icon-wrap { display: flex; width: 60rpx; height: 60rpx; margin-top: 10rpx; justify-content: center; align-items: center; border-radius: 30rpx; background-color: #FFE9D8; } |
| | | .status-icon { width: 32rpx; height: 32rpx; } |
| | | .product-card { display: flex; height: 192rpx; padding: 24rpx 28rpx; margin-top: 6rpx; flex-direction: row; align-items: center; border-radius: 28rpx; background-color: #FFFFFF; box-shadow: 0 14rpx 32rpx rgba(86, 69, 49, 0.05); box-sizing: border-box; } |
| | | .product-cover { width: 128rpx; height: 128rpx; border-radius: 20rpx; flex: none; background-color: #D9D9D9; } |
| | | .product-copy { display: flex; height: 128rpx; margin-left: 26rpx; flex: 1; flex-direction: column; justify-content: space-between; } |
| | | .product-name { color: #263043; font-size: 30rpx; line-height: 42rpx; font-weight: 700; } |
| | | .product-price-row, .actual-price { display: flex; flex-direction: row; align-items: flex-end; } |
| | | .product-price-row { justify-content: space-between; } |
| | | .unit-price, .actual-label { color: #344055; font-size: 26rpx; line-height: 37rpx; font-weight: 600; } |
| | | .actual-label { margin-right: 12rpx; } |
| | | .actual-amount { color: #FF6A26; font-size: 35rpx; line-height: 43rpx; font-weight: 700; } |
| | | .section-title { display: block; margin-top: 32rpx; color: #293244; font-size: 31rpx; line-height: 44rpx; font-weight: 700; } |
| | | .info-card { padding: 20rpx 30rpx; margin-top: 22rpx; border-radius: 32rpx; background-color: #FFFFFF; box-shadow: 0 16rpx 38rpx rgba(69, 65, 57, 0.06); box-sizing: border-box; } |
| | | .info-row { display: flex; min-height: 57rpx; flex-direction: row; justify-content: space-between; align-items: center; } |
| | | .info-label { color: #A1A9B6; font-size: 27rpx; line-height: 38rpx; } |
| | | .info-value { color: #414B5E; font-size: 27rpx; line-height: 38rpx; font-weight: 600; text-align: right; } |
| | | .pay-value { color: #FF6439; } |
| | | .time-value { font-size: 25rpx; } |
| | | .muted-value { color: #9AA4B3; font-weight: 500; } |
| | | .last-row { min-height: 60rpx; } |
| | | .benefit-section-title { margin-top: 32rpx; } |
| | | .benefit-card { min-height: 238rpx; padding: 34rpx 34rpx 28rpx; margin-top: 22rpx; border-width: 1rpx; border-style: solid; border-color: #E4BE59; border-radius: 38rpx; background-image: linear-gradient(135deg, #C99D32 0%, #F7E69D 55%, #E5BD4E 100%); box-shadow: 0 18rpx 38rpx rgba(188, 139, 37, 0.20); box-sizing: border-box; position: relative; overflow: hidden; } |
| | | .benefit-glow { width: 200rpx; height: 200rpx; border-radius: 100rpx; position: absolute; top: -75rpx; right: -18rpx; background-color: rgba(255, 255, 255, 0.18); } |
| | | .benefit-head { display: flex; flex-direction: row; justify-content: space-between; align-items: center; position: relative; } |
| | | .customer-name { color: #FFFFFF; font-size: 39rpx; line-height: 55rpx; font-weight: 700; } |
| | | .talent-tag { padding: 9rpx 20rpx; border-radius: 24rpx; color: #FFFFFF; font-size: 22rpx; line-height: 31rpx; font-weight: 600; background-color: rgba(149, 104, 22, 0.46); } |
| | | .customer-phone { display: block; margin-top: 15rpx; color: #FFFFFF; font-size: 29rpx; line-height: 41rpx; font-weight: 600; position: relative; } |
| | | .benefit-line { display: flex; margin-top: 22rpx; flex-direction: row; align-items: center; position: relative; } |
| | | .benefit-label { color: rgba(255, 255, 255, 0.78); font-size: 23rpx; line-height: 33rpx; } |
| | | .benefit-content { margin-left: 18rpx; color: #FFFFFF; font-size: 23rpx; line-height: 33rpx; font-weight: 600; } |
| | | .bottom-space { height: 190rpx; } |
| | | .bottom-actions { display: flex; height: 154rpx; padding: 20rpx 96rpx 34rpx; background-color: #FFFFFF; box-sizing: border-box; position: fixed; right: 0; bottom: 0; left: 0; z-index: 10; } |
| | | .confirm-button { display: flex; flex: 1; height: 94rpx; justify-content: center; align-items: center; border-radius: 48rpx; color: #FFFFFF; font-size: 29rpx; line-height: 41rpx; font-weight: 700; background-image: linear-gradient(90deg, #FF7625 0%, #FF962F 100%); box-shadow: 0 12rpx 24rpx rgba(255, 107, 36, 0.22); } |
| | | .button-disabled { opacity: 0.62; box-shadow: none; } |
| | | </style> |
| | |
| | | .page { display: flex; flex: 1; flex-direction: column; width: 100%; height: 100vh; overflow: hidden; background-image: linear-gradient(180deg, #FFE3C4 0%, #FFF5EA 390rpx, #F6F5F2 760rpx); } |
| | | .navigation-bar { display: flex; height: 174rpx; flex: none; position: relative; box-sizing: border-box; } |
| | | .back-button { display: flex; width: 72rpx; height: 72rpx; justify-content: center; align-items: center; position: absolute; top: 82rpx; left: 28rpx; z-index: 2; } |
| | | .back-icon { width: 40rpx; height: 40rpx; transform: rotate(180deg); } |
| | | .back-icon { width: 40rpx; height: 40rpx; } |
| | | .detail-scroll { flex: 1; height: 0; padding: 10rpx 34rpx 0; box-sizing: border-box; } |
| | | .status-panel { display: flex; min-height: 126rpx; flex-direction: row; justify-content: space-between; align-items: flex-start; } |
| | | .status-copy { display: flex; flex: 1; flex-direction: column; } |
| | |
| | | <template> |
| | | <view class="page"> |
| | | <view class="navigation-bar"><view class="back-button" @tap="returnToScan"><image class="back-icon" src="/static/icons/lucide/back-left.svg" mode="aspectFit"></image></view><text class="navigation-title">核销结果</text></view> |
| | | <view class="result-content"><view class="success-shell"><image class="success-icon" src="/static/icons/lucide/badge-check-orange.svg" mode="aspectFit"></image></view><text class="result-title">核销成功</text><text class="result-message">{{message}}</text><view class="return-button" @tap="returnToScan"><text>继续核销</text></view></view> |
| | | <view class="result-content"><view class="success-shell"><image class="success-icon" src="/static/icons/lucide/badge-check-orange.svg" mode="aspectFit"></image></view><text class="result-title">核销成功</text><text class="result-message">订单号:{{orderNo}}</text><view class="return-button" @tap="viewOrder"><text>查看订单</text></view></view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup lang="uts"> |
| | | const message = ref('本次核销已完成') |
| | | onLoad((options: any) => { if (options != null && options.message != null && ('' + options.message).length > 0) message.value = decodeURIComponent('' + options.message) }) |
| | | const orderNo = ref('-') |
| | | onLoad((options: any) => { if (options != null && options.orderNo != null && ('' + options.orderNo).length > 0) orderNo.value = decodeURIComponent('' + options.orderNo) }) |
| | | const viewOrder = () => { |
| | | if (orderNo.value == '-' || orderNo.value.length == 0) { |
| | | uni.showToast({ title: '订单号缺失', icon: 'none' }) |
| | | return |
| | | } |
| | | uni.navigateTo({ url: '/pages/order-detail/order-detail?orderNo=' + encodeURIComponent(orderNo.value) }) |
| | | } |
| | | const returnToScan = () => uni.navigateBack({ delta: 1, fail: () => uni.reLaunch({ url: '/pages/scan/scan' }) }) |
| | | </script> |
| | | |
| | |
| | | .navigation-bar { display: flex; height: 176rpx; flex: none; justify-content: center; align-items: flex-end; padding-bottom: 26rpx; box-sizing: border-box; background-color: #FFFFFF; position: relative; } |
| | | .navigation-title { color: #293344; font-size: 34rpx; line-height: 48rpx; font-weight: 700; } |
| | | .back-button { display: flex; width: 72rpx; height: 72rpx; justify-content: center; align-items: center; position: absolute; left: 24rpx; bottom: 14rpx; } |
| | | .back-icon { width: 38rpx; height: 38rpx; transform: rotate(180deg); } |
| | | .back-icon { width: 38rpx; height: 38rpx; } |
| | | .result-content { display: flex; flex: 1; padding: 176rpx 64rpx 0; flex-direction: column; align-items: center; box-sizing: border-box; } |
| | | .success-shell { display: flex; width: 170rpx; height: 170rpx; justify-content: center; align-items: center; border-radius: 85rpx; background-color: #FFF0E7; box-shadow: 0 16rpx 34rpx rgba(255, 107, 44, 0.13); } |
| | | .success-icon { width: 100rpx; height: 100rpx; } |
| | | .result-title { margin-top: 42rpx; color: #273345; font-size: 38rpx; line-height: 54rpx; font-weight: 700; } |
| | | .result-message { margin-top: 20rpx; color: #8A94A3; font-size: 25rpx; line-height: 36rpx; text-align: center; } |
| | | .return-button { display: flex; width: 100%; height: 88rpx; margin-top: 94rpx; justify-content: center; align-items: center; border-radius: 44rpx; color: #FFFFFF; font-size: 29rpx; line-height: 41rpx; font-weight: 700; background-image: linear-gradient(90deg, #FF7625 0%, #FF9630 100%); } |
| | | .return-button { display: flex; width: 240rpx; height: 72rpx; margin-top: 34rpx; justify-content: center; align-items: center; border-width: 1rpx; border-style: solid; border-color: #FFC9A7; border-radius: 38rpx; color: #FF5B3B; font-size: 26rpx; line-height: 37rpx; font-weight: 600; background-color: #FFF8EF; } |
| | | </style> |
| | |
| | | <scroll-view class="form-scroll" scroll-y="true" :show-scrollbar="false" :enhanced="false"> |
| | | <view class="available-row"><text class="available-label">当前可提</text><text class="available-value">¥{{availableText}}</text></view> |
| | | <view class="form-card"> |
| | | <view class="field-block"><view class="field-row"><text class="field-label">提现金额</text><view class="amount-input-shell" :class="error.length > 0 ? 'field-invalid' : ''"><text class="amount-prefix">¥</text><input class="amount-input" v-model="amount" type="digit" maxlength="10" placeholder="请输入提现金额" placeholder-class="input-placeholder" @input="clearError" /></view></view><text v-if="error.length > 0" class="field-error">{{error}}</text></view> |
| | | <view class="field-block bank-field-block"><view class="field-row bank-field-row"><text class="field-label">提现银行卡</text><view v-if="selected == null" class="select-bank-button" @tap="chooseBank"><text>选择银行卡</text></view><view v-else class="selected-bank-card"><view class="selected-bank-copy"><text class="selected-card-number">{{selected.cardMasked}}</text><text class="selected-bank-name">{{selected.bankName}}</text></view><view class="change-bank-button" @tap="chooseBank"><text>更换</text></view></view></view></view> |
| | | <view class="submit-button" :class="submitting ? 'submit-disabled' : ''" @tap="submit"><text>{{submitting ? '提交中' : '发起提现'}}</text></view> |
| | | <view class="field-block"> |
| | | <view class="field-row"> |
| | | <text class="field-label">提现金额</text> |
| | | <view class="amount-input-shell" :class="error.length > 0 ? 'field-invalid' : ''"> |
| | | <text class="amount-prefix">¥</text> |
| | | <input class="amount-input" v-model="amount" type="digit" maxlength="10" placeholder="请输入" placeholder-class="input-placeholder" @input="clearError" /> |
| | | </view> |
| | | </view> |
| | | <text v-if="error.length > 0" class="field-error">{{error}}</text> |
| | | </view> |
| | | <view class="field-divider"></view> |
| | | <view class="field-block bank-field-block"> |
| | | <view class="field-row bank-field-row"> |
| | | <text class="field-label">提现银行卡</text> |
| | | <view class="bank-value"> |
| | | <view v-if="selected != null" class="selected-bank-copy"> |
| | | <text class="selected-bank-name">{{selected.bankName}}</text> |
| | | <text class="selected-card-number">{{selected.cardMasked}}</text> |
| | | </view> |
| | | <text v-else class="bank-placeholder">请选择银行卡</text> |
| | | </view> |
| | | <view class="select-bank-button" @tap="chooseBank"><text>{{selected == null ? '选择' : '更换'}}</text></view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view class="submit-button" :class="submitting ? 'submit-disabled' : ''" @tap="submit"><text>{{submitting ? '提交中' : '发起提现'}}</text></view> |
| | | <view class="safe-space"></view> |
| | | </scroll-view> |
| | | </view> |
| | |
| | | </script> |
| | | |
| | | <style> |
| | | page { background-color: #F4F6F9; }.page { display: flex; flex: 1; 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; position: relative; box-sizing: border-box; }.back-button { display: flex; width: 64rpx; height: 64rpx; justify-content: center; align-items: center; position: absolute; left: 38rpx; top: 89rpx; z-index: 2; }.back-icon { width: 38rpx; height: 38rpx; transform: rotate(180deg); }.page-title { width: 100%; color: #202B3E; font-size: 34rpx; line-height: 48rpx; text-align: center; font-weight: 700; }.form-scroll { flex: 1; height: 0; padding: 0 34rpx; box-sizing: border-box; }.available-row { display: flex; height: 88rpx; padding: 0 16rpx; align-items: center; }.available-label { color: #7D8797; font-size: 27rpx; }.available-value { margin-left: 16rpx; color: #FF6B2C; font-size: 30rpx; font-weight: 700; }.form-card { padding: 34rpx 30rpx 36rpx; border-radius: 34rpx; background-color: #FFFFFF; box-shadow: 0 16rpx 38rpx rgba(49,55,66,.06); }.field-block { display: flex; flex-direction: column; }.bank-field-block { margin-top: 34rpx; }.field-row { display: flex; min-height: 94rpx; align-items: center; }.bank-field-row { align-items: flex-start; }.field-label { width: 156rpx; padding-top: 26rpx; flex: none; color: #2C3748; font-size: 28rpx; font-weight: 700; }.amount-input-shell { display: flex; height: 94rpx; padding: 0 24rpx; flex: 1; align-items: center; border: 1rpx solid #E3E7ED; border-radius: 22rpx; background-color: #FFFEFD; box-sizing: border-box; }.amount-prefix { color: #2C3748; font-size: 29rpx; font-weight: 700; }.amount-input { height: 92rpx; margin-left: 10rpx; flex: 1; color: #2C3748; font-size: 29rpx; }.input-placeholder { color: #B5BDC9; font-size: 27rpx; }.field-invalid { border-color: #F0746D; background-color: #FFF9F8; }.field-error { margin: 10rpx 0 0 156rpx; color: #E95B55; font-size: 23rpx; }.select-bank-button { display: flex; min-width: 190rpx; height: 74rpx; padding: 0 28rpx; margin-top: 10rpx; align-items: center; justify-content: center; border: 2rpx solid #FFC3A4; border-radius: 38rpx; color: #FF6B2C; background-color: #FFF7F2; font-size: 26rpx; font-weight: 700; }.selected-bank-card { display: flex; min-height: 118rpx; padding: 22rpx 20rpx 20rpx 24rpx; flex: 1; align-items: center; border-radius: 24rpx; color: #FFFFFF; background-image: linear-gradient(110deg,#FF8D37 0%,#FF6D2C 100%); box-shadow: 0 12rpx 24rpx rgba(255,107,44,.18); box-sizing: border-box; }.selected-bank-copy { display: flex; min-width: 0; flex: 1; flex-direction: column; }.selected-card-number { color: #FFFFFF; font-size: 27rpx; font-weight: 700; }.selected-bank-name { margin-top: 8rpx; color: rgba(255,255,255,.88); font-size: 23rpx; }.change-bank-button { display: flex; width: 86rpx; height: 56rpx; margin-left: 16rpx; flex: none; align-items: center; justify-content: center; border: 1rpx solid rgba(255,255,255,.78); border-radius: 28rpx; color: #FFFFFF; font-size: 23rpx; font-weight: 700; }.submit-button { display: flex; width: 100%; height: 88rpx; margin-top: 54rpx; align-items: center; justify-content: center; border-radius: 44rpx; color: #FFFFFF; background-image: linear-gradient(90deg,#FF7625 0%,#FF9630 100%); font-size: 29rpx; font-weight: 700; box-shadow: 0 12rpx 24rpx rgba(255,107,36,.20); }.submit-disabled { opacity: .58; }.safe-space { height: 70rpx; } |
| | | page { background-color: #F4F6F9; }.page { display: flex; flex: 1; 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; position: relative; box-sizing: border-box; }.back-button { display: flex; width: 64rpx; height: 64rpx; justify-content: center; align-items: center; position: absolute; left: 38rpx; top: 89rpx; z-index: 2; }.back-icon { width: 38rpx; height: 38rpx; transform: rotate(180deg); }.page-title { width: 100%; color: #202B3E; font-size: 34rpx; line-height: 48rpx; text-align: center; font-weight: 700; }.form-scroll { flex: 1; height: 0; padding: 0 34rpx; box-sizing: border-box; }.available-row { display: flex; height: 72rpx; padding: 0 12rpx; align-items: center; }.available-label { color: #7D8797; font-size: 25rpx; }.available-value { margin-left: 12rpx; color: #2C3748; font-size: 27rpx; font-weight: 700; }.form-card { padding: 6rpx 28rpx; border-radius: 16rpx; background-color: #FFFFFF; box-shadow: 0 8rpx 24rpx rgba(49,55,66,.05); }.field-block { display: flex; flex-direction: column; }.bank-field-block { margin-top: 0; }.field-row { display: flex; min-height: 104rpx; align-items: center; }.bank-field-row { align-items: center; }.field-label { width: 178rpx; padding-top: 0; flex: none; color: #5F6978; font-size: 27rpx; font-weight: 400; }.amount-input-shell { display: flex; height: 70rpx; padding: 0 18rpx; flex: 1; align-items: center; border: 1rpx solid #E3E7ED; border-radius: 8rpx; background-color: #FFFFFF; box-sizing: border-box; }.amount-prefix { color: #8B94A2; font-size: 27rpx; font-weight: 400; }.amount-input { height: 68rpx; margin-left: 8rpx; flex: 1; color: #2C3748; font-size: 27rpx; }.input-placeholder { color: #B5BDC9; font-size: 25rpx; }.field-invalid { border-color: #F0746D; background-color: #FFF9F8; }.field-error { margin: 0 0 14rpx 178rpx; color: #E95B55; font-size: 22rpx; }.field-divider { height: 1rpx; margin-left: 178rpx; background-color: #EEF0F3; }.bank-value { display: flex; min-width: 0; flex: 1; }.bank-placeholder { color: #B5BDC9; font-size: 25rpx; }.selected-bank-copy { display: flex; min-width: 0; flex: 1; flex-direction: column; }.selected-bank-name { color: #2C3748; font-size: 25rpx; }.selected-card-number { margin-top: 4rpx; color: #8A93A1; font-size: 22rpx; }.select-bank-button { display: flex; width: 84rpx; height: 52rpx; padding: 0; margin: 0 0 0 14rpx; flex: none; align-items: center; justify-content: center; border: 1rpx solid #58A5F8; border-radius: 8rpx; color: #318CF0; background-color: #FFFFFF; font-size: 23rpx; font-weight: 400; }.submit-button { display: flex; width: 360rpx; height: 72rpx; margin: 34rpx auto 0; align-items: center; justify-content: center; border-radius: 8rpx; color: #FFFFFF; background-color: #409EFF; font-size: 27rpx; font-weight: 500; box-shadow: none; }.submit-disabled { opacity: .58; }.safe-space { height: 70rpx; } |
| | | .back-icon { transform: none; } |
| | | </style> |
| | |
| | | .page { display: flex; flex: 1; 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; position: relative; background-color: #F4F6F9; box-sizing: border-box; } |
| | | .back-button { display: flex; width: 64rpx; height: 64rpx; justify-content: center; align-items: center; position: absolute; left: 38rpx; top: 89rpx; z-index: 2; } |
| | | .back-icon { width: 38rpx; height: 38rpx; transform: rotate(180deg); } |
| | | .back-icon { width: 38rpx; height: 38rpx; } |
| | | .page-title { width: 100%; color: #202B3E; font-size: 34rpx; line-height: 48rpx; text-align: center; font-weight: 700; } |
| | | .result-content { display: flex; flex: 1; padding: 112rpx 64rpx 0; flex-direction: column; align-items: center; box-sizing: border-box; } |
| | | .success-icon-shell { display: flex; width: 170rpx; height: 170rpx; justify-content: center; align-items: center; border-radius: 85rpx; background-color: #FFF0E7; box-shadow: 0 16rpx 34rpx rgba(255, 107, 44, 0.13); } |
| | |
| | | customerMobile: string |
| | | cardLevel: string |
| | | orderAmount: number |
| | | basePayAmount?: number |
| | | couponAmount?: number |
| | | talentDiscountAmount?: number |
| | | payAmount: number |
| | | subsidyAmount: number |
| | | createdAt: string |
| | |
| | | } |
| | | |
| | | export type MerchantOrderDetail = MerchantOrder & { |
| | | basePayAmount: number |
| | | couponAmount: number |
| | | talentDiscountAmount: number |
| | | benefitName: string |
| | | merchantName: string |
| | | canRevoke: boolean |
| | |
| | | customerMobile: value != null && value.customerMobile != null ? '' + value.customerMobile : '', |
| | | cardLevel: value != null && value.cardLevel != null ? '' + value.cardLevel : '卓越人才卡', |
| | | orderAmount: numberValue(value != null ? value.orderAmount : 0), |
| | | basePayAmount: numberValue(value != null ? value.basePayAmount : 0), |
| | | couponAmount: numberValue(value != null ? value.couponAmount : 0), |
| | | talentDiscountAmount: numberValue(value != null ? value.talentDiscountAmount : 0), |
| | | payAmount: numberValue(value != null ? value.payAmount : 0), |
| | | subsidyAmount: numberValue(value != null ? value.subsidyAmount : 0), |
| | | createdAt: value != null && value.createdAt != null ? '' + value.createdAt : '', |
| | |
| | | const order = toOrder(value) |
| | | return { |
| | | ...order, |
| | | basePayAmount: order.basePayAmount != null ? order.basePayAmount : order.orderAmount, |
| | | couponAmount: order.couponAmount != null ? order.couponAmount : 0, |
| | | talentDiscountAmount: order.talentDiscountAmount != null ? order.talentDiscountAmount : 0, |
| | | benefitName: value != null && value.benefitName != null ? '' + value.benefitName : order.title, |
| | | merchantName: value != null && value.merchantName != null ? '' + value.merchantName : '', |
| | | canRevoke: value != null && value.canRevoke == true, |
| | |
| | | return createdOrder(existingOrderId); |
| | | } |
| | | ensurePending(source); |
| | | if (expired(source.get("expiresAt"))) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "人才码已失效,请重新扫码"); |
| | | } |
| | | Map<String, Object> rule = rule(principal.merchantId(), string(source.get("cardLevel")), request.ruleId()); |
| | | Map<String, Object> rule = rule(principal.merchantId(), string(source.get("cardLevel")), request.ruleId()); |
| | | Map<String, Object> coupon = usableCoupon(principal.merchantId(), number(source.get("talentId")), string(source.get("cardLevel")), request.couponId(), discountedAmount(rule, request.amount())); |
| | | Map<String, Object> amounts = quote(rule, coupon, request.amount()); |
| | | Long dynamicCodeId = number(source.get("dynamicCodeId")); |
| | | int codeUpdated = jdbc.update(""" |
| | | update talent_dynamic_code set status = 1, merchant_id = ?, used_at = now() |
| | | where id = ? and status = 0 and expires_at > now() |
| | | where id = ? and status = 0 |
| | | """, principal.merchantId(), dynamicCodeId); |
| | | if (codeUpdated == 0) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "人才码已被使用或已失效"); |
| | |
| | | if (number(source.get("attemptStatus")) != ATTEMPT_PENDING) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "该人才码已处理,请重新扫码"); |
| | | } |
| | | if (expired(source.get("expiresAt"))) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "人才码已失效,请重新扫码"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private List<Map<String, Object>> rules(Long merchantId, String cardLevel) { |
| | | List<Map<String, Object>> configured = configuredRules(merchantId, cardLevel); |
| | |
| | | .map(benefit -> string(benefit.get("benefitText"))).findFirst().orElse("以当前商户到店消费规则为准"); |
| | | } |
| | | |
| | | String benefitTextForMerchant(Long merchantId, String cardLevel) { |
| | | return benefitText(merchantId, cardLevel); |
| | | } |
| | | |
| | | private boolean matchesCardLevel(String configured, String actual) { |
| | | return normalizedCardLevel(configured).equals(normalizedCardLevel(actual)); |
| | | } |
| | |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.time.Duration; |
| | | import java.math.BigDecimal; |
| | | import java.util.ArrayList; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | |
| | | import com.talentcard.server.business.DatabaseRows; |
| | | import com.talentcard.server.business.PageResult; |
| | | import com.talentcard.server.business.merchant.auth.MerchantPrincipal; |
| | | import com.talentcard.server.business.portal.ProductPriceCalculator; |
| | | import com.talentcard.server.common.BusinessException; |
| | | import com.talentcard.server.common.ErrorCode; |
| | | import org.springframework.jdbc.core.JdbcTemplate; |
| | |
| | | |
| | | @Service |
| | | public class MerchantWorkService { |
| | | private static final long REVOCATION_HOURS = 24; |
| | | private static final long REVOCATION_HOURS = 3; |
| | | |
| | | private final JdbcTemplate jdbcTemplate; |
| | | private final ManualConsumptionService manualConsumptionService; |
| | | |
| | | public MerchantWorkService(JdbcTemplate jdbcTemplate) { |
| | | public MerchantWorkService(JdbcTemplate jdbcTemplate, ManualConsumptionService manualConsumptionService) { |
| | | this.jdbcTemplate = jdbcTemplate; |
| | | this.manualConsumptionService = manualConsumptionService; |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | |
| | | arguments.addAll(filters.arguments()); |
| | | |
| | | String recordSql = """ |
| | | select o.id, o.order_no, tu.name as talent_name, tu.mobile as talent_mobile, tc.card_level, |
| | | b.benefit_name, o.order_type, o.order_amount, o.subsidy_amount, o.pay_amount, |
| | | select o.id, o.order_no, tu.name as talent_name, tu.mobile as talent_mobile, tu.gender, tc.card_level, |
| | | b.benefit_name, o.product_name_snapshot, o.order_type, o.order_amount, o.subsidy_amount, o.pay_amount, |
| | | o.order_status, o.verify_status, o.created_at, verified.verify_time |
| | | from benefit_order o |
| | | left join talent_user tu on tu.id = o.talent_id |
| | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> order(String orderNo, Long merchantId, Long storeId) { |
| | | List<Map<String, Object>> rows = query(""" |
| | | select o.id, o.order_no, o.talent_id, tu.name as talent_name, tu.mobile as talent_mobile, |
| | | select o.id, o.order_no, o.talent_id, tu.name as talent_name, tu.mobile as talent_mobile, tu.gender, |
| | | o.card_id, tc.card_no, tc.card_level, o.benefit_id, b.benefit_name, |
| | | o.merchant_id, m.merchant_name, o.order_type, o.order_amount, |
| | | o.merchant_id, m.merchant_name, o.product_id, o.product_name_snapshot, p.rights_config, |
| | | o.order_type, o.order_amount, o.base_pay_amount, o.coupon_amount, |
| | | o.subsidy_amount, o.pay_amount, o.order_status, o.verify_status, o.created_at, |
| | | vr.verify_time, vr.status as verify_record_status |
| | | from benefit_order o |
| | |
| | | left join talent_card tc on tc.id = o.card_id |
| | | left join benefit b on b.id = o.benefit_id |
| | | left join merchant m on m.id = o.merchant_id |
| | | left join merchant_product p on p.id = o.product_id and p.deleted = 0 |
| | | left join verify_record vr on vr.order_id = o.id and vr.status = 1 |
| | | where o.deleted = 0 and o.order_no = ? and o.merchant_id = ? and (o.store_id is null or o.store_id = ?) |
| | | """, orderNo, merchantId, storeId); |
| | |
| | | result.put("category", category); |
| | | result.put("status", status); |
| | | result.put("statusText", "pending_payment".equals(status) ? "待支付" : ("completed".equals(status) ? "已完成" : "待核销")); |
| | | result.put("customerName", maskName(String.valueOf(order.get("talentName")))); |
| | | result.put("customerName", displayName(order.get("talentName"), number(order.get("gender")))); |
| | | result.put("customerMobile", maskMobile(String.valueOf(order.get("talentMobile")))); |
| | | result.put("title", order.get("benefitName") == null ? ("store".equals(category) ? "到店消费订单" : "团购订单") : String.valueOf(order.get("benefitName"))); |
| | | result.put("title", orderTitle(order, category)); |
| | | result.put("benefitName", currentBenefitText(order, category)); |
| | | long orderAmount = number(order.get("orderAmount")) == null ? 0 : number(order.get("orderAmount")); |
| | | long basePayAmount = number(order.get("basePayAmount")) == null ? orderAmount : number(order.get("basePayAmount")); |
| | | long couponAmount = number(order.get("couponAmount")) == null ? 0 : number(order.get("couponAmount")); |
| | | result.put("talentDiscountAmount", Math.max(0, orderAmount - basePayAmount)); |
| | | result.put("couponAmount", Math.max(0, couponAmount)); |
| | | LocalDateTime verifyTime = time(order.get("verifyTime")); |
| | | boolean canRevoke = "GROUP_BUY".equals(orderType) && verifyTime != null && Duration.between(verifyTime, LocalDateTime.now()).toHours() < REVOCATION_HOURS && verifyStatus != null && verifyStatus == 1; |
| | | result.put("canRevoke", canRevoke); |
| | |
| | | result.put("category", category); |
| | | result.put("status", status); |
| | | result.put("statusText", statusText(status)); |
| | | result.put("customerName", maskName(String.valueOf(order.get("talentName")))); |
| | | result.put("customerName", displayName(order.get("talentName"), number(order.get("gender")))); |
| | | result.put("customerMobile", maskMobile(String.valueOf(order.get("talentMobile")))); |
| | | result.put("title", order.get("benefitName") == null ? categoryTitle(category) : String.valueOf(order.get("benefitName"))); |
| | | result.put("title", orderTitle(order, category)); |
| | | return result; |
| | | } |
| | | |
| | | private String orderTitle(Map<String, Object> order, String category) { |
| | | String snapshot = text(order.get("productNameSnapshot")); |
| | | if (!snapshot.isBlank()) return snapshot; |
| | | String benefit = text(order.get("benefitName")); |
| | | return benefit.isBlank() ? categoryTitle(category) : benefit; |
| | | } |
| | | |
| | | private String currentBenefitText(Map<String, Object> order, String category) { |
| | | String cardLevel = text(order.get("cardLevel")); |
| | | if ("store".equals(category)) { |
| | | Long merchantId = number(order.get("merchantId")); |
| | | return merchantId == null ? "以当前商户到店消费规则为准" : manualConsumptionService.benefitTextForMerchant(merchantId, cardLevel); |
| | | } |
| | | if ("group".equals(category)) { |
| | | ProductPriceCalculator.ProductRight right = ProductPriceCalculator.productRight(order.get("rightsConfig"), cardLevel); |
| | | if (right != null) return productBenefitText(right); |
| | | } |
| | | String configured = text(order.get("benefitName")); |
| | | return configured.isBlank() ? "以当前订单权益规则为准" : configured; |
| | | } |
| | | |
| | | private String productBenefitText(ProductPriceCalculator.ProductRight right) { |
| | | String discountText = discountText(right.discount()); |
| | | if (!right.free()) return discountText.isBlank() ? "以商品配置为准" : "按市价" + discountText; |
| | | String freeText = right.limitCount() > 0 ? "免费" + right.limitCount() + "次/" + cycleText(right.resetCycle()) : "免费"; |
| | | return discountText.isBlank() ? freeText : freeText + ",超出次数按市价" + discountText; |
| | | } |
| | | |
| | | private String discountText(BigDecimal discount) { |
| | | if (discount == null || discount.signum() <= 0) return ""; |
| | | return discount.stripTrailingZeros().toPlainString() + "折"; |
| | | } |
| | | |
| | | private String cycleText(String cycle) { |
| | | String value = normalize(cycle).toUpperCase(); |
| | | return switch (value) { |
| | | case "DAY", "DAILY", "每天" -> "天"; |
| | | case "WEEK", "WEEKLY", "每周" -> "周"; |
| | | case "MONTH", "MONTHLY", "每月" -> "月"; |
| | | default -> "年"; |
| | | }; |
| | | } |
| | | |
| | | private String category(String orderType) { |
| | |
| | | return value.length() == 1 ? value + "*" : value.substring(0, 1) + "*".repeat(value.length() - 1); |
| | | } |
| | | |
| | | private String displayName(Object rawName, Long gender) { |
| | | String name = text(rawName); |
| | | if (name.isBlank()) return ""; |
| | | if (gender != null && gender == 1) return name.substring(0, 1) + "先生"; |
| | | if (gender != null && gender == 2) return name.substring(0, 1) + "女士"; |
| | | return maskName(name); |
| | | } |
| | | |
| | | private String text(Object value) { |
| | | return value == null || "null".equals(String.valueOf(value)) ? "" : String.valueOf(value).trim(); |
| | | } |
| | | |
| | | private String maskMobile(String value) { |
| | | if (value == null || value.length() < 7 || "null".equals(value)) return ""; |
| | | return value.substring(0, 3) + "****" + value.substring(value.length() - 4); |
| | |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.time.temporal.TemporalAdjusters; |
| | | import java.util.ArrayList; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> policies(int page, int size) { |
| | | return page(""" |
| | | select id, policy_no, title, subtitle, category_name, cover_url, tags, author, source, view_count, is_top, publish_time |
| | | PageResult<Map<String, Object>> result = page(""" |
| | | select id, policy_no, title, subtitle, category_name, cover_url, tags, author, source, content, view_count, is_top, publish_time |
| | | from policy_article |
| | | where deleted = 0 and status = 1 |
| | | order by is_top desc, sort_no desc, publish_time desc, id desc |
| | | """, "select count(*) from policy_article where deleted = 0 and status = 1", page, size); |
| | | List<Map<String, Object>> records = result.records().stream().map(row -> { |
| | | Map<String, Object> item = new LinkedHashMap<>(row); |
| | | item.put("contentPreview", htmlToText((String) item.get("content"))); |
| | | item.remove("content"); |
| | | return item; |
| | | }).toList(); |
| | | return new PageResult<>(records, result.page(), result.size(), result.total()); |
| | | } |
| | | |
| | | @Transactional |
| | |
| | | public Map<String, Object> merchantDetail(Long id, Long talentId) { |
| | | Map<String, Object> merchant = one(""" |
| | | select id, merchant_name, merchant_type, contact_name, contact_mobile, logo_url, cover_url, address, |
| | | business_status, longitude, latitude, group_buying_enabled, usage_instructions, rights_text, |
| | | business_status, longitude, latitude, group_buying_enabled, usage_instructions, rights_text, merchant_rights_config, |
| | | service_label, status, is_recommend, sort_no |
| | | from merchant |
| | | where deleted = 0 and id = ? |
| | |
| | | merchant.put("groupBuyingEnabled", ((Number) merchant.getOrDefault("groupBuyingEnabled", 1)).intValue() == 1); |
| | | merchant.put("couponAvailable", hasActiveMerchantCoupon(id)); |
| | | merchant.put("discountText", merchantLowestTalentDiscount(id, talentId)); |
| | | String cardLevel = activeProductCardLevel(talentId); |
| | | Map<String, Object> configuredRight = matchingMerchantRight(merchant.get("merchantRightsConfig"), cardLevel); |
| | | if (configuredRight != null) { |
| | | merchant.put("rightsTag", textOrDefault(configuredRight.get("tag"), "")); |
| | | merchant.put("rightsText", textOrDefault(configuredRight.get("detail"), textOrDefault(merchant.get("rightsText"), ""))); |
| | | merchant.put("applicableCards", textOrDefault(configuredRight.get("tag"), cardLevelText(cardLevel) + "可用")); |
| | | } |
| | | merchant.put("usageInstructions", merchantUsageInstructions( |
| | | id, merchant.get("merchantRightsConfig"), cardLevel, merchant.get("usageInstructions"))); |
| | | List<String> heroImages = new ArrayList<>(); |
| | | String coverUrl = textOrDefault(merchant.get("coverUrl"), ""); |
| | | if (!coverUrl.isBlank()) heroImages.add(coverUrl); |
| | | for (Map<String, Object> product : query(""" |
| | | select cover_url |
| | | from merchant_product |
| | | where merchant_id = ? and deleted = 0 and status = 1 and audit_status = 2 |
| | | and cover_url is not null and cover_url <> '' |
| | | order by sort_no desc, id desc |
| | | limit 8 |
| | | """, id)) { |
| | | String imageUrl = textOrDefault(product.get("coverUrl"), ""); |
| | | if (!imageUrl.isBlank() && !heroImages.contains(imageUrl)) heroImages.add(imageUrl); |
| | | } |
| | | merchant.put("heroImages", heroImages); |
| | | return merchant; |
| | | } |
| | | |
| | | private Map<String, Object> matchingMerchantRight(Object rightsConfig, String cardLevel) { |
| | | if (cardLevel == null) return null; |
| | | for (Map<String, Object> right : merchantRightsConfigs(rightsConfig)) { |
| | | if (booleanValue(right.get("enabled")) && sameCardLevel(right.get("level"), cardLevel)) return right; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private String cardLevelText(String cardLevel) { |
| | | return switch (normalizedCardLevel(cardLevel)) { |
| | | case "EXCELLENT" -> "卓越卡"; |
| | | case "OUTSTANDING" -> "杰出卡"; |
| | | case "ELITE" -> "菁英卡"; |
| | | default -> "人才卡"; |
| | | }; |
| | | } |
| | | |
| | | private String merchantUsageInstructions(Long merchantId, Object rightsConfig, String cardLevel, Object fallback) { |
| | | if (cardLevel != null) { |
| | | for (Map<String, Object> right : merchantRightsConfigs(rightsConfig)) { |
| | | if (!booleanValue(right.get("enabled")) || !booleanValue(right.get("consumeEnabled")) |
| | | || !booleanValue(right.get("storeConsume")) || !sameCardLevel(right.get("level"), cardLevel)) continue; |
| | | String description = textOrDefault(right.get("consumeDesc"), ""); |
| | | if (!description.isBlank()) return description; |
| | | } |
| | | List<Map<String, Object>> benefits = query(""" |
| | | select card_level, benefit_text |
| | | from merchant_card_benefit |
| | | where merchant_id = ? and status = 1 |
| | | order by sort_no desc, id desc |
| | | """, merchantId); |
| | | for (Map<String, Object> benefit : benefits) { |
| | | if (sameCardLevel(benefit.get("cardLevel"), cardLevel)) { |
| | | String description = textOrDefault(benefit.get("benefitText"), ""); |
| | | if (!description.isBlank()) return description; |
| | | } |
| | | } |
| | | } |
| | | return textOrDefault(fallback, "以当前商户到店消费规则为准"); |
| | | } |
| | | |
| | | private List<Map<String, Object>> merchantRightsConfigs(Object value) { |
| | | if (value == null || String.valueOf(value).isBlank()) return List.of(); |
| | | try { |
| | | return OBJECT_MAPPER.readValue(String.valueOf(value), new TypeReference<List<Map<String, Object>>>() {}); |
| | | } catch (Exception ignored) { |
| | | return List.of(); |
| | | } |
| | | } |
| | | |
| | | private boolean booleanValue(Object value) { |
| | | if (value instanceof Boolean bool) return bool; |
| | | if (value instanceof Number number) return number.intValue() != 0; |
| | | String text = value == null ? "" : String.valueOf(value).trim(); |
| | | return "true".equalsIgnoreCase(text) || "1".equals(text) || "yes".equalsIgnoreCase(text); |
| | | } |
| | | |
| | | private boolean sameCardLevel(Object configured, String actual) { |
| | | return normalizedCardLevel(configured).equals(normalizedCardLevel(actual)); |
| | | } |
| | | |
| | | private String normalizedCardLevel(Object value) { |
| | | String level = value == null ? "" : String.valueOf(value).trim().toUpperCase(Locale.ROOT); |
| | | if (level.contains("卓越") || "EXCELLENT".equals(level)) return "EXCELLENT"; |
| | | if (level.contains("杰出") || "OUTSTANDING".equals(level)) return "OUTSTANDING"; |
| | | if (level.contains("菁英") || "ELITE".equals(level)) return "ELITE"; |
| | | return level; |
| | | } |
| | | |
| | | private boolean hasActiveMerchantCoupon(Long merchantId) { |
| | |
| | | from merchant_product |
| | | where merchant_id = ? and deleted = 0 and status = 1 and audit_status = 2 |
| | | """, merchantId)) { |
| | | BigDecimal originalPrice = decimal(product.get("originalPrice")); |
| | | if (originalPrice.signum() <= 0) originalPrice = decimal(product.get("salePrice")); |
| | | if (originalPrice.signum() <= 0) continue; |
| | | ProductPriceCalculator.ProductPrice price = ProductPriceCalculator.calculate( |
| | | originalPrice, product.get("salePrice"), product.get("rightsConfig"), cardLevel, |
| | | usedFreeCount(talentId, product.get("id"), product.get("rightsConfig"), cardLevel)); |
| | | if (!price.personalized()) continue; |
| | | BigDecimal rate = price.salePrice().divide(originalPrice, 6, RoundingMode.HALF_UP); |
| | | ProductPriceCalculator.ProductRight right = ProductPriceCalculator.productRight( |
| | | product.get("rightsConfig"), cardLevel); |
| | | if (right == null || right.discount().signum() <= 0) continue; |
| | | BigDecimal rate = right.discount().divide(BigDecimal.TEN, 6, RoundingMode.HALF_UP); |
| | | if (lowestRate == null || rate.compareTo(lowestRate) < 0) { |
| | | lowestRate = rate; |
| | | } |
| | | } |
| | | if (lowestRate == null || lowestRate.compareTo(BigDecimal.ONE) >= 0) return ""; |
| | | if (lowestRate.signum() == 0) return "免费"; |
| | | double discount = Math.round(lowestRate.doubleValue() * 100) / 10.0; |
| | | return discount + "折"; |
| | | } |
| | |
| | | BigDecimal personalizedPrice = personalizedProductPrice(product, talentId); |
| | | product.put("salePrice", personalizedPrice); |
| | | product.put("personalized", talentId != null && personalizedPrice.compareTo(configuredSalePrice) != 0); |
| | | product.put("discountText", discountText(personalizedPrice, decimal(product.get("originalPrice")))); |
| | | Long merchantId = ((Number) product.get("merchantId")).longValue(); |
| | | product.put("couponAvailable", hasActiveMerchantCoupon(merchantId)); |
| | | product.put("discountText", merchantLowestTalentDiscount(merchantId, talentId)); |
| | | return product; |
| | | } |
| | | |
| | |
| | | import tools.jackson.core.type.TypeReference; |
| | | import tools.jackson.databind.ObjectMapper; |
| | | |
| | | final class ProductPriceCalculator { |
| | | public final class ProductPriceCalculator { |
| | | private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); |
| | | private static final TypeReference<List<Map<String, Object>>> RIGHTS_TYPE = new TypeReference<>() {}; |
| | | |
| | |
| | | return new ProductPrice(original, calculatedSale.setScale(2, RoundingMode.HALF_UP), right != null); |
| | | } |
| | | |
| | | static ProductRight productRight(Object rightsConfig, Object cardLevel) { |
| | | public static ProductRight productRight(Object rightsConfig, Object cardLevel) { |
| | | Map<String, Object> right = matchingRight(rightsConfig, cardLevel); |
| | | if (right == null) return null; |
| | | boolean free = booleanValue(right.get("free")); |
| | | return new ProductRight( |
| | | booleanValue(right.get("free")), |
| | | free, |
| | | intValue(right.get("limitCount")), |
| | | text(right.get("resetCycle"))); |
| | | text(right.get("resetCycle")), |
| | | discountValue(right.get(free ? "exceedDiscount" : "discount"))); |
| | | } |
| | | |
| | | private static Map<String, Object> matchingRight(Object rightsConfig, Object cardLevel) { |
| | |
| | | record ProductPrice(BigDecimal originalPrice, BigDecimal salePrice, boolean personalized) { |
| | | } |
| | | |
| | | record ProductRight(boolean free, int limitCount, String resetCycle) { |
| | | public record ProductRight(boolean free, int limitCount, String resetCycle, BigDecimal discount) { |
| | | } |
| | | } |
| | |
| | | List<Map<String, Object>> cards = query(""" |
| | | select c.id as card_id, c.card_no, c.card_level, c.valid_start, c.valid_end, c.status as card_status, |
| | | c.is_family_card_enabled, c.family_card_limit, |
| | | t.id as talent_id, t.name, t.gender, t.work_unit, t.status as talent_status |
| | | t.id as talent_id, t.name, t.gender, t.work_unit, t.status as talent_status, |
| | | t.advisor_id, t.service_staff_id |
| | | from talent_card c |
| | | join talent_user t on t.id = c.talent_id and t.deleted = 0 |
| | | where c.deleted = 0 and c.talent_id = ? and c.card_type = 'PERSONAL' |
| | |
| | | card.put("familyCardLimit", ((Number) row.getOrDefault("familyCardLimit", 0)).intValue()); |
| | | result.put("hasTalentCard", true); |
| | | result.put("card", card); |
| | | advisor(level).ifPresent(advisor -> { |
| | | String consultUrl = value(advisor.get("consultUrl"), ""); |
| | | advisor.put("consultUrl", isAllowedExternalWebUrl(consultUrl) ? consultUrl : ""); |
| | | result.put("advisor", advisor); |
| | | java.util.Optional<Map<String, Object>> advisor = serviceStaff(row.get("serviceStaffId") instanceof Number staffId ? staffId.longValue() : null); |
| | | if (advisor.isEmpty()) { |
| | | advisor = advisor(row.get("advisorId") instanceof Number advisorId ? advisorId.longValue() : null); |
| | | } |
| | | if (advisor.isEmpty()) { |
| | | advisor = advisor(level); |
| | | } |
| | | advisor.ifPresent(item -> { |
| | | String consultUrl = value(item.get("consultUrl"), ""); |
| | | item.put("consultUrl", isAllowedExternalWebUrl(consultUrl) ? consultUrl : ""); |
| | | result.put("advisor", item); |
| | | }); |
| | | return result; |
| | | } |
| | |
| | | return java.util.Optional.of(result); |
| | | } |
| | | |
| | | private java.util.Optional<Map<String, Object>> advisor(Long advisorId) { |
| | | if (advisorId == null) return java.util.Optional.empty(); |
| | | List<Map<String, Object>> advisors = query(""" |
| | | select advisor_name, department, mobile, consult_url |
| | | from talent_card_advisor |
| | | where id = ? and status = 1 |
| | | limit 1 |
| | | """, advisorId); |
| | | if (advisors.isEmpty()) return java.util.Optional.empty(); |
| | | Map<String, Object> row = advisors.get(0); |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("name", row.get("advisorName")); |
| | | result.put("department", row.get("department")); |
| | | result.put("mobile", row.get("mobile")); |
| | | result.put("consultUrl", row.get("consultUrl")); |
| | | return java.util.Optional.of(result); |
| | | } |
| | | |
| | | private java.util.Optional<Map<String, Object>> serviceStaff(Long staffId) { |
| | | if (staffId == null) return java.util.Optional.empty(); |
| | | List<Map<String, Object>> staff = query(""" |
| | | select display_name, position_name, mobile |
| | | from admin_operator |
| | | where id = ? and deleted = 0 and status = 1 |
| | | limit 1 |
| | | """, staffId); |
| | | if (staff.isEmpty()) return java.util.Optional.empty(); |
| | | Map<String, Object> row = staff.get(0); |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("name", row.get("displayName")); |
| | | result.put("department", value(row.get("positionName"), "")); |
| | | result.put("mobile", row.get("mobile")); |
| | | result.put("consultUrl", ""); |
| | | return java.util.Optional.of(result); |
| | | } |
| | | |
| | | private boolean isExcellenceCardLevel(String level) { |
| | | if (level == null) return false; |
| | | String normalized = level.trim().toUpperCase(Locale.ROOT); |
| | |
| | | void adminCanPublishNewsForThePortalAndContentIsSanitized() { |
| | | Map<String, Object> draft = adminBusinessService.createNews(new NewsArticleRequest( |
| | | "后台发布资讯", "用于联调", "", "东区发布", "管理员", |
| | | "<p>安全正文</p><script>alert('x')</script>", true, 12, 0)); |
| | | "<p>安全正文</p><script>alert('x')</script>", true, 12, 0), "测试管理员"); |
| | | Long id = ((Number) draft.get("id")).longValue(); |
| | | |
| | | assertThat(portalService.news(1, 10).total()).isZero(); |