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

View File

@@ -1,14 +1,16 @@
<script setup>
import { watch, onMounted, ref, onBeforeUnmount } from "vue";
import { useRouter } from "vue-router";
import { useMessage } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import { useGlobalState } from '../store'
import { CloudDownloadRound } from '@vicons/material'
import { CloudDownloadRound, ReplyFilled } from '@vicons/material'
import { useIsMobile } from '../utils/composables'
import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
const message = useMessage()
const isMobile = useIsMobile()
const router = useRouter()
const props = defineProps({
enableUserDeleteEmail: {
@@ -31,9 +33,16 @@ const props = defineProps({
default: () => { },
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 autoRefreshInterval = ref(30)
const data = ref([])
@@ -48,7 +57,7 @@ const curAttachments = ref([])
const curMail = ref(null);
const { t } = useI18n({
locale: 'zh',
locale: localeCache.value || 'zh',
messages: {
en: {
success: 'Success',
@@ -59,7 +68,8 @@ const { t } = useI18n({
downloadMail: 'Download Mail',
pleaseSelectMail: "Please select a mail to view.",
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: {
success: '成功',
@@ -70,7 +80,8 @@ const { t } = useI18n({
attachments: '查看附件',
pleaseSelectMail: "请选择一封邮件查看。",
delete: '删除',
deleteMailTip: '确定要删除这封邮件吗'
deleteMailTip: '确定要删除这封邮件吗?',
reply: '回复'
}
}
});
@@ -132,7 +143,7 @@ const getAttachments = (attachments) => {
};
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 () => {
@@ -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) => {
mailboxSplitSize.value = size;
}
@@ -231,9 +261,17 @@ onBeforeUnmount(() => {
</n-button>
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
:href="getDownloadEmlUrl(curMail.raw)">
<n-icon :component="CloudDownloadRound" />
<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-space>
<iframe v-if="useIframeShowMail" :srcdoc="curMail.message"
style="margin-top: 10px;width: 100%; height: 100%;">
@@ -251,13 +289,14 @@ onBeforeUnmount(() => {
<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" />
</div>
<n-switch v-model:value="autoRefresh" size="small">
<n-switch v-model:value="autoRefresh" size="small" :round="false">
<template #checked>
{{ t('autoRefresh') }}
{{ t('refreshAfter', { msg: autoRefreshInterval }) }}
</template>
<template #unchecked>
{{ t('autoRefresh') }}
</template></n-switch>
</template>
</n-switch>
<n-button @click="refresh" size="small" type="primary">
{{ t('refresh') }}
</n-button>
@@ -316,6 +355,12 @@ onBeforeUnmount(() => {
<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-space>
<div v-html="curMail.message" style="margin-top: 10px;"></div>
</n-card>

View File

@@ -29,7 +29,15 @@ export const useGlobalState = createGlobalState(
source_prefix: '',
name: '',
}
})
});
const sendMailModel = useStorage('sendMailModel', {
fromName: "",
toName: "",
toMail: "",
subject: "",
contentType: 'text',
content: "",
});
const showAuth = ref(false);
const showPassword = ref(false);
const showAdminAuth = ref(false);
@@ -37,7 +45,6 @@ export const useGlobalState = createGlobalState(
const adminAuth = useStorage('adminAuth', '');
const jwt = useStorage('jwt', '');
const localeCache = useStorage('locale', 'zh');
const themeSwitch = useStorage('themeSwitch', false);
const adminTab = ref("account");
const adminMailTabAddress = ref("");
const adminSendBoxTabAddress = ref("");
@@ -48,13 +55,13 @@ export const useGlobalState = createGlobalState(
toggleDark,
loading,
settings,
sendMailModel,
openSettings,
showAuth,
showPassword,
auth,
jwt,
localeCache,
themeSwitch,
adminAuth,
showAdminAuth,
adminTab,

View File

@@ -7,6 +7,7 @@ function humanFileSize(size) {
export async function processItem(item) {
// Try to parse the email using mail-parser-wasm
item.originalSource = item.source;
try {
const { parse_message } = await import('mail-parser-wasm');
const parsedEmail = parse_message(item.raw);

View File

@@ -16,7 +16,7 @@ const deleteMail = async (curMailId) => {
<template>
<div v-if="settings.address">
<MailBox :showEMailTo="false" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
<MailBox :showEMailTo="false" :showReply="true" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
:fetchMailData="fetchMailData" :deleteMail="deleteMail" />
</div>
</template>

View File

@@ -1,10 +1,8 @@
<script setup>
import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { DomEditor } from '@wangeditor/editor'
import { useI18n } from 'vue-i18n'
import { onMounted, onBeforeUnmount, ref, shallowRef } from 'vue'
import { useStorage } from '@vueuse/core'
import AdminContact from '../admin/AdminContact.vue'
import { useGlobalState } from '../../store'
@@ -15,16 +13,8 @@ const message = useMessage()
const isPreview = ref(false)
const editorRef = shallowRef()
const mailModel = useStorage('mailModelCache', {
fromName: "",
toName: "",
toMail: "",
subject: "",
contentType: 'text',
content: "",
})
const { settings } = useGlobalState()
const { settings, sendMailModel } = useGlobalState()
const { t } = useI18n({
locale: 'zh',
@@ -81,15 +71,15 @@ const send = async () => {
method: 'POST',
body:
JSON.stringify({
from_name: mailModel.value.fromName,
to_name: mailModel.value.toName,
to_mail: mailModel.value.toMail,
subject: mailModel.value.subject,
is_html: mailModel.value.contentType != 'text',
content: mailModel.value.content,
from_name: sendMailModel.value.fromName,
to_name: sendMailModel.value.toName,
to_mail: sendMailModel.value.toMail,
subject: sendMailModel.value.subject,
is_html: sendMailModel.value.contentType != 'text',
content: sendMailModel.value.content,
})
})
mailModel.value = {
sendMailModel.value = {
fromName: "",
toName: "",
toMail: "",
@@ -170,43 +160,43 @@ onMounted(async () => {
<n-button type="primary" @click="send">{{ t('send') }}</n-button>
</div>
<div class="left">
<n-form :model="mailModel">
<n-form :model="sendMailModel">
<n-form-item :label="t('fromName')" label-placement="top">
<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-group>
</n-form-item>
<n-form-item :label="t('toName')" label-placement="top">
<n-input-group>
<n-input v-model:value="mailModel.toName" />
<n-input v-model:value="mailModel.toMail" />
<n-input v-model:value="sendMailModel.toName" />
<n-input v-model:value="sendMailModel.toMail" />
</n-input-group>
</n-form-item>
<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 :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"
:label="option.label" />
</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;">
{{ isPreview ? t('edit') : t('preview') }}
</n-button>
</n-form-item>
<n-form-item :label="t('content')" label-placement="top">
<n-card v-if="isPreview">
<div v-html="mailModel.content" />
<div v-html="sendMailModel.content" />
</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"
: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" />
</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
}" />
</n-form-item>