Files
hfb_sys/frontend/src/features/admin/components/PaymentConfigDialog.vue
T
2026-06-09 20:50:57 +08:00

425 lines
13 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 } 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 '@/shared/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 defaultGateways: Record<string, string> = {
leshua: 'https://paygate.leshuazf.com/cgi-bin/lepos_pay_gateway.cgi',
lakala: 'https://test.wsmsd.cn/sit',
mock: '',
}
function defaultExtraConfig(provider: string) {
if (provider === 'lakala') {
return {
app_id: '',
serial_no: '',
term_no: '',
lakala_cert: '',
pay_mode: 'AUTO',
order_expire_minutes: 15,
}
}
return {}
}
function makeDefaultForm(provider = 'leshua'): CreatePaymentConfigRequest {
return {
name: '',
provider,
merchant_id: '',
gateway_url: defaultGateways[provider] || '',
sign_key: '',
notify_key: '',
notify_url: '',
jump_url: '',
pay_way: 'ZFBZF',
jspay_flag: '2',
sign_type: provider === 'lakala' ? 'SHA256withRSA' : 'MD5',
extra_config: defaultExtraConfig(provider),
is_default: false,
status: 'active',
environment: provider === 'lakala' ? 'sandbox' : 'production',
business_tags: [],
}
}
const formData = ref<CreatePaymentConfigRequest>(makeDefaultForm())
const dialogTitle = computed(() => {
const titles = {
create: '新增支付配置',
edit: '编辑支付配置',
view: '查看支付配置',
}
return titles[props.mode]
})
const isReadonly = computed(() => props.mode === 'view')
const isLakala = computed(() => formData.value.provider === 'lakala')
const isMock = computed(() => formData.value.provider === 'mock')
const signKeyLabel = computed(() => (isLakala.value ? '商户私钥' : '签名密钥'))
const notifyKeyLabel = computed(() => (isLakala.value ? '通知证书' : '通知密钥'))
const extraConfig = computed<Record<string, any>>(() => formData.value.extra_config || {})
function normalizeExtraConfig(provider: string, extraConfig?: Record<string, any> | null) {
const extra = {
...defaultExtraConfig(provider),
...(extraConfig || {}),
}
if (provider === 'lakala' && !extra.pay_mode) {
extra.pay_mode = 'AUTO'
}
return extra
}
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,
extra_config: normalizeExtraConfig(props.config.provider, props.config.extra_config),
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 = makeDefaultForm()
showSecrets.value = false
advancedOpen.value = []
}
},
{ immediate: true }
)
function handleProviderChange(provider: string) {
if (props.mode !== 'create') return
const currentName = formData.value.name
const currentNotifyURL = formData.value.notify_url
formData.value = {
...makeDefaultForm(provider),
name: currentName,
notify_url: currentNotifyURL,
}
}
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,
extra_config: formData.value.extra_config,
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'"
@change="handleProviderChange"
>
<el-option label="乐刷支付" value="leshua" />
<el-option label="拉卡拉支付" value="lakala" />
<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 v-if="!isMock" label="网关地址" required>
<el-input v-model="formData.gateway_url" placeholder="支付网关URL" :disabled="isReadonly" />
</el-form-item>
<el-form-item v-if="isLakala" label="App ID" required>
<el-input
v-model="extraConfig.app_id"
placeholder="拉卡拉开放平台 appId"
:disabled="isReadonly"
/>
</el-form-item>
<el-form-item v-if="isLakala" label="证书序列号" required>
<el-input
v-model="extraConfig.serial_no"
placeholder="商户证书序列号"
:disabled="isReadonly"
/>
</el-form-item>
<el-form-item v-if="isLakala" label="终端号" required>
<el-input v-model="extraConfig.term_no" placeholder="拉卡拉终端号" :disabled="isReadonly" />
</el-form-item>
<el-form-item v-if="!isMock" :label="signKeyLabel" required>
<el-input
v-model="formData.sign_key"
:type="isLakala ? 'textarea' : 'password'"
:rows="isLakala ? 5 : undefined"
:show-password="!isLakala"
:placeholder="
mode === 'edit' ? '留空则不修改' : isLakala ? '请输入商户私钥 PEM' : '请输入签名密钥'
"
:disabled="isReadonly"
/>
</el-form-item>
<el-form-item v-if="!isMock" :label="notifyKeyLabel" required>
<el-input
v-model="formData.notify_key"
:type="isLakala ? 'textarea' : 'password'"
:rows="isLakala ? 5 : undefined"
:show-password="!isLakala"
:placeholder="
mode === 'edit'
? '留空则不修改'
: isLakala
? '请输入拉卡拉通知验签证书 PEM'
: '请输入通知密钥'
"
:disabled="isReadonly"
/>
</el-form-item>
<el-form-item v-if="isLakala" label="平台证书">
<el-input
v-model="extraConfig.lakala_cert"
type="textarea"
:rows="4"
placeholder="拉卡拉平台响应验签证书 PEM,可与通知证书一致"
: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 v-if="!isMock" 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 v-if="isLakala" label="拉卡拉模式">
<el-select v-model="extraConfig.pay_mode" :disabled="isReadonly">
<el-option label="不限制,由收银台选择" value="AUTO" />
<el-option label="支付宝" value="ALIPAY" />
<el-option label="微信" value="WECHAT" />
<el-option label="银联" value="UQRCODEPAY" />
</el-select>
</el-form-item>
<el-form-item v-if="isLakala" label="有效期分钟">
<el-input-number
v-model="extraConfig.order_expire_minutes"
:min="1"
:max="1440"
:disabled="isReadonly"
/>
</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-option label="SHA256withRSA" value="SHA256withRSA" />
</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>