新增支付配置备份导入
This commit is contained in:
@@ -7,6 +7,8 @@ var (
|
|||||||
ErrDuplicateDefault = errors.New("duplicate default config for provider")
|
ErrDuplicateDefault = errors.New("duplicate default config for provider")
|
||||||
ErrInvalidProvider = errors.New("invalid payment provider")
|
ErrInvalidProvider = errors.New("invalid payment provider")
|
||||||
ErrInvalidStatus = errors.New("invalid status")
|
ErrInvalidStatus = errors.New("invalid status")
|
||||||
|
ErrInvalidBackup = errors.New("invalid payment config backup")
|
||||||
|
ErrEmptyBackup = errors.New("payment config backup is empty")
|
||||||
ErrCannotDeleteInUse = errors.New("cannot delete config in use")
|
ErrCannotDeleteInUse = errors.New("cannot delete config in use")
|
||||||
ErrEncryptionFailed = errors.New("encryption failed")
|
ErrEncryptionFailed = errors.New("encryption failed")
|
||||||
ErrDecryptionFailed = errors.New("decryption failed")
|
ErrDecryptionFailed = errors.New("decryption failed")
|
||||||
@@ -116,3 +118,10 @@ type ExportBackup struct {
|
|||||||
Total int `json:"total"`
|
Total int `json:"total"`
|
||||||
Configs []ConfigDTO `json:"configs"`
|
Configs []ConfigDTO `json:"configs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImportBackupResult 支付配置备份导入结果。
|
||||||
|
type ImportBackupResult struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
Created int `json:"created"`
|
||||||
|
Updated int `json:"updated"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -109,6 +109,46 @@ func (h *Handler) ExportBackup(c *gin.Context) {
|
|||||||
c.Data(http.StatusOK, contentType, data)
|
c.Data(http.StatusOK, contentType, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImportBackup 导入支付配置备份
|
||||||
|
// @Summary 导入支付配置备份
|
||||||
|
// @Tags 管理后台-支付配置
|
||||||
|
// @Param body body ExportBackup true "支付配置备份 JSON"
|
||||||
|
// @Success 200 {object} response.Response{data=ImportBackupResult}
|
||||||
|
// @Router /admin/payment-configs/import [post]
|
||||||
|
func (h *Handler) ImportBackup(c *gin.Context) {
|
||||||
|
adminID, ok := currentAdminID(c)
|
||||||
|
if !ok {
|
||||||
|
response.Unauthorized(c, "未授权")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var backup ExportBackup
|
||||||
|
if err := c.ShouldBindJSON(&backup); err != nil {
|
||||||
|
response.BadRequest(c, "备份文件格式错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.service.ImportBackup(backup, adminID, auditMeta(c))
|
||||||
|
if err == ErrInvalidBackup || err == ErrEmptyBackup || err == ErrInvalidProvider ||
|
||||||
|
err == ErrNameRequired || err == ErrMerchantIDRequired || err == ErrGatewayURLRequired ||
|
||||||
|
err == ErrSignKeyRequired || err == ErrNotifyKeyRequired || err == ErrNotifyURLRequired ||
|
||||||
|
err == ErrInvalidSignType || err == ErrInvalidStatus || err == ErrAppIDRequired ||
|
||||||
|
err == ErrSerialNoRequired || err == ErrTermNoRequired {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err == ErrEncryptionFailed {
|
||||||
|
response.Error(c, http.StatusInternalServerError, "encrypt_failed", "密钥加密失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, http.StatusInternalServerError, "internal_error", "导入失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
// Create 创建支付配置
|
// Create 创建支付配置
|
||||||
// @Summary 创建支付配置
|
// @Summary 创建支付配置
|
||||||
// @Tags 管理后台-支付配置
|
// @Tags 管理后台-支付配置
|
||||||
|
|||||||
@@ -192,6 +192,112 @@ func (r *Repository) ExportBackup(actorID uint64, meta AuditMeta) (*ExportBackup
|
|||||||
return backup, nil
|
return backup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImportBackup 导入支付配置备份,按 provider + merchant_id 更新或新增。
|
||||||
|
func (r *Repository) ImportBackup(backup ExportBackup, actorID uint64, meta AuditMeta) (*ImportBackupResult, error) {
|
||||||
|
if backup.Type != "payment_config_backup" || backup.Version <= 0 {
|
||||||
|
return nil, ErrInvalidBackup
|
||||||
|
}
|
||||||
|
if len(backup.Configs) == 0 {
|
||||||
|
return nil, ErrEmptyBackup
|
||||||
|
}
|
||||||
|
|
||||||
|
activeKey := backupActiveKey(backup.Configs)
|
||||||
|
result := &ImportBackupResult{Total: len(backup.Configs)}
|
||||||
|
|
||||||
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if activeKey != "" {
|
||||||
|
if err := deactivateOtherConfigs(tx, 0, actorID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cfg := range backup.Configs {
|
||||||
|
req := importCreateRequest(cfg, activeKey)
|
||||||
|
if err := r.validateCreateRequest(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedSignKey, err := r.encryptor.Encrypt(req.SignKey)
|
||||||
|
if err != nil {
|
||||||
|
return ErrEncryptionFailed
|
||||||
|
}
|
||||||
|
encryptedNotifyKey, err := r.encryptor.Encrypt(req.NotifyKey)
|
||||||
|
if err != nil {
|
||||||
|
return ErrEncryptionFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := map[string]any{
|
||||||
|
"name": req.Name,
|
||||||
|
"gateway_url": req.GatewayURL,
|
||||||
|
"sign_key": encryptedSignKey,
|
||||||
|
"notify_key": encryptedNotifyKey,
|
||||||
|
"notify_url": req.NotifyURL,
|
||||||
|
"jump_url": req.JumpURL,
|
||||||
|
"pay_way": firstNonEmpty(req.PayWay, "ZFBZF"),
|
||||||
|
"jspay_flag": firstNonEmpty(req.JSPayFlag, "2"),
|
||||||
|
"sign_type": firstNonEmpty(req.SignType, defaultSignType(req.Provider)),
|
||||||
|
"extra_config": model.JSONMap(req.ExtraConfig),
|
||||||
|
"is_default": req.IsDefault,
|
||||||
|
"status": firstNonEmpty(req.Status, "active"),
|
||||||
|
"environment": firstNonEmpty(req.Environment, "production"),
|
||||||
|
"business_tags": model.JSONArray(req.BusinessTags),
|
||||||
|
"updated_by": actorID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing model.PaymentMerchantConfig
|
||||||
|
err = tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||||
|
Where("provider = ? AND merchant_id = ?", req.Provider, req.MerchantID).
|
||||||
|
First(&existing).Error
|
||||||
|
if err == nil {
|
||||||
|
if err := tx.Model(&existing).Updates(updates).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result.Updated++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
createdBy := actorID
|
||||||
|
item := model.PaymentMerchantConfig{
|
||||||
|
Name: req.Name,
|
||||||
|
Provider: req.Provider,
|
||||||
|
MerchantID: req.MerchantID,
|
||||||
|
GatewayURL: req.GatewayURL,
|
||||||
|
SignKey: encryptedSignKey,
|
||||||
|
NotifyKey: encryptedNotifyKey,
|
||||||
|
NotifyURL: req.NotifyURL,
|
||||||
|
JumpURL: req.JumpURL,
|
||||||
|
PayWay: firstNonEmpty(req.PayWay, "ZFBZF"),
|
||||||
|
JSPayFlag: firstNonEmpty(req.JSPayFlag, "2"),
|
||||||
|
SignType: firstNonEmpty(req.SignType, defaultSignType(req.Provider)),
|
||||||
|
ExtraConfig: model.JSONMap(req.ExtraConfig),
|
||||||
|
IsDefault: req.IsDefault,
|
||||||
|
Status: firstNonEmpty(req.Status, "active"),
|
||||||
|
Environment: firstNonEmpty(req.Environment, "production"),
|
||||||
|
BusinessTags: req.BusinessTags,
|
||||||
|
CreatedBy: &createdBy,
|
||||||
|
UpdatedBy: &actorID,
|
||||||
|
}
|
||||||
|
if err := tx.Create(&item).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result.Created++
|
||||||
|
}
|
||||||
|
|
||||||
|
return appendAuditLog(tx, actorID, "payment_config.import", 0, meta, map[string]any{
|
||||||
|
"total": result.Total,
|
||||||
|
"created": result.Created,
|
||||||
|
"updated": result.Updated,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
// FindActiveByProvider 查询提供商的所有激活配置
|
// FindActiveByProvider 查询提供商的所有激活配置
|
||||||
func (r *Repository) FindActiveByProvider(provider string) ([]model.PaymentMerchantConfig, error) {
|
func (r *Repository) FindActiveByProvider(provider string) ([]model.PaymentMerchantConfig, error) {
|
||||||
var items []model.PaymentMerchantConfig
|
var items []model.PaymentMerchantConfig
|
||||||
@@ -547,6 +653,9 @@ func (r *Repository) validateCreateRequest(req CreateRequest) error {
|
|||||||
if req.Provider == "" {
|
if req.Provider == "" {
|
||||||
return ErrInvalidProvider
|
return ErrInvalidProvider
|
||||||
}
|
}
|
||||||
|
if !isValidProvider(req.Provider) {
|
||||||
|
return ErrInvalidProvider
|
||||||
|
}
|
||||||
if req.MerchantID == "" {
|
if req.MerchantID == "" {
|
||||||
return ErrMerchantIDRequired
|
return ErrMerchantIDRequired
|
||||||
}
|
}
|
||||||
@@ -608,6 +717,55 @@ func firstNonEmpty(vals ...string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isValidProvider(value string) bool {
|
||||||
|
return value == "leshua" || value == "lakala" || value == "mock"
|
||||||
|
}
|
||||||
|
|
||||||
|
func backupActiveKey(configs []ConfigDTO) string {
|
||||||
|
for _, cfg := range configs {
|
||||||
|
if cfg.Status == "active" || cfg.IsDefault {
|
||||||
|
return paymentConfigImportKey(cfg.Provider, cfg.MerchantID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func importCreateRequest(cfg ConfigDTO, activeKey string) CreateRequest {
|
||||||
|
status := cfg.Status
|
||||||
|
if status == "" {
|
||||||
|
status = "disabled"
|
||||||
|
}
|
||||||
|
isActive := activeKey != "" && paymentConfigImportKey(cfg.Provider, cfg.MerchantID) == activeKey
|
||||||
|
if isActive {
|
||||||
|
status = "active"
|
||||||
|
} else if status == "active" {
|
||||||
|
status = "disabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateRequest{
|
||||||
|
Name: cfg.Name,
|
||||||
|
Provider: cfg.Provider,
|
||||||
|
MerchantID: cfg.MerchantID,
|
||||||
|
GatewayURL: cfg.GatewayURL,
|
||||||
|
SignKey: cfg.SignKey,
|
||||||
|
NotifyKey: cfg.NotifyKey,
|
||||||
|
NotifyURL: cfg.NotifyURL,
|
||||||
|
JumpURL: cfg.JumpURL,
|
||||||
|
PayWay: cfg.PayWay,
|
||||||
|
JSPayFlag: cfg.JSPayFlag,
|
||||||
|
SignType: cfg.SignType,
|
||||||
|
ExtraConfig: cfg.ExtraConfig,
|
||||||
|
IsDefault: isActive,
|
||||||
|
Status: status,
|
||||||
|
Environment: cfg.Environment,
|
||||||
|
BusinessTags: cfg.BusinessTags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func paymentConfigImportKey(provider string, merchantID string) string {
|
||||||
|
return provider + ":" + merchantID
|
||||||
|
}
|
||||||
|
|
||||||
func defaultSignType(provider string) string {
|
func defaultSignType(provider string) string {
|
||||||
if provider == "lakala" {
|
if provider == "lakala" {
|
||||||
return "SHA256withRSA"
|
return "SHA256withRSA"
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package paymentconfig
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestImportCreateRequestKeepsOnlyOneActiveConfig(t *testing.T) {
|
||||||
|
configs := []ConfigDTO{
|
||||||
|
{Provider: "lakala", MerchantID: "M1", Status: "active", IsDefault: true},
|
||||||
|
{Provider: "leshua", MerchantID: "M2", Status: "active", IsDefault: false},
|
||||||
|
}
|
||||||
|
activeKey := backupActiveKey(configs)
|
||||||
|
|
||||||
|
first := importCreateRequest(configs[0], activeKey)
|
||||||
|
second := importCreateRequest(configs[1], activeKey)
|
||||||
|
|
||||||
|
if first.Status != "active" || !first.IsDefault {
|
||||||
|
t.Fatalf("first config status/default = %s/%v, want active/true", first.Status, first.IsDefault)
|
||||||
|
}
|
||||||
|
if second.Status != "disabled" || second.IsDefault {
|
||||||
|
t.Fatalf("second config status/default = %s/%v, want disabled/false", second.Status, second.IsDefault)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImportCreateRequestPreservesTestingConfig(t *testing.T) {
|
||||||
|
cfg := ConfigDTO{
|
||||||
|
Provider: "lakala",
|
||||||
|
MerchantID: "M1",
|
||||||
|
Status: "testing",
|
||||||
|
}
|
||||||
|
|
||||||
|
req := importCreateRequest(cfg, "")
|
||||||
|
|
||||||
|
if req.Status != "testing" || req.IsDefault {
|
||||||
|
t.Fatalf("status/default = %s/%v, want testing/false", req.Status, req.IsDefault)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,11 @@ func (s *Service) ExportBackup(actorID uint64, meta AuditMeta) (*ExportBackup, e
|
|||||||
return s.repo.ExportBackup(actorID, meta)
|
return s.repo.ExportBackup(actorID, meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImportBackup 导入支付配置备份。
|
||||||
|
func (s *Service) ImportBackup(backup ExportBackup, actorID uint64, meta AuditMeta) (*ImportBackupResult, error) {
|
||||||
|
return s.repo.ImportBackup(backup, actorID, meta)
|
||||||
|
}
|
||||||
|
|
||||||
// Create 创建配置
|
// Create 创建配置
|
||||||
func (s *Service) Create(req CreateRequest, actorID uint64, meta AuditMeta) (*ConfigDTO, error) {
|
func (s *Service) Create(req CreateRequest, actorID uint64, meta AuditMeta) (*ConfigDTO, error) {
|
||||||
return s.repo.Create(req, actorID, meta)
|
return s.repo.Create(req, actorID, meta)
|
||||||
|
|||||||
@@ -425,6 +425,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
|||||||
if paymentConfigHandler != nil {
|
if paymentConfigHandler != nil {
|
||||||
adminRoutes.GET("/payment-configs", requirePerm("payment_config:list"), paymentConfigHandler.List)
|
adminRoutes.GET("/payment-configs", requirePerm("payment_config:list"), paymentConfigHandler.List)
|
||||||
adminRoutes.GET("/payment-configs/export", requirePerm("payment_config:view_secret"), paymentConfigHandler.ExportBackup)
|
adminRoutes.GET("/payment-configs/export", requirePerm("payment_config:view_secret"), paymentConfigHandler.ExportBackup)
|
||||||
|
adminRoutes.POST("/payment-configs/import", requirePerm("payment_config:view_secret"), paymentConfigHandler.ImportBackup)
|
||||||
adminRoutes.GET("/payment-configs/:id", requirePerm("payment_config:list"), paymentConfigHandler.Get)
|
adminRoutes.GET("/payment-configs/:id", requirePerm("payment_config:list"), paymentConfigHandler.Get)
|
||||||
adminRoutes.POST("/payment-configs", requirePerm("payment_config:create"), paymentConfigHandler.Create)
|
adminRoutes.POST("/payment-configs", requirePerm("payment_config:create"), paymentConfigHandler.Create)
|
||||||
adminRoutes.PUT("/payment-configs/:id", requirePerm("payment_config:update"), paymentConfigHandler.Update)
|
adminRoutes.PUT("/payment-configs/:id", requirePerm("payment_config:update"), paymentConfigHandler.Update)
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ export interface PaymentConfigListResponse {
|
|||||||
page_size: number
|
page_size: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PaymentConfigImportResult {
|
||||||
|
total: number
|
||||||
|
created: number
|
||||||
|
updated: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreatePaymentConfigRequest {
|
export interface CreatePaymentConfigRequest {
|
||||||
name: string
|
name: string
|
||||||
provider: string
|
provider: string
|
||||||
@@ -102,6 +108,11 @@ export async function exportPaymentConfigBackup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function importPaymentConfigBackup(payload: unknown) {
|
||||||
|
const { data } = await apiClient.post<ApiResponse<PaymentConfigImportResult>>('/admin/payment-configs/import', payload)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
export async function createPaymentConfig(payload: CreatePaymentConfigRequest) {
|
export async function createPaymentConfig(payload: CreatePaymentConfigRequest) {
|
||||||
const { data } = await apiClient.post<ApiResponse<PaymentConfig>>('/admin/payment-configs', payload)
|
const { data } = await apiClient.post<ApiResponse<PaymentConfig>>('/admin/payment-configs', payload)
|
||||||
return data.data
|
return data.data
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { Plus, Refresh, View, Edit, Delete, Download } from '@element-plus/icons-vue'
|
import { Plus, Refresh, View, Edit, Delete, Download, Upload } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fetchPaymentConfigs,
|
fetchPaymentConfigs,
|
||||||
deletePaymentConfig,
|
deletePaymentConfig,
|
||||||
exportPaymentConfigBackup,
|
exportPaymentConfigBackup,
|
||||||
|
importPaymentConfigBackup,
|
||||||
type PaymentConfig,
|
type PaymentConfig,
|
||||||
} from '@/features/admin/api/paymentConfig'
|
} from '@/features/admin/api/paymentConfig'
|
||||||
import { formatDateTime } from '@/utils/time'
|
import { formatDateTime } from '@/utils/time'
|
||||||
@@ -19,6 +20,8 @@ const total = ref(0)
|
|||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(20)
|
const pageSize = ref(20)
|
||||||
const exporting = ref(false)
|
const exporting = ref(false)
|
||||||
|
const importing = ref(false)
|
||||||
|
const backupFileInput = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const dialogMode = ref<'create' | 'edit' | 'view'>('create')
|
const dialogMode = ref<'create' | 'edit' | 'view'>('create')
|
||||||
@@ -133,6 +136,48 @@ async function handleExportBackup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleImportBackup() {
|
||||||
|
backupFileInput.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleImportFile(event: Event) {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
const file = input.files?.[0]
|
||||||
|
input.value = ''
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
let backup: unknown
|
||||||
|
try {
|
||||||
|
backup = JSON.parse(await file.text())
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('备份文件不是有效的 JSON')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'导入会按服务商和商户号新增或更新配置,并恢复备份中的启用配置。备份文件包含支付密钥明文,请确认来源可信。',
|
||||||
|
'导入支付配置备份',
|
||||||
|
{
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '导入',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
importing.value = true
|
||||||
|
const result = await importPaymentConfigBackup(backup)
|
||||||
|
ElMessage.success(`导入成功:新增 ${result.created} 个,更新 ${result.updated} 个`)
|
||||||
|
await loadConfigs()
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel' && error !== 'close') {
|
||||||
|
ElMessage.error(readError(error, '导入备份失败'))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
importing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function downloadBlob(blob: Blob, filename: string) {
|
function downloadBlob(blob: Blob, filename: string) {
|
||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
@@ -221,10 +266,19 @@ function deleteDisabledReason(row: PaymentConfig) {
|
|||||||
<el-option v-for="opt in environmentOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
<el-option v-for="opt in environmentOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-button :icon="Refresh" @click="loadConfigs">刷新</el-button>
|
<el-button :icon="Refresh" @click="loadConfigs">刷新</el-button>
|
||||||
|
<el-button :icon="Upload" :loading="importing" @click="handleImportBackup">导入备份</el-button>
|
||||||
<el-button :icon="Download" :loading="exporting" @click="handleExportBackup">导出备份</el-button>
|
<el-button :icon="Download" :loading="exporting" @click="handleExportBackup">导出备份</el-button>
|
||||||
<el-button type="primary" :icon="Plus" @click="handleCreate">新增配置</el-button>
|
<el-button type="primary" :icon="Plus" @click="handleCreate">新增配置</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref="backupFileInput"
|
||||||
|
class="backup-file-input"
|
||||||
|
type="file"
|
||||||
|
accept="application/json,.json"
|
||||||
|
@change="handleImportFile"
|
||||||
|
/>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
:data="filteredConfigs"
|
:data="filteredConfigs"
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
@@ -356,6 +410,10 @@ function deleteDisabledReason(row: PaymentConfig) {
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.backup-file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.payment-config-table {
|
.payment-config-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user