支持后台支付配置管理

This commit is contained in:
yml
2026-06-06 17:49:22 +08:00
parent 13543b3dcb
commit 4d40883da6
29 changed files with 3173 additions and 198 deletions
@@ -0,0 +1,391 @@
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import { computed, onMounted, ref } from 'vue'
import { Plus, Refresh, View, Edit, Delete } from '@element-plus/icons-vue'
import {
fetchPaymentConfigs,
deletePaymentConfig,
type PaymentConfig,
} from '@/features/admin/api/paymentConfig'
import { formatDateTime } from '@/utils/time'
import { readError } from '@/utils/error'
import PaymentConfigDialog from '../components/PaymentConfigDialog.vue'
const loading = ref(false)
const configs = ref<PaymentConfig[]>([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(20)
const dialogVisible = ref(false)
const dialogMode = ref<'create' | 'edit' | 'view'>('create')
const currentConfig = ref<PaymentConfig | null>(null)
const filterProvider = ref('')
const filterStatus = ref('')
const filterEnvironment = ref('')
const providerOptions = [
{ label: '全部', value: '' },
{ label: '乐刷支付', value: 'leshua' },
{ label: '模拟支付', value: 'mock' },
]
const statusOptions = [
{ label: '全部', value: '' },
{ label: '启用', value: 'active' },
{ label: '禁用', value: 'disabled' },
{ label: '测试中', value: 'testing' },
]
const environmentOptions = [
{ label: '全部', value: '' },
{ label: '生产环境', value: 'production' },
{ label: '测试环境', value: 'sandbox' },
]
const filteredConfigs = computed(() => {
return configs.value
})
onMounted(() => {
loadConfigs()
})
async function loadConfigs() {
loading.value = true
try {
const res = await fetchPaymentConfigs({
provider: filterProvider.value || undefined,
status: filterStatus.value || undefined,
environment: filterEnvironment.value || undefined,
page: currentPage.value,
page_size: pageSize.value,
})
configs.value = res.items
total.value = res.total
} catch (error) {
ElMessage.error(readError(error, '加载配置失败'))
} finally {
loading.value = false
}
}
function handleCreate() {
currentConfig.value = null
dialogMode.value = 'create'
dialogVisible.value = true
}
function handleView(row: PaymentConfig) {
currentConfig.value = row
dialogMode.value = 'view'
dialogVisible.value = true
}
function handleEdit(row: PaymentConfig) {
currentConfig.value = row
dialogMode.value = 'edit'
dialogVisible.value = true
}
async function handleDelete(row: PaymentConfig) {
try {
await ElMessageBox.confirm(`确定要删除配置"${row.name}"吗?`, '确认删除', {
type: 'warning',
})
await deletePaymentConfig(row.id)
ElMessage.success('删除成功')
loadConfigs()
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(readError(error, '删除失败'))
}
}
}
function handleDialogClose() {
dialogVisible.value = false
currentConfig.value = null
}
function handleSaved() {
dialogVisible.value = false
loadConfigs()
}
function formatProvider(provider: string) {
const map: Record<string, string> = {
leshua: '乐刷支付',
mock: '模拟支付',
}
return map[provider] || provider
}
function formatStatus(status: string) {
const map: Record<string, string> = {
active: '启用',
disabled: '禁用',
testing: '测试中',
}
return map[status] || status
}
function formatEnvironment(env: string) {
const map: Record<string, string> = {
production: '生产',
sandbox: '测试',
}
return map[env] || env
}
function formatAmount(amountCent: number) {
return (amountCent / 100).toFixed(2)
}
function getStatusType(status: string) {
const map: Record<string, 'success' | 'info' | 'warning'> = {
active: 'success',
disabled: 'info',
testing: 'warning',
}
return map[status] || 'info'
}
</script>
<template>
<section class="page">
<div class="page-header">
<p class="eyebrow">Payment Configs</p>
<h1>支付配置管理</h1>
<p>管理多个支付商户配置支持乐刷支付等多种支付渠道</p>
</div>
<div class="filter-bar">
<el-select v-model="filterProvider" placeholder="支付服务商" style="width: 150px" @change="loadConfigs">
<el-option v-for="opt in providerOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
<el-select v-model="filterStatus" placeholder="状态" style="width: 120px" @change="loadConfigs">
<el-option v-for="opt in statusOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
<el-select v-model="filterEnvironment" placeholder="环境" style="width: 120px" @change="loadConfigs">
<el-option v-for="opt in environmentOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
<el-button :icon="Refresh" @click="loadConfigs">刷新</el-button>
<el-button type="primary" :icon="Plus" @click="handleCreate">新增配置</el-button>
</div>
<el-table
:data="filteredConfigs"
v-loading="loading"
class="payment-config-table"
stripe
>
<el-table-column prop="name" label="配置名称" min-width="220" show-overflow-tooltip />
<el-table-column prop="provider" label="服务商" width="120">
<template #default="{ row }">
{{ formatProvider(row.provider) }}
</template>
</el-table-column>
<el-table-column prop="merchant_id" label="商户号" min-width="150" />
<el-table-column prop="environment" label="环境" width="86" align="center">
<template #default="{ row }">
<el-tag :type="row.environment === 'production' ? 'success' : 'warning'" size="small">
{{ formatEnvironment(row.environment) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="86" align="center">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">
{{ formatStatus(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="is_default" label="默认" width="86" align="center">
<template #default="{ row }">
<el-tag v-if="row.is_default" type="primary" size="small">默认</el-tag>
</template>
</el-table-column>
<el-table-column label="使用统计" min-width="150">
<template #default="{ row }">
<div class="usage-cell">
<div>交易: {{ row.total_transactions }} </div>
<div>金额: ¥{{ formatAmount(row.total_amount_cent) }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="last_used_at" label="最后使用" min-width="180" show-overflow-tooltip>
<template #default="{ row }">
{{ row.last_used_at ? formatDateTime(row.last_used_at, '-') : '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="226" class-name="operation-column">
<template #default="{ row }">
<div class="action-buttons">
<el-button class="action-button view" size="small" :icon="View" @click="handleView(row)">
查看
</el-button>
<el-button class="action-button edit" size="small" :icon="Edit" @click="handleEdit(row)">
编辑
</el-button>
<el-button class="action-button delete" size="small" :icon="Delete" @click="handleDelete(row)">
删除
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="loadConfigs"
@size-change="loadConfigs"
/>
</div>
<PaymentConfigDialog
v-model="dialogVisible"
:mode="dialogMode"
:config="currentConfig"
@close="handleDialogClose"
@saved="handleSaved"
/>
</section>
</template>
<style scoped>
.page {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header .eyebrow {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #6b7280;
margin-bottom: 8px;
}
.page-header h1 {
font-size: 24px;
font-weight: 600;
margin: 0 0 8px 0;
}
.page-header p {
color: #6b7280;
margin: 0;
}
.filter-bar {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.payment-config-table {
width: 100%;
border-radius: 0;
}
.payment-config-table :deep(.el-table__cell) {
padding: 11px 0;
}
.payment-config-table :deep(.cell) {
padding: 0 14px;
line-height: 1.45;
}
.payment-config-table :deep(th.el-table__cell) {
background: #ffffff;
color: #737b8b;
font-weight: 600;
}
.usage-cell {
display: grid;
gap: 4px;
font-size: 12px;
line-height: 1.35;
color: #3f4654;
white-space: nowrap;
}
.action-buttons {
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.action-buttons :deep(.el-button + .el-button) {
margin-left: 0;
}
.action-button {
min-width: 54px;
height: 28px;
padding: 0 7px;
border-radius: 6px;
font-weight: 500;
}
.payment-config-table :deep(.operation-column .cell) {
padding: 0 10px;
}
.action-button.view {
color: #1d4ed8;
background: #eff6ff;
border-color: #bfdbfe;
}
.action-button.view:hover {
color: #ffffff;
background: #2563eb;
border-color: #2563eb;
}
.action-button.edit {
color: #047857;
background: #ecfdf5;
border-color: #a7f3d0;
}
.action-button.edit:hover {
color: #ffffff;
background: #059669;
border-color: #059669;
}
.action-button.delete {
color: #dc2626;
background: #fef2f2;
border-color: #fecaca;
}
.action-button.delete:hover {
color: #ffffff;
background: #dc2626;
border-color: #dc2626;
}
.pagination {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
</style>