优化
This commit is contained in:
@@ -53,7 +53,7 @@
|
|||||||
| 装柜完成后的退税报关资料 | `src/modules/first-leg/views/FirstLegOrdersView.vue` 的“报关资料”、`components/CustomsDocumentsDialog.vue`、`api/customs.ts` | 只显示登录客户在同柜的退税入仓码;报关开始前可上传、替换、调整同一资料覆盖的一个或多个入仓码及删除误传,全部写入携带幂等键和版本;开始后只读冻结资料,整柜完成后才能下载平台回传的一对一完成文件。Backend `firstleg/customs/container/`、`customs-v1.yaml`,二进制归 `file/` |
|
| 装柜完成后的退税报关资料 | `src/modules/first-leg/views/FirstLegOrdersView.vue` 的“报关资料”、`components/CustomsDocumentsDialog.vue`、`api/customs.ts` | 只显示登录客户在同柜的退税入仓码;报关开始前可上传、替换、调整同一资料覆盖的一个或多个入仓码及删除误传,全部写入携带幂等键和版本;开始后只读冻结资料,整柜完成后才能下载平台回传的一对一完成文件。Backend `firstleg/customs/container/`、`customs-v1.yaml`,二进制归 `file/` |
|
||||||
| 统一订单展示 | `src/modules/order-management/` | `src/modules/first-leg/` 与对应后端订单契约 |
|
| 统一订单展示 | `src/modules/order-management/` | `src/modules/first-leg/` 与对应后端订单契约 |
|
||||||
| 客户财务账单、入仓码联查、收款、未收余额与客户确认 | `src/modules/finance/`;确认与明细入口 `views/FinanceBillsView.vue`、`api/finance.ts`,入仓码通过 `src/modules/first-leg/public.ts` 复用 `InboundCodeAssociationDialog` | Backend `finance/`;账单行只显示服务端 `inboundCode` 并按登录客户查询业务关系,缺失显示“—”,不得回退内部 ID。收款方式由服务端枚举映射中文。出账或已出账调整后为 `PENDING_CONFIRMATION`,客户确认提交登录成员、`expectedVersion` 和幂等键 |
|
| 客户财务账单、入仓码联查、收款、未收余额与客户确认 | `src/modules/finance/`;确认与明细入口 `views/FinanceBillsView.vue`、`api/finance.ts`,入仓码通过 `src/modules/first-leg/public.ts` 复用 `InboundCodeAssociationDialog` | Backend `finance/`;账单行只显示服务端 `inboundCode` 并按登录客户查询业务关系,缺失显示“—”,不得回退内部 ID。收款方式由服务端枚举映射中文。出账或已出账调整后为 `PENDING_CONFIRMATION`,客户确认提交登录成员、`expectedVersion` 和幂等键 |
|
||||||
| 客户资金账户、授信与充值申请 | `src/modules/finance/views/FinanceAccountView.vue`、`api/account.ts` | 显示当前企业 CNY 实际余额、冻结金额、授信额度、授信有效期、已用授信、冻结授信、总可用余额和不可变流水;数据加载前显示“—”,不得先伪装为零。授信到期日当天仍有效,次日已用授信并入实际余额,负数实际余额会阻止提交新订单。充值只展示服务端启用的微信/支付宝静态收款码,客户上传 JPG/PNG 付款截图后进入待审核。客户范围只取登录态,无收款码时禁止提交,不模拟支付成功 |
|
| 客户资金账户、授信与充值申请 | `src/modules/finance/views/FinanceAccountView.vue`、`api/account.ts` | 显示当前企业 CNY 实际余额、冻结金额、授信额度、授信有效期、已用授信、冻结授信、总可用余额和不可变流水;数据加载前显示“—”,不得先伪装为零。授信到期日当天仍有效,次日已用授信并入实际余额,负数实际余额会阻止提交新订单。充值只展示服务端启用的平台统一静态收款码,客户上传 JPG/PNG 付款截图后进入待审核。客户范围只取登录态,无收款码时禁止提交,不模拟支付成功 |
|
||||||
| 海外采购、美国本土卡与美本土散板 | `src/modules/cross-border/routes.ts`、`components/BusinessOrdersView.vue`、`api/businessOrders.ts` | 三个路由均为 `active`,客户范围只取 Token;采购创建的 `customerName` 只取当前认证会话企业名称,不允许自由输入或冒用其他客户。Backend `lastleg-service/` 的 `procurement/`、`trucking/`、`looseboard/` 客户接口 |
|
| 海外采购、美国本土卡与美本土散板 | `src/modules/cross-border/routes.ts`、`components/BusinessOrdersView.vue`、`api/businessOrders.ts` | 三个路由均为 `active`,客户范围只取 Token;采购创建的 `customerName` 只取当前认证会话企业名称,不允许自由输入或冒用其他客户。Backend `lastleg-service/` 的 `procurement/`、`trucking/`、`looseboard/` 客户接口 |
|
||||||
| 海外仓入库订单 | `src/modules/order-management/routes.ts`、`views/InboundOrdersView.vue`、`api/inboundPlans.ts` | `orders/inbound` 为只读列表/详情,权限 `merchant:warehouse:inbound:list`;不展示或提交内部 ID |
|
| 海外仓入库订单 | `src/modules/order-management/routes.ts`、`views/InboundOrdersView.vue`、`api/inboundPlans.ts` | `orders/inbound` 为只读列表/详情,权限 `merchant:warehouse:inbound:list`;不展示或提交内部 ID |
|
||||||
| 海外仓贴标、仓储转运、货物废弃 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 三个路由均为 `active`;退货预报另见本节独立入口;贴标申请先选国外中转仓和有库存历史入仓单,换标类型为只换 SKU、只换箱唛、SKU+箱唛。涉及箱唛换标时,整张贴标申请只选择一个 FBA 目的仓,全部箱级明细统一提交该目的仓,模板和明细不再逐行选择目的仓。箱唛换标明细一行固定代表一箱,箱数不可编辑,提交也固定为 1。货物明细外层始终提供“SKU 关联”“箱号关联”“SKU 混箱换标”三套模板的下载、上传,不随换标类型切换;上传只进入待导入关系区,客户可勾选部分或全选后导入本次明细,只有进入明细的库存才随申请提交和冻结。明细也支持手动新增编辑,组合换标以仓库唛头连接旧/新 SKU 和新 FBA 箱唛关系。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 ID |
|
| 海外仓贴标、仓储转运、货物废弃 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 三个路由均为 `active`;退货预报另见本节独立入口;贴标申请先选国外中转仓和有库存历史入仓单,换标类型为只换 SKU、只换箱唛、SKU+箱唛。涉及箱唛换标时,整张贴标申请只选择一个 FBA 目的仓,全部箱级明细统一提交该目的仓,模板和明细不再逐行选择目的仓。箱唛换标明细一行固定代表一箱,箱数不可编辑,提交也固定为 1。货物明细外层始终提供“SKU 关联”“箱号关联”“SKU 混箱换标”三套模板的下载、上传,不随换标类型切换;上传只进入待导入关系区,客户可勾选部分或全选后导入本次明细,只有进入明细的库存才随申请提交和冻结。明细也支持手动新增编辑,组合换标以仓库唛头连接旧/新 SKU 和新 FBA 箱唛关系。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 ID |
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -78,11 +78,6 @@ export interface RechargeRequest {
|
|||||||
reviewedAt: string | null
|
reviewedAt: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RechargeChannel {
|
|
||||||
enabled: boolean
|
|
||||||
qrCodeUrl: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getFinanceAccount() {
|
export function getFinanceAccount() {
|
||||||
return apiRequest<FinanceAccount>({ method: 'GET', url: '/client/finance/v1/account' })
|
return apiRequest<FinanceAccount>({ method: 'GET', url: '/client/finance/v1/account' })
|
||||||
}
|
}
|
||||||
@@ -105,10 +100,6 @@ export async function getRechargeRequests(page: number, size: number) {
|
|||||||
return normalizePageResult(result)
|
return normalizePageResult(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRechargeChannel() {
|
|
||||||
return apiRequest<RechargeChannel>({ method: 'GET', url: '/client/finance/v1/recharge-channel' })
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createRechargeRequest(amount: string, file: File, idempotencyKey: string) {
|
export function createRechargeRequest(amount: string, file: File, idempotencyKey: string) {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('amount', amount)
|
formData.append('amount', amount)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const routesSource = readFileSync(new URL('../routes.ts', import.meta.url), 'utf
|
|||||||
const permissionsSource = readFileSync(new URL('../../tenant-management/contracts.ts', import.meta.url), 'utf8')
|
const permissionsSource = readFileSync(new URL('../../tenant-management/contracts.ts', import.meta.url), 'utf8')
|
||||||
|
|
||||||
test('uses only authenticated client finance account endpoints without customer scope', () => {
|
test('uses only authenticated client finance account endpoints without customer scope', () => {
|
||||||
for (const path of ['/client/finance/v1/account', '/client/finance/v1/account/ledger', '/client/finance/v1/recharge-requests', '/client/finance/v1/recharge-channel']) {
|
for (const path of ['/client/finance/v1/account', '/client/finance/v1/account/ledger', '/client/finance/v1/recharge-requests']) {
|
||||||
assert.match(apiSource, new RegExp(path.replaceAll('/', '\\/')))
|
assert.match(apiSource, new RegExp(path.replaceAll('/', '\\/')))
|
||||||
}
|
}
|
||||||
assert.match(apiSource, /headers: \{ 'Idempotency-Key': idempotencyKey \}/)
|
assert.match(apiSource, /headers: \{ 'Idempotency-Key': idempotencyKey \}/)
|
||||||
@@ -54,8 +54,9 @@ test('renders server balances, ledger and recharge states without internal ident
|
|||||||
|
|
||||||
test('matches the frozen recharge workflow and preserves an idempotency key for failed retries', () => {
|
test('matches the frozen recharge workflow and preserves an idempotency key for failed retries', () => {
|
||||||
assert.match(viewSource, /title="账户充值" width="760px"/)
|
assert.match(viewSource, /title="账户充值" width="760px"/)
|
||||||
for (const amount of ['50.00', '100.00', '200.00', '500.00']) assert.match(viewSource, new RegExp(amount.replace('.', '\\.')))
|
for (const amount of ['1000', '5000', '20000', '50000']) assert.match(viewSource, new RegExp(amount))
|
||||||
assert.match(viewSource, /rechargeChannel\.value\?\.enabled && rechargeChannel\.value\.qrCodeUrl/)
|
assert.match(viewSource, /自定义/)
|
||||||
|
assert.match(viewSource, /src="\/customer-recharge-qr\.png"/)
|
||||||
assert.match(viewSource, /alt="平台收款二维码"/)
|
assert.match(viewSource, /alt="平台收款二维码"/)
|
||||||
assert.match(viewSource, /请扫描平台收款二维码,付款后上传截图。/)
|
assert.match(viewSource, /请扫描平台收款二维码,付款后上传截图。/)
|
||||||
assert.match(viewSource, /accept="\.jpg,\.jpeg,\.png,image\/jpeg,image\/png"/)
|
assert.match(viewSource, /accept="\.jpg,\.jpeg,\.png,image\/jpeg,image\/png"/)
|
||||||
|
|||||||
@@ -9,12 +9,10 @@ import {
|
|||||||
createRechargeRequest,
|
createRechargeRequest,
|
||||||
getAccountLedger,
|
getAccountLedger,
|
||||||
getFinanceAccount,
|
getFinanceAccount,
|
||||||
getRechargeChannel,
|
|
||||||
getRechargeRequests,
|
getRechargeRequests,
|
||||||
type AccountChangeType,
|
type AccountChangeType,
|
||||||
type AccountLedgerEntry,
|
type AccountLedgerEntry,
|
||||||
type FinanceAccount,
|
type FinanceAccount,
|
||||||
type RechargeChannel,
|
|
||||||
type RechargeRequest,
|
type RechargeRequest,
|
||||||
type RechargeStatus,
|
type RechargeStatus,
|
||||||
} from '../api/account'
|
} from '../api/account'
|
||||||
@@ -23,10 +21,9 @@ const auth = useAuthStore()
|
|||||||
const account = ref<FinanceAccount | null>(null)
|
const account = ref<FinanceAccount | null>(null)
|
||||||
const ledger = ref<AccountLedgerEntry[]>([])
|
const ledger = ref<AccountLedgerEntry[]>([])
|
||||||
const rechargeRequests = ref<RechargeRequest[]>([])
|
const rechargeRequests = ref<RechargeRequest[]>([])
|
||||||
const rechargeChannel = ref<RechargeChannel | null>(null)
|
|
||||||
const pageLoading = ref(false)
|
const pageLoading = ref(false)
|
||||||
const pageError = ref('')
|
const pageError = ref('')
|
||||||
const channelsError = ref('')
|
const qrCodeLoadError = ref(false)
|
||||||
const ledgerPage = ref(1)
|
const ledgerPage = ref(1)
|
||||||
const ledgerTotal = ref(0)
|
const ledgerTotal = ref(0)
|
||||||
const ledgerPageSize = ref(10)
|
const ledgerPageSize = ref(10)
|
||||||
@@ -40,10 +37,11 @@ const voucherFile = ref<File | null>(null)
|
|||||||
const voucherUpload = ref<UploadInstance>()
|
const voucherUpload = ref<UploadInstance>()
|
||||||
let pendingIdempotencyKey = ''
|
let pendingIdempotencyKey = ''
|
||||||
|
|
||||||
const form = reactive({ amount: '100.00' })
|
const form = reactive({ amount: '1000' })
|
||||||
const quickAmounts = ['50.00', '100.00', '200.00', '500.00']
|
const quickAmounts = ['1000', '5000', '20000', '50000']
|
||||||
|
const amountMode = ref<'preset' | 'custom'>('preset')
|
||||||
const canRecharge = computed(() => auth.hasPermission('merchant:finance:recharge:create'))
|
const canRecharge = computed(() => auth.hasPermission('merchant:finance:recharge:create'))
|
||||||
const channelReady = computed(() => Boolean(rechargeChannel.value?.enabled && rechargeChannel.value.qrCodeUrl))
|
const channelReady = computed(() => !qrCodeLoadError.value)
|
||||||
const amountValid = computed(() => {
|
const amountValid = computed(() => {
|
||||||
const value = Number(form.amount)
|
const value = Number(form.amount)
|
||||||
return Number.isFinite(value) && value > 0 && /^\d+(?:\.\d{1,2})?$/.test(form.amount.trim())
|
return Number.isFinite(value) && value > 0 && /^\d+(?:\.\d{1,2})?$/.test(form.amount.trim())
|
||||||
@@ -106,7 +104,7 @@ function amountClass(amount: string) {
|
|||||||
async function loadAccountData() {
|
async function loadAccountData() {
|
||||||
pageLoading.value = true
|
pageLoading.value = true
|
||||||
pageError.value = ''
|
pageError.value = ''
|
||||||
channelsError.value = ''
|
qrCodeLoadError.value = false
|
||||||
const requests = [getFinanceAccount(), getAccountLedger(ledgerPage.value, ledgerPageSize.value), getRechargeRequests(rechargePage.value, rechargePageSize.value)] as const
|
const requests = [getFinanceAccount(), getAccountLedger(ledgerPage.value, ledgerPageSize.value), getRechargeRequests(rechargePage.value, rechargePageSize.value)] as const
|
||||||
try {
|
try {
|
||||||
const [accountResult, ledgerResult, rechargeResult] = await Promise.all(requests)
|
const [accountResult, ledgerResult, rechargeResult] = await Promise.all(requests)
|
||||||
@@ -125,14 +123,6 @@ async function loadAccountData() {
|
|||||||
} finally {
|
} finally {
|
||||||
pageLoading.value = false
|
pageLoading.value = false
|
||||||
}
|
}
|
||||||
if (canRecharge.value) {
|
|
||||||
try {
|
|
||||||
rechargeChannel.value = await getRechargeChannel()
|
|
||||||
} catch (error) {
|
|
||||||
rechargeChannel.value = null
|
|
||||||
channelsError.value = messageOf(error, '收款二维码加载失败,请稍后重试')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeLedgerPage(nextPage: number) {
|
async function changeLedgerPage(nextPage: number) {
|
||||||
@@ -157,13 +147,26 @@ async function changeRechargePage(nextPage: number) {
|
|||||||
|
|
||||||
function openRecharge() {
|
function openRecharge() {
|
||||||
submitError.value = ''
|
submitError.value = ''
|
||||||
|
qrCodeLoadError.value = false
|
||||||
|
amountMode.value = 'preset'
|
||||||
|
form.amount = '1000'
|
||||||
rechargeVisible.value = true
|
rechargeVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleQrCodeError() {
|
||||||
|
qrCodeLoadError.value = true
|
||||||
|
}
|
||||||
|
|
||||||
function chooseAmount(amount: string) {
|
function chooseAmount(amount: string) {
|
||||||
|
amountMode.value = 'preset'
|
||||||
form.amount = amount
|
form.amount = amount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function chooseCustomAmount() {
|
||||||
|
amountMode.value = 'custom'
|
||||||
|
if (quickAmounts.includes(form.amount)) form.amount = ''
|
||||||
|
}
|
||||||
|
|
||||||
function handleVoucherChange(uploadFile: UploadFile, _uploadFiles: UploadFiles) {
|
function handleVoucherChange(uploadFile: UploadFile, _uploadFiles: UploadFiles) {
|
||||||
const file = uploadFile.raw
|
const file = uploadFile.raw
|
||||||
if (!file) return
|
if (!file) return
|
||||||
@@ -276,16 +279,17 @@ onMounted(() => { void loadAccountData() })
|
|||||||
<el-dialog v-model="rechargeVisible" title="账户充值" width="760px" :close-on-click-modal="false" :close-on-press-escape="!submitting" :show-close="!submitting" class="recharge-dialog">
|
<el-dialog v-model="rechargeVisible" title="账户充值" width="760px" :close-on-click-modal="false" :close-on-press-escape="!submitting" :show-close="!submitting" class="recharge-dialog">
|
||||||
<div class="recharge-form">
|
<div class="recharge-form">
|
||||||
<label class="field-label" for="recharge-amount">充值金额</label>
|
<label class="field-label" for="recharge-amount">充值金额</label>
|
||||||
<el-input id="recharge-amount" v-model="form.amount" size="large" inputmode="decimal" placeholder="请输入充值金额" aria-label="充值金额"><template #prefix>¥</template></el-input>
|
<el-input v-if="amountMode === 'custom'" id="recharge-amount" v-model="form.amount" size="large" inputmode="decimal" placeholder="请输入充值金额" aria-label="充值金额"><template #prefix>¥</template></el-input>
|
||||||
<div class="quick-amounts" aria-label="快捷充值金额">
|
<div class="quick-amounts" aria-label="快捷充值金额">
|
||||||
<button v-for="amount in quickAmounts" :key="amount" type="button" :class="{ active: form.amount === amount }" @click="chooseAmount(amount)">¥{{ Number(amount) }}</button>
|
<button v-for="amount in quickAmounts" :key="amount" type="button" :class="{ active: amountMode === 'preset' && form.amount === amount }" @click="chooseAmount(amount)">¥{{ Number(amount).toLocaleString() }}</button>
|
||||||
|
<button type="button" :class="{ active: amountMode === 'custom' }" @click="chooseCustomAmount">自定义</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="channelsError" class="channel-warning" role="alert">{{ channelsError }}</div>
|
<div v-if="channelReady" class="qr-panel">
|
||||||
<div v-else-if="channelReady" class="qr-panel">
|
<img src="/customer-recharge-qr.png" alt="平台收款二维码" @error="handleQrCodeError">
|
||||||
<img :src="rechargeChannel?.qrCodeUrl || ''" alt="平台收款二维码">
|
|
||||||
<div><strong>扫码付款</strong><p>请扫描平台收款二维码,付款后上传截图。</p></div>
|
<div><strong>扫码付款</strong><p>请扫描平台收款二维码,付款后上传截图。</p></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="qrCodeLoadError" class="channel-warning" role="alert">平台收款二维码资源不可用,请联系管理员更新二维码</div>
|
||||||
<div v-else class="channel-warning" role="status">收款二维码尚未配置,暂不能提交充值申请。</div>
|
<div v-else class="channel-warning" role="status">收款二维码尚未配置,暂不能提交充值申请。</div>
|
||||||
|
|
||||||
<div class="voucher-upload">
|
<div class="voucher-upload">
|
||||||
@@ -317,7 +321,7 @@ onMounted(() => { void loadAccountData() })
|
|||||||
.amount-positive { color: #078a72; }.amount-negative { color: #b42318; }
|
.amount-positive { color: #078a72; }.amount-negative { color: #b42318; }
|
||||||
.account-state { display: flex; min-height: 240px; align-items: center; justify-content: center; flex-direction: column; gap: 12px; border-radius: 8px; }.account-state--error { border: 1px solid #f5c2bd; background: #fff8f7; color: #b42318; }.account-state p { margin: 0; }
|
.account-state { display: flex; min-height: 240px; align-items: center; justify-content: center; flex-direction: column; gap: 12px; border-radius: 8px; }.account-state--error { border: 1px solid #f5c2bd; background: #fff8f7; color: #b42318; }.account-state p { margin: 0; }
|
||||||
.recharge-form { display: flex; flex-direction: column; gap: 16px; }.field-label { color: #4e6075; font-size: 14px; font-weight: 650; }
|
.recharge-form { display: flex; flex-direction: column; gap: 16px; }.field-label { color: #4e6075; font-size: 14px; font-weight: 650; }
|
||||||
.quick-amounts { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }.quick-amounts button { min-height: 54px; border: 1px solid #dbe3ed; border-radius: 10px; background: #f8fafc; color: #506077; cursor: pointer; font: inherit; }.quick-amounts button.active { border: 2px solid #1671c9; background: #eef8f7; color: #1467b8; font-weight: 700; }
|
.quick-amounts { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; }.quick-amounts button { min-height: 54px; border: 1px solid #dbe3ed; border-radius: 10px; background: #f8fafc; color: #506077; cursor: pointer; font: inherit; }.quick-amounts button.active { border: 2px solid #1671c9; background: #eef8f7; color: #1467b8; font-weight: 700; }
|
||||||
.qr-panel { display: grid; grid-template-columns: 210px minmax(0, 1fr); align-items: center; gap: 24px; padding: 20px; border: 1px solid #dbe3ed; border-radius: 12px; background: #f8fafc; }.qr-panel img { width: 210px; height: 210px; border: 1px solid #dae3ed; border-radius: 10px; background: #fff; object-fit: contain; }.qr-panel strong { color: var(--navy); font-size: 17px; }.qr-panel p { margin: 8px 0 0; color: #62758d; }
|
.qr-panel { display: grid; grid-template-columns: 210px minmax(0, 1fr); align-items: center; gap: 24px; padding: 20px; border: 1px solid #dbe3ed; border-radius: 12px; background: #f8fafc; }.qr-panel img { width: 210px; height: 210px; border: 1px solid #dae3ed; border-radius: 10px; background: #fff; object-fit: contain; }.qr-panel strong { color: var(--navy); font-size: 17px; }.qr-panel p { margin: 8px 0 0; color: #62758d; }
|
||||||
.channel-warning { padding: 24px; border: 1px solid #f0d39b; border-radius: 10px; background: #fff9ed; color: #8b5b12; text-align: center; }.voucher-upload { display: grid; gap: 10px; }.voucher-upload :deep(.el-button) { display: inline-flex; align-items: center; gap: 6px; }.submit-error { margin: 0; color: #b42318; font-size: 13px; }.dialog-actions { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }.dialog-actions .el-button { min-height: 46px; margin: 0; }
|
.channel-warning { padding: 24px; border: 1px solid #f0d39b; border-radius: 10px; background: #fff9ed; color: #8b5b12; text-align: center; }.voucher-upload { display: grid; gap: 10px; }.voucher-upload :deep(.el-button) { display: inline-flex; align-items: center; gap: 6px; }.submit-error { margin: 0; color: #b42318; font-size: 13px; }.dialog-actions { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }.dialog-actions .el-button { min-height: 46px; margin: 0; }
|
||||||
@media (max-width: 1200px) { .balance-summary { grid-template-columns: repeat(3, minmax(0, 1fr)); }.balance-summary article { border-bottom: 1px solid var(--client-border); }.balance-summary article:nth-child(3n) { border-right: 0; }.balance-summary article:nth-last-child(-n + 3) { border-bottom: 0; } }
|
@media (max-width: 1200px) { .balance-summary { grid-template-columns: repeat(3, minmax(0, 1fr)); }.balance-summary article { border-bottom: 1px solid var(--client-border); }.balance-summary article:nth-child(3n) { border-right: 0; }.balance-summary article:nth-last-child(-n + 3) { border-bottom: 0; } }
|
||||||
|
|||||||
Reference in New Issue
Block a user