feat: UI: MailBox add reply button (#187)

This commit is contained in:
Dream Hunter
2024-05-02 20:32:15 +08:00
committed by GitHub
parent 042736b67f
commit 6e02e9b20b
6 changed files with 90 additions and 43 deletions
+4
View File
@@ -1,5 +1,9 @@
# CHANGE LOG # CHANGE LOG
## main branch to be released
- UI: 添加回复邮件按钮
## v0.3.1 ## v0.3.1
### DB Changes ### DB Changes
+54 -9
View File
@@ -1,14 +1,16 @@
<script setup> <script setup>
import { watch, onMounted, ref, onBeforeUnmount } from "vue"; import { watch, onMounted, ref, onBeforeUnmount } from "vue";
import { useRouter } from "vue-router";
import { useMessage } from 'naive-ui' import { useMessage } from 'naive-ui'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useGlobalState } from '../store' import { useGlobalState } from '../store'
import { CloudDownloadRound } from '@vicons/material' import { CloudDownloadRound, ReplyFilled } from '@vicons/material'
import { useIsMobile } from '../utils/composables' import { useIsMobile } from '../utils/composables'
import { processItem, getDownloadEmlUrl } from '../utils/email-parser' import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
const message = useMessage() const message = useMessage()
const isMobile = useIsMobile() const isMobile = useIsMobile()
const router = useRouter()
const props = defineProps({ const props = defineProps({
enableUserDeleteEmail: { enableUserDeleteEmail: {
@@ -31,9 +33,16 @@ const props = defineProps({
default: () => { }, default: () => { },
requried: false requried: false
}, },
showReply: {
type: Boolean,
default: false,
requried: false
}
}) })
const { themeSwitch, mailboxSplitSize, useIframeShowMail } = useGlobalState() const {
localeCache, isDark, mailboxSplitSize, useIframeShowMail, sendMailModel
} = useGlobalState()
const autoRefresh = ref(false) const autoRefresh = ref(false)
const autoRefreshInterval = ref(30) const autoRefreshInterval = ref(30)
const data = ref([]) const data = ref([])
@@ -48,7 +57,7 @@ const curAttachments = ref([])
const curMail = ref(null); const curMail = ref(null);
const { t } = useI18n({ const { t } = useI18n({
locale: 'zh', locale: localeCache.value || 'zh',
messages: { messages: {
en: { en: {
success: 'Success', success: 'Success',
@@ -59,7 +68,8 @@ const { t } = useI18n({
downloadMail: 'Download Mail', downloadMail: 'Download Mail',
pleaseSelectMail: "Please select a mail to view.", pleaseSelectMail: "Please select a mail to view.",
delete: 'Delete', delete: 'Delete',
deleteMailTip: 'Are you sure you want to delete this mail?' deleteMailTip: 'Are you sure you want to delete this mail?',
reply: 'Reply'
}, },
zh: { zh: {
success: '成功', success: '成功',
@@ -70,7 +80,8 @@ const { t } = useI18n({
attachments: '查看附件', attachments: '查看附件',
pleaseSelectMail: "请选择一封邮件查看。", pleaseSelectMail: "请选择一封邮件查看。",
delete: '删除', delete: '删除',
deleteMailTip: '确定要删除这封邮件吗' deleteMailTip: '确定要删除这封邮件吗?',
reply: '回复'
} }
} }
}); });
@@ -132,7 +143,7 @@ const getAttachments = (attachments) => {
}; };
const mailItemClass = (row) => { const mailItemClass = (row) => {
return curMail.value && row.id == curMail.value.id ? (themeSwitch.value ? 'overlay overlay-dark-backgroud' : 'overlay overlay-light-backgroud') : ''; return curMail.value && row.id == curMail.value.id ? (isDark.value ? 'overlay overlay-dark-backgroud' : 'overlay overlay-light-backgroud') : '';
}; };
const deleteMail = async () => { const deleteMail = async () => {
@@ -146,6 +157,25 @@ const deleteMail = async () => {
} }
}; };
const replyMail = async () => {
const emailRegex = /(.+?) <(.+?)>/;
let toMail = curMail.value.originalSource;
let toName = ""
const match = emailRegex.exec(curMail.value.source);
if (match) {
toName = match[1];
toMail = match[2];
}
Object.assign(sendMailModel.value, {
toName: toName,
toMail: toMail,
subject: localeCache.value == 'zh' ? `回复: ${curMail.value.subject}` : `Re: ${curMail.value.subject}`,
contentType: 'text',
content: "",
});
await router.push('/send');
};
const onSpiltSizeChange = (size) => { const onSpiltSizeChange = (size) => {
mailboxSplitSize.value = size; mailboxSplitSize.value = size;
} }
@@ -231,9 +261,17 @@ onBeforeUnmount(() => {
</n-button> </n-button>
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'" <n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
:href="getDownloadEmlUrl(curMail.raw)"> :href="getDownloadEmlUrl(curMail.raw)">
<template #icon>
<n-icon :component="CloudDownloadRound" /> <n-icon :component="CloudDownloadRound" />
</template>
{{ t('downloadMail') }} {{ t('downloadMail') }}
</n-button> </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-space> </n-space>
<iframe v-if="useIframeShowMail" :srcdoc="curMail.message" <iframe v-if="useIframeShowMail" :srcdoc="curMail.message"
style="margin-top: 10px;width: 100%; height: 100%;"> style="margin-top: 10px;width: 100%; height: 100%;">
@@ -251,13 +289,14 @@ onBeforeUnmount(() => {
<div style="display: inline-block; margin-top: 10px; margin-bottom: 10px;"> <div style="display: inline-block; margin-top: 10px; margin-bottom: 10px;">
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" /> <n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
</div> </div>
<n-switch v-model:value="autoRefresh" size="small"> <n-switch v-model:value="autoRefresh" size="small" :round="false">
<template #checked> <template #checked>
{{ t('autoRefresh') }} {{ t('refreshAfter', { msg: autoRefreshInterval }) }}
</template> </template>
<template #unchecked> <template #unchecked>
{{ t('autoRefresh') }} {{ t('autoRefresh') }}
</template></n-switch> </template>
</n-switch>
<n-button @click="refresh" size="small" type="primary"> <n-button @click="refresh" size="small" type="primary">
{{ t('refresh') }} {{ t('refresh') }}
</n-button> </n-button>
@@ -316,6 +355,12 @@ onBeforeUnmount(() => {
<n-icon :component="CloudDownloadRound" /> <n-icon :component="CloudDownloadRound" />
{{ t('downloadMail') }} {{ t('downloadMail') }}
</n-button> </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-space> </n-space>
<div v-html="curMail.message" style="margin-top: 10px;"></div> <div v-html="curMail.message" style="margin-top: 10px;"></div>
</n-card> </n-card>
+10 -3
View File
@@ -29,7 +29,15 @@ export const useGlobalState = createGlobalState(
source_prefix: '', source_prefix: '',
name: '', name: '',
} }
}) });
const sendMailModel = useStorage('sendMailModel', {
fromName: "",
toName: "",
toMail: "",
subject: "",
contentType: 'text',
content: "",
});
const showAuth = ref(false); const showAuth = ref(false);
const showPassword = ref(false); const showPassword = ref(false);
const showAdminAuth = ref(false); const showAdminAuth = ref(false);
@@ -37,7 +45,6 @@ export const useGlobalState = createGlobalState(
const adminAuth = useStorage('adminAuth', ''); const adminAuth = useStorage('adminAuth', '');
const jwt = useStorage('jwt', ''); const jwt = useStorage('jwt', '');
const localeCache = useStorage('locale', 'zh'); const localeCache = useStorage('locale', 'zh');
const themeSwitch = useStorage('themeSwitch', false);
const adminTab = ref("account"); const adminTab = ref("account");
const adminMailTabAddress = ref(""); const adminMailTabAddress = ref("");
const adminSendBoxTabAddress = ref(""); const adminSendBoxTabAddress = ref("");
@@ -48,13 +55,13 @@ export const useGlobalState = createGlobalState(
toggleDark, toggleDark,
loading, loading,
settings, settings,
sendMailModel,
openSettings, openSettings,
showAuth, showAuth,
showPassword, showPassword,
auth, auth,
jwt, jwt,
localeCache, localeCache,
themeSwitch,
adminAuth, adminAuth,
showAdminAuth, showAdminAuth,
adminTab, adminTab,
+1
View File
@@ -7,6 +7,7 @@ function humanFileSize(size) {
export async function processItem(item) { export async function processItem(item) {
// Try to parse the email using mail-parser-wasm // Try to parse the email using mail-parser-wasm
item.originalSource = item.source;
try { try {
const { parse_message } = await import('mail-parser-wasm'); const { parse_message } = await import('mail-parser-wasm');
const parsedEmail = parse_message(item.raw); const parsedEmail = parse_message(item.raw);
+1 -1
View File
@@ -16,7 +16,7 @@ const deleteMail = async (curMailId) => {
<template> <template>
<div v-if="settings.address"> <div v-if="settings.address">
<MailBox :showEMailTo="false" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail" <MailBox :showEMailTo="false" :showReply="true" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
:fetchMailData="fetchMailData" :deleteMail="deleteMail" /> :fetchMailData="fetchMailData" :deleteMail="deleteMail" />
</div> </div>
</template> </template>
+19 -29
View File
@@ -1,10 +1,8 @@
<script setup> <script setup>
import '@wangeditor/editor/dist/css/style.css' import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue' import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { DomEditor } from '@wangeditor/editor'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { onMounted, onBeforeUnmount, ref, shallowRef } from 'vue' import { onMounted, onBeforeUnmount, ref, shallowRef } from 'vue'
import { useStorage } from '@vueuse/core'
import AdminContact from '../admin/AdminContact.vue' import AdminContact from '../admin/AdminContact.vue'
import { useGlobalState } from '../../store' import { useGlobalState } from '../../store'
@@ -15,16 +13,8 @@ const message = useMessage()
const isPreview = ref(false) const isPreview = ref(false)
const editorRef = shallowRef() const editorRef = shallowRef()
const mailModel = useStorage('mailModelCache', {
fromName: "",
toName: "",
toMail: "",
subject: "",
contentType: 'text',
content: "",
})
const { settings } = useGlobalState() const { settings, sendMailModel } = useGlobalState()
const { t } = useI18n({ const { t } = useI18n({
locale: 'zh', locale: 'zh',
@@ -81,15 +71,15 @@ const send = async () => {
method: 'POST', method: 'POST',
body: body:
JSON.stringify({ JSON.stringify({
from_name: mailModel.value.fromName, from_name: sendMailModel.value.fromName,
to_name: mailModel.value.toName, to_name: sendMailModel.value.toName,
to_mail: mailModel.value.toMail, to_mail: sendMailModel.value.toMail,
subject: mailModel.value.subject, subject: sendMailModel.value.subject,
is_html: mailModel.value.contentType != 'text', is_html: sendMailModel.value.contentType != 'text',
content: mailModel.value.content, content: sendMailModel.value.content,
}) })
}) })
mailModel.value = { sendMailModel.value = {
fromName: "", fromName: "",
toName: "", toName: "",
toMail: "", toMail: "",
@@ -170,43 +160,43 @@ onMounted(async () => {
<n-button type="primary" @click="send">{{ t('send') }}</n-button> <n-button type="primary" @click="send">{{ t('send') }}</n-button>
</div> </div>
<div class="left"> <div class="left">
<n-form :model="mailModel"> <n-form :model="sendMailModel">
<n-form-item :label="t('fromName')" label-placement="top"> <n-form-item :label="t('fromName')" label-placement="top">
<n-input-group> <n-input-group>
<n-input v-model:value="mailModel.fromName" /> <n-input v-model:value="sendMailModel.fromName" />
<n-input :value="settings.address" disabled /> <n-input :value="settings.address" disabled />
</n-input-group> </n-input-group>
</n-form-item> </n-form-item>
<n-form-item :label="t('toName')" label-placement="top"> <n-form-item :label="t('toName')" label-placement="top">
<n-input-group> <n-input-group>
<n-input v-model:value="mailModel.toName" /> <n-input v-model:value="sendMailModel.toName" />
<n-input v-model:value="mailModel.toMail" /> <n-input v-model:value="sendMailModel.toMail" />
</n-input-group> </n-input-group>
</n-form-item> </n-form-item>
<n-form-item :label="t('subject')" label-placement="top"> <n-form-item :label="t('subject')" label-placement="top">
<n-input v-model:value="mailModel.subject" /> <n-input v-model:value="sendMailModel.subject" />
</n-form-item> </n-form-item>
<n-form-item :label="t('options')" label-placement="top"> <n-form-item :label="t('options')" label-placement="top">
<n-radio-group v-model:value="mailModel.contentType"> <n-radio-group v-model:value="sendMailModel.contentType">
<n-radio-button v-for="option in contentTypes" :key="option.value" :value="option.value" <n-radio-button v-for="option in contentTypes" :key="option.value" :value="option.value"
:label="option.label" /> :label="option.label" />
</n-radio-group> </n-radio-group>
<n-button v-if="mailModel.contentType != 'text'" @click="isPreview = !isPreview" <n-button v-if="sendMailModel.contentType != 'text'" @click="isPreview = !isPreview"
style="margin-left: 10px;"> style="margin-left: 10px;">
{{ isPreview ? t('edit') : t('preview') }} {{ isPreview ? t('edit') : t('preview') }}
</n-button> </n-button>
</n-form-item> </n-form-item>
<n-form-item :label="t('content')" label-placement="top"> <n-form-item :label="t('content')" label-placement="top">
<n-card v-if="isPreview"> <n-card v-if="isPreview">
<div v-html="mailModel.content" /> <div v-html="sendMailModel.content" />
</n-card> </n-card>
<div v-else-if="mailModel.contentType == 'rich'" style="border: 1px solid #ccc"> <div v-else-if="sendMailModel.contentType == 'rich'" style="border: 1px solid #ccc">
<Toolbar style="border-bottom: 1px solid #ccc" :defaultConfig="toolbarConfig" <Toolbar style="border-bottom: 1px solid #ccc" :defaultConfig="toolbarConfig"
:editor="editorRef" mode="default" /> :editor="editorRef" mode="default" />
<Editor style="height: 500px; overflow-y: hidden;" v-model="mailModel.content" <Editor style="height: 500px; overflow-y: hidden;" v-model="sendMailModel.content"
:defaultConfig="editorConfig" mode="default" @onCreated="handleCreated" /> :defaultConfig="editorConfig" mode="default" @onCreated="handleCreated" />
</div> </div>
<n-input v-else type="textarea" v-model:value="mailModel.content" :autosize="{ <n-input v-else type="textarea" v-model:value="sendMailModel.content" :autosize="{
minRows: 3 minRows: 3
}" /> }" />
</n-form-item> </n-form-item>