feat: 支付渠道、邮箱配置、存储管理页面添加批量操作功能
- 添加表格多选功能 - 添加批量启用/禁用/删除功能 - 添加统计卡片(总数、已启用、已禁用、启用率) - 后端添加批量操作API接口 - 存储配置批量删除保护本地存储
This commit is contained in:
@@ -20,6 +20,8 @@ func SetupEmailConfigRoutes(r *gin.RouterGroup) {
|
|||||||
emailConfigs.DELETE("/:id", handleDeleteSystemEmailConfig)
|
emailConfigs.DELETE("/:id", handleDeleteSystemEmailConfig)
|
||||||
emailConfigs.PUT("/:id/status", handleUpdateSystemEmailConfigStatus)
|
emailConfigs.PUT("/:id/status", handleUpdateSystemEmailConfigStatus)
|
||||||
emailConfigs.POST("/:id/test", handleTestSystemEmailConfig)
|
emailConfigs.POST("/:id/test", handleTestSystemEmailConfig)
|
||||||
|
emailConfigs.PUT("/batch/status", handleBatchUpdateEmailConfigStatus)
|
||||||
|
emailConfigs.DELETE("/batch", handleBatchDeleteEmailConfigs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,3 +303,84 @@ func handleTestSystemEmailConfig(c *gin.Context) {
|
|||||||
"message": "测试邮件已发送",
|
"message": "测试邮件已发送",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BatchUpdateEmailConfigStatusRequest struct {
|
||||||
|
IDs []uint `json:"ids"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleBatchUpdateEmailConfigStatus(c *gin.Context) {
|
||||||
|
var req BatchUpdateEmailConfigStatusRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 400,
|
||||||
|
"message": "参数错误",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Status != "active" && req.Status != "inactive" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 400,
|
||||||
|
"message": "无效的状态",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.IDs) == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 400,
|
||||||
|
"message": "请选择要更新的邮箱配置",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Model(&model.EmailConfig{}).Where("id IN ?", req.IDs).Update("status", req.Status).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"code": 500,
|
||||||
|
"message": "批量更新失败",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": 200,
|
||||||
|
"message": "批量更新成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type BatchDeleteEmailConfigsRequest struct {
|
||||||
|
IDs []uint `json:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleBatchDeleteEmailConfigs(c *gin.Context) {
|
||||||
|
var req BatchDeleteEmailConfigsRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 400,
|
||||||
|
"message": "参数错误",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.IDs) == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 400,
|
||||||
|
"message": "请选择要删除的邮箱配置",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Where("id IN ?", req.IDs).Delete(&model.EmailConfig{}).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"code": 500,
|
||||||
|
"message": "批量删除失败",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": 200,
|
||||||
|
"message": "批量删除成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ func SetupStorageConfigRoutes(r *gin.RouterGroup) {
|
|||||||
storageConfigs.PUT("/:id/status", handleUpdateStorageConfigStatus)
|
storageConfigs.PUT("/:id/status", handleUpdateStorageConfigStatus)
|
||||||
storageConfigs.PUT("/:id/default", handleSetDefaultStorageConfig)
|
storageConfigs.PUT("/:id/default", handleSetDefaultStorageConfig)
|
||||||
storageConfigs.POST("/:id/test", handleTestStorageConfig)
|
storageConfigs.POST("/:id/test", handleTestStorageConfig)
|
||||||
|
storageConfigs.PUT("/batch/status", handleBatchUpdateStorageConfigStatus)
|
||||||
|
storageConfigs.DELETE("/batch", handleBatchDeleteStorageConfigs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,3 +319,94 @@ func handleTestStorageConfig(c *gin.Context) {
|
|||||||
"message": "连接成功",
|
"message": "连接成功",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BatchUpdateStorageConfigStatusRequest struct {
|
||||||
|
IDs []uint `json:"ids"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleBatchUpdateStorageConfigStatus(c *gin.Context) {
|
||||||
|
var req BatchUpdateStorageConfigStatusRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 400,
|
||||||
|
"message": "参数错误",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Status != "active" && req.Status != "inactive" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 400,
|
||||||
|
"message": "无效的状态",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.IDs) == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 400,
|
||||||
|
"message": "请选择要更新的存储配置",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Model(&model.StorageConfig{}).Where("id IN ?", req.IDs).Update("status", req.Status).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"code": 500,
|
||||||
|
"message": "批量更新失败",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": 200,
|
||||||
|
"message": "批量更新成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type BatchDeleteStorageConfigsRequest struct {
|
||||||
|
IDs []uint `json:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleBatchDeleteStorageConfigs(c *gin.Context) {
|
||||||
|
var req BatchDeleteStorageConfigsRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 400,
|
||||||
|
"message": "参数错误",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.IDs) == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 400,
|
||||||
|
"message": "请选择要删除的存储配置",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var localConfigs []model.StorageConfig
|
||||||
|
database.DB.Where("id IN ? AND type = ?", req.IDs, "local").Find(&localConfigs)
|
||||||
|
if len(localConfigs) > 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 400,
|
||||||
|
"message": "本地存储不能删除",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Where("id IN ?", req.IDs).Delete(&model.StorageConfig{}).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"code": 500,
|
||||||
|
"message": "批量删除失败",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": 200,
|
||||||
|
"message": "批量删除成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ func SetupSystemSettingsRoutes(r *gin.RouterGroup) {
|
|||||||
paymentChannels.PUT("/:id", handleUpdatePaymentChannel)
|
paymentChannels.PUT("/:id", handleUpdatePaymentChannel)
|
||||||
paymentChannels.DELETE("/:id", handleDeletePaymentChannel)
|
paymentChannels.DELETE("/:id", handleDeletePaymentChannel)
|
||||||
paymentChannels.PUT("/:id/status", handleUpdatePaymentChannelStatus)
|
paymentChannels.PUT("/:id/status", handleUpdatePaymentChannelStatus)
|
||||||
|
paymentChannels.PUT("/batch/status", handleBatchUpdatePaymentChannelStatus)
|
||||||
|
paymentChannels.DELETE("/batch", handleBatchDeletePaymentChannels)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,3 +684,84 @@ func handleUpdatePaymentChannelStatus(c *gin.Context) {
|
|||||||
"message": "更新成功",
|
"message": "更新成功",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BatchUpdatePaymentChannelStatusRequest struct {
|
||||||
|
IDs []uint `json:"ids"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleBatchUpdatePaymentChannelStatus(c *gin.Context) {
|
||||||
|
var req BatchUpdatePaymentChannelStatusRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 400,
|
||||||
|
"message": "参数错误",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Status != "active" && req.Status != "inactive" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 400,
|
||||||
|
"message": "无效的状态",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.IDs) == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 400,
|
||||||
|
"message": "请选择要更新的支付渠道",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Model(&model.PaymentChannel{}).Where("id IN ?", req.IDs).Update("status", req.Status).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"code": 500,
|
||||||
|
"message": "批量更新失败",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": 200,
|
||||||
|
"message": "批量更新成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type BatchDeletePaymentChannelsRequest struct {
|
||||||
|
IDs []uint `json:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleBatchDeletePaymentChannels(c *gin.Context) {
|
||||||
|
var req BatchDeletePaymentChannelsRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 400,
|
||||||
|
"message": "参数错误",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.IDs) == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"code": 400,
|
||||||
|
"message": "请选择要删除的支付渠道",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Where("id IN ?", req.IDs).Delete(&model.PaymentChannel{}).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"code": 500,
|
||||||
|
"message": "批量删除失败",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": 200,
|
||||||
|
"message": "批量删除成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { computed } from 'vue'
|
|||||||
import type { DataTableProps } from '@/components/data-table/types'
|
import type { DataTableProps } from '@/components/data-table/types'
|
||||||
import type { EmailConfig } from '@/pages/admin/email-settings/data/schema'
|
import type { EmailConfig } from '@/pages/admin/email-settings/data/schema'
|
||||||
|
|
||||||
|
import BulkActions from '@/components/data-table/bulk-actions.vue'
|
||||||
import DataTable from '@/components/data-table/data-table.vue'
|
import DataTable from '@/components/data-table/data-table.vue'
|
||||||
import { SelectColumn } from '@/components/data-table/table-columns'
|
import { SelectColumn } from '@/components/data-table/table-columns'
|
||||||
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
|
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
|
||||||
@@ -21,6 +22,9 @@ const props = defineProps<Omit<DataTableProps<EmailConfig>, 'columns'> & {
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'refresh': []
|
'refresh': []
|
||||||
|
'batchEnable': []
|
||||||
|
'batchDisable': []
|
||||||
|
'batchDelete': []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const t = (key: string) => key
|
const t = (key: string) => key
|
||||||
@@ -59,11 +63,24 @@ defineExpose({
|
|||||||
<template>
|
<template>
|
||||||
<DataTable :columns="columns" :data :loading :table @refresh="emit('refresh')">
|
<DataTable :columns="columns" :data :loading :table @refresh="emit('refresh')">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="space-y-3">
|
||||||
<div class="text-sm text-muted-foreground">
|
<BulkActions :table="table" entity-name="email-configs">
|
||||||
共 {{ data.length }} 个邮箱配置
|
<UiButton variant="outline" size="sm" @click="emit('batchEnable')">
|
||||||
|
批量启用
|
||||||
|
</UiButton>
|
||||||
|
<UiButton variant="outline" size="sm" @click="emit('batchDisable')">
|
||||||
|
批量禁用
|
||||||
|
</UiButton>
|
||||||
|
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
|
||||||
|
批量删除
|
||||||
|
</UiButton>
|
||||||
|
</BulkActions>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div class="text-sm text-muted-foreground">
|
||||||
|
共 {{ data.length }} 个邮箱配置
|
||||||
|
</div>
|
||||||
|
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
|
||||||
</div>
|
</div>
|
||||||
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ const tableRef = ref()
|
|||||||
|
|
||||||
const deleteDialogOpen = ref(false)
|
const deleteDialogOpen = ref(false)
|
||||||
const deleteTarget = ref<EmailConfig | null>(null)
|
const deleteTarget = ref<EmailConfig | null>(null)
|
||||||
|
const batchDeleteDialogOpen = ref(false)
|
||||||
|
const batchDeleteIds = ref<number[]>([])
|
||||||
|
|
||||||
const testDialogOpen = ref(false)
|
const testDialogOpen = ref(false)
|
||||||
const testTarget = ref<EmailConfig | null>(null)
|
const testTarget = ref<EmailConfig | null>(null)
|
||||||
@@ -87,6 +89,38 @@ async function handleDelete() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function confirmBatchDelete(ids: number[]) {
|
||||||
|
batchDeleteIds.value = ids
|
||||||
|
batchDeleteDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleBatchDelete() {
|
||||||
|
try {
|
||||||
|
await api.delete('/dev/email-configs/batch', { ids: batchDeleteIds.value } as any)
|
||||||
|
toast.success('批量删除成功')
|
||||||
|
fetchEmailConfigs()
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
console.error('批量删除失败:', error)
|
||||||
|
toast.error(error.message || '批量删除失败')
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
batchDeleteIds.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function batchToggleStatus(ids: number[], status: string) {
|
||||||
|
try {
|
||||||
|
await api.put('/dev/email-configs/batch/status', { ids, status })
|
||||||
|
toast.success('批量更新成功')
|
||||||
|
fetchEmailConfigs()
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
console.error('批量更新状态失败:', error)
|
||||||
|
toast.error(error.message || '批量更新失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function openTestDialog(config: EmailConfig) {
|
function openTestDialog(config: EmailConfig) {
|
||||||
testTarget.value = config
|
testTarget.value = config
|
||||||
testEmail.value = ''
|
testEmail.value = ''
|
||||||
@@ -131,7 +165,7 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="grid gap-4 sm:grid-cols-3">
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<UiCard>
|
<UiCard>
|
||||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
<UiCardTitle class="text-sm font-medium">
|
<UiCardTitle class="text-sm font-medium">
|
||||||
@@ -173,6 +207,20 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</UiCardContent>
|
</UiCardContent>
|
||||||
</UiCard>
|
</UiCard>
|
||||||
|
|
||||||
|
<UiCard>
|
||||||
|
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
|
<UiCardTitle class="text-sm font-medium">
|
||||||
|
启用率
|
||||||
|
</UiCardTitle>
|
||||||
|
<Icon icon="lucide:percent" class="size-4 text-muted-foreground" />
|
||||||
|
</UiCardHeader>
|
||||||
|
<UiCardContent>
|
||||||
|
<div class="text-2xl font-bold">
|
||||||
|
{{ emailConfigs.length ? Math.round(activeCount / emailConfigs.length * 100) : 0 }}%
|
||||||
|
</div>
|
||||||
|
</UiCardContent>
|
||||||
|
</UiCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UiCard>
|
<UiCard>
|
||||||
@@ -186,6 +234,9 @@ onMounted(() => {
|
|||||||
:on-delete="confirmDelete"
|
:on-delete="confirmDelete"
|
||||||
:on-test="openTestDialog"
|
:on-test="openTestDialog"
|
||||||
@refresh="fetchEmailConfigs"
|
@refresh="fetchEmailConfigs"
|
||||||
|
@batch-enable="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status === 'inactive').map((r: any) => r.original.id) || [], 'active')"
|
||||||
|
@batch-disable="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status === 'active').map((r: any) => r.original.id) || [], 'inactive')"
|
||||||
|
@batch-delete="confirmBatchDelete(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [])"
|
||||||
/>
|
/>
|
||||||
</UiCardContent>
|
</UiCardContent>
|
||||||
</UiCard>
|
</UiCard>
|
||||||
@@ -206,6 +257,21 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
v-model:open="batchDeleteDialogOpen"
|
||||||
|
destructive
|
||||||
|
confirm-button-text="删除"
|
||||||
|
cancel-button-text="取消"
|
||||||
|
@confirm="handleBatchDelete"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
批量删除邮箱配置
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
确定要删除选中的 {{ batchDeleteIds.length }} 个邮箱配置吗?此操作不可撤销。
|
||||||
|
</template>
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
<UiDialog v-model:open="testDialogOpen">
|
<UiDialog v-model:open="testDialogOpen">
|
||||||
<UiDialogContent class="sm:max-w-md">
|
<UiDialogContent class="sm:max-w-md">
|
||||||
<UiDialogHeader>
|
<UiDialogHeader>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { computed } from 'vue'
|
|||||||
import type { DataTableProps } from '@/components/data-table/types'
|
import type { DataTableProps } from '@/components/data-table/types'
|
||||||
import type { PaymentChannel } from '@/pages/admin/payment-channels/data/schema'
|
import type { PaymentChannel } from '@/pages/admin/payment-channels/data/schema'
|
||||||
|
|
||||||
|
import BulkActions from '@/components/data-table/bulk-actions.vue'
|
||||||
import DataTable from '@/components/data-table/data-table.vue'
|
import DataTable from '@/components/data-table/data-table.vue'
|
||||||
import { SelectColumn } from '@/components/data-table/table-columns'
|
import { SelectColumn } from '@/components/data-table/table-columns'
|
||||||
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
|
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
|
||||||
@@ -20,6 +21,9 @@ const props = defineProps<Omit<DataTableProps<PaymentChannel>, 'columns'> & {
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'refresh': []
|
'refresh': []
|
||||||
|
'batchEnable': []
|
||||||
|
'batchDisable': []
|
||||||
|
'batchDelete': []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const t = (key: string) => key
|
const t = (key: string) => key
|
||||||
@@ -57,11 +61,24 @@ defineExpose({
|
|||||||
<template>
|
<template>
|
||||||
<DataTable :columns="columns" :data :loading :table @refresh="emit('refresh')">
|
<DataTable :columns="columns" :data :loading :table @refresh="emit('refresh')">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="space-y-3">
|
||||||
<div class="text-sm text-muted-foreground">
|
<BulkActions :table="table" entity-name="payment-channels">
|
||||||
共 {{ data.length }} 个支付渠道
|
<UiButton variant="outline" size="sm" @click="emit('batchEnable')">
|
||||||
|
批量启用
|
||||||
|
</UiButton>
|
||||||
|
<UiButton variant="outline" size="sm" @click="emit('batchDisable')">
|
||||||
|
批量禁用
|
||||||
|
</UiButton>
|
||||||
|
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
|
||||||
|
批量删除
|
||||||
|
</UiButton>
|
||||||
|
</BulkActions>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div class="text-sm text-muted-foreground">
|
||||||
|
共 {{ data.length }} 个支付渠道
|
||||||
|
</div>
|
||||||
|
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
|
||||||
</div>
|
</div>
|
||||||
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ const tableRef = ref()
|
|||||||
|
|
||||||
const deleteDialogOpen = ref(false)
|
const deleteDialogOpen = ref(false)
|
||||||
const deleteTarget = ref<PaymentChannel | null>(null)
|
const deleteTarget = ref<PaymentChannel | null>(null)
|
||||||
|
const batchDeleteDialogOpen = ref(false)
|
||||||
|
const batchDeleteIds = ref<number[]>([])
|
||||||
|
|
||||||
const activeCount = computed(() => paymentChannels.value.filter(c => c.status === 'active').length)
|
const activeCount = computed(() => paymentChannels.value.filter(c => c.status === 'active').length)
|
||||||
const inactiveCount = computed(() => paymentChannels.value.filter(c => c.status === 'inactive').length)
|
const inactiveCount = computed(() => paymentChannels.value.filter(c => c.status === 'inactive').length)
|
||||||
@@ -82,6 +84,38 @@ async function handleDelete() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function confirmBatchDelete(ids: number[]) {
|
||||||
|
batchDeleteIds.value = ids
|
||||||
|
batchDeleteDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleBatchDelete() {
|
||||||
|
try {
|
||||||
|
await api.delete('/dev/payment-channels/batch', { ids: batchDeleteIds.value } as any)
|
||||||
|
toast.success('批量删除成功')
|
||||||
|
fetchPaymentChannels()
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
console.error('批量删除失败:', error)
|
||||||
|
toast.error(error.message || '批量删除失败')
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
batchDeleteIds.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function batchToggleStatus(ids: number[], status: string) {
|
||||||
|
try {
|
||||||
|
await api.put('/dev/payment-channels/batch/status', { ids, status })
|
||||||
|
toast.success('批量更新成功')
|
||||||
|
fetchPaymentChannels()
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
console.error('批量更新状态失败:', error)
|
||||||
|
toast.error(error.message || '批量更新失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchPaymentChannels()
|
fetchPaymentChannels()
|
||||||
})
|
})
|
||||||
@@ -101,7 +135,7 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="grid gap-4 sm:grid-cols-3">
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<UiCard>
|
<UiCard>
|
||||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
<UiCardTitle class="text-sm font-medium">
|
<UiCardTitle class="text-sm font-medium">
|
||||||
@@ -143,6 +177,20 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</UiCardContent>
|
</UiCardContent>
|
||||||
</UiCard>
|
</UiCard>
|
||||||
|
|
||||||
|
<UiCard>
|
||||||
|
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
|
<UiCardTitle class="text-sm font-medium">
|
||||||
|
启用率
|
||||||
|
</UiCardTitle>
|
||||||
|
<Icon icon="lucide:percent" class="size-4 text-muted-foreground" />
|
||||||
|
</UiCardHeader>
|
||||||
|
<UiCardContent>
|
||||||
|
<div class="text-2xl font-bold">
|
||||||
|
{{ paymentChannels.length ? Math.round(activeCount / paymentChannels.length * 100) : 0 }}%
|
||||||
|
</div>
|
||||||
|
</UiCardContent>
|
||||||
|
</UiCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UiCard>
|
<UiCard>
|
||||||
@@ -155,6 +203,9 @@ onMounted(() => {
|
|||||||
:on-edit="goToEdit"
|
:on-edit="goToEdit"
|
||||||
:on-delete="confirmDelete"
|
:on-delete="confirmDelete"
|
||||||
@refresh="fetchPaymentChannels"
|
@refresh="fetchPaymentChannels"
|
||||||
|
@batch-enable="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status === 'inactive').map((r: any) => r.original.id) || [], 'active')"
|
||||||
|
@batch-disable="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status === 'active').map((r: any) => r.original.id) || [], 'inactive')"
|
||||||
|
@batch-delete="confirmBatchDelete(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [])"
|
||||||
/>
|
/>
|
||||||
</UiCardContent>
|
</UiCardContent>
|
||||||
</UiCard>
|
</UiCard>
|
||||||
@@ -174,5 +225,20 @@ onMounted(() => {
|
|||||||
确定要删除支付渠道"{{ deleteTarget?.name }}"吗?此操作不可撤销。
|
确定要删除支付渠道"{{ deleteTarget?.name }}"吗?此操作不可撤销。
|
||||||
</template>
|
</template>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
v-model:open="batchDeleteDialogOpen"
|
||||||
|
destructive
|
||||||
|
confirm-button-text="删除"
|
||||||
|
cancel-button-text="取消"
|
||||||
|
@confirm="handleBatchDelete"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
批量删除支付渠道
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
确定要删除选中的 {{ batchDeleteIds.length }} 个支付渠道吗?此操作不可撤销。
|
||||||
|
</template>
|
||||||
|
</ConfirmDialog>
|
||||||
</BasicPage>
|
</BasicPage>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { computed } from 'vue'
|
|||||||
import type { DataTableProps } from '@/components/data-table/types'
|
import type { DataTableProps } from '@/components/data-table/types'
|
||||||
import type { StorageConfig } from '@/pages/admin/storage-configs/data/schema'
|
import type { StorageConfig } from '@/pages/admin/storage-configs/data/schema'
|
||||||
|
|
||||||
|
import BulkActions from '@/components/data-table/bulk-actions.vue'
|
||||||
import DataTable from '@/components/data-table/data-table.vue'
|
import DataTable from '@/components/data-table/data-table.vue'
|
||||||
import { SelectColumn } from '@/components/data-table/table-columns'
|
import { SelectColumn } from '@/components/data-table/table-columns'
|
||||||
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
|
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
|
||||||
@@ -21,6 +22,9 @@ const props = defineProps<Omit<DataTableProps<StorageConfig>, 'columns'> & {
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'refresh': []
|
'refresh': []
|
||||||
|
'batchEnable': []
|
||||||
|
'batchDisable': []
|
||||||
|
'batchDelete': []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const t = (key: string) => key
|
const t = (key: string) => key
|
||||||
@@ -58,11 +62,24 @@ defineExpose({
|
|||||||
<template>
|
<template>
|
||||||
<DataTable :columns="columns" :data :loading :table @refresh="emit('refresh')">
|
<DataTable :columns="columns" :data :loading :table @refresh="emit('refresh')">
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="space-y-3">
|
||||||
<div class="text-sm text-muted-foreground">
|
<BulkActions :table="table" entity-name="storage-configs">
|
||||||
共 {{ data.length }} 个存储配置
|
<UiButton variant="outline" size="sm" @click="emit('batchEnable')">
|
||||||
|
批量启用
|
||||||
|
</UiButton>
|
||||||
|
<UiButton variant="outline" size="sm" @click="emit('batchDisable')">
|
||||||
|
批量禁用
|
||||||
|
</UiButton>
|
||||||
|
<UiButton variant="destructive" size="sm" @click="emit('batchDelete')">
|
||||||
|
批量删除
|
||||||
|
</UiButton>
|
||||||
|
</BulkActions>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div class="text-sm text-muted-foreground">
|
||||||
|
共 {{ data.length }} 个存储配置
|
||||||
|
</div>
|
||||||
|
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
|
||||||
</div>
|
</div>
|
||||||
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ const router = useRouter()
|
|||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const storageConfigs = ref<StorageConfig[]>([])
|
const storageConfigs = ref<StorageConfig[]>([])
|
||||||
|
const tableRef = ref()
|
||||||
|
|
||||||
const deleteDialogOpen = ref(false)
|
const deleteDialogOpen = ref(false)
|
||||||
const deleteTarget = ref<StorageConfig | null>(null)
|
const deleteTarget = ref<StorageConfig | null>(null)
|
||||||
|
const batchDeleteDialogOpen = ref(false)
|
||||||
|
const batchDeleteIds = ref<number[]>([])
|
||||||
|
|
||||||
const activeCount = computed(() => storageConfigs.value.filter(c => c.status === 'active').length)
|
const activeCount = computed(() => storageConfigs.value.filter(c => c.status === 'active').length)
|
||||||
const inactiveCount = computed(() => storageConfigs.value.filter(c => c.status === 'inactive').length)
|
const inactiveCount = computed(() => storageConfigs.value.filter(c => c.status === 'inactive').length)
|
||||||
@@ -93,6 +96,38 @@ async function handleDelete() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function confirmBatchDelete(ids: number[]) {
|
||||||
|
batchDeleteIds.value = ids
|
||||||
|
batchDeleteDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleBatchDelete() {
|
||||||
|
try {
|
||||||
|
await api.delete('/dev/storage-configs/batch', { ids: batchDeleteIds.value } as any)
|
||||||
|
toast.success('批量删除成功')
|
||||||
|
fetchStorageConfigs()
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
console.error('批量删除失败:', error)
|
||||||
|
toast.error(error.message || '批量删除失败')
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
batchDeleteIds.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function batchToggleStatus(ids: number[], status: string) {
|
||||||
|
try {
|
||||||
|
await api.put('/dev/storage-configs/batch/status', { ids, status })
|
||||||
|
toast.success('批量更新成功')
|
||||||
|
fetchStorageConfigs()
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
console.error('批量更新状态失败:', error)
|
||||||
|
toast.error(error.message || '批量更新失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchStorageConfigs()
|
fetchStorageConfigs()
|
||||||
})
|
})
|
||||||
@@ -112,7 +147,7 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="grid gap-4 sm:grid-cols-3">
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<UiCard>
|
<UiCard>
|
||||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
<UiCardTitle class="text-sm font-medium">
|
<UiCardTitle class="text-sm font-medium">
|
||||||
@@ -154,11 +189,26 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</UiCardContent>
|
</UiCardContent>
|
||||||
</UiCard>
|
</UiCard>
|
||||||
|
|
||||||
|
<UiCard>
|
||||||
|
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
|
<UiCardTitle class="text-sm font-medium">
|
||||||
|
启用率
|
||||||
|
</UiCardTitle>
|
||||||
|
<Icon icon="lucide:percent" class="size-4 text-muted-foreground" />
|
||||||
|
</UiCardHeader>
|
||||||
|
<UiCardContent>
|
||||||
|
<div class="text-2xl font-bold">
|
||||||
|
{{ storageConfigs.length ? Math.round(activeCount / storageConfigs.length * 100) : 0 }}%
|
||||||
|
</div>
|
||||||
|
</UiCardContent>
|
||||||
|
</UiCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UiCard>
|
<UiCard>
|
||||||
<UiCardContent class="p-6">
|
<UiCardContent class="p-6">
|
||||||
<DataTable
|
<DataTable
|
||||||
|
ref="tableRef"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:data="storageConfigs"
|
:data="storageConfigs"
|
||||||
:on-toggle-status="toggleStatus"
|
:on-toggle-status="toggleStatus"
|
||||||
@@ -166,6 +216,9 @@ onMounted(() => {
|
|||||||
:on-edit="goToEdit"
|
:on-edit="goToEdit"
|
||||||
:on-delete="confirmDelete"
|
:on-delete="confirmDelete"
|
||||||
@refresh="fetchStorageConfigs"
|
@refresh="fetchStorageConfigs"
|
||||||
|
@batch-enable="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status === 'inactive').map((r: any) => r.original.id) || [], 'active')"
|
||||||
|
@batch-disable="batchToggleStatus(tableRef?.table?.getSelectedRowModel().rows.filter((r: any) => r.original.status === 'active').map((r: any) => r.original.id) || [], 'inactive')"
|
||||||
|
@batch-delete="confirmBatchDelete(tableRef?.table?.getSelectedRowModel().rows.map((r: any) => r.original.id) || [])"
|
||||||
/>
|
/>
|
||||||
</UiCardContent>
|
</UiCardContent>
|
||||||
</UiCard>
|
</UiCard>
|
||||||
@@ -185,5 +238,20 @@ onMounted(() => {
|
|||||||
确定要删除存储配置"{{ deleteTarget?.name }}"吗?此操作不可撤销。
|
确定要删除存储配置"{{ deleteTarget?.name }}"吗?此操作不可撤销。
|
||||||
</template>
|
</template>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
v-model:open="batchDeleteDialogOpen"
|
||||||
|
destructive
|
||||||
|
confirm-button-text="删除"
|
||||||
|
cancel-button-text="取消"
|
||||||
|
@confirm="handleBatchDelete"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
批量删除存储配置
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
确定要删除选中的 {{ batchDeleteIds.length }} 个存储配置吗?此操作不可撤销。
|
||||||
|
</template>
|
||||||
|
</ConfirmDialog>
|
||||||
</BasicPage>
|
</BasicPage>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user