1.配置新版支付模块-菜单和接口都已重构

2.优化充值提现页面
3.菜单翻译问题
4.备份数据库
This commit is contained in:
2026-04-30 11:37:46 +08:00
parent e8c2b9d345
commit c7fc754573
23 changed files with 4042 additions and 400 deletions

View File

@@ -1,16 +1,27 @@
export default {
desc: 'Mobile pay & receipt settings: platform coin labels, currencies and rates, deposit pay channels (on/off and sort; all tiers are supported automatically), withdraw banks, limits, copy, and required withdraw fields.',
desc: 'Mobile pay & receipt settings: platform coin labels, currencies and rates, deposit pay channels (on/off and sort; supported currencies per channel), withdraw banks, limits and copy. DDPay-required fields follow the gateway doc and API contracts—not toggles on this page.',
btn_save: 'Save',
btn_add_row: 'Add row',
sec_platform: 'Platform coin labels',
tab_cashier: 'Deposit/Withdraw config',
tab_tiers: 'Deposit tiers',
tab_deposit_banks: 'Deposit banks',
tab_withdraw_banks: 'Withdraw banks',
platform_label_zh: 'Label (Chinese)',
platform_label_en: 'Label (English)',
sec_currencies: 'Currencies (deposit/withdraw selectors)',
sec_deposit_channels: 'Deposit pay channels',
deposit_channels_hint: 'Display names come from the registry; here you only set enabled state and sort order. All enabled channels automatically support all deposit tiers.',
deposit_channels_hint: 'Only the DDPay channel is built-in. Set enabled state, sort and supported fiat currencies per row.',
currency_rates_hint: 'Deposit rate: platform coins credited per 1 fiat paid. Withdraw rate: platform coins needed per 1 fiat redeemed (e.g. 100 ⇒ 100 coins = 1 fiat unit).',
err_dup_code: 'Duplicate currency codes are not allowed.',
sec_banks: 'Withdraw bank codes',
sec_deposit_banks: 'Deposit banks by currency',
deposit_banks_hint: 'Used for DDPay deposit bank options and client display, maintained by currency_code.',
sec_withdraw_banks: 'Withdraw banks by currency',
withdraw_banks_hint: 'Used for withdrawCreate bank_code mapping and client display, maintained by currency_code.',
sec_deposit_tiers: 'Deposit tiers (embedded)',
sec_tabbed_config: 'Banks & deposit tiers (tabs)',
deposit_tiers_hint: 'Moved into this page. When deleting currencies, related tiers will be previewed and removed together after confirmation.',
btn_save_tiers: 'Save tiers',
sec_limits: 'Minimum withdraw (fiat amount; match your copy currency)',
min_ewallet: 'E-wallet minimum',
min_bank: 'Bank minimum',
@@ -24,11 +35,17 @@ export default {
processing_en: 'Processing note (EN)',
fee_note_zh: 'Fee note (ZH)',
fee_note_en: 'Fee note (EN)',
sec_fields: 'Withdraw form (required)',
field_cardholder: 'Cardholder name',
field_bank_account: 'Bank account',
field_email: 'Payee email',
field_mobile: 'Payee mobile',
sec_ddpay_spec: 'DDPay integration (read-only)',
ddpay_spec_intro:
'Withdrawals use DDPay Payout (mobile: withdrawCreate). Deposits with channel ddpay use depositCreate. Below is a short field summary; see the DDPay doc and the mobile API draft in the repo for details.',
ddpay_spec_li_withdraw:
'Withdraw (required): withdraw_coin, receive_type=bank, receive_account, receiver_name (as registered with the bank), bank_code (must match a code from “Withdraw banks by currency” on this page), idempotency_key; bank_branch optional (server sends N/A if omitted).',
ddpay_spec_li_bank_table:
'English bank name maps to DDPay bank[name] and must match the official full bank names, or payout may be rejected.',
ddpay_spec_li_deposit:
'Deposit (ddpay): payment_type (01=FPX, 02=duitnow, 03=ewallet), payer_name, payer_bank_name (full name per official deposit bank list).',
ddpay_spec_li_doc:
'Gateway doc: docs/DDPay Payment Gateway_v1.1.3_zh.md (and PDF); HTTPS and callback rules follow the vendor spec.',
col_code: 'Code',
col_label_zh: 'Name (ZH)',
col_label_en: 'Name (EN)',
@@ -36,6 +53,17 @@ export default {
col_deposit_rate: 'Deposit rate',
col_withdraw_rate: 'Withdraw rate',
col_bank_code: 'Bank code',
col_currency_code: 'Currency',
col_tier_id: 'Tier ID',
col_title_zh: 'Title (ZH)',
col_title_en: 'Title (EN)',
col_pay_amount: 'Pay amount',
col_amount: 'Platform amount',
col_bonus_amount: 'Bonus',
col_tier_sort: 'Sort',
col_tier_status: 'Enabled',
msg_delete_currency_prune_tiers: 'Deleting currency {currency} will also remove {count} related deposit tier(s). Continue?',
msg_affected_tier_ids: 'Affected tiers',
col_name_zh: 'Bank (ZH)',
col_name_en: 'Bank (EN)',
ph_ratio: 'e.g. 100',
@@ -45,4 +73,5 @@ export default {
ch_display_name: 'Display name',
ch_sort: 'Sort',
ch_status: 'Enabled',
ch_currency_codes: 'Supported currencies',
}

View File

@@ -1,16 +1,27 @@
export default {
desc: '配置移动端支付与收款展示:平台币名称、货币与汇率、充值支付渠道(开关/排序,自动兼容全部档位)、提现银行、最低限额文案提现表单字段。',
desc: '配置移动端支付与收款展示:平台币名称、货币与汇率、充值支付渠道(开关/排序,以及按渠道配置支持币种)、提现银行、最低限额文案提现/充值与 DDPay 对接的必填字段由网关文档与接口约定,不在此页配置。',
btn_save: '保存',
btn_add_row: '新增一行',
sec_platform: '平台币展示名',
tab_cashier: '充值/提现配置',
tab_tiers: '充值档位配置',
tab_deposit_banks: '充值银行配置',
tab_withdraw_banks: '提现银行配置',
platform_label_zh: '名称(中文)',
platform_label_en: '名称(英文)',
sec_currencies: '货币列表(充值/提现货币下拉)',
sec_deposit_channels: '充值支付渠道',
deposit_channels_hint: '展示名由环境注册表决定此处维护启用状态排序;所有启用渠道自动兼容全部充值档位。',
deposit_channels_hint: '当前仅 DDPay 渠道。展示名由注册表决定此处维护启用状态排序与各渠道支持的法币币种。',
currency_rates_hint: '充值汇率:每支付 1 单位该货币到账的平台币;提现汇率:每兑换 1 单位该货币所需平台币(例 100 表示 100 平台币 = 1 单位)。',
err_dup_code: '货币代码不能重复,请检查后再保存。',
sec_banks: '提现支持银行代码',
sec_deposit_banks: '充值支持银行(按币种)',
deposit_banks_hint: '用于 DDPay 入金参数辅助与客户端展示,按 currency_code 维护可选银行。',
sec_withdraw_banks: '提现支持银行(按币种)',
withdraw_banks_hint: '用于 withdrawCreate 的 bank_code 映射与客户端展示,按 currency_code 维护。',
sec_deposit_tiers: '充值档位(内嵌维护)',
sec_tabbed_config: '银行与充值档位(标签页)',
deposit_tiers_hint: '已并入当前页面。删除币种时若存在关联档位,将提示并同步删除对应档位。',
btn_save_tiers: '保存档位',
sec_limits: '提现最低限额(法币金额,与文案中币种一致)',
min_ewallet: '电子钱包最低',
min_bank: '银行最低',
@@ -24,11 +35,17 @@ export default {
processing_en: '到账说明(英文)',
fee_note_zh: '手续费说明(中文)',
fee_note_en: '手续费说明(英文)',
sec_fields: '提现表单字段(必填',
field_cardholder: '持卡人姓名',
field_bank_account: '银行账号',
field_email: '收款邮箱',
field_mobile: '收款手机',
sec_ddpay_spec: 'DDPay 对接说明(只读',
ddpay_spec_intro:
'当前提现走 DDPay 出金Payout移动端调用 withdrawCreate充值渠道为 ddpay 时调用 depositCreate。下列为字段约定摘要详细以仓库内 DDPay 文档与《36字花-移动端接口设计草案》为准。',
ddpay_spec_li_withdraw:
'提现必填withdraw_coin、receive_type=bank、receive_account收款账号、receiver_name与银行登记一致、bank_code须与本页「提现支持银行按币种」中 code 一致、idempotency_keybank_branch 选填,不传则服务端按 N/A 提交。',
ddpay_spec_li_bank_table:
'「银行名(英文)」将映射为 DDPay 的 bank[name],请与 DDPay 官方银行全称列表一致,否则出金可能被拒。',
ddpay_spec_li_deposit:
'充值channel_code=ddpay必填payment_type官方取值 01=FPX、02=duitnow、03=ewallet、payer_name、payer_bank_name须与官方入金银行列表全称一致。',
ddpay_spec_li_doc:
'官方文档docs/DDPay Payment Gateway_v1.1.3_zh.md回调与 HTTPS 要求以文档为准。',
col_code: '代码',
col_label_zh: '中文名',
col_label_en: '英文名',
@@ -36,6 +53,17 @@ export default {
col_deposit_rate: '充值汇率',
col_withdraw_rate: '提现汇率',
col_bank_code: '银行代码',
col_currency_code: '币种',
col_tier_id: '档位ID',
col_title_zh: '标题(中文)',
col_title_en: '标题(英文)',
col_pay_amount: '支付金额',
col_amount: '平台币',
col_bonus_amount: '赠送',
col_tier_sort: '排序',
col_tier_status: '启用',
msg_delete_currency_prune_tiers: '将删除币种 {currency},并删除关联充值档位 {count} 条,是否继续?',
msg_affected_tier_ids: '影响档位',
col_name_zh: '银行名(中文)',
col_name_en: '银行名(英文)',
ph_ratio: '如 100',
@@ -46,4 +74,5 @@ export default {
ch_display_name: '展示名称',
ch_sort: '排序',
ch_status: '启用',
ch_currency_codes: '支持币种',
}

View File

@@ -10,9 +10,16 @@
</el-button>
</div>
<el-tabs v-model="activeMainTab" class="main-tabs">
<el-tab-pane :label="t('config.financeCashierConfig.tab_cashier')" name="cashier"></el-tab-pane>
<el-tab-pane :label="t('config.financeCashierConfig.tab_tiers')" name="tiers"></el-tab-pane>
<el-tab-pane :label="t('config.financeCashierConfig.tab_deposit_banks')" name="depositBanks"></el-tab-pane>
<el-tab-pane :label="t('config.financeCashierConfig.tab_withdraw_banks')" name="withdrawBanks"></el-tab-pane>
</el-tabs>
<el-scrollbar max-height="calc(100vh - 220px)">
<el-form v-loading="loading" label-width="160px" class="finance-cashier-form">
<el-card shadow="never" class="section-card">
<el-card v-if="activeMainTab === 'cashier'" shadow="never" class="section-card">
<template #header>{{ t('config.financeCashierConfig.sec_platform') }}</template>
<el-form-item :label="t('config.financeCashierConfig.platform_label_zh')">
<el-input v-model="form.platform_coin.label_zh" maxlength="32" class="w400" />
@@ -22,7 +29,7 @@
</el-form-item>
</el-card>
<el-card shadow="never" class="section-card">
<el-card v-if="activeMainTab === 'cashier'" shadow="never" class="section-card">
<template #header>
<span>{{ t('config.financeCashierConfig.sec_currencies') }}</span>
<el-button v-if="canSave" type="primary" link class="ml-2" @click="addCurrency">{{ t('config.financeCashierConfig.btn_add_row') }}</el-button>
@@ -66,13 +73,13 @@
</el-table-column>
<el-table-column :label="t('Operate')" width="90" align="center">
<template #default="{ $index }">
<el-button v-if="canSave" type="danger" link @click="removeRow(form.currencies, $index)">{{ t('Delete') }}</el-button>
<el-button v-if="canSave" type="danger" link @click="removeCurrencyRow($index)">{{ t('Delete') }}</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-card shadow="never" class="section-card">
<el-card v-if="activeMainTab === 'cashier'" shadow="never" class="section-card">
<template #header>{{ t('config.financeCashierConfig.sec_deposit_channels') }}</template>
<p class="hint">{{ t('config.financeCashierConfig.deposit_channels_hint') }}</p>
<el-table :data="form.channels" border stripe size="small">
@@ -103,15 +110,101 @@
<el-switch v-model="row.status" :active-value="1" :inactive-value="0" />
</template>
</el-table-column>
<el-table-column :label="t('config.financeCashierConfig.ch_currency_codes')" min-width="260">
<template #default="{ row }">
<div class="currency-codes-cell">
<el-checkbox-group v-model="row.currency_codes" size="small">
<el-checkbox
v-for="c in form.currencies"
:key="c.code"
:label="c.code"
>
{{ currencyLabel(c.code) }}
</el-checkbox>
</el-checkbox-group>
</div>
</template>
</el-table-column>
</el-table>
</el-card>
<el-card shadow="never" class="section-card">
<el-card v-if="activeMainTab === 'tiers'" shadow="never" class="section-card">
<template #header>
<span>{{ t('config.financeCashierConfig.sec_banks') }}</span>
<el-button v-if="canSave" type="primary" link class="ml-2" @click="addBank">{{ t('config.financeCashierConfig.btn_add_row') }}</el-button>
<span>{{ t('config.financeCashierConfig.sec_deposit_tiers') }}</span>
<el-button v-if="canSave" type="primary" link class="ml-2" @click="addTier">{{ t('config.financeCashierConfig.btn_add_row') }}</el-button>
<el-button v-if="canSave" type="success" link class="ml-2" :loading="tiersSaving" @click="saveTiers">{{ t('config.financeCashierConfig.btn_save_tiers') }}</el-button>
</template>
<el-table :data="form.withdraw_banks" border stripe size="small">
<p class="hint">{{ t('config.financeCashierConfig.deposit_tiers_hint') }}</p>
<el-table :data="tiers" border stripe size="small">
<el-table-column :label="t('config.financeCashierConfig.col_tier_id')" min-width="130">
<template #default="{ row }">
<el-input v-model="row.id" maxlength="32" />
</template>
</el-table-column>
<el-table-column :label="t('config.financeCashierConfig.col_title_zh')" min-width="140">
<template #default="{ row }">
<el-input v-model="row.title" maxlength="64" />
</template>
</el-table-column>
<el-table-column :label="t('config.financeCashierConfig.col_title_en')" min-width="140">
<template #default="{ row }">
<el-input v-model="row.title_en" maxlength="64" />
</template>
</el-table-column>
<el-table-column :label="t('config.financeCashierConfig.col_currency_code')" width="120">
<template #default="{ row }">
<el-select v-model="row.currency" class="w100p" filterable>
<el-option v-for="c in form.currencies" :key="c.code" :label="currencyLabel(c.code)" :value="c.code" />
</el-select>
</template>
</el-table-column>
<el-table-column :label="t('config.financeCashierConfig.col_pay_amount')" width="120">
<template #default="{ row }">
<el-input v-model="row.pay_amount" />
</template>
</el-table-column>
<el-table-column :label="t('config.financeCashierConfig.col_amount')" width="120">
<template #default="{ row }">
<el-input v-model="row.amount" />
</template>
</el-table-column>
<el-table-column :label="t('config.financeCashierConfig.col_bonus_amount')" width="120">
<template #default="{ row }">
<el-input v-model="row.bonus_amount" />
</template>
</el-table-column>
<el-table-column :label="t('config.financeCashierConfig.col_tier_sort')" width="120">
<template #default="{ row }">
<el-input-number v-model="row.sort" :min="0" :max="99999" :controls="false" class="w100p" />
</template>
</el-table-column>
<el-table-column :label="t('config.financeCashierConfig.col_tier_status')" width="90" align="center">
<template #default="{ row }">
<el-switch v-model="row.status" :active-value="1" :inactive-value="0" />
</template>
</el-table-column>
<el-table-column :label="t('Operate')" width="90" align="center">
<template #default="{ $index }">
<el-button v-if="canSave" type="danger" link @click="removeRow(tiers, $index)">{{ t('Delete') }}</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-card v-if="activeMainTab === 'depositBanks'" shadow="never" class="section-card">
<template #header>
<span>{{ t('config.financeCashierConfig.sec_deposit_banks') }}</span>
<el-button v-if="canSave" type="primary" link class="ml-2" @click="addDepositBank">{{ t('config.financeCashierConfig.btn_add_row') }}</el-button>
</template>
<p class="hint">{{ t('config.financeCashierConfig.deposit_banks_hint') }}</p>
<el-table :data="form.deposit_banks" border stripe size="small">
<el-table-column :label="t('config.financeCashierConfig.col_currency_code')" width="140">
<template #default="{ row }">
<el-select v-model="row.currency_code" class="w100p" filterable>
<el-option v-for="c in form.currencies" :key="c.code" :label="currencyLabel(c.code)" :value="c.code" />
</el-select>
</template>
</el-table-column>
<el-table-column :label="t('config.financeCashierConfig.col_bank_code')" width="140">
<template #default="{ row }">
<el-input v-model="row.code" maxlength="32" :placeholder="t('config.financeCashierConfig.ph_bank_code')" />
@@ -129,7 +222,49 @@
</el-table-column>
<el-table-column :label="t('config.financeCashierConfig.col_sort')" width="108">
<template #default="{ row }">
<el-input-number v-model="row.sort" :min="0" :max="99999" :controls="true" class="w100p" @change="resortBanksInPlace" />
<el-input-number v-model="row.sort" :min="0" :max="99999" :controls="true" class="w100p" @change="resortDepositBanksInPlace" />
</template>
</el-table-column>
<el-table-column :label="t('Operate')" width="90" align="center">
<template #default="{ $index }">
<el-button v-if="canSave" type="danger" link @click="removeRow(form.deposit_banks, $index)">{{ t('Delete') }}</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-card v-if="activeMainTab === 'withdrawBanks'" shadow="never" class="section-card">
<template #header>
<span>{{ t('config.financeCashierConfig.sec_withdraw_banks') }}</span>
<el-button v-if="canSave" type="primary" link class="ml-2" @click="addWithdrawBank">{{ t('config.financeCashierConfig.btn_add_row') }}</el-button>
</template>
<p class="hint">{{ t('config.financeCashierConfig.withdraw_banks_hint') }}</p>
<el-table :data="form.withdraw_banks" border stripe size="small">
<el-table-column :label="t('config.financeCashierConfig.col_currency_code')" width="140">
<template #default="{ row }">
<el-select v-model="row.currency_code" class="w100p" filterable>
<el-option v-for="c in form.currencies" :key="c.code" :label="currencyLabel(c.code)" :value="c.code" />
</el-select>
</template>
</el-table-column>
<el-table-column :label="t('config.financeCashierConfig.col_bank_code')" width="140">
<template #default="{ row }">
<el-input v-model="row.code" maxlength="32" :placeholder="t('config.financeCashierConfig.ph_bank_code')" />
</template>
</el-table-column>
<el-table-column :label="t('config.financeCashierConfig.col_name_zh')" min-width="140">
<template #default="{ row }">
<el-input v-model="row.name_zh" maxlength="64" />
</template>
</el-table-column>
<el-table-column :label="t('config.financeCashierConfig.col_name_en')" min-width="140">
<template #default="{ row }">
<el-input v-model="row.name_en" maxlength="64" />
</template>
</el-table-column>
<el-table-column :label="t('config.financeCashierConfig.col_sort')" width="108">
<template #default="{ row }">
<el-input-number v-model="row.sort" :min="0" :max="99999" :controls="true" class="w100p" @change="resortWithdrawBanksInPlace" />
</template>
</el-table-column>
<el-table-column :label="t('Operate')" width="90" align="center">
@@ -140,7 +275,7 @@
</el-table>
</el-card>
<el-card shadow="never" class="section-card">
<el-card v-if="activeMainTab === 'cashier'" shadow="never" class="section-card">
<template #header>{{ t('config.financeCashierConfig.sec_limits') }}</template>
<el-form-item :label="t('config.financeCashierConfig.min_ewallet')">
<el-input v-model="form.withdraw_limits.min_ewallet" class="w240" />
@@ -150,7 +285,7 @@
</el-form-item>
</el-card>
<el-card shadow="never" class="section-card">
<el-card v-if="activeMainTab === 'cashier'" shadow="never" class="section-card">
<template #header>{{ t('config.financeCashierConfig.sec_copy') }}</template>
<el-form-item :label="t('config.financeCashierConfig.rate_mode')">
<el-select v-model="form.withdraw_copy.rate_mode" class="w240">
@@ -178,20 +313,17 @@
</el-form-item>
</el-card>
<el-card shadow="never" class="section-card">
<template #header>{{ t('config.financeCashierConfig.sec_fields') }}</template>
<el-form-item :label="t('config.financeCashierConfig.field_cardholder')">
<el-switch v-model="form.withdraw_fields.require_cardholder" />
</el-form-item>
<el-form-item :label="t('config.financeCashierConfig.field_bank_account')">
<el-switch v-model="form.withdraw_fields.require_bank_account" />
</el-form-item>
<el-form-item :label="t('config.financeCashierConfig.field_email')">
<el-switch v-model="form.withdraw_fields.require_email" />
</el-form-item>
<el-form-item :label="t('config.financeCashierConfig.field_mobile')">
<el-switch v-model="form.withdraw_fields.require_mobile" />
</el-form-item>
<el-card v-if="activeMainTab === 'cashier'" shadow="never" class="section-card">
<template #header>{{ t('config.financeCashierConfig.sec_ddpay_spec') }}</template>
<el-alert type="info" :closable="false" show-icon class="mb-2">
{{ t('config.financeCashierConfig.ddpay_spec_intro') }}
</el-alert>
<ul class="ddpay-spec-list">
<li>{{ t('config.financeCashierConfig.ddpay_spec_li_withdraw') }}</li>
<li>{{ t('config.financeCashierConfig.ddpay_spec_li_bank_table') }}</li>
<li>{{ t('config.financeCashierConfig.ddpay_spec_li_deposit') }}</li>
<li>{{ t('config.financeCashierConfig.ddpay_spec_li_doc') }}</li>
</ul>
</el-card>
</el-form>
</el-scrollbar>
@@ -201,7 +333,7 @@
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import createAxios from '/@/utils/axios'
import { auth } from '/@/utils/common'
@@ -220,17 +352,35 @@ type CurrencyRow = {
deposit_coins_per_fiat: string
withdraw_coins_per_fiat: string
}
type BankRow = { code: string; name_zh: string; name_en: string; sort: number }
type ChannelRow = { code: string; sort: number; status: number; tier_ids: string[] }
type BankRow = { currency_code: string; code: string; name_zh: string; name_en: string; sort: number }
type ChannelRow = { code: string; sort: number; status: number; tier_ids: string[]; currency_codes: string[] }
type TierRow = {
id: string
title: string
title_en: string
currency: string
pay_amount: string
amount: string
bonus_amount: string
desc: string
desc_en: string
sort: number
status: number
}
type RegistryMeta = { name?: string; name_en?: string; sort?: number }
const loading = ref(false)
const saving = ref(false)
const registry = ref<Record<string, RegistryMeta>>({})
const activeMainTab = ref('cashier')
const originalCurrencyCodes = ref<string[]>([])
const tiers = ref<TierRow[]>([])
const tiersSaving = ref(false)
const form = reactive({
platform_coin: { label_zh: '', label_en: '' },
currencies: [] as CurrencyRow[],
deposit_banks: [] as BankRow[],
withdraw_banks: [] as BankRow[],
withdraw_limits: { min_ewallet: '0', min_bank: '0' },
withdraw_copy: {
@@ -242,12 +392,6 @@ const form = reactive({
fee_note_en: '',
rate_mode: 'fixed',
},
withdraw_fields: {
require_cardholder: true,
require_bank_account: true,
require_email: true,
require_mobile: true,
},
channels: [] as ChannelRow[],
})
@@ -301,8 +445,26 @@ function resortCurrenciesInPlace() {
})
}
function resortBanksInPlace() {
function resortDepositBanksInPlace() {
form.deposit_banks.sort((a, b) => {
const cc = String(a.currency_code || '').localeCompare(String(b.currency_code || ''))
if (cc !== 0) {
return cc
}
const ds = rowSortValue(a) - rowSortValue(b)
if (ds !== 0) {
return ds
}
return String(a.code || '').localeCompare(String(b.code || ''))
})
}
function resortWithdrawBanksInPlace() {
form.withdraw_banks.sort((a, b) => {
const cc = String(a.currency_code || '').localeCompare(String(b.currency_code || ''))
if (cc !== 0) {
return cc
}
const ds = rowSortValue(a) - rowSortValue(b)
if (ds !== 0) {
return ds
@@ -345,25 +507,92 @@ function channelDisplayName(code: string): string {
return code
}
function normalizeChannelRow(c: Record<string, unknown>): ChannelRow {
function currencyLabel(code: string): string {
const cur = form.currencies.find((x) => x.code === code)
if (!cur) {
return code
}
const loc = String(locale.value ?? '').toLowerCase().replaceAll('_', '-')
const preferEn = loc === 'en' || loc.startsWith('en-')
if (preferEn) {
if (typeof cur.label_en === 'string' && cur.label_en !== '') {
return cur.label_en
}
}
if (typeof cur.label_zh === 'string' && cur.label_zh !== '') {
return cur.label_zh
}
return cur.label_en
}
function normalizeChannelRow(c: Record<string, unknown>, defaultCurrencyCodes: string[]): ChannelRow {
const st = c.status
const statusOn = st === 1 || st === true || st === '1'
const rawCodes = c.currency_codes
const isArray = Array.isArray(rawCodes)
// null/undefined 表示“兼容全部币种”(历史配置默认行为)
const codesCandidate = isArray ? (rawCodes as unknown[]) : null
const all = new Set(defaultCurrencyCodes)
let cc: string[]
if (codesCandidate === null) {
cc = defaultCurrencyCodes
} else {
cc = codesCandidate
.map((x) => (typeof x === 'string' ? x.trim().toUpperCase() : ''))
.filter((x) => x !== '')
.filter((x) => all.has(x))
}
return {
code: typeof c.code === 'string' ? c.code : '',
sort: rowSortValue({ sort: c.sort }),
status: statusOn ? 1 : 0,
tier_ids: [],
currency_codes: cc,
}
}
function addBank() {
form.withdraw_banks.push({ code: '', name_zh: '', name_en: '', sort: nextSort(form.withdraw_banks) })
function addDepositBank() {
form.deposit_banks.push({ currency_code: '', code: '', name_zh: '', name_en: '', sort: nextSort(form.deposit_banks) })
}
function addWithdrawBank() {
form.withdraw_banks.push({ currency_code: '', code: '', name_zh: '', name_en: '', sort: nextSort(form.withdraw_banks) })
}
function removeRow<T>(arr: T[], index: number) {
arr.splice(index, 1)
}
async function removeCurrencyRow(index: number) {
const row = form.currencies[index]
if (!row) {
return
}
const code = String(row.code || '').trim().toUpperCase()
if (code === '') {
form.currencies.splice(index, 1)
return
}
const affected = tiers.value.filter((x) => String(x.currency || '').trim().toUpperCase() === code)
if (affected.length === 0) {
form.currencies.splice(index, 1)
return
}
const sample = affected.slice(0, 10).map((x) => x.id).join(',')
const confirmed = await ElMessageBox.confirm(
t('config.financeCashierConfig.msg_delete_currency_prune_tiers', { currency: code, count: affected.length }) +
(sample !== '' ? `\n${t('config.financeCashierConfig.msg_affected_tier_ids')}: ${sample}` : ''),
t('Warning'),
{ type: 'warning', confirmButtonText: t('OK'), cancelButtonText: t('Cancel') }
).then(() => true).catch(() => false)
if (!confirmed) {
return
}
form.currencies.splice(index, 1)
}
async function load() {
loading.value = true
try {
@@ -399,29 +628,100 @@ async function load() {
: '100',
}))
form.currencies.splice(0, form.currencies.length, ...normalized)
originalCurrencyCodes.value = normalized.map((x) => x.code)
resortCurrenciesInPlace()
const defaultCurrencyCodes = normalized.map((x) => String(x.code || '')).filter((x) => x !== '')
const depositBankList = Array.isArray(f.deposit_banks) ? f.deposit_banks : []
const depositBanksNorm: BankRow[] = depositBankList.map((b: Record<string, unknown>) => ({
currency_code: typeof b.currency_code === 'string' ? b.currency_code : '',
code: typeof b.code === 'string' ? b.code : '',
name_zh: typeof b.name_zh === 'string' ? b.name_zh : '',
name_en: typeof b.name_en === 'string' ? b.name_en : '',
sort: rowSortValue({ sort: b.sort }),
}))
form.deposit_banks.splice(0, form.deposit_banks.length, ...depositBanksNorm)
resortDepositBanksInPlace()
const bankList = Array.isArray(f.withdraw_banks) ? f.withdraw_banks : []
const banksNorm: BankRow[] = bankList.map((b: Record<string, unknown>) => ({
currency_code: typeof b.currency_code === 'string' ? b.currency_code : '',
code: typeof b.code === 'string' ? b.code : '',
name_zh: typeof b.name_zh === 'string' ? b.name_zh : '',
name_en: typeof b.name_en === 'string' ? b.name_en : '',
sort: rowSortValue({ sort: b.sort }),
}))
form.withdraw_banks.splice(0, form.withdraw_banks.length, ...banksNorm)
resortBanksInPlace()
resortWithdrawBanksInPlace()
const chList = Array.isArray(f.channels) ? f.channels : []
const channelsNorm: ChannelRow[] = chList.map((c) => normalizeChannelRow(c as Record<string, unknown>))
const channelsNorm: ChannelRow[] = chList.map((c) => normalizeChannelRow(c as Record<string, unknown>, defaultCurrencyCodes))
form.channels.splice(0, form.channels.length, ...channelsNorm)
resortChannelsInPlace()
Object.assign(form.withdraw_limits, f.withdraw_limits || {})
Object.assign(form.withdraw_copy, f.withdraw_copy || {})
Object.assign(form.withdraw_fields, f.withdraw_fields || {})
await loadTiers()
}
} finally {
loading.value = false
}
}
async function loadTiers() {
const res = await createAxios({
url: '/admin/config.FinanceCashierConfig/tierList',
method: 'get',
})
const listRaw = res.code === 1 && Array.isArray(res.data?.list) ? res.data.list : []
tiers.value = listRaw.map((row: Record<string, unknown>) => ({
id: typeof row.id === 'string' ? row.id : '',
title: typeof row.title === 'string' ? row.title : '',
title_en: typeof row.title_en === 'string' ? row.title_en : '',
currency: typeof row.currency === 'string' ? row.currency : '',
pay_amount: typeof row.pay_amount === 'string' ? row.pay_amount : String(row.pay_amount ?? ''),
amount: typeof row.amount === 'string' ? row.amount : String(row.amount ?? ''),
bonus_amount: typeof row.bonus_amount === 'string' ? row.bonus_amount : String(row.bonus_amount ?? '0'),
desc: typeof row.desc === 'string' ? row.desc : '',
desc_en: typeof row.desc_en === 'string' ? row.desc_en : '',
sort: rowSortValue({ sort: row.sort }),
status: row.status === 1 || row.status === true || row.status === '1' ? 1 : 0,
}))
}
function addTier() {
const currency = form.currencies.length > 0 ? form.currencies[0].code : ''
tiers.value.push({
id: `t_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
title: '',
title_en: '',
currency,
pay_amount: '',
amount: '',
bonus_amount: '0',
desc: '',
desc_en: '',
sort: 10,
status: 1,
})
}
async function saveTiers() {
if (!canSave) {
return
}
tiersSaving.value = true
try {
await createAxios({
url: '/admin/config.FinanceCashierConfig/tierSave',
method: 'post',
data: {
items: JSON.parse(JSON.stringify(tiers.value)),
},
showSuccessMessage: true,
})
await loadTiers()
} finally {
tiersSaving.value = false
}
}
async function onSave() {
if (!auth('save')) {
return
@@ -435,14 +735,67 @@ async function onSave() {
return
}
resortCurrenciesInPlace()
resortBanksInPlace()
const allowed = new Set(form.currencies.map((c) => String(c.code || '').trim().toUpperCase()).filter((x) => x !== ''))
for (const b of form.deposit_banks) {
b.currency_code = String(b.currency_code || '').trim().toUpperCase()
b.code = String(b.code || '').trim().toLowerCase()
if (!allowed.has(b.currency_code)) {
b.currency_code = ''
}
}
for (const b of form.withdraw_banks) {
b.currency_code = String(b.currency_code || '').trim().toUpperCase()
b.code = String(b.code || '').trim().toLowerCase()
if (!allowed.has(b.currency_code)) {
b.currency_code = ''
}
}
resortDepositBanksInPlace()
resortWithdrawBanksInPlace()
resortChannelsInPlace()
const removedCurrencyCodes = originalCurrencyCodes.value.filter((x) => !allowed.has(x))
let pruneTierCurrencies: string[] = []
if (removedCurrencyCodes.length > 0) {
const affected = tiers.value.filter((x) => removedCurrencyCodes.includes(String(x.currency || '').toUpperCase()))
if (affected.length > 0) {
const sample = affected.slice(0, 10).map((x) => x.id).join(', ')
const detail = sample !== '' ? `\n${t('config.financeCashierConfig.msg_affected_tier_ids')}: ${sample}` : ''
const confirmed = await ElMessageBox.confirm(
t('config.financeCashierConfig.msg_delete_currency_prune_tiers', {
currency: removedCurrencyCodes.join(','),
count: affected.length,
}) + detail,
t('Warning'),
{
type: 'warning',
confirmButtonText: t('OK'),
cancelButtonText: t('Cancel'),
}
).then(() => true).catch(() => false)
if (!confirmed) {
return
}
pruneTierCurrencies = removedCurrencyCodes
}
}
// 仅保留配置了的币种码(后端还会校验)
for (const ch of form.channels) {
const raw = Array.isArray(ch.currency_codes) ? ch.currency_codes : []
const normalized = raw
.map((x) => (typeof x === 'string' ? x.trim().toUpperCase() : ''))
.filter((x) => x !== '')
.filter((x) => allowed.has(x))
ch.currency_codes = Array.from(new Set(normalized))
}
saving.value = true
try {
await createAxios({
url: '/admin/config.FinanceCashierConfig/save',
method: 'post',
data: JSON.parse(JSON.stringify(form)),
data: {
...JSON.parse(JSON.stringify(form)),
prune_tier_currency_codes: pruneTierCurrencies,
},
showSuccessMessage: true,
})
await load()
@@ -484,5 +837,30 @@ onMounted(() => {
.ml-2 {
margin-left: 8px;
}
.mb-2 {
margin-bottom: 10px;
}
.main-tabs {
margin-bottom: 8px;
}
}
.ddpay-spec-list {
margin: 0;
padding-left: 1.25rem;
color: var(--el-text-color-regular);
font-size: 13px;
line-height: 1.65;
}
.ddpay-spec-list li {
margin-bottom: 6px;
}
.currency-codes-cell {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
align-items: center;
padding-right: 6px;
}
</style>