refactor: serve mail states from backend

This commit is contained in:
dreamhunter2333
2026-08-26 00:05:02 +08:00
parent b5d8f64320
commit 04138ebcdb
18 changed files with 291 additions and 155 deletions
+39 -24
View File
@@ -55,20 +55,26 @@ const props = defineProps({
default: false,
required: false
},
enableReadStatus: {
enableMailStates: {
type: Boolean,
default: false,
required: false
},
updateMailReadStatus: {
updateMailState: {
type: Function,
default: () => { },
required: false
},
fetchMailStates: {
type: Function,
default: () => ({ results: [] }),
required: false
},
})
const localFilterKeyword = ref('')
const readStatusFilter = ref('all')
const mailStateFilter = ref(null)
const mailStates = ref([])
const {
isDark, mailboxSplitSize, mailListView, mailListPreviewLineClamp, indexTab, loading, useUTCDate,
@@ -106,20 +112,23 @@ const data = computed(() => {
})
const isMailUnread = (mail) => {
return props.enableReadStatus && mail?.unread === true
return props.enableMailStates && mail?.unread === true
}
const currentPageHasUnread = computed(() => rawData.value.some(isMailUnread))
const readStatusFilterOptions = computed(() => [
{ label: t('allMail'), value: 'all' },
{ label: t('unread'), value: 'unread' },
{ label: t('read'), value: 'read' },
])
const mailStateFilterOptions = computed(() => mailStates.value.map(state => ({
label: state.label || t(state.label_key),
value: state.value,
})))
const updateUnreadState = async (mails, action) => {
if (mails.length === 0) return true
const getReadStateValue = (unread) => {
return mailStates.value.find(state => state.unread === unread)?.value
}
const updateUnreadState = async (mails, state) => {
if (mails.length === 0 || !state) return true
try {
const response = await props.updateMailReadStatus(mails.map(mail => mail.id), action)
const response = await props.updateMailState(mails.map(mail => mail.id), state)
const results = response?.results ?? []
const resultById = new Map(results.map(result => [result.id, result]))
mails.forEach(mail => {
@@ -133,11 +142,11 @@ const updateUnreadState = async (mails, action) => {
}
}
const markMailsRead = async (mails) => updateUnreadState(mails, 'read')
const markMailsRead = async (mails) => updateUnreadState(mails, getReadStateValue(false))
const toggleCurrentMailUnread = async () => {
if (!curMail.value) return
await updateUnreadState([curMail.value], 'toggle')
await updateUnreadState([curMail.value], getReadStateValue(!curMail.value.unread))
}
const openMail = async (mail) => {
@@ -148,7 +157,7 @@ const openMail = async (mail) => {
const markCurrentPageRead = async () => {
if (!await markMailsRead(rawData.value)) return
message.success(t("success"))
if (readStatusFilter.value === 'unread') await refresh()
if (mailStateFilter.value === getReadStateValue(true)) await refresh()
}
const canGoPrevMail = computed(() => {
@@ -232,14 +241,15 @@ watch([page, pageSize], async ([page, pageSize], [oldPage, oldPageSize]) => {
}
})
watch(readStatusFilter, async () => {
watch(mailStateFilter, async (_value, oldValue) => {
if (oldValue === null) return
await backFirstPageAndRefresh()
})
const refresh = async () => {
try {
const { results, count: totalCount } = await props.fetchMailData(
pageSize.value, (page.value - 1) * pageSize.value, readStatusFilter.value
pageSize.value, (page.value - 1) * pageSize.value, mailStateFilter.value
);
loading.value = true;
rawData.value = await Promise.all(results.map(async (item) => {
@@ -388,6 +398,11 @@ const multiActionDownload = async () => {
}
onMounted(async () => {
if (props.enableMailStates) {
const { results = [] } = await props.fetchMailStates()
mailStates.value = results
mailStateFilter.value = results.find(state => state.default)?.value ?? results[0]?.value ?? null
}
await refresh();
});
@@ -440,10 +455,10 @@ onBeforeUnmount(() => {
<n-button @click="backFirstPageAndRefresh" type="primary" tertiary>
{{ t('refresh') }}
</n-button>
<n-button v-if="enableReadStatus && currentPageHasUnread" @click="markCurrentPageRead" tertiary>
<n-button v-if="enableMailStates && currentPageHasUnread" @click="markCurrentPageRead" tertiary>
{{ t('markCurrentPageRead') }}
</n-button>
<n-select v-if="enableReadStatus" v-model:value="readStatusFilter" :options="readStatusFilterOptions"
<n-select v-if="enableMailStates" v-model:value="mailStateFilter" :options="mailStateFilterOptions"
style="width: 120px" />
<n-input v-if="showFilterInput" v-model:value="localFilterKeyword"
:placeholder="t('keywordQueryTip')" style="width: 200px; display: flex; align-items: center;"
@@ -528,7 +543,7 @@ onBeforeUnmount(() => {
style="overflow: auto; max-height: 100vh;">
<MailContentRenderer :mail="curMail" :showEMailTo="showEMailTo"
:enableUserDeleteEmail="enableUserDeleteEmail" :showReply="showReply" :showSaveS3="showSaveS3"
:enableReadStatus="enableReadStatus" :onToggleUnread="toggleCurrentMailUnread"
:enableMailStates="enableMailStates" :onToggleUnread="toggleCurrentMailUnread"
:onDelete="deleteMail" :onReply="replyMail" :onForward="forwardMail" :onSaveToS3="saveToS3Proxy" />
</n-card>
<n-card :bordered="false" embedded class="mail-item" v-else>
@@ -600,7 +615,7 @@ onBeforeUnmount(() => {
<n-button @click="backFirstPageAndRefresh" tertiary size="small" type="primary">
{{ t('refresh') }}
</n-button>
<n-button v-if="enableReadStatus && currentPageHasUnread" @click="markCurrentPageRead" tertiary size="small">
<n-button v-if="enableMailStates && currentPageHasUnread" @click="markCurrentPageRead" tertiary size="small">
{{ t('markCurrentPageRead') }}
</n-button>
</n-space>
@@ -608,8 +623,8 @@ onBeforeUnmount(() => {
<n-input v-model:value="localFilterKeyword"
:placeholder="t('keywordQueryTip')" size="small" clearable />
</div>
<div v-if="enableReadStatus" style="padding: 0 10px; margin-bottom: 10px;">
<n-select v-model:value="readStatusFilter" :options="readStatusFilterOptions" size="small" />
<div v-if="enableMailStates" style="padding: 0 10px; margin-bottom: 10px;">
<n-select v-model:value="mailStateFilter" :options="mailStateFilterOptions" size="small" />
</div>
<div style="overflow: auto; min-height: 60vh; max-height: 100vh;">
<n-list hoverable clickable>
@@ -649,7 +664,7 @@ onBeforeUnmount(() => {
<MailContentRenderer :mail="curMail" :showEMailTo="showEMailTo"
:enableUserDeleteEmail="enableUserDeleteEmail" :showReply="showReply" :showSaveS3="showSaveS3"
:useUTCDate="useUTCDate" :onDelete="deleteMail" :onReply="replyMail" :onForward="forwardMail"
:enableReadStatus="enableReadStatus" :onToggleUnread="toggleCurrentMailUnread"
:enableMailStates="enableMailStates" :onToggleUnread="toggleCurrentMailUnread"
:onSaveToS3="saveToS3Proxy" />
</n-card>
</n-drawer-content>
@@ -34,7 +34,7 @@ const props = defineProps({
type: Boolean,
default: false
},
enableReadStatus: {
enableMailStates: {
type: Boolean,
default: false
},
@@ -154,7 +154,7 @@ const handleSaveToS3 = async (filename, blob) => {
{{ t('downloadMail') }}
</n-button>
<n-button v-if="enableReadStatus" size="small" tertiary type="info" @click="onToggleUnread">
<n-button v-if="enableMailStates" size="small" tertiary type="info" @click="onToggleUnread">
{{ mail.unread ? t('markRead') : t('markUnread') }}
</n-button>
+1 -1
View File
@@ -24,7 +24,7 @@ export const useGlobalState = createGlobalState(
disableAnonymousUserCreateEmail: false,
disableCustomAddressName: false,
enableUserDeleteEmail: false,
enableReadStatus: false,
enableMailStates: false,
enableAutoReply: false,
enableIndexAbout: false,
/** @type {string[]} */
+12 -7
View File
@@ -32,15 +32,15 @@ const SendMail = defineAsyncComponent(() => {
const { t } = useScopedI18n('views.Index')
const fetchMailData = async (limit, offset, readStatus = 'all') => {
const fetchMailData = async (limit, offset, mailState) => {
if (mailIdQuery.value > 0) {
const singleMail = await api.fetch(`/api/mail/${mailIdQuery.value}`);
if (singleMail) return { results: [singleMail], count: 1 };
return { results: [], count: 0 };
}
const readStatusQuery = readStatus === 'all' ? '' : `&read_status=${readStatus}`
const mailStateQuery = mailState ? `&mail_state=${encodeURIComponent(mailState)}` : ''
return await api.fetch(
`/api/mails?limit=${limit}&offset=${offset}${readStatusQuery}`
`/api/mails?limit=${limit}&offset=${offset}${mailStateQuery}`
);
};
@@ -48,13 +48,17 @@ const deleteMail = async (curMailId) => {
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
};
const updateMailReadStatus = async (ids, action) => {
return await api.fetch(`/api/mails/read-status`, {
const updateMailState = async (ids, state) => {
return await api.fetch(`/api/mails/state`, {
method: 'PATCH',
body: JSON.stringify({ ids, action })
body: JSON.stringify({ ids, state })
});
};
const fetchMailStates = async () => {
return await api.fetch(`/api/mail-states`)
}
const deleteSenboxMail = async (curMailId) => {
await api.fetch(`/api/sendbox/${curMailId}`, { method: 'DELETE' });
};
@@ -138,7 +142,8 @@ onMounted(() => {
<MailBox :key="mailBoxKey" :showEMailTo="false" :showReply="openSettings.enableSendMail" :showSaveS3="openSettings.isS3Enabled"
:saveToS3="saveToS3" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
:fetchMailData="fetchMailData" :deleteMail="deleteMail" :showFilterInput="true"
:enableReadStatus="openSettings.enableReadStatus" :updateMailReadStatus="updateMailReadStatus" />
:enableMailStates="openSettings.enableMailStates" :updateMailState="updateMailState"
:fetchMailStates="fetchMailStates" />
</n-tab-pane>
<n-tab-pane v-if="openSettings.enableSendMail" name="sendbox" :tab="t('sendbox')">
<SendBox :fetchMailData="fetchSenboxData" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
+20 -7
View File
@@ -26,12 +26,17 @@ const message = useMessage()
const currentPage = ref(1)
const totalCount = ref(0)
const currentMail = ref(null)
const mailStates = ref([])
const showAccountSettingsCard = ref(false)
const currentAutoRefreshInterval = ref(60)
const timer = ref(null)
const { t } = useScopedI18n('views.index.SimpleIndex')
const getReadStateValue = (unread) => {
return mailStates.value.find(state => state.unread === unread)?.value
}
// 复制地址
const copyAddress = async () => {
try {
@@ -50,10 +55,12 @@ const fetchMails = async () => {
totalCount.value = count > 0 ? count : totalCount.value;
const rawMail = results && results.length > 0 ? results[0] : null
currentMail.value = rawMail ? await processItem(rawMail) : null
if (openSettings.value.enableReadStatus && rawMail) {
const response = await api.fetch(`/api/mails/read-status`, {
if (openSettings.value.enableMailStates && rawMail) {
const state = getReadStateValue(false)
if (!state) return
const response = await api.fetch(`/api/mails/state`, {
method: 'PATCH',
body: JSON.stringify({ ids: [rawMail.id], action: 'read' })
body: JSON.stringify({ ids: [rawMail.id], state })
})
const updatedMail = response.results?.[0]
if (updatedMail) currentMail.value.unread = updatedMail.unread
@@ -65,13 +72,15 @@ const fetchMails = async () => {
}
const toggleCurrentMailUnread = async () => {
if (!currentMail.value || !openSettings.value.enableReadStatus) return
if (!currentMail.value || !openSettings.value.enableMailStates) return
try {
const response = await api.fetch(`/api/mails/read-status`, {
const state = getReadStateValue(!currentMail.value.unread)
if (!state) return
const response = await api.fetch(`/api/mails/state`, {
method: 'PATCH',
body: JSON.stringify({
ids: [currentMail.value.id],
action: 'toggle',
state,
})
})
const updatedMail = response.results?.[0]
@@ -131,6 +140,10 @@ watch(currentPage, () => {
onMounted(async () => {
await api.getSettings()
if (openSettings.value.enableMailStates) {
const { results = [] } = await api.fetch(`/api/mail-states`)
mailStates.value = results
}
await fetchMails()
// 启动自动刷新
@@ -245,7 +258,7 @@ onBeforeUnmount(() => {
<div style="margin-top: 16px;">
<MailContentRenderer :mail="currentMail" :showEMailTo="false" :showReply="false"
:enableUserDeleteEmail="openSettings.enableUserDeleteEmail" :showSaveS3="false"
:enableReadStatus="openSettings.enableReadStatus" :onToggleUnread="toggleCurrentMailUnread"
:enableMailStates="openSettings.enableMailStates" :onToggleUnread="toggleCurrentMailUnread"
:onDelete="deleteMail" />
</div>
</div>
+11 -7
View File
@@ -20,12 +20,12 @@ const queryMail = () => {
mailBoxKey.value = Date.now();
}
const fetchMailData = async (limit, offset, readStatus = 'all') => {
const fetchMailData = async (limit, offset, mailState) => {
return await api.fetch(
`/user_api/mails`
+ `?limit=${limit}`
+ `&offset=${offset}`
+ (readStatus === 'all' ? '' : `&read_status=${readStatus}`)
+ (mailState ? `&mail_state=${encodeURIComponent(mailState)}` : '')
+ (addressFilter.value ? `&address=${addressFilter.value}` : '')
);
}
@@ -51,13 +51,17 @@ const deleteMail = async (curMailId) => {
await api.fetch(`/user_api/mails/${curMailId}`, { method: 'DELETE' });
};
const updateMailReadStatus = async (ids, action) => {
return await api.fetch(`/user_api/mails/read-status`, {
const updateMailState = async (ids, state) => {
return await api.fetch(`/user_api/mails/state`, {
method: 'PATCH',
body: JSON.stringify({ ids, action })
body: JSON.stringify({ ids, state })
});
};
const fetchMailStates = async () => {
return await api.fetch(`/user_api/mail-states`)
}
watch(addressFilter, async (newValue) => {
queryMail();
});
@@ -78,7 +82,7 @@ onMounted(() => {
</n-input-group>
<div style="margin-top: 10px;"></div>
<MailBox :key="mailBoxKey" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail" :fetchMailData="fetchMailData"
:deleteMail="deleteMail" :showFilterInput="true" :enableReadStatus="openSettings.enableReadStatus"
:updateMailReadStatus="updateMailReadStatus" />
:deleteMail="deleteMail" :showFilterInput="true" :enableMailStates="openSettings.enableMailStates"
:updateMailState="updateMailState" :fetchMailStates="fetchMailStates" />
</div>
</template>