feat: 支付管理重构 - 移除设置页支付Tab/添加bepusdt/内置图标/按类型区分配置

This commit is contained in:
2026-05-26 11:31:27 +08:00
parent 7c91cdb4c9
commit 0a746a3139
2 changed files with 206 additions and 89 deletions
+203 -41
View File
@@ -4,9 +4,14 @@
<el-button type="primary" @click="openAdd">添加通道</el-button>
</div>
<div class="table-card">
<el-table :data="channels">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="通道名称" />
<el-table :data="channels" style="width: 100%">
<el-table-column prop="id" label="ID" width="60" />
<el-table-column label="图标" width="60">
<template #default="{ row }">
<span class="channel-icon" v-html="getIconSvg(row.icon || row.type)"></span>
</template>
</el-table-column>
<el-table-column prop="name" label="通道名称" min-width="120" />
<el-table-column prop="code" label="通道代码" width="120" />
<el-table-column prop="type" label="类型" width="100">
<template #default="{ row }">
@@ -18,7 +23,7 @@
{{ (row.fee_rate * 100).toFixed(2) }}%
</template>
</el-table-column>
<el-table-column label="金额限制" width="180">
<el-table-column label="金额限制" width="160">
<template #default="{ row }">
{{ row.min_amount }} - {{ row.max_amount || '∞' }}
</template>
@@ -30,7 +35,7 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sort_order" label="排序" width="80" />
<el-table-column prop="sort_order" label="排序" width="60" />
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="editChannel(row)">编辑</el-button>
@@ -43,7 +48,7 @@
</el-table>
</div>
<el-dialog v-model="showAdd" :title="editing ? '编辑支付通道' : '添加支付通道'" width="600px">
<el-form :model="form" label-width="100px">
<el-form :model="form" label-width="120px">
<el-form-item label="通道名称" required>
<el-input v-model="form.name" placeholder="如:支付宝" />
</el-form-item>
@@ -51,16 +56,27 @@
<el-input v-model="form.code" placeholder="如:alipay" :disabled="!!editing" />
</el-form-item>
<el-form-item label="支付类型" required>
<el-select v-model="form.type" placeholder="选择支付类型">
<el-option label="支付宝" value="alipay" />
<el-option label="微信支付" value="wechat" />
<el-option label="银行卡" value="bank" />
<el-option label="USDT" value="usdt" />
<el-option label="其他" value="other" />
<el-select v-model="form.type" placeholder="选择支付类型" @change="onTypeChange">
<el-option v-for="t in channelTypes" :key="t.value" :label="t.label" :value="t.value">
<span style="display:flex;align-items:center;gap:8px">
<span v-html="t.icon"></span>
<span>{{ t.label }}</span>
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="图标">
<el-input v-model="form.icon" placeholder="图标URL" />
<div class="icon-selector">
<div
v-for="ic in availableIcons"
:key="ic.name"
class="icon-option"
:class="{ active: form.icon === ic.name }"
@click="form.icon = ic.name"
:title="ic.label"
v-html="ic.svg"
></div>
</div>
</el-form-item>
<el-form-item label="手续费率">
<el-input-number v-model="form.fee_rate" :min="0" :max="1" :step="0.001" :precision="4" />
@@ -72,9 +88,82 @@
<el-form-item label="最大金额">
<el-input-number v-model="form.max_amount" :min="0" :precision="2" />
</el-form-item>
<el-divider content-position="left">通道配置</el-divider>
<template v-if="form.type === 'alipay'">
<el-form-item label="AppID">
<el-input v-model="channelConfig.alipay_app_id" placeholder="支付宝应用ID" />
</el-form-item>
<el-form-item label="应用私钥">
<el-input v-model="channelConfig.alipay_private_key" type="textarea" :rows="3" placeholder="RSA2私钥" />
</el-form-item>
<el-form-item label="支付宝公钥">
<el-input v-model="channelConfig.alipay_public_key" type="textarea" :rows="3" placeholder="支付宝公钥" />
</el-form-item>
<el-form-item label="回调地址">
<el-input v-model="channelConfig.alipay_notify_url" placeholder="支付结果通知URL" />
</el-form-item>
</template>
<template v-else-if="form.type === 'wechat'">
<el-form-item label="AppID">
<el-input v-model="channelConfig.wechat_app_id" placeholder="公众号/小程序AppID" />
</el-form-item>
<el-form-item label="商户号">
<el-input v-model="channelConfig.wechat_mch_id" placeholder="微信支付商户号" />
</el-form-item>
<el-form-item label="API密钥">
<el-input v-model="channelConfig.wechat_api_key" type="password" show-password placeholder="APIv2/v3密钥" />
</el-form-item>
<el-form-item label="回调地址">
<el-input v-model="channelConfig.wechat_notify_url" placeholder="支付结果通知URL" />
</el-form-item>
</template>
<template v-else-if="form.type === 'bepusdt'">
<el-form-item label="API地址">
<el-input v-model="channelConfig.bepusdt_api_url" placeholder="如:https://pay.example.com" />
</el-form-item>
<el-form-item label="商户ID">
<el-input v-model="channelConfig.bepusdt_merchant_id" placeholder="BepUsdt商户ID" />
</el-form-item>
<el-form-item label="商户密钥">
<el-input v-model="channelConfig.bepusdt_secret_key" type="password" show-password placeholder="商户密钥" />
</el-form-item>
<el-form-item label="回调地址">
<el-input v-model="channelConfig.bepusdt_notify_url" placeholder="支付结果通知URL" />
</el-form-item>
<el-form-item label="USDT网络">
<el-select v-model="channelConfig.bepusdt_network" placeholder="选择网络">
<el-option label="TRC20" value="trc20" />
<el-option label="ERC20" value="erc20" />
<el-option label="BEP20" value="bep20" />
</el-select>
</el-form-item>
</template>
<template v-else-if="form.type === 'bank'">
<el-form-item label="银行名称">
<el-input v-model="channelConfig.bank_name" placeholder="如:中国工商银行" />
</el-form-item>
<el-form-item label="开户人">
<el-input v-model="channelConfig.bank_account_name" placeholder="持卡人姓名" />
</el-form-item>
<el-form-item label="银行卡号">
<el-input v-model="channelConfig.bank_account_number" placeholder="银行卡号" />
</el-form-item>
<el-form-item label="开户行">
<el-input v-model="channelConfig.bank_branch" placeholder="开户支行" />
</el-form-item>
</template>
<template v-else>
<el-form-item label="配置信息">
<el-input v-model="form.config" type="textarea" :rows="4" placeholder="JSON格式的配置信息" />
</el-form-item>
</template>
<el-form-item label="排序">
<el-input-number v-model="form.sort_order" :min="0" />
</el-form-item>
@@ -113,18 +202,51 @@ const form = reactive({
sort_order: 0
})
const typeNames: Record<string, string> = {
alipay: '支付宝',
wechat: '微信支付',
bank: '银行卡',
usdt: 'USDT',
other: '其他'
const channelConfig = reactive<Record<string, string>>({})
const channelTypes = [
{ value: 'alipay', label: '支付宝', icon: '<svg viewBox="0 0 24 24" width="16" height="16" fill="#1677ff"><rect rx="4" width="24" height="24" fill="#1677ff"/><text x="12" y="17" text-anchor="middle" fill="#fff" font-size="12" font-weight="bold">支</text></svg>' },
{ value: 'wechat', label: '微信支付', icon: '<svg viewBox="0 0 24 24" width="16" height="16"><rect rx="4" width="24" height="24" fill="#07c160"/><text x="12" y="17" text-anchor="middle" fill="#fff" font-size="12" font-weight="bold">微</text></svg>' },
{ value: 'bepusdt', label: 'BepUsdt', icon: '<svg viewBox="0 0 24 24" width="16" height="16"><rect rx="4" width="24" height="24" fill="#26a17b"/><text x="12" y="17" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">₮</text></svg>' },
{ value: 'bank', label: '银行卡', icon: '<svg viewBox="0 0 24 24" width="16" height="16"><rect rx="4" width="24" height="24" fill="#e6a23c"/><text x="12" y="17" text-anchor="middle" fill="#fff" font-size="12" font-weight="bold">卡</text></svg>' },
{ value: 'other', label: '其他', icon: '<svg viewBox="0 0 24 24" width="16" height="16"><rect rx="4" width="24" height="24" fill="#909399"/><text x="12" y="17" text-anchor="middle" fill="#fff" font-size="14" font-weight="bold">…</text></svg>' },
]
const typeNames: Record<string, string> = {}
channelTypes.forEach(t => { typeNames[t.value] = t.label })
const availableIcons = [
{ name: 'alipay', label: '支付宝', svg: '<svg viewBox="0 0 24 24" width="28" height="28"><rect rx="4" width="24" height="24" fill="#1677ff"/><text x="12" y="17" text-anchor="middle" fill="#fff" font-size="12" font-weight="bold">支</text></svg>' },
{ name: 'wechat', label: '微信', svg: '<svg viewBox="0 0 24 24" width="28" height="28"><rect rx="4" width="24" height="24" fill="#07c160"/><text x="12" y="17" text-anchor="middle" fill="#fff" font-size="12" font-weight="bold">微</text></svg>' },
{ name: 'usdt', label: 'USDT', svg: '<svg viewBox="0 0 24 24" width="28" height="28"><rect rx="4" width="24" height="24" fill="#26a17b"/><text x="12" y="17" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">₮</text></svg>' },
{ name: 'bank', label: '银行卡', svg: '<svg viewBox="0 0 24 24" width="28" height="28"><rect rx="4" width="24" height="24" fill="#e6a23c"/><text x="12" y="17" text-anchor="middle" fill="#fff" font-size="12" font-weight="bold">卡</text></svg>' },
{ name: 'visa', label: 'VISA', svg: '<svg viewBox="0 0 24 24" width="28" height="28"><rect rx="4" width="24" height="24" fill="#1a1f71"/><text x="12" y="16" text-anchor="middle" fill="#fff" font-size="8" font-weight="bold">VISA</text></svg>' },
{ name: 'mastercard', label: 'MasterCard', svg: '<svg viewBox="0 0 24 24" width="28" height="28"><rect rx="4" width="24" height="24" fill="#252525"/><circle cx="9" cy="12" r="6" fill="#eb001b" opacity="0.8"/><circle cx="15" cy="12" r="6" fill="#f79e1b" opacity="0.8"/></svg>' },
{ name: 'crypto', label: '加密货币', svg: '<svg viewBox="0 0 24 24" width="28" height="28"><rect rx="4" width="24" height="24" fill="#f7931a"/><text x="12" y="17" text-anchor="middle" fill="#fff" font-size="14" font-weight="bold">₿</text></svg>' },
]
const iconSvgMap: Record<string, string> = {}
availableIcons.forEach(ic => { iconSvgMap[ic.name] = ic.svg })
function getIconSvg(icon: string) {
return iconSvgMap[icon] || iconSvgMap[icon] || `<svg viewBox="0 0 24 24" width="28" height="28"><rect rx="4" width="24" height="24" fill="#909399"/><text x="12" y="17" text-anchor="middle" fill="#fff" font-size="12">?</text></svg>`
}
function getTypeName(type: string) {
return typeNames[type] || type
}
function onTypeChange(type: string) {
const iconMap: Record<string, string> = {
alipay: 'alipay',
wechat: 'wechat',
bepusdt: 'usdt',
bank: 'bank',
}
form.icon = iconMap[type] || ''
Object.keys(channelConfig).forEach(k => delete channelConfig[k])
}
async function fetchChannels() {
try {
const res: any = await adminApi.getPaymentChannels()
@@ -137,34 +259,28 @@ async function fetchChannels() {
function openAdd() {
editing.value = null
Object.assign(form, {
name: '',
code: '',
type: 'alipay',
icon: '',
fee_rate: 0,
min_amount: 0,
max_amount: 0,
config: '',
is_enabled: true,
sort_order: 0
name: '', code: '', type: 'alipay', icon: 'alipay',
fee_rate: 0, min_amount: 0, max_amount: 0, config: '',
is_enabled: true, sort_order: 0
})
Object.keys(channelConfig).forEach(k => delete channelConfig[k])
showAdd.value = true
}
function editChannel(row: any) {
editing.value = row
Object.assign(form, {
name: row.name,
code: row.code,
type: row.type,
icon: row.icon || '',
fee_rate: row.fee_rate || 0,
min_amount: row.min_amount || 0,
max_amount: row.max_amount || 0,
config: row.config || '',
is_enabled: row.is_enabled,
sort_order: row.sort_order || 0
name: row.name, code: row.code, type: row.type, icon: row.icon || '',
fee_rate: row.fee_rate || 0, min_amount: row.min_amount || 0, max_amount: row.max_amount || 0,
config: row.config || '', is_enabled: row.is_enabled, sort_order: row.sort_order || 0
})
Object.keys(channelConfig).forEach(k => delete channelConfig[k])
if (row.config) {
try {
const parsed = JSON.parse(row.config)
Object.assign(channelConfig, parsed)
} catch {}
}
showAdd.value = true
}
@@ -175,10 +291,14 @@ async function saveChannel() {
}
saving.value = true
try {
const payload: any = { ...form }
if (form.type !== 'other' && Object.keys(channelConfig).length > 0) {
payload.config = JSON.stringify(channelConfig)
}
if (editing.value) {
await adminApi.updatePaymentChannel(editing.value.id, form)
await adminApi.updatePaymentChannel(editing.value.id, payload)
} else {
await adminApi.createPaymentChannel(form)
await adminApi.createPaymentChannel(payload)
}
ElMessage.success('保存成功')
showAdd.value = false
@@ -217,6 +337,43 @@ onMounted(fetchChannels)
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
.table-card { background: #2d2d44; border: 1px solid rgba(255, 255, 255, 0.06); border-radius: 12px; padding: 20px; }
.channel-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
}
.icon-selector {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.icon-option {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
cursor: pointer;
transition: all 0.15s;
background: rgba(255, 255, 255, 0.03);
&:hover {
border-color: rgba(78, 110, 242, 0.4);
background: rgba(78, 110, 242, 0.08);
}
&.active {
border-color: #4e6ef2;
background: rgba(78, 110, 242, 0.15);
}
}
:deep(.el-table) {
--el-table-bg-color: transparent;
--el-table-tr-bg-color: transparent;
@@ -302,4 +459,9 @@ onMounted(fetchChannels)
color: #fff;
&::placeholder { color: rgba(255, 255, 255, 0.3); }
}
:deep(.el-divider__text) {
color: rgba(255, 255, 255, 0.6);
background: #2d2d44;
}
</style>
-45
View File
@@ -14,25 +14,6 @@
<el-form-item><el-button type="primary" @click="saveSettings">保存</el-button></el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="支付设置">
<el-form label-width="160px">
<el-form-item label="启用支付方式">
<el-checkbox-group v-model="enabledPayments">
<el-checkbox label="balance">余额支付</el-checkbox>
<el-checkbox label="alipay">支付宝</el-checkbox>
<el-checkbox label="wechat">微信支付</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-divider content-position="left">支付宝配置</el-divider>
<el-form-item label="支付宝AppID"><el-input v-model="alipayConfig.app_id" placeholder="应用ID" /></el-form-item>
<el-form-item label="支付宝私钥"><el-input v-model="alipayConfig.private_key" type="textarea" :rows="3" placeholder="应用私钥" /></el-form-item>
<el-divider content-position="left">微信支付配置</el-divider>
<el-form-item label="微信AppID"><el-input v-model="wechatConfig.app_id" placeholder="公众号/小程序AppID" /></el-form-item>
<el-form-item label="微信MchID"><el-input v-model="wechatConfig.mch_id" placeholder="商户号" /></el-form-item>
<el-form-item label="微信API密钥"><el-input v-model="wechatConfig.api_key" type="password" show-password placeholder="APIv2密钥" /></el-form-item>
<el-form-item><el-button type="primary" @click="savePayment">保存</el-button></el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="SMTP设置">
<el-form :model="smtp" label-width="160px">
<el-form-item label="SMTP服务器"><el-input v-model="smtp.smtp_host" /></el-form-item>
@@ -62,9 +43,6 @@ const settings = reactive<Record<string, any>>({
invite_credit_reward: 0
})
const verifyEnabled = ref('true')
const enabledPayments = ref<string[]>(['balance'])
const alipayConfig = reactive({ app_id: '', private_key: '' })
const wechatConfig = reactive({ app_id: '', mch_id: '', api_key: '' })
const smtp = reactive({ smtp_host: '', smtp_port: '587', smtp_user: '', smtp_password: '', smtp_from: '' })
async function fetchSettings() {
@@ -72,20 +50,6 @@ async function fetchSettings() {
const data = res.data || {}
Object.keys(settings).forEach(k => { if (data[k] !== undefined) settings[k] = data[k] })
verifyEnabled.value = data.verification_code_enabled || 'true'
if (data.enabled_payments) {
try {
enabledPayments.value = JSON.parse(data.enabled_payments)
} catch { enabledPayments.value = ['balance'] }
}
if (data.alipay_config) {
try { Object.assign(alipayConfig, JSON.parse(data.alipay_config)) } catch {}
}
if (data.wechat_config) {
try { Object.assign(wechatConfig, JSON.parse(data.wechat_config)) } catch {}
}
Object.keys(smtp).forEach(k => { if (data[k] !== undefined) (smtp as any)[k] = data[k] })
}
@@ -94,15 +58,6 @@ async function saveSettings() {
ElMessage.success('保存成功')
}
async function savePayment() {
await adminApi.updatePayment({
enabled_payments: JSON.stringify(enabledPayments.value),
alipay_config: JSON.stringify(alipayConfig),
wechat_config: JSON.stringify(wechatConfig)
})
ElMessage.success('保存成功')
}
async function saveSMTP() {
await adminApi.updateSMTP(smtp)
ElMessage.success('保存成功')