mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-05 07:27:27 +08:00
feat: add single-mail read status (#1125)
This commit is contained in:
@@ -21,7 +21,8 @@ const instance = axios.create({
|
||||
});
|
||||
|
||||
const apiFetch = async (path, options = {}) => {
|
||||
loading.value = true;
|
||||
const showLoading = options.showLoading !== false;
|
||||
if (showLoading) loading.value = true;
|
||||
try {
|
||||
// Get browser fingerprint for request tracking
|
||||
const fingerprint = await getFingerprint();
|
||||
@@ -67,7 +68,7 @@ const apiFetch = async (path, options = {}) => {
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
if (showLoading) loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +100,7 @@ const getOpenSettings = async (message, notification) => {
|
||||
disableAnonymousUserCreateEmail: res["disableAnonymousUserCreateEmail"] || false,
|
||||
disableCustomAddressName: res["disableCustomAddressName"] || false,
|
||||
enableUserDeleteEmail: res["enableUserDeleteEmail"] || false,
|
||||
enableMailReadStatus: res["enableMailReadStatus"] === true,
|
||||
enableAutoReply: res["enableAutoReply"] || false,
|
||||
enableIndexAbout: res["enableIndexAbout"] || false,
|
||||
copyright: res["copyright"] || openSettings.value.copyright,
|
||||
|
||||
@@ -55,6 +55,14 @@ const props = defineProps({
|
||||
default: false,
|
||||
required: false
|
||||
},
|
||||
enableMailReadStatus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
updateMailReadStatus: {
|
||||
type: Function,
|
||||
default: () => { }
|
||||
},
|
||||
})
|
||||
|
||||
const localFilterKeyword = ref('')
|
||||
@@ -94,6 +102,29 @@ const data = computed(() => {
|
||||
});
|
||||
})
|
||||
|
||||
const openMail = (mail) => {
|
||||
curMail.value = mail
|
||||
if (mail?.is_unread !== 1 || !props.enableMailReadStatus) return
|
||||
mail.is_unread = 0
|
||||
void props.updateMailReadStatus(mail.id, false).catch(() => {
|
||||
mail.is_unread = 1
|
||||
})
|
||||
}
|
||||
|
||||
const toggleCurrentMailUnread = async () => {
|
||||
if (!curMail.value || !props.enableMailReadStatus) return
|
||||
const mail = curMail.value
|
||||
const previousValue = mail.is_unread
|
||||
const isUnread = previousValue !== 1
|
||||
mail.is_unread = isUnread ? 1 : 0
|
||||
try {
|
||||
await props.updateMailReadStatus(mail.id, isUnread)
|
||||
message.success(t("success"))
|
||||
} catch {
|
||||
mail.is_unread = previousValue
|
||||
}
|
||||
}
|
||||
|
||||
const canGoPrevMail = computed(() => {
|
||||
if (!curMail.value) return false
|
||||
const currentIndex = data.value.findIndex(mail => mail.id === curMail.value.id)
|
||||
@@ -111,12 +142,12 @@ const prevMail = async () => {
|
||||
const currentIndex = data.value.findIndex(mail => mail.id === curMail.value.id)
|
||||
|
||||
if (currentIndex > 0) {
|
||||
curMail.value = data.value[currentIndex - 1]
|
||||
openMail(data.value[currentIndex - 1])
|
||||
} else if (page.value > 1) {
|
||||
page.value--
|
||||
await refresh()
|
||||
if (data.value.length > 0) {
|
||||
curMail.value = data.value[data.value.length - 1]
|
||||
openMail(data.value[data.value.length - 1])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,12 +157,12 @@ const nextMail = async () => {
|
||||
const currentIndex = data.value.findIndex(mail => mail.id === curMail.value.id)
|
||||
|
||||
if (currentIndex < data.value.length - 1) {
|
||||
curMail.value = data.value[currentIndex + 1]
|
||||
openMail(data.value[currentIndex + 1])
|
||||
} else if (count.value > page.value * pageSize.value) {
|
||||
page.value++
|
||||
await refresh()
|
||||
if (data.value.length > 0) {
|
||||
curMail.value = data.value[0]
|
||||
openMail(data.value[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,7 +236,7 @@ const backFirstPageAndRefresh = async () => {
|
||||
await refresh();
|
||||
}
|
||||
|
||||
const clickRow = async (row) => {
|
||||
const clickRow = (row) => {
|
||||
if (multiActionMode.value) {
|
||||
row.checked = !row.checked;
|
||||
curMail.value = row;
|
||||
@@ -215,7 +246,7 @@ const clickRow = async (row) => {
|
||||
curMail.value = null;
|
||||
return;
|
||||
}
|
||||
curMail.value = row;
|
||||
openMail(row);
|
||||
};
|
||||
|
||||
|
||||
@@ -397,7 +428,7 @@ onBeforeUnmount(() => {
|
||||
<div style="overflow: auto; min-height: 60vh; max-height: 100vh;">
|
||||
<n-list hoverable clickable>
|
||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
||||
:class="mailItemClass(row)">
|
||||
:class="[mailItemClass(row), { 'mail-list-unread': enableMailReadStatus && row.is_unread === 1 }]">
|
||||
<template #prefix v-if="multiActionMode">
|
||||
<n-checkbox v-model:checked="row.checked" />
|
||||
</template>
|
||||
@@ -461,6 +492,7 @@ onBeforeUnmount(() => {
|
||||
style="overflow: auto; max-height: 100vh;">
|
||||
<MailContentRenderer :mail="curMail" :showEMailTo="showEMailTo"
|
||||
:enableUserDeleteEmail="enableUserDeleteEmail" :showReply="showReply" :showSaveS3="showSaveS3"
|
||||
:enableMailReadStatus="enableMailReadStatus" :onUpdateMailReadStatus="toggleCurrentMailUnread"
|
||||
:onDelete="deleteMail" :onReply="replyMail" :onForward="forwardMail" :onSaveToS3="saveToS3Proxy" />
|
||||
</n-card>
|
||||
<n-card :bordered="false" embedded class="mail-item" v-else>
|
||||
@@ -475,7 +507,7 @@ onBeforeUnmount(() => {
|
||||
<div v-else class="mail-list-scroll">
|
||||
<n-list hoverable clickable>
|
||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
||||
:class="mailItemClass(row)">
|
||||
:class="[mailItemClass(row), { 'mail-list-unread': enableMailReadStatus && row.is_unread === 1 }]">
|
||||
<template #prefix v-if="multiActionMode">
|
||||
<n-checkbox v-model:checked="row.checked" />
|
||||
</template>
|
||||
@@ -536,7 +568,8 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
<div style="overflow: auto; min-height: 60vh; max-height: 100vh;">
|
||||
<n-list hoverable clickable>
|
||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)">
|
||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
||||
:class="{ 'mail-list-unread': enableMailReadStatus && row.is_unread === 1 }">
|
||||
<n-thing :title="row.subject">
|
||||
<template #description>
|
||||
<n-tag type="info">
|
||||
@@ -567,6 +600,7 @@ onBeforeUnmount(() => {
|
||||
<n-card :bordered="false" embedded style="overflow: auto;">
|
||||
<MailContentRenderer :mail="curMail" :showEMailTo="showEMailTo"
|
||||
:enableUserDeleteEmail="enableUserDeleteEmail" :showReply="showReply" :showSaveS3="showSaveS3"
|
||||
:enableMailReadStatus="enableMailReadStatus" :onUpdateMailReadStatus="toggleCurrentMailUnread"
|
||||
:useUTCDate="useUTCDate" :onDelete="deleteMail" :onReply="replyMail" :onForward="forwardMail"
|
||||
:onSaveToS3="saveToS3Proxy" />
|
||||
</n-card>
|
||||
@@ -676,6 +710,21 @@ onBeforeUnmount(() => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mail-list-unread :deep(.n-thing-header__title) {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mail-list-unread :deep(.n-thing-header__title)::before {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-right: 8px;
|
||||
border-radius: 50%;
|
||||
background: #2080f0;
|
||||
content: '';
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
|
||||
@@ -34,6 +34,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
enableMailReadStatus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 回调函数 props
|
||||
onDelete: {
|
||||
type: Function,
|
||||
@@ -50,6 +54,10 @@ const props = defineProps({
|
||||
onSaveToS3: {
|
||||
type: Function,
|
||||
default: () => { }
|
||||
},
|
||||
onUpdateMailReadStatus: {
|
||||
type: Function,
|
||||
default: () => { }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -57,6 +65,7 @@ const showTextMail = ref(preferShowTextMail.value);
|
||||
const showAttachments = ref(false);
|
||||
const curAttachments = ref([]);
|
||||
const attachmentLoding = ref(false);
|
||||
const readStatusUpdating = ref(false);
|
||||
const showFullscreen = ref(false);
|
||||
|
||||
// Per-mail consent, deliberately independent of the global setting: it only
|
||||
@@ -96,6 +105,15 @@ const handleForward = () => {
|
||||
props.onForward();
|
||||
};
|
||||
|
||||
const handleUpdateMailReadStatus = async () => {
|
||||
readStatusUpdating.value = true;
|
||||
try {
|
||||
await props.onUpdateMailReadStatus();
|
||||
} finally {
|
||||
readStatusUpdating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleSaveToS3 = async (filename, blob) => {
|
||||
attachmentLoding.value = true;
|
||||
@@ -138,6 +156,11 @@ const handleSaveToS3 = async (filename, blob) => {
|
||||
{{ t('attachments') }}
|
||||
</n-button>
|
||||
|
||||
<n-button v-if="enableMailReadStatus" size="small" tertiary type="info" :loading="readStatusUpdating"
|
||||
@click="handleUpdateMailReadStatus">
|
||||
{{ mail.is_unread === 1 ? t('markAsRead') : t('markAsUnread') }}
|
||||
</n-button>
|
||||
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="mail.id + '.eml'"
|
||||
:href="getDownloadEmlUrl(mail.raw)">
|
||||
<template #icon>
|
||||
|
||||
@@ -190,6 +190,14 @@ export const MESSAGE_REGISTRY = {
|
||||
"en": "Fullscreen",
|
||||
"zh": "全屏"
|
||||
},
|
||||
"markAsRead": {
|
||||
"en": "Mark as Read",
|
||||
"zh": "标为已读"
|
||||
},
|
||||
"markAsUnread": {
|
||||
"en": "Mark as Unread",
|
||||
"zh": "标为未读"
|
||||
},
|
||||
"loadRemoteImages": {
|
||||
"en": "Load Images",
|
||||
"zh": "加载图片"
|
||||
@@ -1198,6 +1206,10 @@ export const MESSAGE_REGISTRY = {
|
||||
"en": "Previous",
|
||||
"zh": "上一页"
|
||||
},
|
||||
"readStatusUpdated": {
|
||||
"en": "Read status updated",
|
||||
"zh": "已读状态已更新"
|
||||
},
|
||||
"refreshAfter": {
|
||||
"en": "Refresh After {msg} Seconds",
|
||||
"zh": "{msg}秒后刷新"
|
||||
|
||||
@@ -24,6 +24,7 @@ export const useGlobalState = createGlobalState(
|
||||
disableAnonymousUserCreateEmail: false,
|
||||
disableCustomAddressName: false,
|
||||
enableUserDeleteEmail: false,
|
||||
enableMailReadStatus: false,
|
||||
enableAutoReply: false,
|
||||
enableIndexAbout: false,
|
||||
/** @type {string[]} */
|
||||
|
||||
@@ -45,6 +45,14 @@ const deleteMail = async (curMailId) => {
|
||||
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
|
||||
};
|
||||
|
||||
const updateMailReadStatus = async (id, isUnread) => {
|
||||
await api.fetch(`/api/mails/${id}/read`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ isUnread }),
|
||||
showLoading: false
|
||||
})
|
||||
}
|
||||
|
||||
const deleteSenboxMail = async (curMailId) => {
|
||||
await api.fetch(`/api/sendbox/${curMailId}`, { method: 'DELETE' });
|
||||
};
|
||||
@@ -127,7 +135,8 @@ onMounted(() => {
|
||||
</div>
|
||||
<MailBox :key="mailBoxKey" :showEMailTo="false" :showReply="openSettings.enableSendMail" :showSaveS3="openSettings.isS3Enabled"
|
||||
:saveToS3="saveToS3" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||
:fetchMailData="fetchMailData" :deleteMail="deleteMail" :showFilterInput="true" />
|
||||
:fetchMailData="fetchMailData" :deleteMail="deleteMail" :showFilterInput="true"
|
||||
:enableMailReadStatus="openSettings.enableMailReadStatus" :updateMailReadStatus="updateMailReadStatus" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="openSettings.enableSendMail" name="sendbox" :tab="t('sendbox')">
|
||||
<SendBox :fetchMailData="fetchSenboxData" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||
|
||||
@@ -56,6 +56,24 @@ const fetchMails = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const toggleCurrentMailUnread = async () => {
|
||||
if (!currentMail.value || !openSettings.value.enableMailReadStatus) return
|
||||
const mail = currentMail.value
|
||||
const previousValue = mail.is_unread
|
||||
const isUnread = previousValue !== 1
|
||||
mail.is_unread = isUnread ? 1 : 0
|
||||
try {
|
||||
await api.fetch(`/api/mails/${mail.id}/read`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ isUnread }),
|
||||
showLoading: false
|
||||
})
|
||||
message.success(t('readStatusUpdated'))
|
||||
} catch {
|
||||
mail.is_unread = previousValue
|
||||
}
|
||||
}
|
||||
|
||||
// 删除邮件
|
||||
const deleteMail = async () => {
|
||||
if (!currentMail.value) return;
|
||||
@@ -216,10 +234,15 @@ onBeforeUnmount(() => {
|
||||
<n-empty :description="t('noMails')" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<h3 v-if="currentMail.subject">{{ currentMail.subject }}</h3>
|
||||
<h3 v-if="currentMail.subject"
|
||||
:class="{ 'mail-title-unread': openSettings.enableMailReadStatus && currentMail.is_unread === 1 }">
|
||||
{{ currentMail.subject }}
|
||||
</h3>
|
||||
<div style="margin-top: 16px;">
|
||||
<MailContentRenderer :mail="currentMail" :showEMailTo="false" :showReply="false"
|
||||
:enableUserDeleteEmail="openSettings.enableUserDeleteEmail" :showSaveS3="false"
|
||||
:enableMailReadStatus="openSettings.enableMailReadStatus"
|
||||
:onUpdateMailReadStatus="toggleCurrentMailUnread"
|
||||
:onDelete="deleteMail" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -246,4 +269,19 @@ onBeforeUnmount(() => {
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mail-title-unread {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mail-title-unread::before {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-right: 8px;
|
||||
border-radius: 50%;
|
||||
background: #2080f0;
|
||||
content: '';
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user