Files
hfb_sys/frontend/src/features/admin/views/AdminPaymentConfigsView.vue
T
2026-06-06 23:43:57 +08:00

416 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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: 'lakala' },
{ 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) {
if (!canDeleteConfig(row)) {
ElMessage.warning('该配置已有交易记录,需要保留用于退款、查询和回调验签')
return
}
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: '乐刷支付',
lakala: '拉卡拉支付',
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'
}
function canDeleteConfig(row: PaymentConfig) {
return row.total_transactions <= 0
}
function deleteDisabledReason(row: PaymentConfig) {
return canDeleteConfig(row) ? '删除配置' : '已有交易记录,需要保留用于退款、查询和回调验签'
}
</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-tooltip :content="deleteDisabledReason(row)" placement="top">
<span>
<el-button
class="action-button delete"
size="small"
:icon="Delete"
:disabled="!canDeleteConfig(row)"
@click="handleDelete(row)"
>
删除
</el-button>
</span>
</el-tooltip>
</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>