支持拉卡拉多渠道支付和测试充值
This commit is contained in:
@@ -29,23 +29,48 @@ const showSecrets = ref(false)
|
||||
const loadingSecrets = ref(false)
|
||||
const advancedOpen = ref<string[]>([])
|
||||
|
||||
const formData = ref<CreatePaymentConfigRequest>({
|
||||
name: '',
|
||||
provider: 'leshua',
|
||||
merchant_id: '',
|
||||
gateway_url: 'https://paygate.leshuazf.com/cgi-bin/lepos_pay_gateway.cgi',
|
||||
sign_key: '',
|
||||
notify_key: '',
|
||||
notify_url: '',
|
||||
jump_url: '',
|
||||
pay_way: 'ZFBZF',
|
||||
jspay_flag: '2',
|
||||
sign_type: 'MD5',
|
||||
is_default: false,
|
||||
status: 'active',
|
||||
environment: 'production',
|
||||
business_tags: [],
|
||||
})
|
||||
const defaultGateways: Record<string, string> = {
|
||||
leshua: 'https://paygate.leshuazf.com/cgi-bin/lepos_pay_gateway.cgi',
|
||||
lakala: 'https://test.wsmsd.cn/sit',
|
||||
mock: '',
|
||||
}
|
||||
|
||||
function defaultExtraConfig(provider: string) {
|
||||
if (provider === 'lakala') {
|
||||
return {
|
||||
app_id: '',
|
||||
serial_no: '',
|
||||
term_no: '',
|
||||
lakala_cert: '',
|
||||
pay_mode: 'ALIPAY',
|
||||
order_expire_minutes: 15,
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
function makeDefaultForm(provider = 'leshua'): CreatePaymentConfigRequest {
|
||||
return {
|
||||
name: '',
|
||||
provider,
|
||||
merchant_id: '',
|
||||
gateway_url: defaultGateways[provider] || '',
|
||||
sign_key: '',
|
||||
notify_key: '',
|
||||
notify_url: '',
|
||||
jump_url: '',
|
||||
pay_way: 'ZFBZF',
|
||||
jspay_flag: '2',
|
||||
sign_type: provider === 'lakala' ? 'SHA256withRSA' : 'MD5',
|
||||
extra_config: defaultExtraConfig(provider),
|
||||
is_default: false,
|
||||
status: 'active',
|
||||
environment: provider === 'lakala' ? 'sandbox' : 'production',
|
||||
business_tags: [],
|
||||
}
|
||||
}
|
||||
|
||||
const formData = ref<CreatePaymentConfigRequest>(makeDefaultForm())
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
const titles = {
|
||||
@@ -57,6 +82,16 @@ const dialogTitle = computed(() => {
|
||||
})
|
||||
|
||||
const isReadonly = computed(() => props.mode === 'view')
|
||||
const isLakala = computed(() => formData.value.provider === 'lakala')
|
||||
const isMock = computed(() => formData.value.provider === 'mock')
|
||||
const signKeyLabel = computed(() => (isLakala.value ? '商户私钥' : '签名密钥'))
|
||||
const notifyKeyLabel = computed(() => (isLakala.value ? '通知证书' : '通知密钥'))
|
||||
const extraConfig = computed<Record<string, any>>(() => {
|
||||
if (!formData.value.extra_config) {
|
||||
formData.value.extra_config = {}
|
||||
}
|
||||
return formData.value.extra_config
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
@@ -72,6 +107,7 @@ watch(
|
||||
pay_way: props.config.pay_way,
|
||||
jspay_flag: props.config.jspay_flag,
|
||||
sign_type: props.config.sign_type,
|
||||
extra_config: props.config.extra_config || defaultExtraConfig(props.config.provider),
|
||||
is_default: props.config.is_default,
|
||||
status: props.config.status,
|
||||
environment: props.config.environment,
|
||||
@@ -81,23 +117,7 @@ watch(
|
||||
advancedOpen.value = []
|
||||
} else if (val && !props.config) {
|
||||
// 重置表单
|
||||
formData.value = {
|
||||
name: '',
|
||||
provider: 'leshua',
|
||||
merchant_id: '',
|
||||
gateway_url: 'https://paygate.leshuazf.com/cgi-bin/lepos_pay_gateway.cgi',
|
||||
sign_key: '',
|
||||
notify_key: '',
|
||||
notify_url: '',
|
||||
jump_url: '',
|
||||
pay_way: 'ZFBZF',
|
||||
jspay_flag: '2',
|
||||
sign_type: 'MD5',
|
||||
is_default: false,
|
||||
status: 'active',
|
||||
environment: 'production',
|
||||
business_tags: [],
|
||||
}
|
||||
formData.value = makeDefaultForm()
|
||||
showSecrets.value = false
|
||||
advancedOpen.value = []
|
||||
}
|
||||
@@ -105,6 +125,17 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function handleProviderChange(provider: string) {
|
||||
if (props.mode !== 'create') return
|
||||
const currentName = formData.value.name
|
||||
const currentNotifyURL = formData.value.notify_url
|
||||
formData.value = {
|
||||
...makeDefaultForm(provider),
|
||||
name: currentName,
|
||||
notify_url: currentNotifyURL,
|
||||
}
|
||||
}
|
||||
|
||||
async function handleViewSecrets() {
|
||||
if (!props.config) return
|
||||
loadingSecrets.value = true
|
||||
@@ -138,6 +169,7 @@ async function handleSave() {
|
||||
pay_way: formData.value.pay_way,
|
||||
jspay_flag: formData.value.jspay_flag,
|
||||
sign_type: formData.value.sign_type,
|
||||
extra_config: formData.value.extra_config,
|
||||
is_default: formData.value.is_default,
|
||||
status: formData.value.status,
|
||||
environment: formData.value.environment,
|
||||
@@ -175,36 +207,61 @@ function handleClose() {
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="支付服务商" required>
|
||||
<el-select v-model="formData.provider" :disabled="mode !== 'create'">
|
||||
<el-select v-model="formData.provider" :disabled="mode !== 'create'" @change="handleProviderChange">
|
||||
<el-option label="乐刷支付" value="leshua" />
|
||||
<el-option label="拉卡拉支付" value="lakala" />
|
||||
<el-option label="模拟支付" value="mock" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="商户号" required>
|
||||
<el-input v-model="formData.merchant_id" placeholder="乐刷商户号" :disabled="isReadonly" />
|
||||
<el-input v-model="formData.merchant_id" placeholder="渠道商户号" :disabled="isReadonly" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="网关地址" required>
|
||||
<el-form-item v-if="!isMock" label="网关地址" required>
|
||||
<el-input v-model="formData.gateway_url" placeholder="支付网关URL" :disabled="isReadonly" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="签名密钥" required>
|
||||
<el-form-item v-if="isLakala" label="App ID" required>
|
||||
<el-input v-model="extraConfig.app_id" placeholder="拉卡拉开放平台 appId" :disabled="isReadonly" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="isLakala" label="证书序列号" required>
|
||||
<el-input v-model="extraConfig.serial_no" placeholder="商户证书序列号" :disabled="isReadonly" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="isLakala" label="终端号" required>
|
||||
<el-input v-model="extraConfig.term_no" placeholder="拉卡拉终端号" :disabled="isReadonly" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="!isMock" :label="signKeyLabel" required>
|
||||
<el-input
|
||||
v-model="formData.sign_key"
|
||||
type="password"
|
||||
show-password
|
||||
:placeholder="mode === 'edit' ? '留空则不修改' : '请输入签名密钥'"
|
||||
:type="isLakala ? 'textarea' : 'password'"
|
||||
:rows="isLakala ? 5 : undefined"
|
||||
:show-password="!isLakala"
|
||||
:placeholder="mode === 'edit' ? '留空则不修改' : isLakala ? '请输入商户私钥 PEM' : '请输入签名密钥'"
|
||||
:disabled="isReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="通知密钥" required>
|
||||
<el-form-item v-if="!isMock" :label="notifyKeyLabel" required>
|
||||
<el-input
|
||||
v-model="formData.notify_key"
|
||||
type="password"
|
||||
show-password
|
||||
:placeholder="mode === 'edit' ? '留空则不修改' : '请输入通知密钥'"
|
||||
:type="isLakala ? 'textarea' : 'password'"
|
||||
:rows="isLakala ? 5 : undefined"
|
||||
:show-password="!isLakala"
|
||||
:placeholder="mode === 'edit' ? '留空则不修改' : isLakala ? '请输入拉卡拉通知验签证书 PEM' : '请输入通知密钥'"
|
||||
:disabled="isReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="isLakala" label="平台证书">
|
||||
<el-input
|
||||
v-model="extraConfig.lakala_cert"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="拉卡拉平台响应验签证书 PEM,可与通知证书一致"
|
||||
:disabled="isReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -213,7 +270,7 @@ function handleClose() {
|
||||
<el-button :loading="loadingSecrets" @click="handleViewSecrets">查看密钥</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="回调地址" required>
|
||||
<el-form-item v-if="!isMock" label="回调地址" required>
|
||||
<el-input v-model="formData.notify_url" placeholder="异步通知回调地址" :disabled="isReadonly" />
|
||||
</el-form-item>
|
||||
|
||||
@@ -254,6 +311,18 @@ function handleClose() {
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="isLakala" label="拉卡拉模式">
|
||||
<el-select v-model="extraConfig.pay_mode" :disabled="isReadonly">
|
||||
<el-option label="支付宝" value="ALIPAY" />
|
||||
<el-option label="微信" value="WECHAT" />
|
||||
<el-option label="银联" value="UQRCODEPAY" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="isLakala" label="有效期分钟">
|
||||
<el-input-number v-model="extraConfig.order_expire_minutes" :min="1" :max="1440" :disabled="isReadonly" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="支付形态">
|
||||
<el-select v-model="formData.jspay_flag" :disabled="isReadonly">
|
||||
<el-option label="H5 / JSAPI" value="1" />
|
||||
@@ -264,6 +333,7 @@ function handleClose() {
|
||||
<el-form-item label="签名类型">
|
||||
<el-select v-model="formData.sign_type" :disabled="isReadonly">
|
||||
<el-option label="MD5" value="MD5" />
|
||||
<el-option label="SHA256withRSA" value="SHA256withRSA" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-collapse-item>
|
||||
|
||||
@@ -29,6 +29,7 @@ const filterEnvironment = ref('')
|
||||
const providerOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '乐刷支付', value: 'leshua' },
|
||||
{ label: '拉卡拉支付', value: 'lakala' },
|
||||
{ label: '模拟支付', value: 'mock' },
|
||||
]
|
||||
|
||||
@@ -91,6 +92,10 @@ function handleEdit(row: PaymentConfig) {
|
||||
}
|
||||
|
||||
async function handleDelete(row: PaymentConfig) {
|
||||
if (!canDeleteConfig(row)) {
|
||||
ElMessage.warning('该配置已有交易记录,需要保留用于退款、查询和回调验签')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要删除配置"${row.name}"吗?`, '确认删除', {
|
||||
type: 'warning',
|
||||
@@ -118,6 +123,7 @@ function handleSaved() {
|
||||
function formatProvider(provider: string) {
|
||||
const map: Record<string, string> = {
|
||||
leshua: '乐刷支付',
|
||||
lakala: '拉卡拉支付',
|
||||
mock: '模拟支付',
|
||||
}
|
||||
return map[provider] || provider
|
||||
@@ -152,6 +158,14 @@ function getStatusType(status: string) {
|
||||
}
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
function canDeleteConfig(row: PaymentConfig) {
|
||||
return row.total_transactions <= 0
|
||||
}
|
||||
|
||||
function deleteDisabledReason(row: PaymentConfig) {
|
||||
return canDeleteConfig(row) ? '删除配置' : '已有交易记录,需要保留用于退款、查询和回调验签'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -159,7 +173,7 @@ function getStatusType(status: string) {
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Payment Configs</p>
|
||||
<h1>支付配置管理</h1>
|
||||
<p>管理多个支付商户配置,支持乐刷支付等多种支付渠道。</p>
|
||||
<p>管理多个支付商户配置,支持乐刷、拉卡拉等多种支付渠道。</p>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
@@ -230,9 +244,19 @@ function getStatusType(status: string) {
|
||||
<el-button class="action-button edit" size="small" :icon="Edit" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button class="action-button delete" size="small" :icon="Delete" @click="handleDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
<el-tooltip :content="deleteDisabledReason(row)" placement="top">
|
||||
<span>
|
||||
<el-button
|
||||
class="action-button delete"
|
||||
size="small"
|
||||
:icon="Delete"
|
||||
:disabled="!canDeleteConfig(row)"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { CircleCheck, Lock, Money, Refresh, Tickets, Wallet as WalletIcon } from '@element-plus/icons-vue'
|
||||
import { CircleCheck, Coin, Loading, Lock, Money, Refresh, Tickets, Wallet as WalletIcon } from '@element-plus/icons-vue'
|
||||
import QRCode from 'qrcode'
|
||||
|
||||
import {
|
||||
fetchWalletBalance,
|
||||
fetchWalletLedger,
|
||||
queryWalletRechargePayment,
|
||||
startWalletRechargePayment,
|
||||
type WalletAccount,
|
||||
type WalletLedger,
|
||||
} from '@/features/wallet'
|
||||
import type { PaymentOrder } from '@/features/orders'
|
||||
import { balanceTypeLabel, ledgerDirectionLabel, walletStatusLabel } from '@/utils/statusLabels'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
|
||||
@@ -20,6 +24,15 @@ const ledger = ref<WalletLedger[]>([])
|
||||
const currentPage = ref(1)
|
||||
const currentPageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const isDev = import.meta.env.DEV
|
||||
const quickRechargeAmounts = [0.01, 1, 10, 100]
|
||||
const devRechargeAmount = ref(0.01)
|
||||
const devRecharging = ref(false)
|
||||
const rechargeDialogVisible = ref(false)
|
||||
const activeRechargePayment = ref<PaymentOrder | null>(null)
|
||||
const rechargeQRCodeURL = ref('')
|
||||
const rechargeQRGenerating = ref(false)
|
||||
const checkingRecharge = ref(false)
|
||||
|
||||
const walletMetrics = computed(() => {
|
||||
if (!account.value) {
|
||||
@@ -77,6 +90,96 @@ function handleWithdraw() {
|
||||
router.push('/wallet/withdrawal')
|
||||
}
|
||||
|
||||
function selectDevRechargeAmount(amount: number) {
|
||||
devRechargeAmount.value = amount
|
||||
}
|
||||
|
||||
function rechargePayURL(payment = activeRechargePayment.value) {
|
||||
return payment?.jspay_url || payment?.td_code || payment?.jspay_info || ''
|
||||
}
|
||||
|
||||
async function renderRechargeQRCode(payment = activeRechargePayment.value) {
|
||||
const payURL = rechargePayURL(payment)
|
||||
rechargeQRCodeURL.value = ''
|
||||
if (!payURL) return
|
||||
rechargeQRGenerating.value = true
|
||||
try {
|
||||
rechargeQRCodeURL.value = await QRCode.toDataURL(payURL, {
|
||||
width: 220,
|
||||
margin: 1,
|
||||
errorCorrectionLevel: 'M',
|
||||
color: {
|
||||
dark: '#111827',
|
||||
light: '#ffffff',
|
||||
},
|
||||
})
|
||||
} catch {
|
||||
ElMessage.error('二维码生成失败')
|
||||
} finally {
|
||||
rechargeQRGenerating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDevRecharge() {
|
||||
if (!isDev) return
|
||||
if (devRechargeAmount.value <= 0) {
|
||||
ElMessage.warning('请输入充值金额')
|
||||
return
|
||||
}
|
||||
devRecharging.value = true
|
||||
try {
|
||||
const payment = await startWalletRechargePayment(devRechargeAmount.value)
|
||||
activeRechargePayment.value = payment
|
||||
rechargeDialogVisible.value = true
|
||||
await renderRechargeQRCode(payment)
|
||||
if (payment.paid) {
|
||||
ElMessage.success('充值成功')
|
||||
await loadWallet()
|
||||
} else {
|
||||
ElMessage.success('测试支付单已创建')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '创建测试充值失败'))
|
||||
} finally {
|
||||
devRecharging.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function checkDevRechargeStatus() {
|
||||
if (!activeRechargePayment.value || checkingRecharge.value) return
|
||||
checkingRecharge.value = true
|
||||
try {
|
||||
const payment = await queryWalletRechargePayment(activeRechargePayment.value.id)
|
||||
activeRechargePayment.value = payment
|
||||
await renderRechargeQRCode(payment)
|
||||
if (payment.paid) {
|
||||
ElMessage.success('充值成功')
|
||||
rechargeDialogVisible.value = false
|
||||
await loadWallet()
|
||||
} else {
|
||||
ElMessage.info('支付暂未完成')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '查询支付状态失败'))
|
||||
} finally {
|
||||
checkingRecharge.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openRechargePayURL() {
|
||||
const payURL = rechargePayURL()
|
||||
if (!payURL) return
|
||||
window.open(payURL, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
function readError(error: unknown, fallback: string) {
|
||||
if (typeof error === 'object' && error && 'response' in error) {
|
||||
const response = (error as { response?: { data?: { message?: string } } }).response
|
||||
return response?.data?.message || fallback
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
function formatMoney(value: number) {
|
||||
return `¥${Number(value || 0).toFixed(2)}`
|
||||
}
|
||||
@@ -156,6 +259,35 @@ function amountPrefix(direction: string) {
|
||||
</div>
|
||||
|
||||
<div class="wallet-workspace">
|
||||
<section v-if="isDev" class="recharge-panel">
|
||||
<div class="panel-title">
|
||||
<div class="panel-title-icon is-orange">
|
||||
<el-icon><Coin /></el-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h2>测试充值</h2>
|
||||
<p>开发环境可见,用于快速验证当前支付渠道配置。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-amounts">
|
||||
<button
|
||||
v-for="amount in quickRechargeAmounts"
|
||||
:key="amount"
|
||||
type="button"
|
||||
:class="{ active: devRechargeAmount === amount }"
|
||||
@click="selectDevRechargeAmount(amount)"
|
||||
>
|
||||
{{ formatMoney(amount) }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="recharge-action-row">
|
||||
<el-input-number v-model="devRechargeAmount" :min="0.01" :precision="2" :step="1" />
|
||||
<el-button type="primary" :icon="Coin" :loading="devRecharging" @click="handleDevRecharge">
|
||||
创建测试支付
|
||||
</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ledger-summary-card">
|
||||
<div class="panel-title">
|
||||
<div class="panel-title-icon is-blue">
|
||||
@@ -216,6 +348,38 @@ function amountPrefix(direction: string) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="rechargeDialogVisible" title="测试充值支付" width="520px">
|
||||
<div class="recharge-cashier">
|
||||
<div class="cashier-amount">
|
||||
<span>支付金额</span>
|
||||
<strong>{{ activeRechargePayment ? formatMoney(activeRechargePayment.amount_cent / 100) : formatMoney(devRechargeAmount) }}</strong>
|
||||
</div>
|
||||
<div v-if="rechargePayURL()" class="cashier-qr">
|
||||
<div class="qr-box">
|
||||
<el-icon v-if="rechargeQRGenerating" class="qr-loading"><Loading /></el-icon>
|
||||
<img v-else-if="rechargeQRCodeURL" :src="rechargeQRCodeURL" alt="测试充值支付二维码" />
|
||||
<span v-else>支付链接已生成</span>
|
||||
</div>
|
||||
<el-button text type="primary" @click="openRechargePayURL">打开支付链接</el-button>
|
||||
</div>
|
||||
<el-alert
|
||||
v-else
|
||||
title="支付单已创建,但渠道未返回可展示的支付链接"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<div class="cashier-meta" v-if="activeRechargePayment">
|
||||
<span>支付单号:{{ activeRechargePayment.payment_no }}</span>
|
||||
<span>渠道:{{ activeRechargePayment.provider }}</span>
|
||||
<span>状态:{{ activeRechargePayment.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="rechargeDialogVisible = false">关闭</el-button>
|
||||
<el-button type="primary" :loading="checkingRecharge" @click="checkDevRechargeStatus">查询支付状态</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -394,6 +558,11 @@ function amountPrefix(direction: string) {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.panel-title-icon.is-orange {
|
||||
background: #fff4ec;
|
||||
color: #ff6b00;
|
||||
}
|
||||
|
||||
.panel-title h2 {
|
||||
margin: 0;
|
||||
color: #111827;
|
||||
@@ -437,6 +606,70 @@ function amountPrefix(direction: string) {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.recharge-cashier {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.cashier-amount {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid #e6eaf2;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.cashier-amount span,
|
||||
.cashier-meta {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.cashier-amount strong {
|
||||
color: #111a44;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.cashier-qr {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.qr-box {
|
||||
display: grid;
|
||||
width: 236px;
|
||||
height: 236px;
|
||||
place-items: center;
|
||||
border: 1px solid #e6eaf2;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.qr-box img {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
}
|
||||
|
||||
.qr-loading {
|
||||
color: #64748b;
|
||||
font-size: 28px;
|
||||
animation: spin 0.9s linear infinite;
|
||||
}
|
||||
|
||||
.cashier-meta {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.wallet-ledger-table {
|
||||
overflow-x: auto;
|
||||
border: 1px solid #e6eaf2;
|
||||
|
||||
Reference in New Issue
Block a user