feat(code-pay): 补齐云闪付/抖音码牌端页与金额上限及商户简称展示

This commit is contained in:
bootx
2026-07-17 19:54:29 +08:00
parent 7f9974982b
commit c3dd1c2fd5
23 changed files with 455 additions and 36 deletions

View File

@@ -161,6 +161,38 @@ const routeModuleList: Array<RouteRecordRaw> = [
},
],
},
// 码牌支付-云闪付端(静态段须在 /h/:code 之前注册)
{
path: RoutePath.CODE_PAY_UNION,
name: 'CodePayUnion',
component: Layout,
meta: {
title: '码牌支付',
},
children: [
{
path: '',
name: 'CodePayUnionPage',
component: () => import('@/mobile/views/code-pay/union-pay/index.vue'),
},
],
},
// 码牌支付-抖音端
{
path: RoutePath.CODE_PAY_DOUYIN,
name: 'CodePayDouyin',
component: Layout,
meta: {
title: '码牌支付',
},
children: [
{
path: '',
name: 'CodePayDouyinPage',
component: () => import('@/mobile/views/code-pay/douyin/index.vue'),
},
],
},
// 码牌支付入口分发跨端PC 为扫码引导页)
{
path: RoutePath.CODE_PAY,

View File

@@ -70,6 +70,7 @@ onMounted(() => {
<CodePayShell
v-else
:merchant-name="info.name"
:mch-short-name="info.mchShortName"
:display-amount="displayAmount"
:amount-type="info.amountType"
:description="description"

View File

@@ -0,0 +1,151 @@
<script lang="ts" setup>
/**
* 码牌支付-抖音端(黑白主视觉)
*/
import { onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import CodePayShell from '../shared/CodePayShell.vue'
import { useCodePayPage } from '../shared/useCodePayPage'
defineOptions({ name: 'CodePayDouyin' })
const { t } = useI18n()
const route = useRoute()
const code = route.params.code as string
const {
loading,
paying,
loadError,
info,
description,
setDescription,
displayAmount,
paid,
orderNo,
authRedirecting,
init,
onInput,
onDelete,
pay,
closePage,
} = useCodePayPage({ code, clientEnv: 'douyin' })
onMounted(() => {
init()
})
</script>
<template>
<div class="code-pay-page">
<!-- 加载 / 授权中品牌顶栏 + 上浮卡 -->
<template v-if="loading || authRedirecting">
<div class="code-pay-page__brand code-pay-page__brand--douyin" />
<div class="code-pay-page__card">
<van-loading color="#161823" size="28px" />
<!-- 正在授权 / 加载中 -->
<p class="code-pay-page__tip">
{{ authRedirecting ? t('codePay.authorizing') : t('codePay.loading') }}
</p>
</div>
</template>
<!-- 加载失败 -->
<template v-else-if="loadError">
<div class="code-pay-page__brand code-pay-page__brand--douyin" />
<div class="code-pay-page__card">
<div class="code-pay-page__error-icon">
<van-icon name="warning-o" size="40" color="#fa8c16" />
</div>
<p class="code-pay-page__error-title">
{{ t('codePay.loadFailTitle') }}
</p>
<p class="code-pay-page__error">
{{ loadError }}
</p>
</div>
</template>
<CodePayShell
v-else
:merchant-name="info.name"
:mch-short-name="info.mchShortName"
:display-amount="displayAmount"
:amount-type="info.amountType"
:description="description"
:paying="paying"
:paid="paid"
:order-no="orderNo"
brand-color="#161823"
brand-dark="#000000"
@update:description="setDescription"
@input="onInput"
@delete="onDelete"
@pay="pay"
@close="closePage"
/>
</div>
</template>
<style scoped lang="less">
.code-pay-page {
min-height: 100%;
min-height: 100dvh;
background: #f5f7fa;
box-sizing: border-box;
&__brand {
height: 120px;
// 抖音黑白主视觉(近黑 → 纯黑)
&--douyin {
background: linear-gradient(135deg, #161823 0%, #000 100%);
}
}
&__card {
margin: -48px 16px 0;
padding: 40px 20px;
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 16px rgb(0 0 0 / 6%);
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
&__tip {
margin: 0;
font-size: 14px;
color: #909399;
}
&__error-icon {
display: flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
border-radius: 50%;
background: rgb(250 140 22 / 10%);
margin-bottom: 4px;
}
&__error-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
&__error {
margin: 0;
font-size: 13px;
color: #909399;
line-height: 1.6;
}
}
</style>

View File

@@ -2,7 +2,8 @@
/**
* 码牌支付入口分发页
*
* 二维码恒为 /h/:code; 按 UA replace 到微信/支付宝端页, 其它环境提示用钱包扫码。
* 二维码恒为 /h/:code; 按 UA replace 到微信/支付宝/云闪付/抖音端页,
* 其它环境browser 等)提示用钱包扫码。
* 不支持页:四钱包图标 + 加粗标题 + 小号副文案(与聚合 unsupported 一致)。
*/
import { onMounted, ref } from 'vue'
@@ -32,17 +33,22 @@ const walletIcons = [
{ src: douyinSvg, altKey: 'codePay.wallet.douyin' },
]
/** clientEnv → 码牌分端路由 name */
const ENV_ROUTE_NAME: Record<string, string> = {
wechat: 'CodePayWechat',
alipay: 'CodePayAlipay',
union_pay: 'CodePayUnion',
douyin: 'CodePayDouyin',
}
onMounted(() => {
const env = detectClientEnv()
if (env === 'wechat') {
router.replace({ name: 'CodePayWechat', params: { code } })
const routeName = ENV_ROUTE_NAME[env]
if (routeName) {
router.replace({ name: routeName, params: { code } })
return
}
if (env === 'alipay') {
router.replace({ name: 'CodePayAlipay', params: { code } })
return
}
// 云闪付/抖音/浏览器: 提示用钱包扫码
// browser 等非钱包宿主: 提示用钱包扫码
redirecting.value = false
unsupported.value = true
})

View File

@@ -13,6 +13,8 @@ defineOptions({ name: 'CodePayShell' })
const props = withDefaults(defineProps<{
/** 商户/码牌名称 */
merchantName?: string
/** 商户简称(副标题「向{name}付款」) */
mchShortName?: string
/** 展示金额(元字符串) */
displayAmount: string
/** 金额类型 */
@@ -31,6 +33,7 @@ const props = withDefaults(defineProps<{
brandDark?: string
}>(), {
merchantName: '',
mchShortName: '',
amountType: 'random',
description: '',
paying: false,
@@ -41,8 +44,8 @@ const props = withDefaults(defineProps<{
})
const emit = defineEmits<{
/** 金额键盘输入 */
'input': [key: string]
/** 金额键盘输入Vant 数字键可能为 number */
'input': [key: string | number]
/** 金额删除 */
'delete': []
/** 确认支付 */
@@ -60,6 +63,13 @@ const remarkDraft = ref('')
/** 是否固定金额布局 */
const isFixed = computed(() => props.amountType === 'fixed')
/** 副标题:向{商户简称}付款 */
const payToText = computed(() => {
const name = props.mchShortName || t('codePay.merchantDefault')
// 向{name}付款
return t('codePay.payToMerchant', { name })
})
/**
* 打开备注弹窗
*/
@@ -151,9 +161,9 @@ function onPay() {
<div class="code-pay-shell__merchant-name">
{{ merchantName || t('codePay.merchantDefault') }}
</div>
<!-- 商户付款 -->
<!-- {商户简称}付款 -->
<div class="code-pay-shell__merchant-sub">
{{ t('codePay.payToMerchant') }}
{{ payToText }}
</div>
</div>
</div>
@@ -216,7 +226,7 @@ function onPay() {
:show="!paying"
:z-index="100"
@close="onPay"
@input="(k: string) => emit('input', k)"
@input="(k: string | number) => emit('input', k)"
@delete="emit('delete')"
/>

View File

@@ -13,7 +13,7 @@ import {
getCodePayInfo,
} from '@/shared/api/code-pay'
import { invokeJsapiByEnv } from '@/shared/pay/jsapi'
import { yuanToFen } from '@/shared/utils/pay-amount'
import { isAmountOverMax, yuanToFen } from '@/shared/utils/pay-amount'
import { clearPayOpenId, getPayOpenId } from '@/shared/utils/pay-openid'
import {
redirectToPayUrl,
@@ -25,7 +25,7 @@ export interface UseCodePayPageOptions {
/** 码牌编码 */
code: string
/** 写死的客户端环境 */
clientEnv: 'wechat' | 'alipay'
clientEnv: 'wechat' | 'alipay' | 'union_pay' | 'douyin'
}
/**
@@ -107,32 +107,43 @@ export function useCodePayPage(options: UseCodePayPageOptions) {
/**
* 金额键盘输入
* Vant NumberKeyboard 数字键 @input 传 number须统一转 string否则 amount 被污染后 indexOf 崩溃
*/
function onInput(key: string) {
function onInput(key: string | number) {
if (paying.value || paid.value) {
return
}
if (key === '.' && amount.value.includes('.')) {
// 统一为字符串Vant 数字键为 number
const k = String(key)
// 保证 amount 始终是字符串
let current = String(amount.value)
if (k === '.' && current.includes('.')) {
return
}
// 最多两位小数
const dot = amount.value.indexOf('.')
if (dot >= 0 && amount.value.length - dot > 2 && key !== '.') {
const dot = current.indexOf('.')
if (dot >= 0 && current.length - dot > 2 && k !== '.') {
return
}
if (amount.value === '0' && key !== '.') {
amount.value = key
if (current === '0' && k !== '.') {
current = k
}
else {
amount.value += key
current += k
}
// 上限:对齐后端 amount @Max(9999999999 分),超限则忽略本次按键
if (isAmountOverMax(current)) {
return
}
amount.value = current
}
function onDelete() {
if (paying.value || paid.value) {
return
}
amount.value = amount.value.slice(0, -1) || '0'
// String 加固:防止历史脏值导致 slice 失败
amount.value = String(amount.value).slice(0, -1) || '0'
}
/**
@@ -204,11 +215,16 @@ export function useCodePayPage(options: UseCodePayPageOptions) {
}
let amountFen: number | undefined
if (info.value.amountType === 'random') {
amountFen = yuanToFen(amount.value)
amountFen = yuanToFen(String(amount.value))
if (!amountFen) {
showNotify({ type: 'warning', message: t('codePay.amountZero') })
return
}
// 提交前再拦一层(与键盘上限一致)
if (isAmountOverMax(String(amount.value))) {
showNotify({ type: 'warning', message: t('codePay.amountOverMax') })
return
}
}
if (info.value.needOpenId && !openId.value) {
showNotify({ type: 'warning', message: t('codePay.needAuth') })

View File

@@ -0,0 +1,151 @@
<script lang="ts" setup>
/**
* 码牌支付-云闪付端(品牌红)
*/
import { onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import CodePayShell from '../shared/CodePayShell.vue'
import { useCodePayPage } from '../shared/useCodePayPage'
defineOptions({ name: 'CodePayUnion' })
const { t } = useI18n()
const route = useRoute()
const code = route.params.code as string
const {
loading,
paying,
loadError,
info,
description,
setDescription,
displayAmount,
paid,
orderNo,
authRedirecting,
init,
onInput,
onDelete,
pay,
closePage,
} = useCodePayPage({ code, clientEnv: 'union_pay' })
onMounted(() => {
init()
})
</script>
<template>
<div class="code-pay-page">
<!-- 加载 / 授权中品牌顶栏 + 上浮卡 -->
<template v-if="loading || authRedirecting">
<div class="code-pay-page__brand code-pay-page__brand--union" />
<div class="code-pay-page__card">
<van-loading color="#E21836" size="28px" />
<!-- 正在授权 / 加载中 -->
<p class="code-pay-page__tip">
{{ authRedirecting ? t('codePay.authorizing') : t('codePay.loading') }}
</p>
</div>
</template>
<!-- 加载失败 -->
<template v-else-if="loadError">
<div class="code-pay-page__brand code-pay-page__brand--union" />
<div class="code-pay-page__card">
<div class="code-pay-page__error-icon">
<van-icon name="warning-o" size="40" color="#fa8c16" />
</div>
<p class="code-pay-page__error-title">
{{ t('codePay.loadFailTitle') }}
</p>
<p class="code-pay-page__error">
{{ loadError }}
</p>
</div>
</template>
<CodePayShell
v-else
:merchant-name="info.name"
:mch-short-name="info.mchShortName"
:display-amount="displayAmount"
:amount-type="info.amountType"
:description="description"
:paying="paying"
:paid="paid"
:order-no="orderNo"
brand-color="#E21836"
brand-dark="#C41230"
@update:description="setDescription"
@input="onInput"
@delete="onDelete"
@pay="pay"
@close="closePage"
/>
</div>
</template>
<style scoped lang="less">
.code-pay-page {
min-height: 100%;
min-height: 100dvh;
background: #f5f7fa;
box-sizing: border-box;
&__brand {
height: 120px;
// 云闪付品牌红
&--union {
background: linear-gradient(135deg, #e21836 0%, #c41230 100%);
}
}
&__card {
margin: -48px 16px 0;
padding: 40px 20px;
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 16px rgb(0 0 0 / 6%);
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
&__tip {
margin: 0;
font-size: 14px;
color: #909399;
}
&__error-icon {
display: flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
border-radius: 50%;
background: rgb(250 140 22 / 10%);
margin-bottom: 4px;
}
&__error-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
&__error {
margin: 0;
font-size: 13px;
color: #909399;
line-height: 1.6;
}
}
</style>

View File

@@ -70,6 +70,7 @@ onMounted(() => {
<CodePayShell
v-else
:merchant-name="info.name"
:mch-short-name="info.mchShortName"
:display-amount="displayAmount"
:amount-type="info.amountType"
:description="description"

View File

@@ -74,6 +74,18 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/pc/views/code-pay/Index.vue'),
meta: { title: '码牌支付' },
},
{
path: RoutePath.CODE_PAY_UNION,
name: 'PcCodePayUnion',
component: () => import('@/pc/views/code-pay/Index.vue'),
meta: { title: '码牌支付' },
},
{
path: RoutePath.CODE_PAY_DOUYIN,
name: 'PcCodePayDouyin',
component: () => import('@/pc/views/code-pay/Index.vue'),
meta: { title: '码牌支付' },
},
{
path: RoutePath.CODE_PAY,
name: 'PcCodePay',

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
/**
* PC 码牌落地引导手机钱包扫码Web 居中白卡,对齐 PC 聚合 unsupported
* 码牌收款在微信/支付宝移动端完成PC 只做扫码提示。
* 码牌收款在微信/支付宝/云闪付/抖音移动端完成PC 只做扫码提示。
*/
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
@@ -14,7 +14,7 @@ defineOptions({ name: 'PcCodePay' })
const { t } = useI18n()
const route = useRoute()
// 路由 /h/:code 或 /h/wechat|alipay/:code
// 路由 /h/:code 或 /h/wechat|alipay|union-pay|douyin/:code
const code = (route.params.code as string) || ''
/** 四钱包图标 */

View File

@@ -14,6 +14,8 @@ export interface CodePayInfo {
code?: string
/** 码牌名称 */
name?: string
/** 商户简称(展示用; 后端简称空时已回退全称) */
mchShortName?: string
/** 金额类型 random-自定义 / fixed-固定 */
amountType?: AmountType
/** 固定金额(分, amountType=fixed 时返回) */

View File

@@ -5,12 +5,13 @@
"addRemark": "Add remark",
"remarkPlaceholder": "Enter payment remark",
"amountZero": "Amount cannot be 0",
"amountOverMax": "Amount cannot exceed ¥99,999,999.99",
"confirmPay": "Confirm Payment",
"demoPay": "Demo: code {code} payment ¥{amount}",
"loadFail": "Failed to load QR code info, please try again later",
"loadFailTitle": "Load failed",
"merchantDefault": "Merchant",
"payToMerchant": "Pay to merchant",
"payToMerchant": "Pay to {name}",
"paySuccess": "Payment successful",
"payFail": "Payment failed, please retry",
"payLaunched": "Payment launched",

View File

@@ -5,12 +5,13 @@
"addRemark": "Tambah catatan",
"remarkPlaceholder": "Masukkan catatan pembayaran",
"amountZero": "Jumlah tidak boleh 0",
"amountOverMax": "Jumlah tidak boleh melebihi ¥99.999.999,99",
"confirmPay": "Konfirmasi Pembayaran",
"demoPay": "Demo: kode {code} bayar ¥{amount}",
"loadFail": "Gagal memuat info kode QR, coba lagi nanti",
"loadFailTitle": "Gagal memuat",
"merchantDefault": "Merchant",
"payToMerchant": "Bayar ke merchant",
"payToMerchant": "Bayar ke {name}",
"paySuccess": "Pembayaran berhasil",
"payFail": "Pembayaran gagal, coba lagi",
"payLaunched": "Pembayaran diluncurkan",

View File

@@ -5,12 +5,13 @@
"addRemark": "備考を追加",
"remarkPlaceholder": "支払い備考を入力",
"amountZero": "金額は0にできません",
"amountOverMax": "金額は 99999999.99 元を超えられません",
"confirmPay": "支払いを確認",
"demoPay": "デモ:コード {code} 支払い ¥{amount}",
"loadFail": "コード情報の読み込みに失敗しました。しばらくしてから再試行してください",
"loadFailTitle": "読み込み失敗",
"merchantDefault": "加盟店",
"payToMerchant": "加盟店へ支払い",
"payToMerchant": "{name} へ支払い",
"paySuccess": "支払い成功",
"payFail": "支払いに失敗しました。再試行してください",
"payLaunched": "支払いを開始しました",

View File

@@ -5,12 +5,13 @@
"addRemark": "비고 추가",
"remarkPlaceholder": "결제 비고를 입력하세요",
"amountZero": "금액은 0일 수 없습니다",
"amountOverMax": "금액은 99999999.99 원을 초과할 수 없습니다",
"confirmPay": "결제 확인",
"demoPay": "데모: 코드 {code} 결제 ¥{amount}",
"loadFail": "코드 정보를 불러오지 못했습니다. 잠시 후 다시 시도하세요",
"loadFailTitle": "불러오기 실패",
"merchantDefault": "가맹점",
"payToMerchant": "가맹점에 결제",
"payToMerchant": "{name}에 결제",
"paySuccess": "결제 성공",
"payFail": "결제 실패, 다시 시도하세요",
"payLaunched": "결제가 시작되었습니다",

View File

@@ -5,12 +5,13 @@
"addRemark": "Tambah catatan",
"remarkPlaceholder": "Masukkan catatan pembayaran",
"amountZero": "Jumlah tidak boleh 0",
"amountOverMax": "Jumlah tidak boleh melebihi ¥99,999,999.99",
"confirmPay": "Sahkan Pembayaran",
"demoPay": "Demo: kod {code} bayar ¥{amount}",
"loadFail": "Gagal memuatkan info kod QR, cuba lagi kemudian",
"loadFailTitle": "Gagal memuatkan",
"merchantDefault": "Pedagang",
"payToMerchant": "Bayar kepada pedagang",
"payToMerchant": "Bayar kepada {name}",
"paySuccess": "Pembayaran berjaya",
"payFail": "Pembayaran gagal, cuba lagi",
"payLaunched": "Pembayaran dilancarkan",

View File

@@ -5,12 +5,13 @@
"addRemark": "เพิ่มหมายเหตุ",
"remarkPlaceholder": "กรอกหมายเหตุการชำระเงิน",
"amountZero": "จำนวนเงินต้องไม่เป็น 0",
"amountOverMax": "จำนวนเงินต้องไม่เกิน ¥99,999,999.99",
"confirmPay": "ยืนยันการชำระเงิน",
"demoPay": "สาธิต: รหัส {code} ชำระ ¥{amount}",
"loadFail": "โหลดข้อมูลรหัส QR ไม่สำเร็จ โปรดลองอีกครั้ง",
"loadFailTitle": "โหลดไม่สำเร็จ",
"merchantDefault": "ร้านค้า",
"payToMerchant": "ชำระให้ร้านค้า",
"payToMerchant": "ชำระให้ {name}",
"paySuccess": "ชำระเงินสำเร็จ",
"payFail": "ชำระเงินไม่สำเร็จ โปรดลองอีกครั้ง",
"payLaunched": "เปิดการชำระเงินแล้ว",

View File

@@ -5,12 +5,13 @@
"addRemark": "Thêm ghi chú",
"remarkPlaceholder": "Nhập ghi chú thanh toán",
"amountZero": "Số tiền không được là 0",
"amountOverMax": "Số tiền không được vượt quá ¥99.999.999,99",
"confirmPay": "Xác nhận thanh toán",
"demoPay": "Demo: mã {code} thanh toán ¥{amount}",
"loadFail": "Tải thông tin mã QR thất bại, vui lòng thử lại",
"loadFailTitle": "Tải thất bại",
"merchantDefault": "Merchant",
"payToMerchant": "Thanh toán cho merchant",
"payToMerchant": "Thanh toán cho {name}",
"paySuccess": "Thanh toán thành công",
"payFail": "Thanh toán thất bại, vui lòng thử lại",
"payLaunched": "Đã mở thanh toán",

View File

@@ -5,12 +5,13 @@
"addRemark": "添加备注",
"remarkPlaceholder": "请输入支付备注内容",
"amountZero": "金额不可为0",
"amountOverMax": "金额不能超过 99999999.99 元",
"confirmPay": "确认支付",
"demoPay": "演示:码牌 {code} 支付 ¥{amount}",
"loadFail": "码牌信息加载失败, 请稍后重试",
"loadFailTitle": "加载失败",
"merchantDefault": "收款商户",
"payToMerchant": "向商户付款",
"payToMerchant": "向{name}付款",
"paySuccess": "支付成功",
"payFail": "支付失败, 请重试",
"payLaunched": "支付已拉起",

View File

@@ -5,12 +5,13 @@
"addRemark": "新增備註",
"remarkPlaceholder": "請輸入支付備註內容",
"amountZero": "金額不可為 0",
"amountOverMax": "金額不能超過 99999999.99 元",
"confirmPay": "確認支付",
"demoPay": "示範:碼牌 {code} 支付 ¥{amount}",
"loadFail": "碼牌資訊載入失敗,請稍後重試",
"loadFailTitle": "載入失敗",
"merchantDefault": "收款商戶",
"payToMerchant": "向商戶付款",
"payToMerchant": "向{name}付款",
"paySuccess": "支付成功",
"payFail": "支付失敗,請重試",
"payLaunched": "支付已拉起",

View File

@@ -5,12 +5,13 @@
"addRemark": "新增備註",
"remarkPlaceholder": "請輸入支付備註內容",
"amountZero": "金額不可為 0",
"amountOverMax": "金額不能超過 99999999.99 元",
"confirmPay": "確認支付",
"demoPay": "示範:碼牌 {code} 支付 ¥{amount}",
"loadFail": "碼牌資訊載入失敗,請稍後重試",
"loadFailTitle": "載入失敗",
"merchantDefault": "收款商戶",
"payToMerchant": "向商戶付款",
"payToMerchant": "向{name}付款",
"paySuccess": "支付成功",
"payFail": "支付失敗,請重試",
"payLaunched": "支付已拉起",

View File

@@ -47,6 +47,10 @@ export const RoutePath = {
CODE_PAY_WECHAT: '/h/wechat/:code',
/** 码牌支付-支付宝端Mobile 收款PC 同入口扫码引导) */
CODE_PAY_ALIPAY: '/h/alipay/:code',
/** 码牌支付-云闪付端Mobile 收款PC 同入口扫码引导) */
CODE_PAY_UNION: '/h/union-pay/:code',
/** 码牌支付-抖音端Mobile 收款PC 同入口扫码引导) */
CODE_PAY_DOUYIN: '/h/douyin/:code',
/** 商户对账单PC 独占) */
MERCHANT_STATEMENT: '/merchant/statement',
/** 协议展示页(跨端:用户协议/隐私政策,链接可独立分享) */

View File

@@ -2,6 +2,17 @@
* 支付金额工具:分 ↔ 元
*/
/**
* 支付金额上限(分),对齐后端 NormalPayParam @Max(9999999999)
* 即 99999999.99 元
*/
export const MAX_PAY_AMOUNT_FEN = 9_999_999_999
/**
* 支付金额上限(元,展示/比较用)
*/
export const MAX_PAY_AMOUNT_YUAN = MAX_PAY_AMOUNT_FEN / 100
/**
* 分 → 元字符串(两位小数)
* 后端 Long 全局序列化为字符串(防 JS 精度),故需兼容字符串入参
@@ -28,3 +39,15 @@ export function yuanToFen(yuan: string): number {
}
return Math.round(n * 100)
}
/**
* 元字符串是否超过支付上限(含中间输入态如 "100.",按数值比较)
*/
export function isAmountOverMax(yuan: string): boolean {
const n = Number(yuan)
if (!Number.isFinite(n)) {
return false
}
// 与分精度对齐:先转分再比,避免浮点误差
return Math.round(n * 100) > MAX_PAY_AMOUNT_FEN
}