88 lines
7.8 KiB
Vue
88 lines
7.8 KiB
Vue
<template>
|
|
<div class="integration-config-page">
|
|
<div class="page-heading">
|
|
<div><h2>支付渠道配置</h2><p>统一管理支付渠道凭证、接口地址和默认交易参数</p></div>
|
|
<el-button :loading="loading" :icon="Refresh" @click="load">刷新</el-button>
|
|
</div>
|
|
<div v-loading="loading" class="config-layout">
|
|
<aside class="provider-panel">
|
|
<div class="panel-title">渠道 <span>{{ configs.length }}</span></div>
|
|
<button v-for="item in configs" :key="item.provider" type="button" class="provider-item" :class="{ active: selected?.provider === item.provider }" @click="select(item)">
|
|
<span class="provider-copy"><strong>{{ item.name || item.provider }}</strong><small>{{ item.provider }}</small></span>
|
|
<el-tag :type="item.enabled ? 'success' : 'info'" size="small">{{ item.enabled ? '启用' : '停用' }}</el-tag>
|
|
</button>
|
|
</aside>
|
|
<section v-if="selected" class="editor-panel">
|
|
<div class="editor-heading">
|
|
<div><div class="editor-title">{{ selected.name || selected.provider }}</div><div class="editor-subtitle">{{ selected.description || `provider: ${selected.provider}` }}</div></div>
|
|
<el-switch v-model="selected.enabled" active-text="启用渠道" @change="save" />
|
|
</div>
|
|
<el-alert v-if="selected.configured" title="密钥字段已脱敏,保留 ****** 表示继续使用当前密钥。" type="info" :closable="false" class="editor-alert" />
|
|
<el-alert v-else title="该渠道尚未保存,填写字段后保存即可创建配置。" type="warning" :closable="false" class="editor-alert" />
|
|
<el-form label-position="top" class="config-form">
|
|
<div class="field-grid">
|
|
<el-form-item v-for="field in selected.fields" :key="field.key" :label="field.label" :required="field.required">
|
|
<template #label><span>{{ field.label }}</span><span class="field-key">{{ field.key }}</span></template>
|
|
<el-select v-if="field.type === 'select'" v-model="selected.config[field.key]" class="field-control" filterable>
|
|
<el-option v-for="option in field.options" :key="String(option.value)" :label="option.label" :value="option.value" />
|
|
</el-select>
|
|
<el-switch v-else-if="field.type === 'switch'" v-model="selected.config[field.key]" />
|
|
<el-input v-else-if="field.type === 'textarea'" v-model="selected.config[field.key]" class="field-control" type="textarea" :rows="4" :show-password="field.secret" spellcheck="false" />
|
|
<el-input v-else v-model="selected.config[field.key]" class="field-control" :type="field.secret ? 'password' : field.type === 'number' ? 'number' : 'text'" :show-password="field.secret" spellcheck="false" />
|
|
</el-form-item>
|
|
</div>
|
|
</el-form>
|
|
<div class="editor-actions">
|
|
<el-button type="primary" :loading="saving" :icon="Check" @click="save">保存配置</el-button>
|
|
<el-button v-if="selected.configured" type="danger" plain :icon="Delete" @click="remove">删除配置</el-button>
|
|
<el-button text :icon="DocumentCopy" @click="copyConfig">复制 JSON</el-button>
|
|
</div>
|
|
</section>
|
|
<el-empty v-else description="暂无支付渠道" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref } from 'vue'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { Check, Delete, DocumentCopy, Refresh } from '@element-plus/icons-vue'
|
|
import { deleteIntegrationConfig, getIntegrationConfigs, saveIntegrationConfig } from '@/api/integration'
|
|
|
|
const configs = ref([]); const selectedProvider = ref(''); const loading = ref(false); const saving = ref(false)
|
|
const selected = computed(() => configs.value.find((item) => item.provider === selectedProvider.value) || configs.value[0])
|
|
const select = (item) => { selectedProvider.value = item.provider }
|
|
const load = async () => { loading.value = true; try { const res = await getIntegrationConfigs('payment'); if (res.code === 0) { configs.value = (res.data || []).map((item) => ({ ...item, config: { ...(item.config || {}) } })); if (!configs.value.some((item) => item.provider === selectedProvider.value)) selectedProvider.value = configs.value[0]?.provider || '' } } finally { loading.value = false } }
|
|
const save = async () => { if (!selected.value) return; saving.value = true; try { const res = await saveIntegrationConfig('payment', selected.value.provider, { enabled: selected.value.enabled, config: selected.value.config }); if (res.code === 0) { ElMessage.success('支付渠道配置已保存'); await load() } } finally { saving.value = false } }
|
|
const remove = async () => { if (!selected.value) return; await ElMessageBox.confirm(`确认删除 ${selected.value.name || selected.value.provider} 配置吗?`, '删除配置', { type: 'warning' }); const res = await deleteIntegrationConfig('payment', selected.value.provider); if (res.code === 0) { ElMessage.success('配置已删除'); await load() } }
|
|
const copyConfig = async () => { if (!selected.value) return; await navigator.clipboard.writeText(JSON.stringify(selected.value.config || {}, null, 2)); ElMessage.success('JSON 已复制') }
|
|
load()
|
|
</script>
|
|
|
|
<style scoped>
|
|
.integration-config-page { padding: 4px 0 24px; }
|
|
.page-heading { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; }
|
|
.page-heading h2 { margin: 0; color: var(--el-text-color-primary); font-size: 20px; font-weight: 600; }
|
|
.page-heading p { margin: 6px 0 0; color: var(--el-text-color-secondary); font-size: 13px; }
|
|
.config-layout { display: grid; grid-template-columns: 250px minmax(0, 1fr); min-height: 620px; border: 1px solid var(--el-border-color-lighter); background: var(--el-bg-color); }
|
|
.provider-panel { border-right: 1px solid var(--el-border-color-lighter); padding: 14px 10px; }
|
|
.panel-title { display: flex; justify-content: space-between; padding: 2px 10px 12px; color: var(--el-text-color-primary); font-size: 14px; font-weight: 600; }
|
|
.panel-title span { color: var(--el-text-color-secondary); font-weight: 400; }
|
|
.provider-item { display: flex; align-items: center; justify-content: space-between; width: 100%; min-height: 54px; padding: 9px 10px; border: 0; border-left: 3px solid transparent; background: transparent; color: inherit; text-align: left; cursor: pointer; }
|
|
.provider-item:hover { background: var(--el-fill-color-light); }
|
|
.provider-item.active { border-left-color: var(--el-color-primary); background: var(--el-color-primary-light-9); }
|
|
.provider-copy { display: grid; gap: 3px; min-width: 0; }
|
|
.provider-copy strong { overflow: hidden; color: var(--el-text-color-primary); font-size: 14px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
|
|
.provider-copy small { color: var(--el-text-color-secondary); font-size: 11px; }
|
|
.editor-panel { min-width: 0; padding: 22px 28px 24px; }
|
|
.editor-heading { display: flex; align-items: center; justify-content: space-between; gap: 18px; padding-bottom: 18px; border-bottom: 1px solid var(--el-border-color-lighter); }
|
|
.editor-title { color: var(--el-text-color-primary); font-size: 18px; font-weight: 600; }
|
|
.editor-subtitle { margin-top: 5px; color: var(--el-text-color-secondary); font-size: 13px; }
|
|
.editor-alert { margin: 18px 0; }
|
|
.field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 22px; }
|
|
.field-key { margin-left: 8px; color: var(--el-text-color-placeholder); font-size: 11px; font-weight: 400; }
|
|
.field-control { width: 100%; }
|
|
.editor-actions { display: flex; align-items: center; gap: 10px; padding-top: 8px; border-top: 1px solid var(--el-border-color-lighter); }
|
|
@media (max-width: 900px) { .config-layout { grid-template-columns: 1fr; } .provider-panel { border-right: 0; border-bottom: 1px solid var(--el-border-color-lighter); max-height: 260px; overflow-y: auto; } .field-grid { grid-template-columns: 1fr; } .editor-panel { padding: 18px; } }
|
|
</style>
|