1.配置新版支付模块-菜单和接口都已重构
2.优化充值提现页面 3.菜单翻译问题 4.备份数据库
This commit is contained in:
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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_key;bank_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: '支持币种',
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user