feat: add mailbox multi delete and download (#292)

This commit is contained in:
Dream Hunter
2024-06-01 21:23:17 +08:00
committed by GitHub
parent 9725407c77
commit 77155299e0
4 changed files with 326 additions and 96 deletions

View File

@@ -49,7 +49,7 @@ const props = defineProps({
})
const {
isDark, mailboxSplitSize, indexTab,
isDark, mailboxSplitSize, indexTab, loading,
useIframeShowMail, sendMailModel, preferShowTextMail
} = useGlobalState()
const autoRefresh = ref(false)
@@ -66,6 +66,12 @@ const curAttachments = ref([])
const curMail = ref(null);
const showTextMail = ref(preferShowTextMail.value)
const multiActionMode = ref(false)
const showMultiActionDownload = ref(false)
const showMultiActionDelete = ref(false)
const multiActionDownloadZip = ref({})
const multiActionDeleteProgress = ref({ percentage: 0, tip: '0/0' })
const { t } = useI18n({
messages: {
en: {
@@ -75,13 +81,17 @@ const { t } = useI18n({
refresh: 'Refresh',
attachments: 'Show Attachments',
downloadMail: 'Download Mail',
pleaseSelectMail: "Please select a mail to view.",
pleaseSelectMail: "Please select mail",
delete: 'Delete',
deleteMailTip: 'Are you sure you want to delete this mail?',
deleteMailTip: 'Are you sure you want to delete mail?',
reply: 'Reply',
showTextMail: 'Show Text Mail',
showHtmlMail: 'Show Html Mail',
saveToS3: 'Save to S3',
multiAction: 'Multi Action',
cancelMultiAction: 'Cancel Multi Action',
selectAll: 'Select All',
unselectAll: 'Unselect All',
},
zh: {
success: '成功',
@@ -90,13 +100,17 @@ const { t } = useI18n({
refresh: '刷新',
downloadMail: '下载邮件',
attachments: '查看附件',
pleaseSelectMail: "请选择一封邮件查看。",
pleaseSelectMail: "请选择邮件",
delete: '删除',
deleteMailTip: '确定要删除这封邮件吗?',
deleteMailTip: '确定要删除邮件吗?',
reply: '回复',
showTextMail: '显示纯文本邮件',
showHtmlMail: '显示HTML邮件',
saveToS3: '保存到S3',
multiAction: '多选',
cancelMultiAction: '取消多选',
selectAll: '全选',
unselectAll: '取消全选',
}
}
});
@@ -134,12 +148,14 @@ const refresh = async () => {
pageSize.value, (page.value - 1) * pageSize.value
);
data.value = await Promise.all(results.map(async (item) => {
item.checked = false;
return await processItem(item);
}));
if (totalCount > 0) {
count.value = totalCount;
}
if (!isMobile.value && !curMail.value && data.value.length > 0) {
curMail.value = null;
if (!isMobile.value && data.value.length > 0) {
curMail.value = data.value[0];
}
} catch (error) {
@@ -149,6 +165,10 @@ const refresh = async () => {
};
const clickRow = async (row) => {
if (multiActionMode.value) {
row.checked = !row.checked;
return;
}
curMail.value = row;
};
@@ -205,6 +225,82 @@ const saveToS3Proxy = async (filename, blob) => {
}
}
const multiActionModeClick = (enableMulti) => {
if (enableMulti) {
data.value.forEach((item) => {
item.checked = false;
});
multiActionMode.value = true;
} else {
multiActionMode.value = false;
data.value.forEach((item) => {
item.checked = false;
});
}
}
const multiActionSelectAll = (checked) => {
data.value.forEach((item) => {
item.checked = checked;
});
}
const multiActionDeleteMail = async () => {
try {
loading.value = true;
const selectedMails = data.value.filter((item) => item.checked);
if (selectedMails.length === 0) {
message.error(t('pleaseSelectMail'));
return;
}
multiActionDeleteProgress.value = {
percentage: 0,
tip: `0/${selectedMails.length}`
};
for (const [index, mail] of selectedMails.entries()) {
await props.deleteMail(mail.id);
showMultiActionDelete.value = true;
multiActionDeleteProgress.value = {
percentage: Math.floor((index + 1) / selectedMails.length * 100),
tip: `${index + 1}/${selectedMails.length}`
};
}
message.success(t("success"));
await refresh();
} catch (error) {
message.error(error.message || "error");
} finally {
loading.value = false;
showMultiActionDelete.value = true;
}
}
const multiActionDownload = async () => {
try {
loading.value = true;
const selectedMails = data.value.filter((item) => item.checked);
if (selectedMails.length === 0) {
message.error(t('pleaseSelectMail'));
return;
}
const JSZipModlue = await import('jszip');
const JSZip = JSZipModlue.default;
const zip = new JSZip();
for (const mail of selectedMails) {
zip.file(`${mail.id}.eml`, mail.raw);
}
multiActionDownloadZip.value = {
url: URL.createObjectURL(await zip.generateAsync({ type: "blob" })),
filename: `mails-${new Date().toISOString().replace(/:/g, '-')}.zip`
}
showMultiActionDownload.value = true;
} catch (error) {
message.error(error.message || "error");
} finally {
loading.value = false;
}
}
onMounted(async () => {
await refresh();
});
@@ -216,14 +312,38 @@ onBeforeUnmount(() => {
<template>
<div>
<n-split class="left" v-if="!isMobile" direction="horizontal" :max="0.75" :min="0.25"
:default-size="mailboxSplitSize" :on-update:size="onSpiltSizeChange">
<template #1>
<div class="center">
<div style="display: inline-block;">
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
</div>
<n-switch v-model:value="autoRefresh" size="small" :round="false">
<div v-if="!isMobile" class="left">
<div style="margin-bottom: 10px;">
<n-space v-if="multiActionMode">
<n-button @click="multiActionModeClick(false)" tertiary>
{{ t('cancelMultiAction') }}
</n-button>
<n-button @click="multiActionSelectAll(true)" tertiary>
{{ t('selectAll') }}
</n-button>
<n-button @click="multiActionSelectAll(false)" tertiary>
{{ t('unselectAll') }}
</n-button>
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="multiActionDeleteMail">
<template #trigger>
<n-button tertiary type="error">{{ t('delete') }}</n-button>
</template>
{{ t('deleteMailTip') }}
</n-popconfirm>
<n-button @click="multiActionDownload" tertiary type="info">
<template #icon>
<n-icon :component="CloudDownloadRound" />
</template>
{{ t('downloadMail') }}
</n-button>
</n-space>
<n-space v-else>
<n-button @click="multiActionModeClick(true)" type="primary" tertiary>
{{ t('multiAction') }}
</n-button>
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" :page-sizes="[20, 50, 100]"
show-size-picker />
<n-switch v-model:value="autoRefresh" :round="false">
<template #checked>
{{ t('refreshAfter', { msg: autoRefreshInterval }) }}
</template>
@@ -231,90 +351,98 @@ onBeforeUnmount(() => {
{{ t('autoRefresh') }}
</template>
</n-switch>
<n-button @click="refresh" size="small" type="primary" tertiary>
<n-button @click="refresh" type="primary" tertiary>
{{ t('refresh') }}
</n-button>
</div>
<div style="overflow: auto; height: 80vh;">
<n-list hoverable clickable>
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
:class="mailItemClass(row)">
<n-thing :title="row.subject">
<template #description>
<n-tag type="info">
ID: {{ row.id }}
</n-tag>
<n-tag type="info">
{{ `${row.created_at} UTC` }}
</n-tag>
<n-tag type="info">
FROM: {{ row.source }}
</n-tag>
<n-tag v-if="showEMailTo" type="info">
TO: {{ row.address }}
</n-tag>
</n-space>
</div>
<n-split class="left" direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize"
:on-update:size="onSpiltSizeChange">
<template #1>
<div style="overflow: auto; height: 80vh;">
<n-list hoverable clickable>
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
:class="mailItemClass(row)">
<template #prefix v-if="multiActionMode">
<n-checkbox v-model:checked="row.checked" />
</template>
</n-thing>
</n-list-item>
</n-list>
</div>
</template>
<template #2>
<n-card v-if="curMail" class="mail-item" :title="curMail.subject" style="overflow: auto; max-height: 100vh;">
<n-space>
<n-tag type="info">
ID: {{ curMail.id }}
</n-tag>
<n-tag type="info">
{{ `${curMail.created_at} UTC` }}
</n-tag>
<n-tag type="info">
FROM: {{ curMail.source }}
</n-tag>
<n-tag v-if="showEMailTo" type="info">
TO: {{ curMail.address }}
</n-tag>
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="deleteMail">
<template #trigger>
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
</template>
{{ t('deleteMailTip') }}
</n-popconfirm>
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
@click="getAttachments(curMail.attachments)">
{{ t('attachments') }}
</n-button>
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
:href="getDownloadEmlUrl(curMail.raw)">
<template #icon>
<n-icon :component="CloudDownloadRound" />
</template>
{{ t('downloadMail') }}
</n-button>
<n-button v-if="showReply" size="small" tertiary type="info" @click="replyMail">
<template #icon>
<n-icon :component="ReplyFilled" />
</template>
{{ t('reply') }}
</n-button>
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
</n-button>
</n-space>
<pre v-if="showTextMail" style="margin-top: 10px;">{{ curMail.text }}</pre>
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
style="margin-top: 10px;width: 100%; height: 100%;">
</iframe>
<div v-else v-html="curMail.message" style="margin-top: 10px;"></div>
</n-card>
<n-card class="mail-item" v-else>
<n-result status="info" :title="t('pleaseSelectMail')">
</n-result>
</n-card>
</template>
</n-split>
<n-thing :title="row.subject">
<template #description>
<n-tag type="info">
ID: {{ row.id }}
</n-tag>
<n-tag type="info">
{{ `${row.created_at} UTC` }}
</n-tag>
<n-tag type="info">
FROM: {{ row.source }}
</n-tag>
<n-tag v-if="showEMailTo" type="info">
TO: {{ row.address }}
</n-tag>
</template>
</n-thing>
</n-list-item>
</n-list>
</div>
</template>
<template #2>
<n-card v-if="curMail" class="mail-item" :title="curMail.subject" style="overflow: auto; max-height: 100vh;">
<n-space>
<n-tag type="info">
ID: {{ curMail.id }}
</n-tag>
<n-tag type="info">
{{ `${curMail.created_at} UTC` }}
</n-tag>
<n-tag type="info">
FROM: {{ curMail.source }}
</n-tag>
<n-tag v-if="showEMailTo" type="info">
TO: {{ curMail.address }}
</n-tag>
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="deleteMail">
<template #trigger>
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
</template>
{{ t('deleteMailTip') }}
</n-popconfirm>
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
@click="getAttachments(curMail.attachments)">
{{ t('attachments') }}
</n-button>
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
:href="getDownloadEmlUrl(curMail.raw)">
<template #icon>
<n-icon :component="CloudDownloadRound" />
</template>
{{ t('downloadMail') }}
</n-button>
<n-button v-if="showReply" size="small" tertiary type="info" @click="replyMail">
<template #icon>
<n-icon :component="ReplyFilled" />
</template>
{{ t('reply') }}
</n-button>
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
</n-button>
</n-space>
<pre v-if="showTextMail" style="margin-top: 10px;">{{ curMail.text }}</pre>
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
style="margin-top: 10px;width: 100%; height: 100%;">
</iframe>
<div v-else v-html="curMail.message" style="margin-top: 10px;"></div>
</n-card>
<n-card class="mail-item" v-else>
<n-result status="info" :title="t('pleaseSelectMail')">
</n-result>
</n-card>
</template>
</n-split>
</div>
<div class="left" v-else>
<div class="center">
<n-space justify="center">
<div style="display: inline-block;">
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
</div>
@@ -326,10 +454,10 @@ onBeforeUnmount(() => {
{{ t('autoRefresh') }}
</template>
</n-switch>
<n-button @click="refresh" size="small" type="primary">
<n-button @click="refresh" tertiary size="small" type="primary">
{{ t('refresh') }}
</n-button>
</div>
</n-space>
<div style="overflow: auto; height: 80vh;">
<n-list hoverable clickable>
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)">
@@ -433,6 +561,26 @@ onBeforeUnmount(() => {
</n-list>
</n-spin>
</n-modal>
<n-modal v-model:show="showMultiActionDownload" preset="dialog" :title="t('downloadMail')">
<n-tag type="info">
{{ multiActionDownloadZip.filename }}
</n-tag>
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="multiActionDownloadZip.filename"
:href="multiActionDownloadZip.url">
<n-icon :component="CloudDownloadRound" />
{{ t('downloadMail') + " zip" }}
</n-button>
</n-modal>
<n-modal v-model:show="showMultiActionDelete" preset="dialog" :title="t('delete') + t('success')"
negative-text="OK">
<n-space justify="center">
<n-progress type="circle" status="error" :percentage="multiActionDeleteProgress.percentage">
<span style="text-align: center">
{{ multiActionDeleteProgress.tip }}
</span>
</n-progress>
</n-space>
</n-modal>
</div>
</template>