1.新增充值档位配置
2.新增充值/提现配置
This commit is contained in:
16
web/src/lang/backend/en/config/depositChannel.ts
Normal file
16
web/src/lang/backend/en/config/depositChannel.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
desc: 'Pay channels are registered in code and environment variables (display name, code). Here you only toggle availability, sort order, and which deposit tiers each channel accepts. Leave tiers empty to allow all enabled tiers. Changes apply immediately.',
|
||||
btn_save: 'Save',
|
||||
sort: 'Sort',
|
||||
status: 'Enabled',
|
||||
code: 'Channel code',
|
||||
display_name: 'Display name (read-only)',
|
||||
tier_ids: 'Allowed tier ids',
|
||||
tier_ids_ph: 'Empty = all tiers',
|
||||
'quick Search Fields': 'channel code / display name',
|
||||
form_tip: 'Display names come from the code/env registry and cannot be edited here; adjust sort, status, and allowed tiers.',
|
||||
status_on: 'Enabled',
|
||||
status_off: 'Disabled',
|
||||
tier_all: 'All tiers',
|
||||
rule_sort: 'Sort value is required',
|
||||
}
|
||||
@@ -1,29 +1,47 @@
|
||||
export default {
|
||||
title: 'Deposit Tiers',
|
||||
desc: 'Configure the deposit tiers players can pick when creating a deposit order. In the third-party payment mode, only tier specs (name, amount, bonus, description) are maintained; receiving accounts are no longer stored here. Maintain both Chinese and English text for the title and description: the mobile API returns the language matching the request `lang` header, falling back to Chinese if English is blank. Changes take effect immediately.',
|
||||
desc: 'Configure mobile deposit tiers: titles (ZH/EN), pay currency & pay amount, credited platform coins (base + bonus), and stable tier id (tier_key) for orders and channel binding. Disabled tiers are hidden from players. Changes apply immediately.',
|
||||
'quick Search Fields': 'tier id / title (ZH) / title (EN)',
|
||||
form_tip: 'The table is read-only; use toolbar Add / Edit in the dialog. Changes apply immediately.',
|
||||
tier_id_optional: 'Tier id (optional)',
|
||||
tier_id_optional_ph: 'Leave empty to auto-generate',
|
||||
status_on: 'Enabled',
|
||||
status_off: 'Disabled',
|
||||
total_platform: 'Total credit (base + bonus)',
|
||||
rule_title: 'Title (ZH) is required',
|
||||
rule_currency: 'Pay currency is required',
|
||||
rule_pay_amount: 'Pay amount must be a number greater than 0',
|
||||
rule_platform_base: 'Base platform coin must be a number greater than 0',
|
||||
rule_bonus: 'Bonus must be a number no less than 0',
|
||||
btn_add: 'Add Tier',
|
||||
btn_save: 'Save',
|
||||
btn_remove: 'Delete',
|
||||
confirm_remove: 'Delete this deposit tier?',
|
||||
tier_id: 'Tier ID',
|
||||
tier_id: 'Tier key',
|
||||
auto_id: '(generated on save)',
|
||||
sort: 'Sort',
|
||||
status: 'Enabled',
|
||||
title_col: 'Title (ZH)',
|
||||
title_ph: 'e.g. 新手首充、VIP 高额充值',
|
||||
title_ph: 'e.g. First recharge, VIP top-up bundle',
|
||||
title_en_col: 'Title (EN)',
|
||||
title_en_ph: 'e.g. Starter Pack, VIP Recharge',
|
||||
amount: 'Amount',
|
||||
amount_ph: 'e.g. 100.00',
|
||||
bonus_amount: 'Bonus',
|
||||
pay_currency: 'Pay currency',
|
||||
pay_currency_ph: 'Select or type',
|
||||
pay_amount: 'Pay amount (fiat)',
|
||||
pay_amount_ph: 'e.g. 3.00',
|
||||
platform_base: 'Base platform coin',
|
||||
platform_base_ph: 'e.g. 210.00',
|
||||
bonus_amount: 'Bonus platform coin',
|
||||
bonus_ph: 'e.g. 20.00, use 0 if none',
|
||||
platform_coin_suffix: 'coin',
|
||||
desc_col: 'Description (ZH)',
|
||||
desc_ph: 'Optional Chinese description, up to 255 chars',
|
||||
desc_ph: 'Optional description for Chinese locale, up to 255 characters',
|
||||
desc_en_col: 'Description (EN)',
|
||||
desc_en_ph: 'Optional English description, up to 255 chars',
|
||||
currency: '',
|
||||
operate: 'Action',
|
||||
err_title: 'Row {no}: Chinese title is required',
|
||||
err_amount: 'Row {no}: amount must be a number greater than 0',
|
||||
err_title: 'Row {no}: Title (ZH) is required',
|
||||
err_currency: 'Row {no}: pay currency is required',
|
||||
err_pay_amount: 'Row {no}: pay amount must be a number greater than 0',
|
||||
err_platform_base: 'Row {no}: base platform coin must be a number greater than 0',
|
||||
err_bonus: 'Row {no}: bonus must be a number no less than 0',
|
||||
}
|
||||
|
||||
50
web/src/lang/backend/en/config/financeCashierConfig.ts
Normal file
50
web/src/lang/backend/en/config/financeCashierConfig.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
export default {
|
||||
desc: 'Mobile pay & receipt settings: platform coin labels, currencies and rates, deposit pay channels (on/off, sort, tier scope), withdraw banks, limits, copy, and required withdraw fields. Deposit tiers are configured separately.',
|
||||
btn_save: 'Save',
|
||||
btn_add_row: 'Add row',
|
||||
sec_platform: 'Platform coin labels',
|
||||
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, sort order, and applicable deposit tiers. Leave tiers empty to allow all tiers.',
|
||||
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_limits: 'Minimum withdraw (fiat amount; match your copy currency)',
|
||||
min_ewallet: 'E-wallet minimum',
|
||||
min_bank: 'Bank minimum',
|
||||
sec_copy: 'Withdraw page copy',
|
||||
rate_mode: 'Rate display',
|
||||
rate_mode_fixed: 'Fixed rate',
|
||||
rate_mode_live: 'Live rate (display)',
|
||||
rate_hint_zh: 'Rate hint (ZH)',
|
||||
rate_hint_en: 'Rate hint (EN)',
|
||||
processing_zh: 'Processing note (ZH)',
|
||||
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',
|
||||
col_code: 'Code',
|
||||
col_label_zh: 'Name (ZH)',
|
||||
col_label_en: 'Name (EN)',
|
||||
col_sort: 'Sort',
|
||||
col_deposit_rate: 'Deposit rate',
|
||||
col_withdraw_rate: 'Withdraw rate',
|
||||
col_bank_code: 'Bank code',
|
||||
col_name_zh: 'Bank (ZH)',
|
||||
col_name_en: 'Bank (EN)',
|
||||
ph_ratio: 'e.g. 100',
|
||||
ph_currency_code: 'e.g. MYR',
|
||||
ph_bank_code: 'e.g. agrobank',
|
||||
ch_code: 'Channel code',
|
||||
ch_display_name: 'Display name',
|
||||
ch_sort: 'Sort',
|
||||
ch_status: 'Enabled',
|
||||
ch_tier_ids: 'Allowed deposit tiers',
|
||||
ch_tier_ids_ph: 'Empty = all tiers',
|
||||
}
|
||||
3
web/src/lang/backend/en/order/depositChannelOrder.ts
Normal file
3
web/src/lang/backend/en/order/depositChannelOrder.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
'quick Search Fields': 'order no / pay channel / tier id / idempotency key',
|
||||
}
|
||||
16
web/src/lang/backend/zh-cn/config/depositChannel.ts
Normal file
16
web/src/lang/backend/zh-cn/config/depositChannel.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
desc: '支付渠道在代码与环境变量中注册(展示名、code);此处仅配置开关、排序,以及各渠道允许的充值档位(唯一标识 tier id)。留空「适用档位」表示不限制。修改后立即生效。',
|
||||
btn_save: '保存',
|
||||
sort: '排序',
|
||||
status: '启用',
|
||||
code: '渠道代码',
|
||||
display_name: '展示名称(只读)',
|
||||
tier_ids: '适用充值档位',
|
||||
tier_ids_ph: '空=全部档位',
|
||||
'quick Search Fields': '渠道代码 / 展示名称',
|
||||
form_tip: '展示名由代码与环境注册表决定,此处不可改;可调整排序、开关与适用档位。',
|
||||
status_on: '启用',
|
||||
status_off: '停用',
|
||||
tier_all: '全部档位',
|
||||
rule_sort: '请填写排序值',
|
||||
}
|
||||
@@ -1,11 +1,23 @@
|
||||
export default {
|
||||
title: '充值档位',
|
||||
desc: '配置玩家创建充值订单时可选的充值档位。第三方支付模式下仅需维护档位规格:名称、充值金额、赠送金额、描述等;不再保存收款账户信息。充值名称/描述需分别维护中英文两套:移动端接口会根据请求头 `lang` 返回对应语言,英文缺省时回退到中文。修改后立即生效。',
|
||||
desc: '配置移动端充值档位:充值名称(中英文)、支付货币与支付额度、到账平台币(基础+赠送)、唯一档位标识(id/tier_key)用于下单与渠道绑定。关闭「启用」后玩家不可选该档。修改后立即生效。',
|
||||
'quick Search Fields': '唯一标识 / 中文名 / 英文名',
|
||||
form_tip: '列表为只读展示;请使用工具栏「添加 / 编辑」在弹窗中维护。保存后立即生效。',
|
||||
tier_id_optional: '唯一标识(可选)',
|
||||
tier_id_optional_ph: '留空则由系统自动生成',
|
||||
status_on: '启用',
|
||||
status_off: '停用',
|
||||
total_platform: '到账合计(基础+赠送)',
|
||||
rule_title: '请填写中文充值名称',
|
||||
rule_currency: '请选择或填写支付货币',
|
||||
rule_pay_amount: '支付货币额度须为大于 0 的数字',
|
||||
rule_platform_base: '基础平台币须为大于 0 的数字',
|
||||
rule_bonus: '赠送平台币须为不小于 0 的数字',
|
||||
btn_add: '新增档位',
|
||||
btn_save: '保存',
|
||||
btn_remove: '删除',
|
||||
confirm_remove: '确定删除该充值档位?',
|
||||
tier_id: '档位 ID',
|
||||
tier_id: '唯一标识',
|
||||
auto_id: '(保存时生成)',
|
||||
sort: '排序',
|
||||
status: '启用',
|
||||
@@ -13,17 +25,23 @@ export default {
|
||||
title_ph: '例如:新手首充、VIP 高额充值',
|
||||
title_en_col: '充值名称(英文)',
|
||||
title_en_ph: 'e.g. Starter Pack, VIP Recharge',
|
||||
amount: '充值金额',
|
||||
amount_ph: '例如:100.00',
|
||||
bonus_amount: '赠送金额',
|
||||
pay_currency: '支付货币',
|
||||
pay_currency_ph: '选择或输入',
|
||||
pay_amount: '支付货币额度',
|
||||
pay_amount_ph: '例如:3.00',
|
||||
platform_base: '到账平台币(基础)',
|
||||
platform_base_ph: '例如:210.00',
|
||||
bonus_amount: '赠送平台币',
|
||||
bonus_ph: '例如:20.00,无赠送填 0',
|
||||
platform_coin_suffix: '币',
|
||||
desc_col: '描述(中文)',
|
||||
desc_ph: '可选,展示给中文玩家的档位说明,最长 255 字',
|
||||
desc_en_col: '描述(英文)',
|
||||
desc_en_ph: 'Optional English description for EN players, up to 255 chars',
|
||||
currency: '币',
|
||||
operate: '操作',
|
||||
err_title: '第 {no} 行:中文充值名称不能为空',
|
||||
err_amount: '第 {no} 行:充值金额必须为大于 0 的数字',
|
||||
err_bonus: '第 {no} 行:赠送金额必须为不小于 0 的数字',
|
||||
err_currency: '第 {no} 行:请选择支付货币',
|
||||
err_pay_amount: '第 {no} 行:支付货币额度必须为大于 0 的数字',
|
||||
err_platform_base: '第 {no} 行:基础平台币到账必须为大于 0 的数字',
|
||||
err_bonus: '第 {no} 行:赠送平台币必须为不小于 0 的数字',
|
||||
}
|
||||
|
||||
51
web/src/lang/backend/zh-cn/config/financeCashierConfig.ts
Normal file
51
web/src/lang/backend/zh-cn/config/financeCashierConfig.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export default {
|
||||
desc: '配置移动端支付与收款展示:平台币名称、货币与汇率、充值支付渠道(开关/排序/适用档位)、提现银行、最低限额、文案与提现表单字段。充值档位在「充值档位」中维护。',
|
||||
btn_save: '保存',
|
||||
btn_add_row: '新增一行',
|
||||
sec_platform: '平台币展示名',
|
||||
platform_label_zh: '名称(中文)',
|
||||
platform_label_en: '名称(英文)',
|
||||
sec_currencies: '货币列表(充值/提现货币下拉)',
|
||||
sec_deposit_channels: '充值支付渠道',
|
||||
deposit_channels_hint: '展示名由环境注册表决定,此处仅维护启用状态、排序与适用充值档位;不选档位表示全部档位可用。',
|
||||
currency_rates_hint: '充值汇率:每支付 1 单位该货币到账的平台币;提现汇率:每兑换 1 单位该货币所需平台币(例 100 表示 100 平台币 = 1 单位)。',
|
||||
err_dup_code: '货币代码不能重复,请检查后再保存。',
|
||||
sec_banks: '提现支持银行代码',
|
||||
sec_limits: '提现最低限额(法币金额,与文案中币种一致)',
|
||||
min_ewallet: '电子钱包最低',
|
||||
min_bank: '银行最低',
|
||||
sec_copy: '提现页文案',
|
||||
rate_mode: '汇率展示',
|
||||
rate_mode_fixed: '固定汇率',
|
||||
rate_mode_live: '实时汇率(展示用)',
|
||||
rate_hint_zh: '汇率提示(中文)',
|
||||
rate_hint_en: '汇率提示(英文)',
|
||||
processing_zh: '到账说明(中文)',
|
||||
processing_en: '到账说明(英文)',
|
||||
fee_note_zh: '手续费说明(中文)',
|
||||
fee_note_en: '手续费说明(英文)',
|
||||
sec_fields: '提现表单字段(必填)',
|
||||
field_cardholder: '持卡人姓名',
|
||||
field_bank_account: '银行账号',
|
||||
field_email: '收款邮箱',
|
||||
field_mobile: '收款手机',
|
||||
col_code: '代码',
|
||||
col_label_zh: '中文名',
|
||||
col_label_en: '英文名',
|
||||
col_sort: '排序',
|
||||
col_deposit_rate: '充值汇率',
|
||||
col_withdraw_rate: '提现汇率',
|
||||
col_bank_code: '银行代码',
|
||||
col_name_zh: '银行名(中文)',
|
||||
col_name_en: '银行名(英文)',
|
||||
ph_ratio: '如 100',
|
||||
ph_currency_code: '如 MYR',
|
||||
ph_bank_code: '如 agrobank',
|
||||
/** 本页「充值支付渠道」表格(不依赖 depositChannel 路由语言包) */
|
||||
ch_code: '渠道代码',
|
||||
ch_display_name: '展示名称',
|
||||
ch_sort: '排序',
|
||||
ch_status: '启用',
|
||||
ch_tier_ids: '适用充值档位',
|
||||
ch_tier_ids_ph: '不选表示全部档位',
|
||||
}
|
||||
3
web/src/lang/backend/zh-cn/order/depositChannelOrder.ts
Normal file
3
web/src/lang/backend/zh-cn/order/depositChannelOrder.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
'quick Search Fields': '订单号/支付通道/档位ID/幂等键',
|
||||
}
|
||||
155
web/src/views/backend/config/depositChannel/index.vue
Normal file
155
web/src/views/backend/config/depositChannel/index.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div class="default-main ba-table-box">
|
||||
<el-alert class="ba-table-alert" type="info" :closable="false" show-icon>
|
||||
{{ t('config.depositChannel.desc') }}
|
||||
</el-alert>
|
||||
|
||||
<TableHeader
|
||||
:buttons="['refresh', 'edit', 'quickSearch', 'columnDisplay']"
|
||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('config.depositChannel.quick Search Fields') })"
|
||||
></TableHeader>
|
||||
|
||||
<Table ref="tableRef"></Table>
|
||||
|
||||
<PopupForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import PopupForm from './popupForm.vue'
|
||||
import { baTableApi } from '/@/api/common'
|
||||
import { defaultOptButtons } from '/@/components/table'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
|
||||
defineOptions({
|
||||
name: 'config/depositChannel',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit'])
|
||||
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi('/admin/config.DepositChannel/'),
|
||||
{
|
||||
pk: 'code',
|
||||
filter: {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
},
|
||||
defaultOrder: { prop: 'sort', order: 'asc' },
|
||||
extend: {
|
||||
registry: {} as Record<string, { name: string; name_en: string; sort: number }>,
|
||||
tier_options: [] as { id: string; label: string }[],
|
||||
},
|
||||
column: [
|
||||
{ type: 'selection', align: 'center', operator: false },
|
||||
{
|
||||
label: t('config.depositChannel.sort'),
|
||||
prop: 'sort',
|
||||
align: 'center',
|
||||
width: 88,
|
||||
operator: 'RANGE',
|
||||
sortable: 'custom',
|
||||
},
|
||||
{
|
||||
label: t('config.depositChannel.status'),
|
||||
prop: 'status',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
operator: 'eq',
|
||||
sortable: false,
|
||||
render: 'switch',
|
||||
replaceValue: { 0: t('config.depositChannel.status_off'), 1: t('config.depositChannel.status_on') },
|
||||
},
|
||||
{
|
||||
label: t('config.depositChannel.code'),
|
||||
prop: 'code',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{
|
||||
label: t('config.depositChannel.display_name'),
|
||||
prop: 'display_name',
|
||||
align: 'center',
|
||||
minWidth: 130,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
label: t('config.depositChannel.tier_ids'),
|
||||
prop: 'tier_ids',
|
||||
align: 'center',
|
||||
minWidth: 240,
|
||||
operator: false,
|
||||
render: 'tags',
|
||||
formatter: (row: anyObj) => {
|
||||
const ids = row.tier_ids
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return [t('config.depositChannel.tier_all')]
|
||||
}
|
||||
const opts = (baTable.table.extend?.tier_options ?? []) as { id: string; label: string }[]
|
||||
return ids.map((id: string) => {
|
||||
const o = opts.find((x) => x.id === id)
|
||||
return o ? o.label : id
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('Operate'),
|
||||
align: 'center',
|
||||
width: 90,
|
||||
render: 'buttons',
|
||||
buttons: optButtons,
|
||||
operator: false,
|
||||
fixed: 'right',
|
||||
},
|
||||
],
|
||||
dblClickNotEditColumn: [undefined, 'status'],
|
||||
},
|
||||
{
|
||||
defaultItems: {
|
||||
code: '',
|
||||
sort: 10,
|
||||
status: 1,
|
||||
tier_ids: [] as string[],
|
||||
},
|
||||
},
|
||||
{},
|
||||
{
|
||||
getData({ res }) {
|
||||
const d = res.data as
|
||||
| {
|
||||
registry?: Record<string, { name: string; name_en: string; sort: number }>
|
||||
tier_options?: { id: string; label: string }[]
|
||||
}
|
||||
| undefined
|
||||
if (d?.registry) {
|
||||
baTable.table.extend.registry = d.registry
|
||||
}
|
||||
if (d?.tier_options) {
|
||||
baTable.table.extend.tier_options = d.tier_options
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
provide('baTable', baTable)
|
||||
|
||||
onMounted(() => {
|
||||
baTable.table.ref = tableRef.value
|
||||
baTable.mount()
|
||||
baTable.getData()?.then(() => {
|
||||
baTable.initSort()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
109
web/src/views/backend/config/depositChannel/popupForm.vue
Normal file
109
web/src/views/backend/config/depositChannel/popupForm.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="ba-operate-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:model-value="baTable.form.operate === 'Edit'"
|
||||
@close="baTable.toggleForm"
|
||||
>
|
||||
<template #header>
|
||||
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
|
||||
{{ t('Edit') }}
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
|
||||
<div
|
||||
class="ba-operate-form ba-Edit-form"
|
||||
:style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
|
||||
>
|
||||
<el-form
|
||||
v-if="!baTable.form.loading"
|
||||
ref="formRef"
|
||||
@submit.prevent=""
|
||||
@keyup.enter="baTable.onSubmit(formRef)"
|
||||
:model="baTable.form.items"
|
||||
:label-position="config.layout.shrink ? 'top' : 'right'"
|
||||
:label-width="baTable.form.labelWidth + 'px'"
|
||||
:rules="formRules"
|
||||
>
|
||||
<el-alert class="ba-table-alert" type="info" :closable="false" show-icon>
|
||||
{{ t('config.depositChannel.form_tip') }}
|
||||
</el-alert>
|
||||
|
||||
<el-form-item :label="t('config.depositChannel.code')" prop="code">
|
||||
<el-input :model-value="String(baTable.form.items!.code ?? '')" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.depositChannel.display_name')">
|
||||
<el-input :model-value="registryDisplayName" readonly />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('config.depositChannel.sort')" prop="sort">
|
||||
<el-input-number v-model="baTable.form.items!.sort" :min="0" :max="9999" :controls="true" class="w100" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.depositChannel.status')" prop="status">
|
||||
<el-switch v-model="baTable.form.items!.status" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('config.depositChannel.tier_ids')" prop="tier_ids">
|
||||
<el-select
|
||||
v-model="baTable.form.items!.tier_ids"
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
filterable
|
||||
clearable
|
||||
class="w100"
|
||||
:placeholder="t('config.depositChannel.tier_ids_ph')"
|
||||
>
|
||||
<el-option v-for="opt in tierOptions" :key="opt.id" :label="opt.label" :value="opt.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<template #footer>
|
||||
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
|
||||
<el-button @click="baTable.toggleForm()">{{ t('Cancel') }}</el-button>
|
||||
<el-button v-blur :loading="baTable.form.submitLoading" type="primary" @click="baTable.onSubmit(formRef)">
|
||||
{{ t('Save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { computed, inject, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
|
||||
type TierOpt = { id: string; label: string }
|
||||
|
||||
const config = useConfig()
|
||||
const formRef = useTemplateRef<FormInstance>('formRef')
|
||||
const baTable = inject('baTable') as baTable
|
||||
const { t } = useI18n()
|
||||
|
||||
const tierOptions = computed(() => {
|
||||
const raw = baTable.table.extend?.tier_options
|
||||
return Array.isArray(raw) ? (raw as TierOpt[]) : []
|
||||
})
|
||||
|
||||
const registryDisplayName = computed(() => {
|
||||
const code = String(baTable.form.items?.code ?? '')
|
||||
const reg = baTable.table.extend?.registry as Record<string, { name?: string }> | undefined
|
||||
if (!code || !reg || !reg[code]) {
|
||||
return code
|
||||
}
|
||||
const n = reg[code].name
|
||||
return typeof n === 'string' && n !== '' ? n : code
|
||||
})
|
||||
|
||||
const formRules = {}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.w100 {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,278 +1,209 @@
|
||||
<template>
|
||||
<div class="default-main ba-table-box deposit-tier-page">
|
||||
<el-alert type="info" :closable="false" show-icon>
|
||||
<div class="default-main ba-table-box">
|
||||
<el-alert class="ba-table-alert" type="info" :closable="false" show-icon>
|
||||
{{ t('config.depositTier.desc') }}
|
||||
</el-alert>
|
||||
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" :disabled="loading" @click="onAdd">
|
||||
<Icon name="el-icon-Plus" />
|
||||
<span class="ml-6">{{ t('config.depositTier.btn_add') }}</span>
|
||||
</el-button>
|
||||
<el-button type="success" :loading="saving" :disabled="loading" @click="onSave">
|
||||
{{ t('config.depositTier.btn_save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<TableHeader
|
||||
:buttons="['refresh', 'add', 'edit', 'delete', 'quickSearch', 'columnDisplay']"
|
||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('config.depositTier.quick Search Fields') })"
|
||||
></TableHeader>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
:data="items"
|
||||
row-key="_rowKey"
|
||||
max-height="720"
|
||||
class="deposit-tier-table"
|
||||
header-align="center"
|
||||
>
|
||||
<el-table-column prop="sort" :label="t('config.depositTier.sort')" width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-center">
|
||||
<el-input-number v-model="row.sort" :min="0" :max="9999" :controls="false" style="width: 100%; max-width: 160px" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<Table ref="tableRef"></Table>
|
||||
|
||||
<el-table-column :label="t('config.depositTier.status')" width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-center">
|
||||
<el-switch v-model="row.status" :active-value="1" :inactive-value="0" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('config.depositTier.title_col')" min-width="180" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.title" maxlength="64" :placeholder="t('config.depositTier.title_ph')" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('config.depositTier.title_en_col')" min-width="180" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.title_en" maxlength="64" :placeholder="t('config.depositTier.title_en_ph')" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('config.depositTier.amount')" min-width="140" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.amount" :placeholder="t('config.depositTier.amount_ph')">
|
||||
<template #suffix>
|
||||
<span class="currency">{{ t('config.depositTier.currency') }}</span>
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('config.depositTier.bonus_amount')" min-width="140" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.bonus_amount" :placeholder="t('config.depositTier.bonus_ph')">
|
||||
<template #suffix>
|
||||
<span class="currency">{{ t('config.depositTier.currency') }}</span>
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('config.depositTier.desc_col')" min-width="220" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.desc" maxlength="255" :autosize="{ minRows: 1, maxRows: 3 }" type="textarea" :placeholder="t('config.depositTier.desc_ph')" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('config.depositTier.desc_en_col')" min-width="220" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.desc_en" maxlength="255" :autosize="{ minRows: 1, maxRows: 3 }" type="textarea" :placeholder="t('config.depositTier.desc_en_ph')" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('config.depositTier.tier_id')" width="140" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-text class="tier-id" truncated>{{ row.id || t('config.depositTier.auto_id') }}</el-text>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('config.depositTier.operate')" width="90" align="center" header-align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" link @click="onRemove($index)">
|
||||
{{ t('config.depositTier.btn_remove') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<PopupForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import createAxios from '/@/utils/axios'
|
||||
import { auth } from '/@/utils/common'
|
||||
import PopupForm from './popupForm.vue'
|
||||
import { baTableApi } from '/@/api/common'
|
||||
import { defaultOptButtons } from '/@/components/table'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
|
||||
defineOptions({
|
||||
name: 'config/depositTier',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
||||
|
||||
type Tier = {
|
||||
id: string
|
||||
title: string
|
||||
title_en: string
|
||||
amount: string
|
||||
bonus_amount: string
|
||||
desc: string
|
||||
desc_en: string
|
||||
sort: number
|
||||
status: number
|
||||
_rowKey?: string
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const items = ref<Tier[]>([])
|
||||
|
||||
function genRowKey(): string {
|
||||
return 'r_' + Math.random().toString(36).slice(2, 10)
|
||||
}
|
||||
|
||||
function emptyTier(): Tier {
|
||||
return {
|
||||
id: '',
|
||||
title: '',
|
||||
title_en: '',
|
||||
amount: '',
|
||||
bonus_amount: '0',
|
||||
desc: '',
|
||||
desc_en: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
_rowKey: genRowKey(),
|
||||
function formatAmount4(_row: anyObj, _column: any, cellValue: unknown) {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
||||
return '-'
|
||||
}
|
||||
const s = String(cellValue).trim().replace(',', '.')
|
||||
const n = parseFloat(s)
|
||||
if (!Number.isFinite(n)) {
|
||||
return String(cellValue)
|
||||
}
|
||||
return n.toFixed(4)
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await createAxios({
|
||||
url: '/admin/config.DepositTier/index',
|
||||
method: 'get',
|
||||
})
|
||||
if (res.code === 1 && res.data) {
|
||||
const list = (res.data.items || []) as Tier[]
|
||||
items.value = (Array.isArray(list) ? list : []).map((it) => ({
|
||||
...emptyTier(),
|
||||
...it,
|
||||
_rowKey: genRowKey(),
|
||||
}))
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
function formatPayCell(row: anyObj, _column: any, cellValue: unknown) {
|
||||
const amt = formatAmount4(row, _column, cellValue)
|
||||
const cur = row.currency && String(row.currency).trim() !== '' ? String(row.currency).toUpperCase() : ''
|
||||
if (amt === '-' || cur === '') {
|
||||
return amt
|
||||
}
|
||||
return `${amt} ${cur}`
|
||||
}
|
||||
|
||||
function onAdd() {
|
||||
items.value.push(emptyTier())
|
||||
function formatTotalPlatform(row: anyObj) {
|
||||
const a = parseFloat(String(row.amount ?? '0').replace(',', '.'))
|
||||
const b = parseFloat(String(row.bonus_amount ?? '0').replace(',', '.'))
|
||||
const base = Number.isFinite(a) ? a : 0
|
||||
const bonus = Number.isFinite(b) ? b : 0
|
||||
return (base + bonus).toFixed(4)
|
||||
}
|
||||
|
||||
async function onRemove(idx: number) {
|
||||
try {
|
||||
await ElMessageBox.confirm(t('config.depositTier.confirm_remove'), t('Warning'), {
|
||||
type: 'warning',
|
||||
confirmButtonText: t('Delete'),
|
||||
cancelButtonText: t('Cancel'),
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
items.value.splice(idx, 1)
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (!auth('save')) {
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < items.value.length; i++) {
|
||||
const row = items.value[i]
|
||||
if (!row.title || !row.title.trim()) {
|
||||
ElMessage.warning(t('config.depositTier.err_title', { no: i + 1 }))
|
||||
return
|
||||
}
|
||||
const amount = Number(row.amount)
|
||||
if (!row.amount || Number.isNaN(amount) || amount <= 0) {
|
||||
ElMessage.warning(t('config.depositTier.err_amount', { no: i + 1 }))
|
||||
return
|
||||
}
|
||||
const bonusRaw = row.bonus_amount === '' || row.bonus_amount === null || row.bonus_amount === undefined ? '0' : row.bonus_amount
|
||||
const bonus = Number(bonusRaw)
|
||||
if (Number.isNaN(bonus) || bonus < 0) {
|
||||
ElMessage.warning(t('config.depositTier.err_bonus', { no: i + 1 }))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
await createAxios({
|
||||
url: '/admin/config.DepositTier/save',
|
||||
method: 'post',
|
||||
data: {
|
||||
items: items.value.map((r) => ({
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
title_en: r.title_en || '',
|
||||
amount: r.amount,
|
||||
bonus_amount: r.bonus_amount === '' || r.bonus_amount === null || r.bonus_amount === undefined ? '0' : r.bonus_amount,
|
||||
desc: r.desc || '',
|
||||
desc_en: r.desc_en || '',
|
||||
sort: r.sort,
|
||||
status: r.status,
|
||||
})),
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi('/admin/config.DepositTier/'),
|
||||
{
|
||||
pk: 'id',
|
||||
filter: {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
},
|
||||
defaultOrder: { prop: 'sort', order: 'asc' },
|
||||
column: [
|
||||
{ type: 'selection', align: 'center', operator: false },
|
||||
{
|
||||
label: t('config.depositTier.sort'),
|
||||
prop: 'sort',
|
||||
align: 'center',
|
||||
width: 88,
|
||||
operator: 'RANGE',
|
||||
sortable: 'custom',
|
||||
},
|
||||
showSuccessMessage: true,
|
||||
})
|
||||
await load()
|
||||
} finally {
|
||||
saving.value = false
|
||||
{
|
||||
label: t('config.depositTier.status'),
|
||||
prop: 'status',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
operator: 'eq',
|
||||
sortable: false,
|
||||
render: 'switch',
|
||||
replaceValue: { 0: t('config.depositTier.status_off'), 1: t('config.depositTier.status_on') },
|
||||
},
|
||||
{
|
||||
label: t('config.depositTier.tier_id'),
|
||||
prop: 'id',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
label: t('config.depositTier.title_col'),
|
||||
prop: 'title',
|
||||
align: 'center',
|
||||
minWidth: 140,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
label: t('config.depositTier.title_en_col'),
|
||||
prop: 'title_en',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
label: t('config.depositTier.pay_currency'),
|
||||
prop: 'currency',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
operator: 'eq',
|
||||
},
|
||||
{
|
||||
label: t('config.depositTier.pay_amount'),
|
||||
prop: 'pay_amount',
|
||||
align: 'center',
|
||||
minWidth: 130,
|
||||
operator: 'RANGE',
|
||||
formatter: formatPayCell,
|
||||
},
|
||||
{
|
||||
label: t('config.depositTier.platform_base'),
|
||||
prop: 'amount',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
operator: 'RANGE',
|
||||
formatter: formatAmount4,
|
||||
},
|
||||
{
|
||||
label: t('config.depositTier.bonus_amount'),
|
||||
prop: 'bonus_amount',
|
||||
align: 'center',
|
||||
minWidth: 110,
|
||||
operator: 'RANGE',
|
||||
formatter: formatAmount4,
|
||||
},
|
||||
{
|
||||
label: t('config.depositTier.total_platform'),
|
||||
prop: '__total_platform',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
operator: false,
|
||||
formatter: (row: anyObj) => formatTotalPlatform(row),
|
||||
},
|
||||
{
|
||||
label: t('config.depositTier.desc_col'),
|
||||
prop: 'desc',
|
||||
align: 'center',
|
||||
minWidth: 160,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
label: t('Operate'),
|
||||
align: 'center',
|
||||
width: 100,
|
||||
render: 'buttons',
|
||||
buttons: optButtons,
|
||||
operator: false,
|
||||
fixed: 'right',
|
||||
},
|
||||
],
|
||||
dblClickNotEditColumn: [undefined, 'status'],
|
||||
},
|
||||
{
|
||||
defaultItems: {
|
||||
id: '',
|
||||
title: '',
|
||||
title_en: '',
|
||||
currency: 'MYR',
|
||||
pay_amount: '',
|
||||
amount: '',
|
||||
bonus_amount: '0',
|
||||
desc: '',
|
||||
desc_en: '',
|
||||
sort: 10,
|
||||
status: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
provide('baTable', baTable)
|
||||
|
||||
onMounted(() => {
|
||||
void load()
|
||||
baTable.table.ref = tableRef.value
|
||||
baTable.mount()
|
||||
baTable.getData()?.then(() => {
|
||||
baTable.initSort()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.deposit-tier-page {
|
||||
.toolbar {
|
||||
margin: 12px 0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.currency {
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tier-id {
|
||||
display: inline-block;
|
||||
max-width: 120px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.deposit-tier-table {
|
||||
:deep(.el-table__header th),
|
||||
:deep(.el-table__body td) {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.cell-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
152
web/src/views/backend/config/depositTier/popupForm.vue
Normal file
152
web/src/views/backend/config/depositTier/popupForm.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="ba-operate-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:model-value="['Add', 'Edit'].includes(baTable.form.operate!)"
|
||||
@close="baTable.toggleForm"
|
||||
>
|
||||
<template #header>
|
||||
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
|
||||
{{ baTable.form.operate ? t(baTable.form.operate) : '' }}
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
|
||||
<div
|
||||
class="ba-operate-form"
|
||||
:class="'ba-' + baTable.form.operate + '-form'"
|
||||
:style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
|
||||
>
|
||||
<el-form
|
||||
v-if="!baTable.form.loading"
|
||||
ref="formRef"
|
||||
@submit.prevent=""
|
||||
@keyup.enter="baTable.onSubmit(formRef)"
|
||||
:model="baTable.form.items"
|
||||
:label-position="config.layout.shrink ? 'top' : 'right'"
|
||||
:label-width="baTable.form.labelWidth + 'px'"
|
||||
:rules="rules"
|
||||
>
|
||||
<el-alert class="ba-table-alert" type="info" :closable="false" show-icon>
|
||||
{{ t('config.depositTier.form_tip') }}
|
||||
</el-alert>
|
||||
|
||||
<el-form-item v-if="baTable.form.operate === 'Edit'" :label="t('config.depositTier.tier_id')" prop="id">
|
||||
<el-input :model-value="String(baTable.form.items!.id ?? '')" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="baTable.form.operate === 'Add'" :label="t('config.depositTier.tier_id_optional')" prop="id">
|
||||
<el-input v-model="baTable.form.items!.id" clearable :placeholder="t('config.depositTier.tier_id_optional_ph')" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('config.depositTier.sort')" prop="sort">
|
||||
<el-input-number v-model="baTable.form.items!.sort" :min="0" :max="9999" :controls="true" class="w100" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.depositTier.status')" prop="status">
|
||||
<el-switch v-model="baTable.form.items!.status" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('config.depositTier.title_col')" prop="title">
|
||||
<el-input v-model="baTable.form.items!.title" maxlength="64" :placeholder="t('config.depositTier.title_ph')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.depositTier.title_en_col')" prop="title_en">
|
||||
<el-input v-model="baTable.form.items!.title_en" maxlength="64" :placeholder="t('config.depositTier.title_en_ph')" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('config.depositTier.pay_currency')" prop="currency">
|
||||
<el-select v-model="baTable.form.items!.currency" filterable allow-create default-first-option class="w100" :placeholder="t('config.depositTier.pay_currency_ph')">
|
||||
<el-option v-for="c in payCurrencies" :key="c" :label="c" :value="c" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.depositTier.pay_amount')" prop="pay_amount">
|
||||
<el-input v-model="baTable.form.items!.pay_amount" :placeholder="t('config.depositTier.pay_amount_ph')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.depositTier.platform_base')" prop="amount">
|
||||
<el-input v-model="baTable.form.items!.amount" :placeholder="t('config.depositTier.platform_base_ph')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.depositTier.bonus_amount')" prop="bonus_amount">
|
||||
<el-input v-model="baTable.form.items!.bonus_amount" :placeholder="t('config.depositTier.bonus_ph')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.depositTier.desc_col')" prop="desc">
|
||||
<el-input v-model="baTable.form.items!.desc" type="textarea" :rows="2" maxlength="255" :placeholder="t('config.depositTier.desc_ph')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.depositTier.desc_en_col')" prop="desc_en">
|
||||
<el-input v-model="baTable.form.items!.desc_en" type="textarea" :rows="2" maxlength="255" :placeholder="t('config.depositTier.desc_en_ph')" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<template #footer>
|
||||
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
|
||||
<el-button @click="baTable.toggleForm()">{{ t('Cancel') }}</el-button>
|
||||
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
|
||||
{{ t('Save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance, FormItemRule } from 'element-plus'
|
||||
import { inject, reactive, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
|
||||
const config = useConfig()
|
||||
const formRef = useTemplateRef<FormInstance>('formRef')
|
||||
const baTable = inject('baTable') as baTable
|
||||
const { t } = useI18n()
|
||||
|
||||
const payCurrencies = ['MYR', 'CNY', 'USD', 'USDT', 'VND', 'THB', 'SGD', 'IDR']
|
||||
|
||||
function positiveAmountRule(msg: string): FormItemRule {
|
||||
return {
|
||||
validator: (_rule, val, callback) => {
|
||||
const s = val === null || val === undefined ? '' : String(val).trim().replace(',', '.')
|
||||
if (s === '' || !/^-?\d+(\.\d+)?$/.test(s)) {
|
||||
callback(new Error(msg))
|
||||
return
|
||||
}
|
||||
const n = parseFloat(s)
|
||||
if (!Number.isFinite(n) || n <= 0) {
|
||||
callback(new Error(msg))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'blur',
|
||||
}
|
||||
}
|
||||
|
||||
function nonNegAmountRule(msg: string): FormItemRule {
|
||||
return {
|
||||
validator: (_rule, val, callback) => {
|
||||
const s = val === null || val === undefined || val === '' ? '0' : String(val).trim().replace(',', '.')
|
||||
if (!/^-?\d+(\.\d+)?$/.test(s)) {
|
||||
callback(new Error(msg))
|
||||
return
|
||||
}
|
||||
const n = parseFloat(s)
|
||||
if (!Number.isFinite(n) || n < 0) {
|
||||
callback(new Error(msg))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'blur',
|
||||
}
|
||||
}
|
||||
|
||||
const rules = reactive({
|
||||
title: [{ required: true, message: t('config.depositTier.rule_title'), trigger: 'blur' }],
|
||||
currency: [{ required: true, message: t('config.depositTier.rule_currency'), trigger: 'change' }],
|
||||
pay_amount: [positiveAmountRule(t('config.depositTier.rule_pay_amount'))],
|
||||
amount: [positiveAmountRule(t('config.depositTier.rule_platform_base'))],
|
||||
bonus_amount: [nonNegAmountRule(t('config.depositTier.rule_bonus'))],
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.w100 {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
518
web/src/views/backend/config/financeCashierConfig/index.vue
Normal file
518
web/src/views/backend/config/financeCashierConfig/index.vue
Normal file
@@ -0,0 +1,518 @@
|
||||
<template>
|
||||
<div class="default-main finance-cashier-page">
|
||||
<el-alert type="info" :closable="false" show-icon class="mb-3">
|
||||
{{ t('config.financeCashierConfig.desc') }}
|
||||
</el-alert>
|
||||
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" :loading="saving" :disabled="loading" @click="onSave">
|
||||
{{ t('config.financeCashierConfig.btn_save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<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" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.financeCashierConfig.platform_label_en')">
|
||||
<el-input v-model="form.platform_coin.label_en" maxlength="32" class="w400" />
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>
|
||||
<span>{{ t('config.financeCashierConfig.sec_currencies') }}</span>
|
||||
<el-button type="primary" link class="ml-2" @click="addCurrency">{{ t('config.financeCashierConfig.btn_add_row') }}</el-button>
|
||||
</template>
|
||||
<p class="hint">{{ t('config.financeCashierConfig.currency_rates_hint') }}</p>
|
||||
<el-table :data="form.currencies" border stripe size="small">
|
||||
<el-table-column :label="t('config.financeCashierConfig.col_code')" prop="code" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-model="row.code"
|
||||
maxlength="12"
|
||||
:placeholder="t('config.financeCashierConfig.ph_currency_code')"
|
||||
@blur="normalizeCurrencyCode(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('config.financeCashierConfig.col_label_zh')" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.label_zh" maxlength="64" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('config.financeCashierConfig.col_label_en')" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.label_en" maxlength="64" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('config.financeCashierConfig.col_deposit_rate')" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.deposit_coins_per_fiat" :placeholder="t('config.financeCashierConfig.ph_ratio')" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('config.financeCashierConfig.col_withdraw_rate')" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.withdraw_coins_per_fiat" :placeholder="t('config.financeCashierConfig.ph_ratio')" />
|
||||
</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="resortCurrenciesInPlace" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('Operate')" width="90" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" link @click="removeRow(form.currencies, $index)">{{ t('Delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-card 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">
|
||||
<el-table-column :label="t('config.financeCashierConfig.ch_code')" prop="code" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-input :model-value="String(row.code ?? '')" readonly />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('config.financeCashierConfig.ch_display_name')" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<el-input :model-value="channelDisplayName(String(row.code ?? ''))" readonly />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('config.financeCashierConfig.ch_sort')" width="108">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.sort"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
:controls="true"
|
||||
class="w100p"
|
||||
@change="resortChannelsInPlace"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('config.financeCashierConfig.ch_status')" width="100" 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('config.financeCashierConfig.ch_tier_ids')" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.tier_ids"
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
filterable
|
||||
clearable
|
||||
class="w100p"
|
||||
:placeholder="t('config.financeCashierConfig.ch_tier_ids_ph')"
|
||||
>
|
||||
<el-option v-for="opt in tierOptions" :key="opt.id" :label="opt.label" :value="opt.id" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>
|
||||
<span>{{ t('config.financeCashierConfig.sec_banks') }}</span>
|
||||
<el-button type="primary" link class="ml-2" @click="addBank">{{ t('config.financeCashierConfig.btn_add_row') }}</el-button>
|
||||
</template>
|
||||
<el-table :data="form.withdraw_banks" border stripe size="small">
|
||||
<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="resortBanksInPlace" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('Operate')" width="90" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" link @click="removeRow(form.withdraw_banks, $index)">{{ t('Delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-card 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" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.financeCashierConfig.min_bank')">
|
||||
<el-input v-model="form.withdraw_limits.min_bank" class="w240" />
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<el-card 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">
|
||||
<el-option :label="t('config.financeCashierConfig.rate_mode_fixed')" value="fixed" />
|
||||
<el-option :label="t('config.financeCashierConfig.rate_mode_live')" value="live" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.financeCashierConfig.rate_hint_zh')">
|
||||
<el-input v-model="form.withdraw_copy.rate_hint_zh" type="textarea" :rows="2" maxlength="500" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.financeCashierConfig.rate_hint_en')">
|
||||
<el-input v-model="form.withdraw_copy.rate_hint_en" type="textarea" :rows="2" maxlength="500" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.financeCashierConfig.processing_zh')">
|
||||
<el-input v-model="form.withdraw_copy.processing_zh" maxlength="255" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.financeCashierConfig.processing_en')">
|
||||
<el-input v-model="form.withdraw_copy.processing_en" maxlength="255" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.financeCashierConfig.fee_note_zh')">
|
||||
<el-input v-model="form.withdraw_copy.fee_note_zh" type="textarea" :rows="2" maxlength="500" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('config.financeCashierConfig.fee_note_en')">
|
||||
<el-input v-model="form.withdraw_copy.fee_note_en" type="textarea" :rows="2" maxlength="500" />
|
||||
</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>
|
||||
</el-form>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import createAxios from '/@/utils/axios'
|
||||
import { auth } from '/@/utils/common'
|
||||
|
||||
defineOptions({
|
||||
name: 'config/financeCashierConfig',
|
||||
})
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
type CurrencyRow = {
|
||||
code: string
|
||||
label_zh: string
|
||||
label_en: string
|
||||
sort: number
|
||||
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 TierOpt = { id: string; label: string }
|
||||
type RegistryMeta = { name?: string; name_en?: string; sort?: number }
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const registry = ref<Record<string, RegistryMeta>>({})
|
||||
const tierOptions = ref<TierOpt[]>([])
|
||||
|
||||
const form = reactive({
|
||||
platform_coin: { label_zh: '', label_en: '' },
|
||||
currencies: [] as CurrencyRow[],
|
||||
withdraw_banks: [] as BankRow[],
|
||||
withdraw_limits: { min_ewallet: '0', min_bank: '0' },
|
||||
withdraw_copy: {
|
||||
rate_hint_zh: '',
|
||||
rate_hint_en: '',
|
||||
processing_zh: '',
|
||||
processing_en: '',
|
||||
fee_note_zh: '',
|
||||
fee_note_en: '',
|
||||
rate_mode: 'fixed',
|
||||
},
|
||||
withdraw_fields: {
|
||||
require_cardholder: true,
|
||||
require_bank_account: true,
|
||||
require_email: true,
|
||||
require_mobile: true,
|
||||
},
|
||||
channels: [] as ChannelRow[],
|
||||
})
|
||||
|
||||
function rowSortValue(row: { sort?: unknown }): number {
|
||||
const s = row.sort
|
||||
if (typeof s === 'number' && Number.isFinite(s)) {
|
||||
return Math.round(s)
|
||||
}
|
||||
if (typeof s === 'string' && s.trim() !== '') {
|
||||
const n = Number(s)
|
||||
if (!Number.isNaN(n) && Number.isFinite(n)) {
|
||||
return Math.round(n)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function nextSort(rows: { sort?: unknown }[]): number {
|
||||
let m = 0
|
||||
for (const r of rows) {
|
||||
const v = rowSortValue(r)
|
||||
if (v > m) {
|
||||
m = v
|
||||
}
|
||||
}
|
||||
return m > 0 ? m + 10 : 10
|
||||
}
|
||||
|
||||
function addCurrency() {
|
||||
form.currencies.push({
|
||||
code: '',
|
||||
label_zh: '',
|
||||
label_en: '',
|
||||
sort: nextSort(form.currencies),
|
||||
deposit_coins_per_fiat: '100',
|
||||
withdraw_coins_per_fiat: '100',
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeCurrencyCode(row: CurrencyRow) {
|
||||
row.code = String(row.code || '').trim().toUpperCase()
|
||||
}
|
||||
|
||||
function resortCurrenciesInPlace() {
|
||||
form.currencies.sort((a, b) => {
|
||||
const ds = rowSortValue(a) - rowSortValue(b)
|
||||
if (ds !== 0) {
|
||||
return ds
|
||||
}
|
||||
return String(a.code || '').localeCompare(String(b.code || ''))
|
||||
})
|
||||
}
|
||||
|
||||
function resortBanksInPlace() {
|
||||
form.withdraw_banks.sort((a, b) => {
|
||||
const ds = rowSortValue(a) - rowSortValue(b)
|
||||
if (ds !== 0) {
|
||||
return ds
|
||||
}
|
||||
return String(a.code || '').localeCompare(String(b.code || ''))
|
||||
})
|
||||
}
|
||||
|
||||
function resortChannelsInPlace() {
|
||||
form.channels.sort((a, b) => {
|
||||
const ds = rowSortValue(a) - rowSortValue(b)
|
||||
if (ds !== 0) {
|
||||
return ds
|
||||
}
|
||||
return String(a.code || '').localeCompare(String(b.code || ''))
|
||||
})
|
||||
}
|
||||
|
||||
function channelDisplayName(code: string): string {
|
||||
const r = registry.value[code]
|
||||
if (!r) {
|
||||
return code
|
||||
}
|
||||
const loc = String(locale.value ?? '').toLowerCase().replaceAll('_', '-')
|
||||
const preferEn = loc === 'en' || loc.startsWith('en-')
|
||||
if (preferEn) {
|
||||
const ne = r.name_en
|
||||
if (typeof ne === 'string' && ne !== '') {
|
||||
return ne
|
||||
}
|
||||
}
|
||||
const n = r.name
|
||||
if (typeof n === 'string' && n !== '') {
|
||||
return n
|
||||
}
|
||||
const ne = r.name_en
|
||||
if (typeof ne === 'string' && ne !== '') {
|
||||
return ne
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
function normalizeChannelRow(c: Record<string, unknown>): ChannelRow {
|
||||
const tierIds: string[] = []
|
||||
if (Array.isArray(c.tier_ids)) {
|
||||
for (const x of c.tier_ids) {
|
||||
if (typeof x === 'string') {
|
||||
const t = x.trim()
|
||||
if (t !== '') {
|
||||
tierIds.push(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const st = c.status
|
||||
const statusOn = st === 1 || st === true || st === '1'
|
||||
return {
|
||||
code: typeof c.code === 'string' ? c.code : '',
|
||||
sort: rowSortValue({ sort: c.sort }),
|
||||
status: statusOn ? 1 : 0,
|
||||
tier_ids: tierIds,
|
||||
}
|
||||
}
|
||||
|
||||
function addBank() {
|
||||
form.withdraw_banks.push({ code: '', name_zh: '', name_en: '', sort: nextSort(form.withdraw_banks) })
|
||||
}
|
||||
|
||||
function removeRow<T>(arr: T[], index: number) {
|
||||
arr.splice(index, 1)
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await createAxios({
|
||||
url: '/admin/config.FinanceCashierConfig/index',
|
||||
method: 'get',
|
||||
})
|
||||
if (res.code === 1 && res.data && res.data.form) {
|
||||
const f = res.data.form as typeof form
|
||||
const regRaw = res.data.registry
|
||||
registry.value =
|
||||
regRaw !== null && typeof regRaw === 'object' && !Array.isArray(regRaw)
|
||||
? (regRaw as Record<string, RegistryMeta>)
|
||||
: {}
|
||||
const topts = res.data.tier_options
|
||||
tierOptions.value = Array.isArray(topts) ? (topts as TierOpt[]) : []
|
||||
Object.assign(form.platform_coin, f.platform_coin || {})
|
||||
const curList = Array.isArray(f.currencies) ? f.currencies : []
|
||||
const normalized: CurrencyRow[] = curList.map((c: Record<string, unknown>) => ({
|
||||
code: typeof c.code === 'string' ? c.code : '',
|
||||
label_zh: typeof c.label_zh === 'string' ? c.label_zh : '',
|
||||
label_en: typeof c.label_en === 'string' ? c.label_en : '',
|
||||
sort: rowSortValue({ sort: c.sort }),
|
||||
deposit_coins_per_fiat:
|
||||
typeof c.deposit_coins_per_fiat === 'string'
|
||||
? c.deposit_coins_per_fiat
|
||||
: typeof c.deposit_coins_per_fiat === 'number'
|
||||
? String(c.deposit_coins_per_fiat)
|
||||
: '100',
|
||||
withdraw_coins_per_fiat:
|
||||
typeof c.withdraw_coins_per_fiat === 'string'
|
||||
? c.withdraw_coins_per_fiat
|
||||
: typeof c.withdraw_coins_per_fiat === 'number'
|
||||
? String(c.withdraw_coins_per_fiat)
|
||||
: '100',
|
||||
}))
|
||||
form.currencies.splice(0, form.currencies.length, ...normalized)
|
||||
resortCurrenciesInPlace()
|
||||
const bankList = Array.isArray(f.withdraw_banks) ? f.withdraw_banks : []
|
||||
const banksNorm: BankRow[] = bankList.map((b: Record<string, unknown>) => ({
|
||||
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()
|
||||
const chList = Array.isArray(f.channels) ? f.channels : []
|
||||
const channelsNorm: ChannelRow[] = chList.map((c) => normalizeChannelRow(c as Record<string, unknown>))
|
||||
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 || {})
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (!auth('save')) {
|
||||
return
|
||||
}
|
||||
for (const row of form.currencies) {
|
||||
normalizeCurrencyCode(row)
|
||||
}
|
||||
const codes = form.currencies.map((c) => String(c.code || '').trim().toUpperCase()).filter((c) => c !== '')
|
||||
if (new Set(codes).size !== codes.length) {
|
||||
ElMessage.warning(t('config.financeCashierConfig.err_dup_code'))
|
||||
return
|
||||
}
|
||||
resortCurrenciesInPlace()
|
||||
resortBanksInPlace()
|
||||
resortChannelsInPlace()
|
||||
saving.value = true
|
||||
try {
|
||||
await createAxios({
|
||||
url: '/admin/config.FinanceCashierConfig/save',
|
||||
method: 'post',
|
||||
data: JSON.parse(JSON.stringify(form)),
|
||||
showSuccessMessage: true,
|
||||
})
|
||||
await load()
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void load()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.finance-cashier-page {
|
||||
.toolbar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.section-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.hint {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
.w400 {
|
||||
max-width: 400px;
|
||||
}
|
||||
.w240 {
|
||||
max-width: 240px;
|
||||
}
|
||||
.w100p {
|
||||
width: 100%;
|
||||
}
|
||||
.mb-3 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.ml-2 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
214
web/src/views/backend/order/depositChannelOrder/index.vue
Normal file
214
web/src/views/backend/order/depositChannelOrder/index.vue
Normal file
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<div class="default-main ba-table-box">
|
||||
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
|
||||
|
||||
<TableHeader
|
||||
:buttons="['refresh', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('order.depositChannelOrder.quick Search Fields') })"
|
||||
></TableHeader>
|
||||
|
||||
<Table ref="tableRef"></Table>
|
||||
|
||||
<PopupForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import PopupForm from '../depositOrder/popupForm.vue'
|
||||
import { baTableApi } from '/@/api/common'
|
||||
import { defaultOptButtons } from '/@/components/table'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
|
||||
defineOptions({
|
||||
name: 'order/depositChannelOrder',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit'])
|
||||
|
||||
function formatAmount(_row: anyObj, _column: any, cellValue: unknown) {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
||||
return '-'
|
||||
}
|
||||
const s = String(cellValue).trim().replace(',', '.')
|
||||
const n = parseFloat(s)
|
||||
if (!Number.isFinite(n)) {
|
||||
return String(cellValue)
|
||||
}
|
||||
return n.toFixed(2)
|
||||
}
|
||||
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi('/admin/order.DepositChannelOrder/'),
|
||||
{
|
||||
pk: 'id',
|
||||
column: [
|
||||
{ type: 'selection', align: 'center', operator: false },
|
||||
{ label: t('order.depositOrder.id'), prop: 'id', align: 'center', width: 80, operator: 'RANGE', sortable: 'custom' },
|
||||
{
|
||||
label: t('order.depositOrder.order_no'),
|
||||
prop: 'order_no',
|
||||
align: 'center',
|
||||
minWidth: 170,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.user_username'),
|
||||
prop: 'user.username',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
render: 'tags',
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.channel_name'),
|
||||
prop: 'channel.name',
|
||||
align: 'center',
|
||||
minWidth: 110,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
render: 'tags',
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.amount'),
|
||||
prop: 'amount',
|
||||
align: 'center',
|
||||
minWidth: 110,
|
||||
operator: 'RANGE',
|
||||
formatter: formatAmount,
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.bonus_amount'),
|
||||
prop: 'bonus_amount',
|
||||
align: 'center',
|
||||
minWidth: 110,
|
||||
operator: 'RANGE',
|
||||
formatter: formatAmount,
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.status'),
|
||||
prop: 'status',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
operator: 'eq',
|
||||
render: 'tag',
|
||||
effect: 'dark',
|
||||
custom: {
|
||||
'0': 'info',
|
||||
'1': 'success',
|
||||
'2': 'danger',
|
||||
'3': 'warning',
|
||||
},
|
||||
replaceValue: {
|
||||
'0': t('order.depositOrder.status 0'),
|
||||
'1': t('order.depositOrder.status 1'),
|
||||
'2': t('order.depositOrder.status 2'),
|
||||
'3': t('order.depositOrder.status 3'),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.pay_channel'),
|
||||
prop: 'pay_channel',
|
||||
align: 'center',
|
||||
minWidth: 130,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.deposit_tier_id'),
|
||||
prop: 'deposit_tier_id',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.pay_time'),
|
||||
prop: 'pay_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 170,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.idempotency_key'),
|
||||
prop: 'idempotency_key',
|
||||
align: 'center',
|
||||
minWidth: 170,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
showOverflowTooltip: true,
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.remark'),
|
||||
prop: 'remark',
|
||||
align: 'center',
|
||||
minWidth: 150,
|
||||
operator: 'LIKE',
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.create_time'),
|
||||
prop: 'create_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 170,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{
|
||||
label: t('order.depositOrder.update_time'),
|
||||
prop: 'update_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 170,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: t('Operate'),
|
||||
align: 'center',
|
||||
width: 90,
|
||||
render: 'buttons',
|
||||
buttons: optButtons,
|
||||
operator: false,
|
||||
fixed: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
defaultItems: { status: 0, amount: '0.0000', bonus_amount: '0.0000' },
|
||||
}
|
||||
)
|
||||
|
||||
provide('baTable', baTable)
|
||||
|
||||
onMounted(() => {
|
||||
baTable.table.ref = tableRef.value
|
||||
baTable.mount()
|
||||
baTable.getData()?.then(() => {
|
||||
baTable.initSort()
|
||||
baTable.dragSort()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
Reference in New Issue
Block a user