feat: |UI| Optimized minimalist mode homepage, added all email page f… (#708)

feat: |UI| Optimized minimalist mode homepage, added all email page functions (delete/download/attachments/...)
This commit is contained in:
Dream Hunter
2025-08-23 00:23:47 +08:00
committed by GitHub
parent 840496c48f
commit 4084771621
13 changed files with 1713 additions and 1520 deletions
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "cloudflare_temp_email",
"version": "1.0.3",
"version": "1.0.4",
"private": true,
"type": "module",
"scripts": {
@@ -30,7 +30,7 @@
"naive-ui": "^2.42.0",
"postal-mime": "^2.4.4",
"vooks": "^0.2.12",
"vue": "^3.5.18",
"vue": "^3.5.19",
"vue-clipboard3": "^2.0.0",
"vue-i18n": "^11.1.11",
"vue-router": "^4.5.1"
@@ -42,12 +42,12 @@
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.8.0",
"vite": "^6.3.5",
"vite-plugin-pwa": "^1.0.2",
"vite-plugin-pwa": "^1.0.3",
"vite-plugin-top-level-await": "^1.6.0",
"vite-plugin-wasm": "^3.5.0",
"workbox-build": "^7.3.0",
"workbox-window": "^7.3.0",
"wrangler": "^4.28.1"
"wrangler": "^4.32.0"
},
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
}
+789 -779
View File
File diff suppressed because it is too large Load Diff
+14 -149
View File
@@ -3,11 +3,11 @@ import { watch, onMounted, ref, onBeforeUnmount } from "vue";
import { useMessage } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import { useGlobalState } from '../store'
import { CloudDownloadRound, ReplyFilled, ForwardFilled } from '@vicons/material'
import { CloudDownloadRound } from '@vicons/material'
import { useIsMobile } from '../utils/composables'
import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
import { processItem } from '../utils/email-parser'
import { utcToLocalDate } from '../utils';
import ShadowHtmlComponent from "./ShadowHtmlComponent.vue";
import MailContentRenderer from "./MailContentRenderer.vue";
const message = useMessage()
const isMobile = useIsMobile()
@@ -51,8 +51,8 @@ const props = defineProps({
})
const {
isDark, mailboxSplitSize, indexTab, loading, useUTCDate, autoRefresh, configAutoRefreshInterval,
useIframeShowMail, sendMailModel, preferShowTextMail
isDark, mailboxSplitSize, indexTab, loading, useUTCDate,
autoRefresh, configAutoRefreshInterval, sendMailModel
} = useGlobalState()
const autoRefreshInterval = ref(configAutoRefreshInterval.value)
const data = ref([])
@@ -62,10 +62,7 @@ const count = ref(0)
const page = ref(1)
const pageSize = ref(20)
const showAttachments = ref(false)
const curAttachments = ref([])
const curMail = ref(null);
const showTextMail = ref(preferShowTextMail.value)
const multiActionMode = ref(false)
const showMultiActionDownload = ref(false)
@@ -185,10 +182,6 @@ const clickRow = async (row) => {
curMail.value = row;
};
const getAttachments = (attachments) => {
curAttachments.value = attachments;
showAttachments.value = true;
};
const mailItemClass = (row) => {
return curMail.value && row.id == curMail.value.id ? (isDark.value ? 'overlay overlay-dark-backgroud' : 'overlay overlay-light-backgroud') : '';
@@ -237,14 +230,8 @@ const onSpiltSizeChange = (size) => {
mailboxSplitSize.value = size;
}
const attachmentLoding = ref(false)
const saveToS3Proxy = async (filename, blob) => {
attachmentLoding.value = true
try {
await props.saveToS3(curMail.value.id, filename, blob);
} finally {
attachmentLoding.value = false
}
await props.saveToS3(curMail.value.id, filename, blob);
}
const multiActionModeClick = (enableMulti) => {
@@ -415,57 +402,10 @@ onBeforeUnmount(() => {
<template #2>
<n-card :bordered="false" embedded 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">
{{ utcToLocalDate(curMail.created_at, useUTCDate) }}
</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 v-if="showReply" size="small" tertiary type="info" @click="forwardMail">
<template #icon>
<n-icon :component="ForwardFilled" />
</template>
{{ t('forwardMail') }}
</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>
<ShadowHtmlComponent v-else :htmlContent="curMail.message" style="margin-top: 10px;" />
<MailContentRenderer :mail="curMail" :showEMailTo="showEMailTo"
:enableUserDeleteEmail="enableUserDeleteEmail" :showReply="showReply" :showSaveS3="showSaveS3"
:onDelete="deleteMail" :onReply="replyMail" :onForward="forwardMail"
:onSaveToS3="saveToS3Proxy" />
</n-card>
<n-card :bordered="false" embedded class="mail-item" v-else>
<n-result status="info" :title="t('pleaseSelectMail')">
@@ -517,89 +457,14 @@ onBeforeUnmount(() => {
style="height: 80vh;">
<n-drawer-content :title="curMail ? curMail.subject : ''" closable>
<n-card :bordered="false" embedded style="overflow: auto;">
<n-space>
<n-tag type="info">
ID: {{ curMail.id }}
</n-tag>
<n-tag type="info">
{{ utcToLocalDate(curMail.created_at, useUTCDate) }}
</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)">
<n-icon :component="CloudDownloadRound" />
{{ 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 v-if="showReply" size="small" tertiary type="info" @click="forwardMail">
<template #icon>
<n-icon :component="ForwardFilled" />
</template>
{{ t('forwardMail') }}
</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>
<ShadowHtmlComponent :key="curMail.id" v-else :htmlContent="curMail.message" style="margin-top: 10px;" />
<MailContentRenderer :mail="curMail" :showEMailTo="showEMailTo"
:enableUserDeleteEmail="enableUserDeleteEmail" :showReply="showReply" :showSaveS3="showSaveS3"
:useUTCDate="useUTCDate" :onDelete="deleteMail" :onReply="replyMail"
:onForward="forwardMail" :onSaveToS3="saveToS3Proxy" />
</n-card>
</n-drawer-content>
</n-drawer>
</div>
<n-modal v-model:show="showAttachments" preset="dialog" title="Dialog">
<template #header>
<div>{{ t("attachments") }}</div>
</template>
<n-spin v-model:show="attachmentLoding">
<n-list hoverable clickable>
<n-list-item v-for="row in curAttachments" v-bind:key="row.id">
<n-thing class="center" :title="row.filename">
<template #description>
<n-space>
<n-tag type="info">
Size: {{ row.size }}
</n-tag>
<n-button v-if="showSaveS3" @click="saveToS3Proxy(row.filename, row.blob)" ghost type="info"
size="small">
{{ t('saveToS3') }}
</n-button>
</n-space>
</template>
</n-thing>
<template #suffix>
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="row.filename"
:href="row.url">
<n-icon :component="CloudDownloadRound" />
</n-button>
</template>
</n-list-item>
</n-list>
</n-spin>
</n-modal>
<n-modal v-model:show="showMultiActionDownload" preset="dialog" :title="t('downloadMail')">
<n-tag type="info">
{{ multiActionDownloadZip.filename }}
@@ -0,0 +1,282 @@
<script setup>
import { ref } from "vue";
import { useI18n } from 'vue-i18n'
import { CloudDownloadRound, ReplyFilled, ForwardFilled, FullscreenRound } from '@vicons/material'
import ShadowHtmlComponent from "./ShadowHtmlComponent.vue";
import { getDownloadEmlUrl } from '../utils/email-parser';
import { utcToLocalDate } from '../utils';
import { useGlobalState } from '../store';
const { preferShowTextMail, useIframeShowMail, useUTCDate } = useGlobalState();
const { t } = useI18n({
messages: {
en: {
delete: 'Delete',
deleteMailTip: 'Are you sure you want to delete mail?',
attachments: 'View Attachments',
downloadMail: 'Download Mail',
reply: 'Reply',
forward: 'Forward',
showTextMail: 'Show Text Mail',
showHtmlMail: 'Show HTML Mail',
saveToS3: 'Save to S3',
size: 'Size',
fullscreen: 'Fullscreen',
},
zh: {
delete: '删除',
deleteMailTip: '确定要删除邮件吗?',
attachments: '查看附件',
downloadMail: '下载邮件',
reply: '回复',
forward: '转发',
showTextMail: '显示纯文本邮件',
showHtmlMail: '显示HTML邮件',
saveToS3: '保存到S3',
size: '大小',
fullscreen: '全屏',
}
}
});
const props = defineProps({
mail: {
type: Object,
required: true
},
showEMailTo: {
type: Boolean,
default: true
},
enableUserDeleteEmail: {
type: Boolean,
default: false
},
showReply: {
type: Boolean,
default: false
},
showSaveS3: {
type: Boolean,
default: false
},
// 回调函数 props
onDelete: {
type: Function,
default: () => { }
},
onReply: {
type: Function,
default: () => { }
},
onForward: {
type: Function,
default: () => { }
},
onSaveToS3: {
type: Function,
default: () => { }
}
});
const showTextMail = ref(preferShowTextMail.value);
const showAttachments = ref(false);
const curAttachments = ref([]);
const attachmentLoding = ref(false);
const showFullscreen = ref(false);
const handleDelete = () => {
props.onDelete();
};
const handleViewAttachments = () => {
curAttachments.value = props.mail.attachments;
showAttachments.value = true;
};
const handleReply = () => {
props.onReply();
};
const handleForward = () => {
props.onForward();
};
const handleSaveToS3 = async (filename, blob) => {
attachmentLoding.value = true;
try {
await props.onSaveToS3(filename, blob);
} finally {
attachmentLoding.value = false;
}
};
</script>
<template>
<div class="mail-content-renderer">
<!-- 邮件信息标签 -->
<n-space>
<n-tag type="info">
ID: {{ mail.id }}
</n-tag>
<n-tag type="info">
{{ utcToLocalDate(mail.created_at, useUTCDate.value) }}
</n-tag>
<n-tag type="info">
FROM: {{ mail.source }}
</n-tag>
<n-tag v-if="showEMailTo" type="info">
TO: {{ mail.address }}
</n-tag>
<!-- 操作按钮 -->
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="handleDelete">
<template #trigger>
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
</template>
{{ t('deleteMailTip') }}
</n-popconfirm>
<n-button v-if="mail.attachments && mail.attachments.length > 0" size="small" tertiary type="info"
@click="handleViewAttachments">
{{ t('attachments') }}
</n-button>
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="mail.id + '.eml'"
:href="getDownloadEmlUrl(mail.raw)">
<template #icon>
<n-icon :component="CloudDownloadRound" />
</template>
{{ t('downloadMail') }}
</n-button>
<n-button v-if="showReply" size="small" tertiary type="info" @click="handleReply">
<template #icon>
<n-icon :component="ReplyFilled" />
</template>
{{ t('reply') }}
</n-button>
<n-button v-if="showReply" size="small" tertiary type="info" @click="handleForward">
<template #icon>
<n-icon :component="ForwardFilled" />
</template>
{{ t('forward') }}
</n-button>
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
</n-button>
<n-button size="small" tertiary type="info" @click="showFullscreen = true">
<template #icon>
<n-icon :component="FullscreenRound" />
</template>
{{ t('fullscreen') }}
</n-button>
</n-space>
<!-- 邮件内容 -->
<div class="mail-content">
<pre v-if="showTextMail" class="mail-text">{{ mail.text }}</pre>
<iframe v-else-if="useIframeShowMail" :srcdoc="mail.message" class="mail-iframe">
</iframe>
<ShadowHtmlComponent v-else :key="mail.id" :htmlContent="mail.message" class="mail-html" />
</div>
</div>
<n-drawer v-model:show="showFullscreen" width="100%" placement="bottom" :trap-focus="false" :block-scroll="false"
style="height: 100vh;">
<n-drawer-content :title="mail.subject" closable>
<div class="fullscreen-mail-content">
<pre v-if="showTextMail" class="mail-text">{{ mail.text }}</pre>
<iframe v-else-if="useIframeShowMail" :srcdoc="mail.message" class="mail-iframe">
</iframe>
<ShadowHtmlComponent v-else :key="mail.id" :htmlContent="mail.message" class="mail-html" />
</div>
</n-drawer-content>
</n-drawer>
<!-- 附件模态框 -->
<n-modal v-model:show="showAttachments" preset="dialog" title="Dialog">
<template #header>
<div>{{ t('attachments') }}</div>
</template>
<n-spin v-model:show="attachmentLoding">
<n-list hoverable clickable>
<n-list-item v-for="row in curAttachments" v-bind:key="row.id">
<n-thing class="center" :title="row.filename">
<template #description>
<n-space>
<n-tag type="info">
Size: {{ row.size }}
</n-tag>
<n-button v-if="showSaveS3" @click="handleSaveToS3(row.filename, row.blob)" ghost type="info"
size="small">
{{ t('saveToS3') }}
</n-button>
</n-space>
</template>
</n-thing>
<template #suffix>
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="row.filename"
:href="row.url">
<n-icon :component="CloudDownloadRound" />
</n-button>
</template>
</n-list-item>
</n-list>
</n-spin>
</n-modal>
</template>
<style scoped>
.mail-content-renderer {
display: flex;
flex-direction: column;
gap: 10px;
}
.mail-content {
margin-top: 10px;
flex: 1;
}
.mail-text {
white-space: pre-wrap;
word-wrap: break-word;
margin: 0;
padding: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.mail-iframe {
width: 100%;
height: 100%;
border: none;
min-height: 400px;
}
.mail-html {
width: 100%;
height: 100%;
}
.center {
text-align: center;
}
.fullscreen-mail-content {
height: calc(100vh - 120px);
overflow: auto;
}
.fullscreen-mail-content .mail-iframe {
min-height: calc(100vh - 120px);
}
</style>
+53 -22
View File
@@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted, computed, watch } from 'vue'
import { ref, onMounted, computed, watch, onBeforeUnmount } from 'vue'
import { useI18n } from 'vue-i18n'
import { useMessage } from 'naive-ui'
import {
@@ -16,18 +16,18 @@ import { api } from '../../api'
import Login from '../common/Login.vue'
import AccountSettings from './AccountSettings.vue'
import { processItem } from '../../utils/email-parser'
import { utcToLocalDate } from '../../utils'
import ShadowHtmlComponent from '../../components/ShadowHtmlComponent.vue'
import MailContentRenderer from '../../components/MailContentRenderer.vue'
const { jwt, settings, useSimpleIndex, useUTCDate, showAddressCredential } = useGlobalState()
const { jwt, settings, useSimpleIndex, showAddressCredential, openSettings, loading } = useGlobalState()
const message = useMessage()
// 邮件数据
const currentPage = ref(1)
const totalCount = ref(0)
const loading = ref(false)
const currentMail = ref(null)
const showAccountSettingsCard = ref(false)
const currentAutoRefreshInterval = ref(60)
const timer = ref(null)
const { t } = useI18n({
messages: {
@@ -44,6 +44,8 @@ const { t } = useI18n({
accountSettings: "Account Settings",
addressCredential: 'Mail Address Credential',
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login',
deleteSuccess: 'Mail deleted successfully',
refreshAfter: 'Refresh After {msg} Seconds',
},
zh: {
exitSimpleIndex: '退出极简',
@@ -57,7 +59,9 @@ const { t } = useI18n({
mailCount: '{current} / {total} 封邮件',
accountSettings: "账户设置",
addressCredential: '邮箱地址凭证',
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。'
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
deleteSuccess: '邮件删除成功',
refreshAfter: '{msg}秒后刷新',
}
}
})
@@ -86,9 +90,26 @@ const fetchMails = async () => {
}
}
// 删除邮件
const deleteMail = async () => {
if (!currentMail.value) return;
try {
await api.fetch(`/api/mails/${currentMail.value.id}`, { method: 'DELETE' });
message.success(t('deleteSuccess'));
currentMail.value = null;
await refreshMails();
} catch (error) {
console.error('Failed to delete mail:', error);
message.error('删除邮件失败');
}
}
// 刷新邮件
const refreshMails = async () => {
if (loading.value) return
currentPage.value = 1
showAccountSettingsCard.value = false
currentAutoRefreshInterval.value = 60
await fetchMails()
message.success(t('refreshSuccess'))
}
@@ -98,6 +119,7 @@ const currentPageDisplay = computed(() => currentPage.value)
const totalPages = computed(() => Math.max(1, totalCount.value))
const canGoPrev = computed(() => currentPage.value > 1)
const canGoNext = computed(() => currentPage.value < totalPages.value)
const isFirstPage = computed(() => currentPage.value === 1)
const prevPage = async () => {
if (canGoPrev.value) {
@@ -119,6 +141,22 @@ watch(currentPage, () => {
onMounted(async () => {
await api.getSettings()
await fetchMails()
// 启动自动刷新
timer.value = setInterval(async () => {
if (!isFirstPage.value) {
currentAutoRefreshInterval.value = 60
return
}
if (--currentAutoRefreshInterval.value <= 0) {
await refreshMails()
}
}, 1000)
})
onBeforeUnmount(() => {
clearInterval(timer.value)
})
</script>
@@ -169,6 +207,11 @@ onMounted(async () => {
{{ t('accountSettings') }}
</n-button>
</n-flex>
<div v-if="isFirstPage" style="text-align: center; margin-top: 12px;">
<n-text depth="3" size="12">
{{ t('refreshAfter', { msg: Math.max(0, currentAutoRefreshInterval) }) }}
</n-text>
</div>
</n-card>
<!-- 账户设置卡片 -->
@@ -177,7 +220,7 @@ onMounted(async () => {
<AccountSettings />
</n-card>
<n-card :bordered="false" embedded style="text-align: left;">
<n-card v-else :bordered="false" embedded style="text-align: left;">
<div v-if="totalCount > 1">
<n-flex justify="space-between">
@@ -208,22 +251,10 @@ onMounted(async () => {
</div>
<div v-else>
<h3 v-if="currentMail.subject">{{ currentMail.subject }}</h3>
<n-space>
<n-tag type="info">
ID: {{ currentMail.id }}
</n-tag>
<n-tag type="info">
{{ utcToLocalDate(currentMail.created_at, useUTCDate.value) }}
</n-tag>
<n-tag type="info">
FROM: {{ currentMail.source }}
</n-tag>
</n-space>
<div style="margin-top: 16px;">
<ShadowHtmlComponent v-if="currentMail.message" :htmlContent="currentMail.message" />
<pre v-else>{{ currentMail.text }}</pre>
<MailContentRenderer :mail="currentMail" :showEMailTo="false" :showReply="false"
:enableUserDeleteEmail="openSettings.enableUserDeleteEmail" :showSaveS3="false"
:onDelete="deleteMail" />
</div>
</div>
</n-card>