1582 lines
38 KiB
Vue
1582 lines
38 KiB
Vue
<template>
|
||
<view class="cashier-page">
|
||
<view class="mine-entry" @tap="goMine">
|
||
<text class="ri-user-3-line mine-entry-icon"></text>
|
||
<text>我的</text>
|
||
</view>
|
||
|
||
<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 class="empty-mine-link" hover-class="empty-mine-link-hover" @tap="goMine">
|
||
<text class="ri-user-3-line empty-mine-link-icon"></text>
|
||
<text>个人中心</text>
|
||
</view>
|
||
</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-block">
|
||
<view class="benefit-tip">
|
||
{{ benefitTitle }}
|
||
<text class="benefit-value">¥ {{ accumulationAmount }}</text>
|
||
</view>
|
||
</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>
|
||
<!-- #ifdef MP-ALIPAY -->
|
||
<button
|
||
v-if="showAlipayPhoneAuthBtn"
|
||
class="property-row property-row-btn"
|
||
hover-class="property-row-hover"
|
||
open-type="getAuthorize"
|
||
scope="phoneNumber"
|
||
:disabled="paying || bindingPhone"
|
||
@tap="handleAlipayPhoneAuthTap"
|
||
@getAuthorize="handleAlipayPhoneAuthorize"
|
||
@error="handleAlipayPhoneAuthError"
|
||
>
|
||
<text class="property-name li-single-line property-name-action">
|
||
{{ propertyRowText }}
|
||
</text>
|
||
<text v-if="showPropertyArrow" class="ri-arrow-right-s-line property-arrow"></text>
|
||
</button>
|
||
<view
|
||
v-else
|
||
class="property-row"
|
||
:class="{ 'property-row--disabled': alipayPropertyUnbound }"
|
||
hover-class="property-row-hover"
|
||
@tap.stop="handlePropertyRow"
|
||
>
|
||
<text
|
||
class="property-name li-single-line"
|
||
:class="{
|
||
'property-name-action': isPropertyActionText && !alipayPropertyUnbound,
|
||
'property-name-tip': alipayPropertyUnbound
|
||
}"
|
||
>
|
||
{{ propertyRowText }}
|
||
</text>
|
||
<text v-if="showPropertyArrow" class="ri-arrow-right-s-line property-arrow"></text>
|
||
</view>
|
||
<!-- #endif -->
|
||
<!-- #ifndef MP-ALIPAY -->
|
||
<view
|
||
class="property-row"
|
||
hover-class="property-row-hover"
|
||
@tap.stop="handlePropertyRow"
|
||
>
|
||
<text
|
||
class="property-name li-single-line"
|
||
:class="{ 'property-name-action': isPropertyActionText }"
|
||
>
|
||
{{ propertyRowText }}
|
||
</text>
|
||
<text v-if="showPropertyArrow" class="ri-arrow-right-s-line property-arrow"></text>
|
||
</view>
|
||
<!-- #endif -->
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="propertySheetVisible" class="property-sheet-mask" @tap="closePropertySheet">
|
||
<view class="property-sheet" @tap.stop>
|
||
<view class="property-sheet-header">
|
||
<text class="property-sheet-title">{{ propertySheetTitle }}</text>
|
||
<text class="ri-close-line property-sheet-close" @tap="closePropertySheet"></text>
|
||
</view>
|
||
<scroll-view scroll-y class="property-sheet-list">
|
||
<view
|
||
v-for="item in propertyList"
|
||
:key="String(item.merchant_id)"
|
||
class="property-sheet-item"
|
||
:class="{ active: isSameMerchantId(item.merchant_id, propertyMerchantId) }"
|
||
@tap="handleSwitchProperty(item)"
|
||
>
|
||
<text class="property-sheet-item-name li-two-line">{{ item.merchant_name }}</text>
|
||
<text
|
||
v-if="isSameMerchantId(item.merchant_id, propertyMerchantId)"
|
||
class="ri-check-line property-sheet-item-check"
|
||
></text>
|
||
</view>
|
||
</scroll-view>
|
||
<view v-if="!IS_ALIPAY" class="property-sheet-footer" @tap="goBindProperty">
|
||
<text class="ri-add-line property-sheet-footer-icon"></text>
|
||
<text>绑定新房源</text>
|
||
</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, onShow } from '@dcloudio/uni-app';
|
||
import { bindMerchantByUrl, cancelTrade, changeMerchant, merchantListByUrl, supplierByUrl } from '@/api/cashier';
|
||
import type { MerchantListItem } from '@/api/cashier';
|
||
import { bindMobile, resolveLoginPlatform } from '@/api/auth';
|
||
import { postForm } from '@/utils/request';
|
||
import { DEFAULT_MERCHANT, APP_CONFIG, PROPERTY_APP } from '@/config';
|
||
import {
|
||
getPropertyBindingCacheKey,
|
||
getPropertyBindingCacheKeyByHash,
|
||
getSupplierCacheKey,
|
||
getSupplierCacheKeyByHash,
|
||
clearPendingMerchantUrl,
|
||
clearActiveMerchantContext,
|
||
resolveMerchantUrlFromEntry,
|
||
resolveMerchantUrlFromScanResult,
|
||
syncActiveMerchantContext
|
||
} from '@/utils/merchant-context';
|
||
import { encryptedStorage } from '@/utils/storage';
|
||
import { formatPaymentError, isPaymentCancelled, requestPlatformPayment } 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 IS_ALIPAY = resolveLoginPlatform() === 'mp-alipay';
|
||
|
||
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 propertyName = ref('');
|
||
const propertyMerchantId = ref('');
|
||
const propertyList = ref<MerchantListItem[]>([]);
|
||
const propertySheetVisible = ref(false);
|
||
const switchingProperty = ref(false);
|
||
const bindingPhone = ref(false);
|
||
const alipayPropertyUnbound = ref(false);
|
||
const scanning = ref(false);
|
||
/** 已判定无效的进页码,避免 onShow 用同一 launch qrCode 反复弹窗 */
|
||
const rejectedMerchantUrl = ref('');
|
||
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(2);
|
||
});
|
||
const showBenefitTip = computed(() => Number(merchant.value.accumulationRate) > 0);
|
||
const benefitTitle = computed(() => `预计获得${scoreName.value || '物业公积金'}`);
|
||
const propertyRowText = computed(() => {
|
||
if (propertyName.value) return propertyName.value;
|
||
if (propertyList.value.length) return '选择物业';
|
||
if (IS_ALIPAY) {
|
||
if (alipayPropertyUnbound.value) return '暂未绑定,请去微信小程序绑定';
|
||
return '绑定手机号查看房源';
|
||
}
|
||
return '业主去绑定';
|
||
});
|
||
const isPropertyActionText = computed(() => !propertyName.value);
|
||
const showAlipayPhoneAuthBtn = computed(
|
||
() => IS_ALIPAY && !propertyName.value && !propertyList.value.length && !alipayPropertyUnbound.value
|
||
);
|
||
const showPropertyArrow = computed(() => !IS_ALIPAY || !alipayPropertyUnbound.value);
|
||
const propertySheetTitle = computed(() => (propertyName.value ? '切换物业' : '选择物业'));
|
||
|
||
interface PropertyBindingCache {
|
||
merchant_id?: string;
|
||
merchant_name?: string;
|
||
score_name?: string;
|
||
fee_rate?: string | number;
|
||
}
|
||
|
||
interface SupplierCache {
|
||
supplier_id?: string | number;
|
||
supplier_name?: string;
|
||
logo?: string;
|
||
fee_rate?: string | number;
|
||
}
|
||
|
||
const parseFeeRate = (feeRate: unknown) => {
|
||
if (feeRate === undefined || feeRate === null || feeRate === '') return 0;
|
||
const rate = Number(feeRate);
|
||
if (!Number.isFinite(rate) || rate <= 0) return 0;
|
||
return rate / 100;
|
||
};
|
||
|
||
const hasFeeRate = (feeRate: unknown) => feeRate !== undefined && feeRate !== null && feeRate !== '';
|
||
|
||
const resolveFeeRateFromPayload = (payload: Record<string, any> = {}) => {
|
||
const supplierData = payload.supplier || {};
|
||
return supplierData.fee_rate != null ? supplierData.fee_rate : payload.fee_rate;
|
||
};
|
||
|
||
const resolvePropertyFromPayload = (payload: Record<string, any> = {}) => {
|
||
const merchantData = payload.merchant || payload;
|
||
return {
|
||
merchant_id: merchantData.merchant_id,
|
||
merchant_name: merchantData.merchant_name,
|
||
score_name: merchantData.config?.score_name
|
||
};
|
||
};
|
||
|
||
const hasBindingPayload = (payload?: Record<string, any> | null) => {
|
||
if (!payload || typeof payload !== 'object') return false;
|
||
const merchantData = payload.merchant || payload;
|
||
const merchantId = merchantData.merchant_id;
|
||
const merchantName = merchantData.merchant_name;
|
||
return merchantId !== undefined && merchantId !== null && merchantId !== '' && !!merchantName;
|
||
};
|
||
|
||
const buildBindingCache = (payload: Record<string, any> = {}): PropertyBindingCache => {
|
||
const merchantData = payload.merchant || payload;
|
||
return {
|
||
merchant_id: merchantData.merchant_id,
|
||
merchant_name: merchantData.merchant_name,
|
||
score_name: merchantData.config?.score_name,
|
||
fee_rate: resolveFeeRateFromPayload(payload)
|
||
};
|
||
};
|
||
|
||
const clearPropertyBinding = () => {
|
||
propertyName.value = '';
|
||
propertyMerchantId.value = '';
|
||
};
|
||
|
||
const fetchMerchantList = async (merchantUrl: string) => {
|
||
const response = await merchantListByUrl(merchantUrl);
|
||
if (Number(response?.code) !== 200) {
|
||
throw new Error(response?.msg || '获取物业列表失败');
|
||
}
|
||
return Array.isArray(response?.data) ? response.data : [];
|
||
};
|
||
|
||
const applyPropertyInfo = (property?: {
|
||
merchant_id?: string | number;
|
||
merchant_name?: string;
|
||
score_name?: string;
|
||
}) => {
|
||
if (!property) return;
|
||
if (property.merchant_id !== undefined && property.merchant_id !== null && property.merchant_id !== '') {
|
||
propertyMerchantId.value = String(property.merchant_id);
|
||
}
|
||
if (property.merchant_name) {
|
||
propertyName.value = property.merchant_name;
|
||
}
|
||
if (property.score_name) {
|
||
scoreName.value = property.score_name;
|
||
}
|
||
};
|
||
|
||
const isSameMerchantId = (left: string | number, right: string | number) => {
|
||
return String(left) === String(right);
|
||
};
|
||
|
||
const buildSupplierCache = (payload: Record<string, any> = {}, previous?: SupplierCache | null): SupplierCache => {
|
||
const supplierData = payload.supplier || {};
|
||
return {
|
||
supplier_id:
|
||
supplierData.supplier_id != null
|
||
? supplierData.supplier_id
|
||
: payload.supplier_id != null
|
||
? payload.supplier_id
|
||
: previous?.supplier_id,
|
||
supplier_name:
|
||
supplierData.supplier_name != null
|
||
? supplierData.supplier_name
|
||
: payload.supplier_name != null
|
||
? payload.supplier_name
|
||
: previous?.supplier_name,
|
||
logo: supplierData.logo != null ? supplierData.logo : payload.logo != null ? payload.logo : previous?.logo,
|
||
fee_rate: (() => {
|
||
const rate = resolveFeeRateFromPayload(payload);
|
||
return rate != null ? rate : previous?.fee_rate;
|
||
})()
|
||
};
|
||
};
|
||
|
||
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
|
||
};
|
||
};
|
||
|
||
const applyBindingInfo = (binding?: PropertyBindingCache | null) => {
|
||
if (!binding) return;
|
||
if (hasFeeRate(binding.fee_rate)) {
|
||
applyMerchantInfo({
|
||
accumulationRate: parseFeeRate(binding.fee_rate)
|
||
});
|
||
}
|
||
applyPropertyInfo(binding);
|
||
};
|
||
|
||
const applySupplierInfo = (supplier?: SupplierCache | null) => {
|
||
if (!supplier) return;
|
||
const updates: Record<string, unknown> = {
|
||
name: supplier.supplier_name,
|
||
background: supplier.logo
|
||
};
|
||
if (hasFeeRate(supplier.fee_rate)) {
|
||
updates.accumulationRate = parseFeeRate(supplier.fee_rate);
|
||
}
|
||
applyMerchantInfo(updates);
|
||
};
|
||
|
||
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 resetToEmptyMerchantState = () => {
|
||
hasMerchantUrl.value = false;
|
||
activeMerchantUrl.value = '';
|
||
merchant.value = { ...defaultMerchant };
|
||
amount.value = '';
|
||
remark.value = '';
|
||
propertyName.value = '';
|
||
propertyMerchantId.value = '';
|
||
propertyList.value = [];
|
||
propertySheetVisible.value = false;
|
||
alipayPropertyUnbound.value = false;
|
||
scoreName.value = '物业公积金';
|
||
clearPendingMerchantUrl();
|
||
clearActiveMerchantContext();
|
||
};
|
||
|
||
const promptScan = (message?: string) => {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: message || '未识别到商家码,请扫描商家收款码进入收银台',
|
||
confirmText: '去扫码',
|
||
showCancel: false,
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
handleScan();
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
const failInvalidMerchant = (merchantUrl: string, message?: string) => {
|
||
rejectedMerchantUrl.value = merchantUrl;
|
||
resetToEmptyMerchantState();
|
||
promptScan(message);
|
||
};
|
||
|
||
const fetchSupplierByUrl = async (merchantUrl: string) => {
|
||
const response = await supplierByUrl(merchantUrl);
|
||
if (Number(response?.code) !== 200 || !response?.data) {
|
||
return {
|
||
ok: false as const,
|
||
msg: response?.msg || '未识别到商家码,请扫描商家收款码进入收银台'
|
||
};
|
||
}
|
||
|
||
const payload = response.data || {};
|
||
const cacheKey = getSupplierCacheKey(merchantUrl);
|
||
const cached = encryptedStorage.get<SupplierCache>(cacheKey);
|
||
const latestSupplier = buildSupplierCache(payload, cached);
|
||
if (!latestSupplier.supplier_name && !latestSupplier.supplier_id) {
|
||
return {
|
||
ok: false as const,
|
||
msg: response?.msg || '未识别到商家码,请扫描商家收款码进入收银台'
|
||
};
|
||
}
|
||
|
||
applyPropertyInfo(resolvePropertyFromPayload(payload));
|
||
applySupplierInfo(latestSupplier);
|
||
encryptedStorage.set(cacheKey, latestSupplier, SUPPLIER_CACHE_TTL_SECONDS);
|
||
return { ok: true as const, supplier: latestSupplier };
|
||
};
|
||
|
||
const initWithMerchantUrl = async (
|
||
merchantUrl: string,
|
||
options: { force?: boolean } = {}
|
||
) => {
|
||
if (!options.force && rejectedMerchantUrl.value === merchantUrl) {
|
||
resetToEmptyMerchantState();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const [supplierResult, bindResponse] = await Promise.all([
|
||
fetchSupplierByUrl(merchantUrl),
|
||
bindMerchantByUrl(merchantUrl).catch(() => null)
|
||
]);
|
||
|
||
if (!supplierResult.ok) {
|
||
failInvalidMerchant(merchantUrl, supplierResult.msg);
|
||
return;
|
||
}
|
||
|
||
const bindCode = Number(bindResponse?.code);
|
||
const bindMsg = String(bindResponse?.msg || '');
|
||
if (bindResponse && bindCode !== 200 && /重新扫码/.test(bindMsg)) {
|
||
failInvalidMerchant(merchantUrl, bindMsg);
|
||
return;
|
||
}
|
||
|
||
rejectedMerchantUrl.value = '';
|
||
hasMerchantUrl.value = true;
|
||
activeMerchantUrl.value = merchantUrl;
|
||
propertyName.value = '';
|
||
propertyMerchantId.value = '';
|
||
propertyList.value = [];
|
||
propertySheetVisible.value = false;
|
||
alipayPropertyUnbound.value = false;
|
||
scoreName.value = '物业公积金';
|
||
applySupplierInfo(supplierResult.supplier);
|
||
await loadPropertyBinding(merchantUrl);
|
||
} catch (error: any) {
|
||
failInvalidMerchant(
|
||
merchantUrl,
|
||
error?.message || '未识别到商家码,请扫描商家收款码进入收银台'
|
||
);
|
||
}
|
||
};
|
||
|
||
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, { force: true });
|
||
},
|
||
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 resolvePropertyListFallback = async (merchantUrl: string) => {
|
||
try {
|
||
const list = await fetchMerchantList(merchantUrl);
|
||
propertyList.value = list;
|
||
|
||
if (!list.length) {
|
||
clearPropertyBinding();
|
||
encryptedStorage.remove(syncBindingCacheForUrl(merchantUrl));
|
||
}
|
||
} catch (error) {
|
||
console.log('property list fallback', error);
|
||
clearPropertyBinding();
|
||
propertyList.value = [];
|
||
}
|
||
};
|
||
|
||
const loadPropertyBinding = async (merchantUrl: string, options: { skipFallback?: boolean } = {}) => {
|
||
const cacheKey = syncBindingCacheForUrl(merchantUrl);
|
||
const cached = encryptedStorage.get<PropertyBindingCache>(cacheKey);
|
||
if (cached?.merchant_name) {
|
||
applyBindingInfo(cached);
|
||
}
|
||
|
||
try {
|
||
const response = await bindMerchantByUrl(merchantUrl);
|
||
if (Number(response?.code) !== 200 || !hasBindingPayload(response?.data)) {
|
||
if (!options.skipFallback) {
|
||
await resolvePropertyListFallback(merchantUrl);
|
||
}
|
||
return;
|
||
}
|
||
|
||
const latestBinding = buildBindingCache(response.data || {});
|
||
applyBindingInfo(latestBinding);
|
||
encryptedStorage.set(cacheKey, latestBinding, PROPERTY_CACHE_TTL_SECONDS);
|
||
} catch (error) {
|
||
console.log('bind merchant fallback cache', error);
|
||
if (!options.skipFallback) {
|
||
await resolvePropertyListFallback(merchantUrl);
|
||
}
|
||
}
|
||
};
|
||
|
||
const loadSupplier = async (merchantUrl: string) => {
|
||
const cacheKey = getSupplierCacheKey(merchantUrl);
|
||
const cached = encryptedStorage.get<SupplierCache>(cacheKey);
|
||
if (cached) {
|
||
applySupplierInfo(cached);
|
||
}
|
||
|
||
try {
|
||
const result = await fetchSupplierByUrl(merchantUrl);
|
||
if (!result.ok) return false;
|
||
return true;
|
||
} catch (error) {
|
||
console.log('supplier fallback cache', error);
|
||
return false;
|
||
}
|
||
};
|
||
|
||
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 showAlipayUnboundTip = () => {
|
||
uni.showModal({
|
||
title: '暂未绑定房源',
|
||
content: '您在微信小程序中暂无绑定房源,请前往微信小程序完成绑定后再使用。',
|
||
showCancel: false,
|
||
confirmText: '知道了'
|
||
});
|
||
};
|
||
|
||
const refreshPropertyAfterPhoneBind = async () => {
|
||
if (!activeMerchantUrl.value) return;
|
||
|
||
alipayPropertyUnbound.value = false;
|
||
clearPropertyBinding();
|
||
propertyList.value = [];
|
||
encryptedStorage.remove(getPropertyBindingCacheKey(activeMerchantUrl.value));
|
||
await loadPropertyBinding(activeMerchantUrl.value);
|
||
|
||
if (propertyName.value || propertyList.value.length) {
|
||
uni.showToast({
|
||
title: '已匹配到物业信息',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
alipayPropertyUnbound.value = true;
|
||
};
|
||
|
||
const tryParseJson = (value: any) => {
|
||
if (typeof value !== 'string') return value;
|
||
const text = value.trim();
|
||
if (!text || (text[0] !== '{' && text[0] !== '[')) return value;
|
||
try {
|
||
return JSON.parse(text);
|
||
} catch {
|
||
return value;
|
||
}
|
||
};
|
||
|
||
/** 调试日志用:把嵌套 JSON 字符串展开成可展开对象 */
|
||
const toLogObject = (value: any, depth = 0): any => {
|
||
if (value == null || depth > 4) return value;
|
||
const parsed = tryParseJson(value);
|
||
if (Array.isArray(parsed)) {
|
||
return parsed.map((item) => toLogObject(item, depth + 1));
|
||
}
|
||
if (parsed && typeof parsed === 'object') {
|
||
const result: Record<string, any> = {};
|
||
Object.keys(parsed).forEach((key) => {
|
||
result[key] = toLogObject(parsed[key], depth + 1);
|
||
});
|
||
return result;
|
||
}
|
||
return parsed;
|
||
};
|
||
|
||
const logPhoneAuth = (label: string, payload?: any) => {
|
||
console.log(`[收银台][手机号授权] ${label}`, payload === undefined ? undefined : toLogObject(payload));
|
||
};
|
||
|
||
/** 取 getPhoneNumber 密文:response.response */
|
||
const extractAlipayEncryptedData = (phoneRes: any) => {
|
||
const parsed = tryParseJson(phoneRes?.response);
|
||
if (parsed && typeof parsed === 'object') {
|
||
const encrypted = parsed.response;
|
||
return typeof encrypted === 'string' ? encrypted.trim() : '';
|
||
}
|
||
return typeof phoneRes?.response === 'string' ? phoneRes.response.trim() : '';
|
||
};
|
||
|
||
const requestAlipayPhoneNumber = () =>
|
||
new Promise<any>((resolve, reject) => {
|
||
// #ifdef MP-ALIPAY
|
||
if (typeof my !== 'undefined' && typeof my.getPhoneNumber === 'function') {
|
||
my.getPhoneNumber({
|
||
success: resolve,
|
||
fail: reject
|
||
});
|
||
return;
|
||
}
|
||
// #endif
|
||
|
||
uni.getPhoneNumber({
|
||
success: resolve,
|
||
fail: reject
|
||
});
|
||
});
|
||
|
||
const handleAlipayPhoneAuthTap = () => {
|
||
logPhoneAuth('按钮点击');
|
||
};
|
||
|
||
const handleAlipayPhoneAuthError = (event?: any) => {
|
||
logPhoneAuth('error 回调', event);
|
||
};
|
||
|
||
const handleAlipayPhoneAuthorize = async (event?: any) => {
|
||
logPhoneAuth('getAuthorize 回调', event);
|
||
|
||
if (paying.value || bindingPhone.value) return;
|
||
|
||
bindingPhone.value = true;
|
||
try {
|
||
const phoneRes = await requestAlipayPhoneNumber();
|
||
logPhoneAuth('getPhoneNumber 成功', phoneRes);
|
||
|
||
const encryptedData = extractAlipayEncryptedData(phoneRes);
|
||
logPhoneAuth('解析 encryptedData', {
|
||
encryptedData,
|
||
response: tryParseJson(phoneRes?.response)
|
||
});
|
||
|
||
if (!encryptedData) {
|
||
throw new Error('手机号授权失败');
|
||
}
|
||
|
||
const response = await bindMobile(encryptedData);
|
||
logPhoneAuth('bind.mobile 响应', response);
|
||
|
||
if (Number(response?.code) !== 200) {
|
||
throw new Error(response?.msg || '手机号绑定失败');
|
||
}
|
||
|
||
await refreshPropertyAfterPhoneBind();
|
||
} catch (error: any) {
|
||
logPhoneAuth('授权失败', error);
|
||
const errMsg = error?.errMsg || error?.message || '';
|
||
if (errMsg.includes('cancel') || errMsg.includes('拒绝')) return;
|
||
uni.showToast({
|
||
title: errMsg || '手机号授权失败',
|
||
icon: 'none'
|
||
});
|
||
} finally {
|
||
bindingPhone.value = false;
|
||
}
|
||
};
|
||
|
||
const goBindProperty = () => {
|
||
if (IS_ALIPAY) {
|
||
showAlipayUnboundTip();
|
||
return;
|
||
}
|
||
|
||
if (paying.value) return;
|
||
propertySheetVisible.value = false;
|
||
|
||
if (activeMerchantUrl.value) {
|
||
encryptedStorage.remove(getPropertyBindingCacheKey(activeMerchantUrl.value));
|
||
}
|
||
|
||
uni.navigateToMiniProgram({
|
||
appId: PROPERTY_APP.APP_ID,
|
||
path: PROPERTY_APP.BIND_HOUSE_PATH,
|
||
extraData: { from: 'cashier' },
|
||
envVersion: APP_CONFIG.API_BASE_URL.includes('dev.') ? 'trial' : 'release',
|
||
fail: () => {
|
||
uni.showToast({
|
||
title: '跳转失败,请稍后重试',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
});
|
||
};
|
||
|
||
const goMine = () => {
|
||
uni.navigateTo({
|
||
url: '/pages/mine/index'
|
||
});
|
||
};
|
||
|
||
const closePropertySheet = () => {
|
||
propertySheetVisible.value = false;
|
||
};
|
||
|
||
const openPropertySheet = async () => {
|
||
if (!activeMerchantUrl.value) return;
|
||
|
||
const list = await fetchMerchantList(activeMerchantUrl.value);
|
||
if (!list.length) {
|
||
if (IS_ALIPAY) {
|
||
if (alipayPropertyUnbound.value) {
|
||
showAlipayUnboundTip();
|
||
}
|
||
return;
|
||
}
|
||
goBindProperty();
|
||
return;
|
||
}
|
||
|
||
propertyList.value = list;
|
||
propertySheetVisible.value = true;
|
||
};
|
||
|
||
const handlePropertyRow = async () => {
|
||
if (paying.value || switchingProperty.value || bindingPhone.value) return;
|
||
if (!activeMerchantUrl.value) return;
|
||
|
||
if (IS_ALIPAY && alipayPropertyUnbound.value) {
|
||
showAlipayUnboundTip();
|
||
return;
|
||
}
|
||
|
||
if (IS_ALIPAY && showAlipayPhoneAuthBtn.value) {
|
||
return;
|
||
}
|
||
|
||
if (!propertyName.value) {
|
||
try {
|
||
if (propertyList.value.length) {
|
||
propertySheetVisible.value = true;
|
||
return;
|
||
}
|
||
await openPropertySheet();
|
||
} catch (error: any) {
|
||
uni.showToast({
|
||
title: error?.message || '获取物业列表失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await openPropertySheet();
|
||
} catch (error: any) {
|
||
uni.showToast({
|
||
title: error?.message || '获取物业列表失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
};
|
||
|
||
const switchToProperty = async (
|
||
item: MerchantListItem,
|
||
merchantUrl: string,
|
||
options: { closeSheet?: boolean; skipBindingFallback?: boolean } = {}
|
||
) => {
|
||
if (paying.value || switchingProperty.value) return false;
|
||
if (isSameMerchantId(item.merchant_id, propertyMerchantId.value)) {
|
||
if (options.closeSheet) closePropertySheet();
|
||
return true;
|
||
}
|
||
|
||
switchingProperty.value = true;
|
||
try {
|
||
const response = await changeMerchant({
|
||
merchant_id: item.merchant_id,
|
||
url: merchantUrl
|
||
});
|
||
if (Number(response?.code) !== 200) {
|
||
throw new Error(response?.msg || '切换物业失败');
|
||
}
|
||
|
||
if (options.closeSheet) closePropertySheet();
|
||
applyPropertyInfo(item);
|
||
encryptedStorage.set(
|
||
getPropertyBindingCacheKey(merchantUrl),
|
||
{
|
||
merchant_id: item.merchant_id,
|
||
merchant_name: item.merchant_name
|
||
},
|
||
PROPERTY_CACHE_TTL_SECONDS
|
||
);
|
||
await loadPropertyBinding(merchantUrl, {
|
||
skipFallback: options.skipBindingFallback != null ? options.skipBindingFallback : true
|
||
});
|
||
await loadSupplier(merchantUrl);
|
||
return true;
|
||
} catch (error: any) {
|
||
uni.showToast({
|
||
title: error?.message || '切换物业失败',
|
||
icon: 'none'
|
||
});
|
||
return false;
|
||
} finally {
|
||
switchingProperty.value = false;
|
||
}
|
||
};
|
||
|
||
const handleSwitchProperty = async (item: MerchantListItem) => {
|
||
if (!activeMerchantUrl.value) return;
|
||
await switchToProperty(item, activeMerchantUrl.value, { closeSheet: true, skipBindingFallback: true });
|
||
};
|
||
|
||
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,按当前端传入 platform
|
||
const payload = {
|
||
amount: payableAmount.value,
|
||
platform: resolveLoginPlatform(),
|
||
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 payData = payRes?.data || {};
|
||
const tradeNo = payData.trade_no || payData.tradeNo || '';
|
||
if (!tradeNo) {
|
||
throw new Error('缺少交易单号');
|
||
}
|
||
|
||
try {
|
||
await requestPlatformPayment(payData);
|
||
} 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: formatPaymentError(error),
|
||
icon: 'none'
|
||
});
|
||
} finally {
|
||
paying.value = false;
|
||
}
|
||
};
|
||
|
||
onLoad((options: any) => {
|
||
const merchantUrl = resolveMerchantUrlFromEntry(options || {});
|
||
if (!merchantUrl) {
|
||
handleMissingMerchantUrl();
|
||
return;
|
||
}
|
||
clearPendingMerchantUrl();
|
||
initWithMerchantUrl(merchantUrl);
|
||
});
|
||
|
||
onShow(() => {
|
||
if (!hasMerchantUrl.value || !activeMerchantUrl.value) {
|
||
const merchantUrl = resolveMerchantUrlFromEntry({});
|
||
if (merchantUrl && merchantUrl !== rejectedMerchantUrl.value) {
|
||
clearPendingMerchantUrl();
|
||
initWithMerchantUrl(merchantUrl);
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (!hasMerchantUrl.value || !activeMerchantUrl.value) return;
|
||
loadPropertyBinding(activeMerchantUrl.value);
|
||
loadSupplier(activeMerchantUrl.value);
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.cashier-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 100vh;
|
||
background: #FFFFFF;
|
||
color: #111827;
|
||
position: relative;
|
||
}
|
||
|
||
.mine-entry {
|
||
position: fixed;
|
||
top: calc(env(safe-area-inset-top) + 16rpx);
|
||
left: 24rpx;
|
||
z-index: 20;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6rpx;
|
||
padding: 12rpx 20rpx;
|
||
border-radius: 999rpx;
|
||
background: rgba(255, 255, 255, 0.92);
|
||
color: #1f5bd8;
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
box-shadow: 0 8rpx 20rpx rgba(17, 24, 39, 0.08);
|
||
}
|
||
|
||
.mine-entry-icon {
|
||
font-size: 28rpx;
|
||
line-height: 1;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.empty-mine-link {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-top: 24rpx;
|
||
color: rgba(255, 255, 255, 0.96);
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.empty-mine-link-hover {
|
||
opacity: 0.86;
|
||
}
|
||
|
||
.empty-mine-link-icon {
|
||
margin-right: 8rpx;
|
||
font-size: 32rpx;
|
||
line-height: 1;
|
||
}
|
||
|
||
.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-block {
|
||
margin-top: 20rpx;
|
||
padding-left: 4rpx;
|
||
}
|
||
|
||
.benefit-tip {
|
||
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;
|
||
}
|
||
|
||
.property-row {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
margin-top: 28rpx;
|
||
padding: 12rpx 4rpx;
|
||
}
|
||
|
||
.property-row--disabled {
|
||
opacity: 0.92;
|
||
}
|
||
|
||
.property-row-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
width: 100%;
|
||
margin: 0;
|
||
padding: 12rpx 4rpx;
|
||
border: none;
|
||
background: transparent;
|
||
line-height: 1;
|
||
text-align: left;
|
||
}
|
||
|
||
.property-row-btn::after {
|
||
border: none;
|
||
}
|
||
|
||
.property-row-hover {
|
||
opacity: 0.88;
|
||
}
|
||
|
||
.property-name {
|
||
flex: 0 1 auto;
|
||
max-width: calc(100% - 36rpx);
|
||
color: #374151;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.property-name-action {
|
||
color: #1F5BD8;
|
||
}
|
||
|
||
.property-name-tip {
|
||
color: #8B929E;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.property-arrow {
|
||
flex-shrink: 0;
|
||
margin-left: 4rpx;
|
||
color: #C2C7D0;
|
||
font-size: 30rpx;
|
||
line-height: 1;
|
||
}
|
||
|
||
.property-sheet-mask {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 100;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
background: rgba(17, 24, 39, 0.42);
|
||
}
|
||
|
||
.property-sheet {
|
||
width: 100%;
|
||
max-height: 68vh;
|
||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||
border-radius: 24rpx 24rpx 0 0;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.property-sheet-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 28rpx 32rpx 20rpx;
|
||
}
|
||
|
||
.property-sheet-title {
|
||
color: #111827;
|
||
font-size: 32rpx;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.property-sheet-close {
|
||
color: #9CA3AF;
|
||
font-size: 40rpx;
|
||
line-height: 1;
|
||
}
|
||
|
||
.property-sheet-list {
|
||
max-height: 48vh;
|
||
}
|
||
|
||
.property-sheet-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 20rpx;
|
||
padding: 28rpx 32rpx;
|
||
border-top: 1rpx solid #F3F4F6;
|
||
}
|
||
|
||
.property-sheet-item.active {
|
||
background: #F8FAFF;
|
||
}
|
||
|
||
.property-sheet-item-name {
|
||
flex: 1;
|
||
min-width: 0;
|
||
color: #1F2937;
|
||
font-size: 28rpx;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.property-sheet-item.active .property-sheet-item-name {
|
||
color: #1F5BD8;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.property-sheet-item-check {
|
||
flex-shrink: 0;
|
||
color: #1F5BD8;
|
||
font-size: 36rpx;
|
||
line-height: 1;
|
||
}
|
||
|
||
.property-sheet-footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8rpx;
|
||
margin: 8rpx 32rpx 0;
|
||
padding: 24rpx 0;
|
||
border-top: 1rpx solid #F3F4F6;
|
||
color: #1F5BD8;
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.property-sheet-footer-icon {
|
||
font-size: 32rpx;
|
||
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>
|