825 lines
18 KiB
Vue
825 lines
18 KiB
Vue
<template>
|
||
<view class="cashier-page">
|
||
<block v-if="!hasMerchantUrl">
|
||
<view class="empty-page">
|
||
<image class="empty-bg" :src="emptyStateBackground" mode="aspectFill" />
|
||
<view class="empty-overlay"></view>
|
||
<view class="empty-inner">
|
||
<view class="empty-brand-block">
|
||
<text class="empty-brand">{{ brandName }}</text>
|
||
<text class="empty-subtitle">扫描商家收款码,快速完成付款</text>
|
||
</view>
|
||
<view class="empty-action">
|
||
<button class="empty-scan-button" hover-class="empty-scan-button-hover" :loading="scanning" @tap="handleScan">
|
||
<text class="ri-qr-scan-2-line empty-scan-button-icon"></text>
|
||
<text>扫一扫商家收款码</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</block>
|
||
|
||
<block v-else>
|
||
<view class="pay-header" :style="{ backgroundImage: `url(${merchant.background})` }">
|
||
<view class="pay-header-mask"></view>
|
||
<view class="pay-header-scrim"></view>
|
||
<view class="merchant-label">
|
||
<view class="merchant-label-inner">
|
||
<text class="muted">向</text>
|
||
<text class="merchant-name">{{ merchant.name }}</text>
|
||
<text class="muted">付款</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="pay-body">
|
||
<view class="pay-main">
|
||
<view class="amount-zone">
|
||
<view class="amount-display">
|
||
<text class="currency">¥</text>
|
||
<text v-if="amount" class="amount-text">{{ amount }}</text>
|
||
<view v-else class="cursor"></view>
|
||
</view>
|
||
<text v-if="!amount" class="amount-hint">请输入付款金额</text>
|
||
<view v-if="amount && showBenefitTip" class="benefit-tip">
|
||
{{ benefitTitle }}
|
||
<text class="benefit-value">¥ {{ accumulationAmount }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="extra-zone">
|
||
<view class="remark-row">
|
||
<input
|
||
v-model="remark"
|
||
class="remark-input"
|
||
placeholder="付款备注(选填)"
|
||
placeholder-class="remark-placeholder"
|
||
:disabled="paying"
|
||
/>
|
||
</view>
|
||
<view class="bind-row" @tap.stop="handleBindHouse">
|
||
<text class="bind-text">绑定房源</text>
|
||
<text class="bind-hint">后期绑定</text>
|
||
<text class="ri-arrow-right-s-line bind-arrow"></text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="keyboard" :class="{ disabled: paying }">
|
||
<view class="keypad">
|
||
<view class="number-grid">
|
||
<view v-for="key in numberKeys" :key="key" class="key" @tap="appendKey(key)">
|
||
{{ key }}
|
||
</view>
|
||
<view class="key key-zero" @tap="appendKey('0')">0</view>
|
||
<view class="key" @tap="appendKey('.')">.</view>
|
||
</view>
|
||
<view class="action-column">
|
||
<view class="key delete-key" @tap="deleteKey">
|
||
<text class="ri-delete-back-2-line"></text>
|
||
</view>
|
||
<view
|
||
class="confirm-key"
|
||
:class="{ disabled: !canPay, loading: paying }"
|
||
@tap="handlePay"
|
||
>
|
||
<block v-if="paying">支付中</block>
|
||
<block v-else-if="canPay">
|
||
<text>确认支付</text>
|
||
<text class="confirm-amount">¥{{ payableAmount }}</text>
|
||
</block>
|
||
<block v-else>确认支付</block>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="safe-bottom"></view>
|
||
</view>
|
||
</block>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, ref } from 'vue';
|
||
import { onLoad } from '@dcloudio/uni-app';
|
||
import { bindMerchantByUrl, cancelTrade, supplierByUrl } from '@/api/cashier';
|
||
import { postForm } from '@/utils/request';
|
||
import { DEFAULT_MERCHANT } from '@/config';
|
||
import {
|
||
getPropertyBindingCacheKey,
|
||
getPropertyBindingCacheKeyByHash,
|
||
getSupplierCacheKey,
|
||
getSupplierCacheKeyByHash,
|
||
resolveMerchantUrlFromOptions,
|
||
resolveMerchantUrlFromScanResult,
|
||
syncActiveMerchantContext
|
||
} from '@/utils/merchant-context';
|
||
import { encryptedStorage } from '@/utils/storage';
|
||
import { isPaymentCancelled, requestWxPayment } from '@/utils/wxpay';
|
||
|
||
interface MerchantInfo {
|
||
id: string;
|
||
name: string;
|
||
background: string;
|
||
accumulationRate: number;
|
||
}
|
||
|
||
const defaultMerchant = (uni as any).$globalData?.defaultMerchant || DEFAULT_MERCHANT;
|
||
const PROPERTY_CACHE_TTL_SECONDS = 10 * 60;
|
||
const SUPPLIER_CACHE_TTL_SECONDS = 10 * 60;
|
||
const emptyStateBackground = '/static/cashier/bg-image.png';
|
||
const brandName = '卓享汇支付';
|
||
|
||
const hasMerchantUrl = ref(false);
|
||
const merchant = ref<MerchantInfo>({ ...defaultMerchant });
|
||
const amount = ref('');
|
||
const remark = ref('');
|
||
const paying = ref(false);
|
||
const activeMerchantUrl = ref('');
|
||
const scoreName = ref('物业公积金');
|
||
const scanning = ref(false);
|
||
const numberKeys = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||
|
||
const canPay = computed(() => Number(amount.value) > 0 && !paying.value);
|
||
const payableAmount = computed(() => Number(amount.value || 0).toFixed(2));
|
||
const accumulationAmount = computed(() => {
|
||
const value = Number(amount.value || 0) * Number(merchant.value.accumulationRate || 0);
|
||
return value.toFixed(3);
|
||
});
|
||
const showBenefitTip = computed(() => Number(merchant.value.accumulationRate) > 0);
|
||
const benefitTitle = computed(() => `预计获得${scoreName.value || '物业公积金'}`);
|
||
|
||
const parseGiveRate = (giveRate: unknown) => {
|
||
if (giveRate === undefined || giveRate === null || giveRate === '') return 0;
|
||
const rate = Number(giveRate);
|
||
if (!Number.isFinite(rate) || rate <= 0) return 0;
|
||
return rate / 100;
|
||
};
|
||
|
||
const applyMerchantInfo = (data: any = {}) => {
|
||
const name = data.name || data.merchant_name || data.title;
|
||
const background = data.background || data.banner || data.cover || data.bg;
|
||
merchant.value = {
|
||
id: data.id || data.merchant_id || merchant.value.id,
|
||
name: name || merchant.value.name,
|
||
background: background || merchant.value.background,
|
||
accumulationRate:
|
||
data.accumulationRate !== undefined
|
||
? Number(data.accumulationRate)
|
||
: merchant.value.accumulationRate
|
||
};
|
||
};
|
||
|
||
interface PropertyBindingCache {
|
||
merchant_id?: string;
|
||
merchant_name?: string;
|
||
score_name?: string;
|
||
give_rate?: string | number;
|
||
}
|
||
|
||
interface SupplierCache {
|
||
supplier_id?: string | number;
|
||
supplier_name?: string;
|
||
logo?: string;
|
||
}
|
||
|
||
const applyBindingInfo = (binding?: PropertyBindingCache | null) => {
|
||
if (!binding) return;
|
||
applyMerchantInfo({
|
||
merchant_id: binding.merchant_id,
|
||
merchant_name: binding.merchant_name,
|
||
accumulationRate: parseGiveRate(binding.give_rate)
|
||
});
|
||
if (binding.score_name) {
|
||
scoreName.value = binding.score_name;
|
||
}
|
||
};
|
||
|
||
const applySupplierInfo = (supplier?: SupplierCache | null) => {
|
||
if (!supplier) return;
|
||
applyMerchantInfo({
|
||
name: supplier.supplier_name,
|
||
background: supplier.logo
|
||
});
|
||
};
|
||
|
||
const syncBindingCacheForUrl = (merchantUrl: string) => {
|
||
const context = syncActiveMerchantContext(merchantUrl);
|
||
// Strong consistency: when switching merchant QR URL, clear previous merchant cache.
|
||
if (context.switched) {
|
||
encryptedStorage.remove(getPropertyBindingCacheKeyByHash(context.previousHash));
|
||
encryptedStorage.remove(getSupplierCacheKeyByHash(context.previousHash));
|
||
}
|
||
return getPropertyBindingCacheKey(merchantUrl);
|
||
};
|
||
|
||
const initWithMerchantUrl = async (merchantUrl: string) => {
|
||
hasMerchantUrl.value = true;
|
||
activeMerchantUrl.value = merchantUrl;
|
||
await loadPropertyBinding(merchantUrl);
|
||
await loadSupplier(merchantUrl);
|
||
};
|
||
|
||
const promptScan = () => {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '未识别到商家码,请扫描商家收款码进入收银台',
|
||
confirmText: '去扫码',
|
||
showCancel: false,
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
handleScan();
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
const handleScan = () => {
|
||
if (scanning.value) return;
|
||
|
||
scanning.value = true;
|
||
uni.scanCode({
|
||
onlyFromCamera: false,
|
||
scanType: ['qrCode'],
|
||
success: async (res) => {
|
||
const merchantUrl = resolveMerchantUrlFromScanResult(res.result || '');
|
||
if (!merchantUrl) {
|
||
uni.showToast({
|
||
title: '未识别到商家码',
|
||
icon: 'none'
|
||
});
|
||
promptScan();
|
||
return;
|
||
}
|
||
await initWithMerchantUrl(merchantUrl);
|
||
},
|
||
fail: (err: any) => {
|
||
const errMsg = err?.errMsg || '';
|
||
if (errMsg.includes('cancel')) return;
|
||
uni.showToast({
|
||
title: '扫码失败,请重试',
|
||
icon: 'none'
|
||
});
|
||
},
|
||
complete: () => {
|
||
scanning.value = false;
|
||
}
|
||
});
|
||
};
|
||
|
||
const handleMissingMerchantUrl = () => {
|
||
promptScan();
|
||
};
|
||
|
||
const loadPropertyBinding = async (merchantUrl: string) => {
|
||
const cacheKey = syncBindingCacheForUrl(merchantUrl);
|
||
const cached = encryptedStorage.get<PropertyBindingCache>(cacheKey);
|
||
if (cached) {
|
||
applyBindingInfo(cached);
|
||
}
|
||
|
||
try {
|
||
const response = await bindMerchantByUrl(merchantUrl);
|
||
if (Number(response?.code) !== 200 || !response?.data) return;
|
||
|
||
const payload = response.data || {};
|
||
const merchantData = payload.merchant || payload;
|
||
const supplierData = payload.supplier || {};
|
||
const latestBinding: PropertyBindingCache = {
|
||
merchant_id: merchantData.merchant_id,
|
||
merchant_name: merchantData.merchant_name,
|
||
score_name: merchantData.config?.score_name,
|
||
give_rate: supplierData.give_rate ?? payload.give_rate
|
||
};
|
||
applyBindingInfo(latestBinding);
|
||
encryptedStorage.set(cacheKey, latestBinding, PROPERTY_CACHE_TTL_SECONDS);
|
||
} catch (error) {
|
||
console.log('bind merchant fallback cache', error);
|
||
}
|
||
};
|
||
|
||
const loadSupplier = async (merchantUrl: string) => {
|
||
const cacheKey = getSupplierCacheKey(merchantUrl);
|
||
const cached = encryptedStorage.get<SupplierCache>(cacheKey);
|
||
if (cached) {
|
||
applySupplierInfo(cached);
|
||
}
|
||
|
||
try {
|
||
const response = await supplierByUrl(merchantUrl);
|
||
if (Number(response?.code) !== 200 || !response?.data) return;
|
||
|
||
const latestSupplier: SupplierCache = {
|
||
supplier_id: response.data.supplier_id,
|
||
supplier_name: response.data.supplier_name,
|
||
logo: response.data.logo
|
||
};
|
||
applySupplierInfo(latestSupplier);
|
||
encryptedStorage.set(cacheKey, latestSupplier, SUPPLIER_CACHE_TTL_SECONDS);
|
||
} catch (error) {
|
||
console.log('supplier fallback cache', error);
|
||
}
|
||
};
|
||
|
||
const appendKey = (key: string) => {
|
||
if (paying.value) return;
|
||
|
||
if (key === '.') {
|
||
if (!amount.value) {
|
||
amount.value = '0.';
|
||
return;
|
||
}
|
||
if (amount.value.includes('.')) return;
|
||
amount.value += '.';
|
||
return;
|
||
}
|
||
|
||
const nextValue = amount.value === '0' ? key : `${amount.value}${key}`;
|
||
const decimalPart = nextValue.split('.')[1];
|
||
if (decimalPart && decimalPart.length > 2) return;
|
||
if (Number(nextValue) > 999999.99) return;
|
||
amount.value = nextValue;
|
||
};
|
||
|
||
const deleteKey = () => {
|
||
if (paying.value) return;
|
||
amount.value = amount.value.slice(0, -1);
|
||
};
|
||
|
||
const handleBindHouse = () => {
|
||
if (paying.value) return;
|
||
uni.navigateTo({
|
||
url: '/pages/house/bind'
|
||
});
|
||
};
|
||
|
||
const buildResultUrl = (tradeNo: string) => {
|
||
const query = [
|
||
`amount=${payableAmount.value}`,
|
||
`accumulation=${accumulationAmount.value}`,
|
||
`merchant_name=${encodeURIComponent(merchant.value.name)}`,
|
||
`merchant_url=${encodeURIComponent(activeMerchantUrl.value)}`,
|
||
`trade_no=${encodeURIComponent(tradeNo)}`,
|
||
`score_name=${encodeURIComponent(scoreName.value)}`
|
||
].join('&');
|
||
return `/pages/cashier/result?${query}`;
|
||
};
|
||
|
||
const handlePay = async () => {
|
||
if (!canPay.value) return;
|
||
|
||
paying.value = true;
|
||
try {
|
||
if (!activeMerchantUrl.value) {
|
||
throw new Error('缺少支付参数:商户码 url');
|
||
}
|
||
|
||
// 扫商户码支付:直接调用后端 jspay,拿到微信 requestPayment 所需签名参数
|
||
const payload = {
|
||
amount: payableAmount.value,
|
||
platform: 'mp-weixin',
|
||
remark: remark.value.trim(),
|
||
url: activeMerchantUrl.value
|
||
};
|
||
|
||
const payRes = await postForm<any>('user/v1/jspay', payload);
|
||
if (Number(payRes?.code) !== 200) {
|
||
throw new Error(payRes?.msg || '获取支付参数失败');
|
||
}
|
||
|
||
const wechat = payRes?.data || {};
|
||
const params: any = {
|
||
timeStamp: wechat.timeStamp || wechat.time_stamp || wechat.timestamp || '',
|
||
nonceStr: wechat.nonceStr || wechat.nonce_str || '',
|
||
package: wechat.package || wechat.pack || '',
|
||
signType: wechat.signType || wechat.sign_type || '',
|
||
paySign: wechat.paySign || wechat.pay_sign || ''
|
||
};
|
||
|
||
if (!params.paySign) {
|
||
throw new Error('支付参数不完整');
|
||
}
|
||
|
||
const tradeNo = wechat.trade_no || wechat.tradeNo || '';
|
||
if (!tradeNo) {
|
||
throw new Error('缺少交易单号');
|
||
}
|
||
|
||
try {
|
||
await requestWxPayment(params);
|
||
} catch (error) {
|
||
if (isPaymentCancelled(error)) {
|
||
await cancelTrade(tradeNo).catch(() => {});
|
||
throw new Error('已取消支付');
|
||
}
|
||
throw error;
|
||
}
|
||
|
||
uni.redirectTo({
|
||
url: buildResultUrl(tradeNo)
|
||
});
|
||
} catch (error: any) {
|
||
uni.showToast({
|
||
title: error?.message || '支付失败',
|
||
icon: 'none'
|
||
});
|
||
} finally {
|
||
paying.value = false;
|
||
}
|
||
};
|
||
|
||
onLoad((options: any) => {
|
||
const merchantUrl = resolveMerchantUrlFromOptions(options || {});
|
||
if (!merchantUrl) {
|
||
handleMissingMerchantUrl();
|
||
return;
|
||
}
|
||
initWithMerchantUrl(merchantUrl);
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.cashier-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 100vh;
|
||
background: #FFFFFF;
|
||
color: #111827;
|
||
}
|
||
|
||
.empty-page {
|
||
position: relative;
|
||
min-height: 100vh;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.empty-bg {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.empty-overlay {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(
|
||
180deg,
|
||
rgba(15, 35, 95, 0.18) 0%,
|
||
rgba(15, 35, 95, 0.42) 42%,
|
||
rgba(255, 255, 255, 0.92) 78%,
|
||
#ffffff 100%
|
||
);
|
||
}
|
||
|
||
.empty-inner {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
min-height: 100vh;
|
||
padding: 160rpx 48rpx calc(72rpx + env(safe-area-inset-bottom));
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.empty-brand-block {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
text-align: center;
|
||
}
|
||
|
||
.empty-brand {
|
||
font-family: SourceHanSansCN, -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif;
|
||
color: #ffffff;
|
||
font-size: 60rpx;
|
||
font-weight: 600;
|
||
line-height: 1.25;
|
||
letter-spacing: 2rpx;
|
||
text-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.empty-subtitle {
|
||
margin-top: 20rpx;
|
||
color: rgba(255, 255, 255, 0.92);
|
||
font-size: 28rpx;
|
||
line-height: 1.5;
|
||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.18);
|
||
}
|
||
|
||
.empty-action {
|
||
padding-top: 48rpx;
|
||
}
|
||
|
||
.empty-scan-button {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
height: 104rpx;
|
||
padding: 0;
|
||
border-radius: 16rpx;
|
||
background: #1f5bd8;
|
||
color: #ffffff;
|
||
font-size: 32rpx;
|
||
font-weight: 800;
|
||
line-height: 1;
|
||
box-shadow: 0 12rpx 32rpx rgba(31, 91, 216, 0.28);
|
||
}
|
||
|
||
.empty-scan-button::after {
|
||
border: none;
|
||
}
|
||
|
||
.empty-scan-button-icon {
|
||
margin-right: 12rpx;
|
||
font-size: 40rpx;
|
||
line-height: 1;
|
||
}
|
||
|
||
.empty-scan-button-hover {
|
||
background: #174dc1;
|
||
}
|
||
|
||
.brand-only {
|
||
margin: 0;
|
||
}
|
||
|
||
.pay-header {
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
height: 360rpx;
|
||
background-size: cover;
|
||
background-position: center center;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.pay-header-mask {
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 1;
|
||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.14) 0%, rgba(0, 0, 0, 0) 42%);
|
||
}
|
||
|
||
.pay-header-scrim {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 1;
|
||
height: 200rpx;
|
||
background: linear-gradient(
|
||
180deg,
|
||
rgba(0, 0, 0, 0) 0%,
|
||
rgba(0, 0, 0, 0.42) 48%,
|
||
rgba(0, 0, 0, 0.62) 78%,
|
||
rgba(255, 255, 255, 1) 100%
|
||
);
|
||
}
|
||
|
||
.pay-body {
|
||
flex: 1;
|
||
margin-top: -24rpx;
|
||
padding-bottom: 488rpx;
|
||
background: #FFFFFF;
|
||
}
|
||
|
||
.pay-main {
|
||
padding: 0 48rpx;
|
||
}
|
||
|
||
.merchant-label {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 52rpx;
|
||
z-index: 2;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.merchant-label-inner {
|
||
display: flex;
|
||
align-items: baseline;
|
||
max-width: 88%;
|
||
padding: 14rpx 32rpx;
|
||
border-radius: 999rpx;
|
||
background: rgba(0, 0, 0, 0.42);
|
||
}
|
||
|
||
.muted {
|
||
color: rgba(255, 255, 255, 0.9);
|
||
}
|
||
|
||
.merchant-name {
|
||
margin: 0 8rpx;
|
||
color: #FFFFFF;
|
||
font-size: 34rpx;
|
||
font-weight: 800;
|
||
line-height: 1.3;
|
||
text-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.35);
|
||
}
|
||
|
||
.amount-zone {
|
||
margin-top: 56rpx;
|
||
}
|
||
|
||
.amount-display {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
min-height: 100rpx;
|
||
}
|
||
|
||
.currency {
|
||
flex-shrink: 0;
|
||
margin-right: 16rpx;
|
||
color: #050505;
|
||
font-size: 60rpx;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
}
|
||
|
||
.amount-text {
|
||
color: #050505;
|
||
font-size: 88rpx;
|
||
font-weight: 800;
|
||
line-height: 1;
|
||
letter-spacing: -1rpx;
|
||
}
|
||
|
||
.cursor {
|
||
width: 6rpx;
|
||
height: 80rpx;
|
||
border-radius: 8rpx;
|
||
background: #1F5BD8;
|
||
animation: blink 1.1s step-end infinite;
|
||
}
|
||
|
||
@keyframes blink {
|
||
50% {
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
.amount-hint {
|
||
display: block;
|
||
margin-top: 16rpx;
|
||
padding-left: 4rpx;
|
||
color: #C2C7D0;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.benefit-tip {
|
||
margin-top: 20rpx;
|
||
padding-left: 4rpx;
|
||
color: #8B929E;
|
||
font-size: 26rpx;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.benefit-value {
|
||
margin-left: 8rpx;
|
||
color: #4B6CB7;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.extra-zone {
|
||
margin-top: 36rpx;
|
||
padding-top: 28rpx;
|
||
border-top: 1rpx solid #F0F2F5;
|
||
}
|
||
|
||
.remark-row {
|
||
padding: 0 4rpx;
|
||
}
|
||
|
||
.remark-input {
|
||
height: 44rpx;
|
||
color: #1F2937;
|
||
font-size: 28rpx;
|
||
text-align: left;
|
||
}
|
||
|
||
.remark-placeholder {
|
||
color: #C2C7D0;
|
||
}
|
||
|
||
.bind-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
margin-top: 28rpx;
|
||
padding: 8rpx 0;
|
||
}
|
||
|
||
.bind-text {
|
||
color: #8B929E;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.bind-hint {
|
||
margin-left: 10rpx;
|
||
color: #C2C7D0;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.bind-arrow {
|
||
margin-left: 4rpx;
|
||
color: #D1D5DB;
|
||
font-size: 30rpx;
|
||
line-height: 1;
|
||
}
|
||
|
||
.keyboard {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 10;
|
||
padding: 12rpx 12rpx 0;
|
||
background: #F7F7F8;
|
||
border-top: 1rpx solid #ECEEF2;
|
||
}
|
||
|
||
.keyboard.disabled {
|
||
opacity: 0.72;
|
||
}
|
||
|
||
.keypad {
|
||
display: flex;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.number-grid {
|
||
display: grid;
|
||
flex: 1;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.action-column {
|
||
display: grid;
|
||
width: 168rpx;
|
||
grid-template-rows: 96rpx 1fr;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.key,
|
||
.confirm-key {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 96rpx;
|
||
border-radius: 8rpx;
|
||
background: #FFFFFF;
|
||
color: #050505;
|
||
font-size: 44rpx;
|
||
font-weight: 600;
|
||
line-height: 1;
|
||
}
|
||
|
||
.key:active,
|
||
.confirm-key:active {
|
||
opacity: 0.78;
|
||
}
|
||
|
||
.key-zero {
|
||
grid-column: span 2;
|
||
}
|
||
|
||
.delete-key {
|
||
color: #747B86;
|
||
font-size: 40rpx;
|
||
}
|
||
|
||
.confirm-key {
|
||
flex-direction: column;
|
||
gap: 6rpx;
|
||
height: auto;
|
||
min-height: 204rpx;
|
||
padding: 0 12rpx;
|
||
background: #1F5BD8;
|
||
color: #FFFFFF;
|
||
font-size: 26rpx;
|
||
font-weight: 700;
|
||
text-align: center;
|
||
}
|
||
|
||
.confirm-amount {
|
||
font-size: 22rpx;
|
||
font-weight: 600;
|
||
opacity: 0.92;
|
||
}
|
||
|
||
.confirm-key.disabled,
|
||
.confirm-key.loading {
|
||
background: #C8D3EE;
|
||
}
|
||
|
||
.safe-bottom {
|
||
height: calc(16rpx + env(safe-area-inset-bottom));
|
||
}
|
||
</style>
|