mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-06 20:32:55 +08:00
feat: add multi-select batch operations for admin account management (#737)
- Add multi-select functionality with native Naive UI selection column - Implement batch operations: delete, clear inbox, clear sent items - Create reusable executeBatchOperation function to eliminate code duplication - Add error recovery mechanism: failed items remain selected for retry - Add progress modal with real-time percentage display - Support smart skip logic: skip addresses with no mails/sent items - Add i18n support for English and Chinese - Update CHANGELOG.md with new features 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, h, onMounted, watch } from 'vue';
|
||||
import { NBadge } from 'naive-ui'
|
||||
import { ref, h, onMounted, watch, computed } from 'vue';
|
||||
import { NBadge, useMessage } from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
@@ -42,6 +42,16 @@ const { t } = useI18n({
|
||||
resetPassword: 'Reset Password',
|
||||
newPassword: 'New Password',
|
||||
passwordResetSuccess: 'Password reset successfully',
|
||||
selectAll: 'Select All of This Page',
|
||||
unselectAll: 'Unselect All',
|
||||
pleaseSelectAddress: 'Please select address',
|
||||
selectedItems: 'Selected',
|
||||
multiDelete: 'Multi Delete',
|
||||
multiDeleteTip: 'Are you sure to delete selected addresses?',
|
||||
multiClearInbox: 'Multi Clear Inbox',
|
||||
multiClearInboxTip: 'Are you sure to clear inbox for selected addresses?',
|
||||
multiClearSentItems: 'Multi Clear Sent Items',
|
||||
multiClearSentItemsTip: 'Are you sure to clear sent items for selected addresses?',
|
||||
},
|
||||
zh: {
|
||||
name: '名称',
|
||||
@@ -69,6 +79,16 @@ const { t } = useI18n({
|
||||
resetPassword: '重置密码',
|
||||
newPassword: '新密码',
|
||||
passwordResetSuccess: '密码重置成功',
|
||||
selectAll: '全选本页',
|
||||
unselectAll: '取消全选',
|
||||
pleaseSelectAddress: '请选择地址',
|
||||
selectedItems: '已选择',
|
||||
multiDelete: '批量删除',
|
||||
multiDeleteTip: '确定要删除选中的邮箱吗?',
|
||||
multiClearInbox: '批量清空收件箱',
|
||||
multiClearInboxTip: '确定要清空选中邮箱的收件箱吗?',
|
||||
multiClearSentItems: '批量清空发件箱',
|
||||
multiClearSentItemsTip: '确定要清空选中邮箱的发件箱吗?',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -82,6 +102,15 @@ const showResetPassword = ref(false);
|
||||
const curResetPasswordAddressId = ref(0);
|
||||
const newPassword = ref('');
|
||||
|
||||
// Multi-action mode state
|
||||
const checkedRowKeys = ref([]);
|
||||
const showMultiActionModal = ref(false);
|
||||
const multiActionProgress = ref({ percentage: 0, tip: '0/0' });
|
||||
const multiActionTitle = ref('');
|
||||
|
||||
const selectedCount = computed(() => checkedRowKeys.value.length);
|
||||
const showMultiActionBar = computed(() => checkedRowKeys.value.length > 0);
|
||||
|
||||
const addressQuery = ref("")
|
||||
|
||||
const data = ref([])
|
||||
@@ -159,6 +188,98 @@ const resetPassword = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-action mode functions
|
||||
const multiActionSelectAll = () => {
|
||||
checkedRowKeys.value = data.value.map(item => item.id);
|
||||
}
|
||||
|
||||
const multiActionUnselectAll = () => {
|
||||
checkedRowKeys.value = [];
|
||||
}
|
||||
|
||||
// 通用批量操作函数
|
||||
const executeBatchOperation = async ({
|
||||
shouldSkip = () => false,
|
||||
apiCall,
|
||||
title,
|
||||
operationName = 'operation'
|
||||
}) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const selectedAddresses = data.value.filter((item) =>
|
||||
checkedRowKeys.value.includes(item.id)
|
||||
);
|
||||
|
||||
if (selectedAddresses.length === 0) {
|
||||
message.error(t('pleaseSelectAddress'));
|
||||
return;
|
||||
}
|
||||
|
||||
const failedIds = [];
|
||||
const totalCount = selectedAddresses.length;
|
||||
|
||||
multiActionProgress.value = {
|
||||
percentage: 0,
|
||||
tip: `0/${totalCount}`
|
||||
};
|
||||
multiActionTitle.value = title;
|
||||
showMultiActionModal.value = true;
|
||||
|
||||
for (const [index, address] of selectedAddresses.entries()) {
|
||||
try {
|
||||
if (!shouldSkip(address)) {
|
||||
await apiCall(address.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${operationName} failed for address ${address.id}:`, error);
|
||||
failedIds.push(address.id);
|
||||
}
|
||||
multiActionProgress.value = {
|
||||
percentage: Math.floor((index + 1) / totalCount * 100),
|
||||
tip: `${index + 1}/${totalCount}`
|
||||
};
|
||||
}
|
||||
|
||||
await fetchData();
|
||||
checkedRowKeys.value = failedIds;
|
||||
message.success(t("success"));
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const multiActionDeleteAccounts = async () => {
|
||||
await executeBatchOperation({
|
||||
apiCall: (id) => api.adminDeleteAddress(id),
|
||||
title: t('multiDelete') + ' ' + t('success'),
|
||||
operationName: 'Delete'
|
||||
});
|
||||
}
|
||||
|
||||
const multiActionClearInbox = async () => {
|
||||
await executeBatchOperation({
|
||||
shouldSkip: (address) => address.mail_count <= 0,
|
||||
apiCall: (id) => api.fetch(`/admin/clear_inbox/${id}`, {
|
||||
method: 'DELETE'
|
||||
}),
|
||||
title: t('multiClearInbox') + ' ' + t('success'),
|
||||
operationName: 'ClearInbox'
|
||||
});
|
||||
}
|
||||
|
||||
const multiActionClearSentItems = async () => {
|
||||
await executeBatchOperation({
|
||||
shouldSkip: (address) => address.send_count <= 0,
|
||||
apiCall: (id) => api.fetch(`/admin/clear_sent_items/${id}`, {
|
||||
method: 'DELETE'
|
||||
}),
|
||||
title: t('multiClearSentItems') + ' ' + t('success'),
|
||||
operationName: 'ClearSentItems'
|
||||
});
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
addressQuery.value = addressQuery.value.trim()
|
||||
@@ -173,12 +294,15 @@ const fetchData = async () => {
|
||||
count.value = addressCount;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
console.error(error);
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
type: 'selection'
|
||||
},
|
||||
{
|
||||
title: "ID",
|
||||
key: "id"
|
||||
@@ -414,13 +538,43 @@ onMounted(async () => {
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-input-group>
|
||||
<n-input-group style="margin-bottom: 10px;">
|
||||
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')"
|
||||
@keydown.enter="fetchData" />
|
||||
<n-button @click="fetchData" type="primary" tertiary>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
|
||||
<n-space v-if="showMultiActionBar" style="margin-bottom: 10px;">
|
||||
<n-button @click="multiActionSelectAll" tertiary>
|
||||
{{ t('selectAll') }}
|
||||
</n-button>
|
||||
<n-button @click="multiActionUnselectAll" tertiary>
|
||||
{{ t('unselectAll') }}
|
||||
</n-button>
|
||||
<n-popconfirm @positive-click="multiActionDeleteAccounts">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="error">{{ t('multiDelete') }}</n-button>
|
||||
</template>
|
||||
{{ t('multiDeleteTip') }}
|
||||
</n-popconfirm>
|
||||
<n-popconfirm @positive-click="multiActionClearInbox">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="warning">{{ t('multiClearInbox') }}</n-button>
|
||||
</template>
|
||||
{{ t('multiClearInboxTip') }}
|
||||
</n-popconfirm>
|
||||
<n-popconfirm @positive-click="multiActionClearSentItems">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="warning">{{ t('multiClearSentItems') }}</n-button>
|
||||
</template>
|
||||
{{ t('multiClearSentItemsTip') }}
|
||||
</n-popconfirm>
|
||||
<n-tag type="info">
|
||||
{{ t('selectedItems') }}: {{ selectedCount }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
<div style="overflow: auto;">
|
||||
<div style="display: inline-block;">
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
||||
@@ -430,8 +584,21 @@ onMounted(async () => {
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
<n-data-table v-model:checked-row-keys="checkedRowKeys" :columns="columns" :data="data" :bordered="false"
|
||||
:row-key="row => row.id" embedded />
|
||||
</div>
|
||||
|
||||
<!-- Multi-action progress modal -->
|
||||
<n-modal v-model:show="showMultiActionModal" preset="dialog" :title="multiActionTitle" negative-text="OK">
|
||||
<n-space justify="center">
|
||||
<n-progress type="circle" status="info" :percentage="multiActionProgress.percentage">
|
||||
<span style="text-align: center">
|
||||
{{ multiActionProgress.tip }}
|
||||
</span>
|
||||
</n-progress>
|
||||
</n-space>
|
||||
</n-modal>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user