feat: 支付渠道、邮箱配置、存储管理页面添加批量操作功能
- 添加表格多选功能 - 添加批量启用/禁用/删除功能 - 添加统计卡片(总数、已启用、已禁用、启用率) - 后端添加批量操作API接口 - 存储配置批量删除保护本地存储
This commit is contained in:
@@ -20,6 +20,8 @@ func SetupEmailConfigRoutes(r *gin.RouterGroup) {
|
||||
emailConfigs.DELETE("/:id", handleDeleteSystemEmailConfig)
|
||||
emailConfigs.PUT("/:id/status", handleUpdateSystemEmailConfigStatus)
|
||||
emailConfigs.POST("/:id/test", handleTestSystemEmailConfig)
|
||||
emailConfigs.PUT("/batch/status", handleBatchUpdateEmailConfigStatus)
|
||||
emailConfigs.DELETE("/batch", handleBatchDeleteEmailConfigs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,3 +303,84 @@ func handleTestSystemEmailConfig(c *gin.Context) {
|
||||
"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/default", handleSetDefaultStorageConfig)
|
||||
storageConfigs.POST("/:id/test", handleTestStorageConfig)
|
||||
storageConfigs.PUT("/batch/status", handleBatchUpdateStorageConfigStatus)
|
||||
storageConfigs.DELETE("/batch", handleBatchDeleteStorageConfigs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,3 +319,94 @@ func handleTestStorageConfig(c *gin.Context) {
|
||||
"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.DELETE("/:id", handleDeletePaymentChannel)
|
||||
paymentChannels.PUT("/:id/status", handleUpdatePaymentChannelStatus)
|
||||
paymentChannels.PUT("/batch/status", handleBatchUpdatePaymentChannelStatus)
|
||||
paymentChannels.DELETE("/batch", handleBatchDeletePaymentChannels)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -682,3 +684,84 @@ func handleUpdatePaymentChannelStatus(c *gin.Context) {
|
||||
"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 { 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 { SelectColumn } from '@/components/data-table/table-columns'
|
||||
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
|
||||
@@ -21,6 +22,9 @@ const props = defineProps<Omit<DataTableProps<EmailConfig>, 'columns'> & {
|
||||
|
||||
const emit = defineEmits<{
|
||||
'refresh': []
|
||||
'batchEnable': []
|
||||
'batchDisable': []
|
||||
'batchDelete': []
|
||||
}>()
|
||||
|
||||
const t = (key: string) => key
|
||||
@@ -59,11 +63,24 @@ defineExpose({
|
||||
<template>
|
||||
<DataTable :columns="columns" :data :loading :table @refresh="emit('refresh')">
|
||||
<template #toolbar>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="text-sm text-muted-foreground">
|
||||
共 {{ data.length }} 个邮箱配置
|
||||
<div class="space-y-3">
|
||||
<BulkActions :table="table" entity-name="email-configs">
|
||||
<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>
|
||||
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
|
||||
</div>
|
||||
</template>
|
||||
</DataTable>
|
||||
|
||||
@@ -19,6 +19,8 @@ const tableRef = ref()
|
||||
|
||||
const deleteDialogOpen = ref(false)
|
||||
const deleteTarget = ref<EmailConfig | null>(null)
|
||||
const batchDeleteDialogOpen = ref(false)
|
||||
const batchDeleteIds = ref<number[]>([])
|
||||
|
||||
const testDialogOpen = ref(false)
|
||||
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) {
|
||||
testTarget.value = config
|
||||
testEmail.value = ''
|
||||
@@ -131,7 +165,7 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<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>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
@@ -173,6 +207,20 @@ onMounted(() => {
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</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>
|
||||
|
||||
<UiCard>
|
||||
@@ -186,6 +234,9 @@ onMounted(() => {
|
||||
:on-delete="confirmDelete"
|
||||
:on-test="openTestDialog"
|
||||
@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>
|
||||
</UiCard>
|
||||
@@ -206,6 +257,21 @@ onMounted(() => {
|
||||
</template>
|
||||
</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">
|
||||
<UiDialogContent class="sm:max-w-md">
|
||||
<UiDialogHeader>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { computed } from 'vue'
|
||||
import type { DataTableProps } from '@/components/data-table/types'
|
||||
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 { SelectColumn } from '@/components/data-table/table-columns'
|
||||
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
|
||||
@@ -20,6 +21,9 @@ const props = defineProps<Omit<DataTableProps<PaymentChannel>, 'columns'> & {
|
||||
|
||||
const emit = defineEmits<{
|
||||
'refresh': []
|
||||
'batchEnable': []
|
||||
'batchDisable': []
|
||||
'batchDelete': []
|
||||
}>()
|
||||
|
||||
const t = (key: string) => key
|
||||
@@ -57,11 +61,24 @@ defineExpose({
|
||||
<template>
|
||||
<DataTable :columns="columns" :data :loading :table @refresh="emit('refresh')">
|
||||
<template #toolbar>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="text-sm text-muted-foreground">
|
||||
共 {{ data.length }} 个支付渠道
|
||||
<div class="space-y-3">
|
||||
<BulkActions :table="table" entity-name="payment-channels">
|
||||
<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>
|
||||
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
|
||||
</div>
|
||||
</template>
|
||||
</DataTable>
|
||||
|
||||
@@ -19,6 +19,8 @@ const tableRef = ref()
|
||||
|
||||
const deleteDialogOpen = ref(false)
|
||||
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 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(() => {
|
||||
fetchPaymentChannels()
|
||||
})
|
||||
@@ -101,7 +135,7 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<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>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
@@ -143,6 +177,20 @@ onMounted(() => {
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</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>
|
||||
|
||||
<UiCard>
|
||||
@@ -155,6 +203,9 @@ onMounted(() => {
|
||||
:on-edit="goToEdit"
|
||||
:on-delete="confirmDelete"
|
||||
@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>
|
||||
</UiCard>
|
||||
@@ -174,5 +225,20 @@ onMounted(() => {
|
||||
确定要删除支付渠道"{{ deleteTarget?.name }}"吗?此操作不可撤销。
|
||||
</template>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { computed } from 'vue'
|
||||
import type { DataTableProps } from '@/components/data-table/types'
|
||||
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 { SelectColumn } from '@/components/data-table/table-columns'
|
||||
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
|
||||
@@ -21,6 +22,9 @@ const props = defineProps<Omit<DataTableProps<StorageConfig>, 'columns'> & {
|
||||
|
||||
const emit = defineEmits<{
|
||||
'refresh': []
|
||||
'batchEnable': []
|
||||
'batchDisable': []
|
||||
'batchDelete': []
|
||||
}>()
|
||||
|
||||
const t = (key: string) => key
|
||||
@@ -58,11 +62,24 @@ defineExpose({
|
||||
<template>
|
||||
<DataTable :columns="columns" :data :loading :table @refresh="emit('refresh')">
|
||||
<template #toolbar>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="text-sm text-muted-foreground">
|
||||
共 {{ data.length }} 个存储配置
|
||||
<div class="space-y-3">
|
||||
<BulkActions :table="table" entity-name="storage-configs">
|
||||
<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>
|
||||
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
|
||||
</div>
|
||||
</template>
|
||||
</DataTable>
|
||||
|
||||
@@ -15,9 +15,12 @@ const router = useRouter()
|
||||
|
||||
const loading = ref(true)
|
||||
const storageConfigs = ref<StorageConfig[]>([])
|
||||
const tableRef = ref()
|
||||
|
||||
const deleteDialogOpen = ref(false)
|
||||
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 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(() => {
|
||||
fetchStorageConfigs()
|
||||
})
|
||||
@@ -112,7 +147,7 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<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>
|
||||
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
@@ -154,11 +189,26 @@ onMounted(() => {
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</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>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="p-6">
|
||||
<DataTable
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
:data="storageConfigs"
|
||||
:on-toggle-status="toggleStatus"
|
||||
@@ -166,6 +216,9 @@ onMounted(() => {
|
||||
:on-edit="goToEdit"
|
||||
:on-delete="confirmDelete"
|
||||
@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>
|
||||
</UiCard>
|
||||
@@ -185,5 +238,20 @@ onMounted(() => {
|
||||
确定要删除存储配置"{{ deleteTarget?.name }}"吗?此操作不可撤销。
|
||||
</template>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user