支持后台支付配置管理
This commit is contained in:
Vendored
+2
@@ -23,6 +23,8 @@ declare module 'vue' {
|
||||
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { apiClient } from '@/shared/api/client'
|
||||
import type { ApiResponse } from '@/shared/types/types'
|
||||
|
||||
export interface PaymentConfig {
|
||||
id: number
|
||||
name: string
|
||||
provider: string
|
||||
merchant_id: string
|
||||
gateway_url: string
|
||||
sign_key?: string
|
||||
notify_key?: string
|
||||
notify_url: string
|
||||
jump_url: string
|
||||
pay_way: string
|
||||
jspay_flag: string
|
||||
sign_type: string
|
||||
extra_config: Record<string, any> | null
|
||||
is_default: boolean
|
||||
status: string
|
||||
environment: string
|
||||
business_tags: string[] | null
|
||||
total_transactions: number
|
||||
total_amount_cent: number
|
||||
last_used_at: string | null
|
||||
created_by: number | null
|
||||
updated_by: number | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface PaymentConfigListResponse {
|
||||
items: PaymentConfig[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export interface CreatePaymentConfigRequest {
|
||||
name: string
|
||||
provider: string
|
||||
merchant_id: string
|
||||
gateway_url?: string
|
||||
sign_key?: string
|
||||
notify_key?: string
|
||||
notify_url?: string
|
||||
jump_url?: string
|
||||
pay_way?: string
|
||||
jspay_flag?: string
|
||||
sign_type?: string
|
||||
extra_config?: Record<string, any>
|
||||
is_default?: boolean
|
||||
status?: string
|
||||
environment?: string
|
||||
business_tags?: string[]
|
||||
}
|
||||
|
||||
export interface UpdatePaymentConfigRequest {
|
||||
name?: string
|
||||
merchant_id?: string
|
||||
gateway_url?: string
|
||||
sign_key?: string
|
||||
notify_key?: string
|
||||
notify_url?: string
|
||||
jump_url?: string
|
||||
pay_way?: string
|
||||
jspay_flag?: string
|
||||
sign_type?: string
|
||||
extra_config?: Record<string, any>
|
||||
is_default?: boolean
|
||||
status?: string
|
||||
environment?: string
|
||||
business_tags?: string[]
|
||||
}
|
||||
|
||||
export async function fetchPaymentConfigs(params?: {
|
||||
provider?: string
|
||||
status?: string
|
||||
environment?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}) {
|
||||
const { data } = await apiClient.get<ApiResponse<PaymentConfigListResponse>>('/admin/payment-configs', {
|
||||
params,
|
||||
})
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchPaymentConfig(id: number, includeSecret = false) {
|
||||
const { data } = await apiClient.get<ApiResponse<PaymentConfig>>(`/admin/payment-configs/${id}`, {
|
||||
params: { include_secret: includeSecret },
|
||||
})
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function createPaymentConfig(payload: CreatePaymentConfigRequest) {
|
||||
const { data } = await apiClient.post<ApiResponse<PaymentConfig>>('/admin/payment-configs', payload)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function updatePaymentConfig(id: number, payload: UpdatePaymentConfigRequest) {
|
||||
const { data } = await apiClient.put<ApiResponse<PaymentConfig>>(`/admin/payment-configs/${id}`, payload)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function deletePaymentConfig(id: number) {
|
||||
const { data } = await apiClient.delete<ApiResponse<{ message: string }>>(`/admin/payment-configs/${id}`)
|
||||
return data.data
|
||||
}
|
||||
@@ -101,6 +101,10 @@ const resourceLabels: Record<string, string> = {
|
||||
dispute: '仲裁中心',
|
||||
chat: '客服群聊',
|
||||
wallet: '资金流水',
|
||||
withdrawal: '提现审核',
|
||||
payment_config: '支付配置',
|
||||
announcement: '公告管理',
|
||||
notification: '通知管理',
|
||||
admin_user: '管理员管理',
|
||||
role: '角色管理',
|
||||
system_config: '系统配置',
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import {
|
||||
createPaymentConfig,
|
||||
updatePaymentConfig,
|
||||
fetchPaymentConfig,
|
||||
type PaymentConfig,
|
||||
type CreatePaymentConfigRequest,
|
||||
type UpdatePaymentConfigRequest,
|
||||
} from '@/features/admin/api/paymentConfig'
|
||||
import { readError } from '@/utils/error'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
mode: 'create' | 'edit' | 'view'
|
||||
config: PaymentConfig | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', val: boolean): void
|
||||
(e: 'close'): void
|
||||
(e: 'saved'): void
|
||||
}>()
|
||||
|
||||
const submitting = ref(false)
|
||||
const showSecrets = ref(false)
|
||||
const loadingSecrets = ref(false)
|
||||
const advancedOpen = ref<string[]>([])
|
||||
|
||||
const formData = ref<CreatePaymentConfigRequest>({
|
||||
name: '',
|
||||
provider: 'leshua',
|
||||
merchant_id: '',
|
||||
gateway_url: 'https://paygate.leshuazf.com/cgi-bin/lepos_pay_gateway.cgi',
|
||||
sign_key: '',
|
||||
notify_key: '',
|
||||
notify_url: '',
|
||||
jump_url: '',
|
||||
pay_way: 'ZFBZF',
|
||||
jspay_flag: '2',
|
||||
sign_type: 'MD5',
|
||||
is_default: false,
|
||||
status: 'active',
|
||||
environment: 'production',
|
||||
business_tags: [],
|
||||
})
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
const titles = {
|
||||
create: '新增支付配置',
|
||||
edit: '编辑支付配置',
|
||||
view: '查看支付配置',
|
||||
}
|
||||
return titles[props.mode]
|
||||
})
|
||||
|
||||
const isReadonly = computed(() => props.mode === 'view')
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val && props.config) {
|
||||
formData.value = {
|
||||
name: props.config.name,
|
||||
provider: props.config.provider,
|
||||
merchant_id: props.config.merchant_id,
|
||||
gateway_url: props.config.gateway_url,
|
||||
notify_url: props.config.notify_url,
|
||||
jump_url: props.config.jump_url,
|
||||
pay_way: props.config.pay_way,
|
||||
jspay_flag: props.config.jspay_flag,
|
||||
sign_type: props.config.sign_type,
|
||||
is_default: props.config.is_default,
|
||||
status: props.config.status,
|
||||
environment: props.config.environment,
|
||||
business_tags: props.config.business_tags || [],
|
||||
}
|
||||
showSecrets.value = false
|
||||
advancedOpen.value = []
|
||||
} else if (val && !props.config) {
|
||||
// 重置表单
|
||||
formData.value = {
|
||||
name: '',
|
||||
provider: 'leshua',
|
||||
merchant_id: '',
|
||||
gateway_url: 'https://paygate.leshuazf.com/cgi-bin/lepos_pay_gateway.cgi',
|
||||
sign_key: '',
|
||||
notify_key: '',
|
||||
notify_url: '',
|
||||
jump_url: '',
|
||||
pay_way: 'ZFBZF',
|
||||
jspay_flag: '2',
|
||||
sign_type: 'MD5',
|
||||
is_default: false,
|
||||
status: 'active',
|
||||
environment: 'production',
|
||||
business_tags: [],
|
||||
}
|
||||
showSecrets.value = false
|
||||
advancedOpen.value = []
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
async function handleViewSecrets() {
|
||||
if (!props.config) return
|
||||
loadingSecrets.value = true
|
||||
try {
|
||||
const config = await fetchPaymentConfig(props.config.id, true)
|
||||
formData.value.sign_key = config.sign_key || ''
|
||||
formData.value.notify_key = config.notify_key || ''
|
||||
showSecrets.value = true
|
||||
ElMessage.success('密钥已加载')
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '加载密钥失败'))
|
||||
} finally {
|
||||
loadingSecrets.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
submitting.value = true
|
||||
try {
|
||||
if (props.mode === 'create') {
|
||||
await createPaymentConfig(formData.value)
|
||||
ElMessage.success('创建成功')
|
||||
} else if (props.mode === 'edit') {
|
||||
if (!props.config) return
|
||||
const payload: UpdatePaymentConfigRequest = {
|
||||
name: formData.value.name,
|
||||
merchant_id: formData.value.merchant_id,
|
||||
gateway_url: formData.value.gateway_url,
|
||||
notify_url: formData.value.notify_url,
|
||||
jump_url: formData.value.jump_url,
|
||||
pay_way: formData.value.pay_way,
|
||||
jspay_flag: formData.value.jspay_flag,
|
||||
sign_type: formData.value.sign_type,
|
||||
is_default: formData.value.is_default,
|
||||
status: formData.value.status,
|
||||
environment: formData.value.environment,
|
||||
business_tags: formData.value.business_tags,
|
||||
}
|
||||
// 只有在修改了密钥时才传递
|
||||
if (formData.value.sign_key) {
|
||||
payload.sign_key = formData.value.sign_key
|
||||
}
|
||||
if (formData.value.notify_key) {
|
||||
payload.notify_key = formData.value.notify_key
|
||||
}
|
||||
await updatePaymentConfig(props.config.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
emit('saved')
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '保存失败'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
emit('update:modelValue', false)
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog :model-value="modelValue" :title="dialogTitle" width="700px" @close="handleClose">
|
||||
<el-form :model="formData" label-width="120px">
|
||||
<el-form-item label="配置名称" required>
|
||||
<el-input v-model="formData.name" placeholder="如:乐刷生产商户1" :disabled="isReadonly" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="支付服务商" required>
|
||||
<el-select v-model="formData.provider" :disabled="mode !== 'create'">
|
||||
<el-option label="乐刷支付" value="leshua" />
|
||||
<el-option label="模拟支付" value="mock" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="商户号" required>
|
||||
<el-input v-model="formData.merchant_id" placeholder="乐刷商户号" :disabled="isReadonly" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="网关地址" required>
|
||||
<el-input v-model="formData.gateway_url" placeholder="支付网关URL" :disabled="isReadonly" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="签名密钥" required>
|
||||
<el-input
|
||||
v-model="formData.sign_key"
|
||||
type="password"
|
||||
show-password
|
||||
:placeholder="mode === 'edit' ? '留空则不修改' : '请输入签名密钥'"
|
||||
:disabled="isReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="通知密钥" required>
|
||||
<el-input
|
||||
v-model="formData.notify_key"
|
||||
type="password"
|
||||
show-password
|
||||
:placeholder="mode === 'edit' ? '留空则不修改' : '请输入通知密钥'"
|
||||
:disabled="isReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="mode === 'view' && !showSecrets" label="">
|
||||
<el-button :loading="loadingSecrets" @click="handleViewSecrets">查看密钥</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="回调地址" required>
|
||||
<el-input v-model="formData.notify_url" placeholder="异步通知回调地址" :disabled="isReadonly" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="是否默认">
|
||||
<el-switch v-model="formData.is_default" :disabled="isReadonly" />
|
||||
<span class="form-hint">每个服务商只能有一个默认配置</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="formData.status" :disabled="isReadonly">
|
||||
<el-option label="启用" value="active" />
|
||||
<el-option label="禁用" value="disabled" />
|
||||
<el-option label="测试中" value="testing" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="环境">
|
||||
<el-select v-model="formData.environment" :disabled="isReadonly">
|
||||
<el-option label="生产环境" value="production" />
|
||||
<el-option label="测试环境" value="sandbox" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-collapse v-model="advancedOpen" class="advanced-config">
|
||||
<el-collapse-item name="advanced">
|
||||
<template #title>
|
||||
<span class="advanced-title">高级配置</span>
|
||||
</template>
|
||||
|
||||
<el-form-item label="跳转地址">
|
||||
<el-input v-model="formData.jump_url" placeholder="支付完成跳转地址" :disabled="isReadonly" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="支付方式">
|
||||
<el-select v-model="formData.pay_way" :disabled="isReadonly">
|
||||
<el-option label="支付宝" value="ZFBZF" />
|
||||
<el-option label="微信" value="WXZF" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="支付形态">
|
||||
<el-select v-model="formData.jspay_flag" :disabled="isReadonly">
|
||||
<el-option label="H5 / JSAPI" value="1" />
|
||||
<el-option label="简易支付 / 收银台" value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="签名类型">
|
||||
<el-select v-model="formData.sign_type" :disabled="isReadonly">
|
||||
<el-option label="MD5" value="MD5" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">{{ isReadonly ? '关闭' : '取消' }}</el-button>
|
||||
<el-button v-if="!isReadonly" type="primary" :loading="submitting" @click="handleSave">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.advanced-config {
|
||||
margin-top: 4px;
|
||||
border-top: 1px solid #edf0f5;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.advanced-config :deep(.el-collapse-item__header) {
|
||||
height: 44px;
|
||||
padding-left: 120px;
|
||||
border-bottom: 0;
|
||||
color: #4b5563;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.advanced-config :deep(.el-collapse-item__wrap) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.advanced-config :deep(.el-collapse-item__content) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.advanced-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
margin-left: 8px;
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
User,
|
||||
UserFilled,
|
||||
Wallet,
|
||||
Setting
|
||||
Setting,
|
||||
CreditCard
|
||||
} from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, ref } from 'vue'
|
||||
@@ -47,7 +48,8 @@ const allNavItems: NavItem[] = [
|
||||
{ label: '仲裁中心', to: '/admin/disputes', icon: ScaleToOriginal, permission: 'dispute:view' },
|
||||
{ label: '客服群聊', to: '/admin/chats', icon: ChatDotRound, permission: 'chat:view' },
|
||||
{ label: '资金流水', to: '/admin/wallet-ledger', icon: Wallet, permission: 'wallet:view' },
|
||||
{ label: '提现审核', to: '/admin/withdrawals', icon: Money, permission: 'withdrawal:approve' },
|
||||
{ label: '提现审核', to: '/admin/withdrawals', icon: Money, permission: 'withdrawal:list' },
|
||||
{ label: '支付配置', to: '/admin/payment-configs', icon: CreditCard, permission: 'payment_config:list' },
|
||||
{ label: '公告管理', to: '/admin/announcements', icon: Bell, permission: 'announcement:view' },
|
||||
{ label: '系统配置', to: '/admin/system-configs', icon: Operation, permission: 'system_config:view' },
|
||||
{ label: '审计日志', to: '/admin/audit-logs', icon: Document, permission: 'audit_log:view' },
|
||||
|
||||
@@ -82,6 +82,12 @@ export const adminRoutes: RouteRecordRaw[] = [
|
||||
component: () => import('@/features/admin/views/AdminSystemConfigsView.vue'),
|
||||
meta: adminMeta,
|
||||
},
|
||||
{
|
||||
path: '/admin/payment-configs',
|
||||
name: 'admin-payment-configs',
|
||||
component: () => import('@/features/admin/views/AdminPaymentConfigsView.vue'),
|
||||
meta: adminMeta,
|
||||
},
|
||||
{
|
||||
path: '/admin/audit-logs',
|
||||
name: 'admin-audit-logs',
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 从错误对象中提取错误消息
|
||||
* @param error 错误对象
|
||||
* @param fallback 默认错误消息
|
||||
* @returns 错误消息
|
||||
*/
|
||||
export function readError(error: unknown, fallback: string): string {
|
||||
if (typeof error === 'object' && error && 'response' in error) {
|
||||
const response = (error as { response?: { data?: { message?: string } } }).response
|
||||
return response?.data?.message || fallback
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message || fallback
|
||||
}
|
||||
if (typeof error === 'string') {
|
||||
return error
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
Reference in New Issue
Block a user