Files
cloud-mail/mail-vue/src/layout/write/index.vue
T
2025-09-03 08:31:44 +08:00

587 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="send" v-show="show">
<div class="write-box" @click.stop>
<div class="title">
<div class="title-left">
<span class="title-text">
<Icon icon="hugeicons:quill-write-01" width="28" height="28"/>
</span>
<span class="sender">{{ $t('sender') }}:</span>
<span class="sender-name">{{ form.name }}</span>
<span class="send-email"><{{ form.sendEmail }}></span>
</div>
<div @click="close" style="cursor: pointer;">
<Icon icon="material-symbols-light:close-rounded" width="22" height="22"/>
</div>
</div>
<div class="container">
<el-input-tag @add-tag="addTagChange" tag-type="primary" size="default" v-model="form.receiveEmail"
:placeholder="$t('ruleEmailsInputDesc')">
<template #prefix>
<div class="item-title">{{ $t('recipient') }}</div>
</template>
<template #suffix>
<span class="distribute" :class="form.manyType ? 'checked' : ''"
@click.stop="checkDistribute">{{ $t('sendSeparately') }}</span>
</template>
</el-input-tag>
<el-input v-model="form.subject" :placeholder="$t('subjectInputDesc')">
<template #prefix>
<div class="item-title">{{ $t('subject') }}</div>
</template>
</el-input>
<tinyEditor :def-value="defValue" ref="editor" @change="change"/>
<div class="button-item">
<div class="att-add" @click="chooseFile">
<Icon icon="iconamoon:attachment-fill" width="24" height="24"/>
</div>
<div class="att-clear" @click="clearContent">
<Icon icon="icon-park-outline:clear-format" width="24" height="24 "/>
</div>
<div class="att-list">
<div class="att-item" v-for="(item,index) in form.attachments" :key="index">
<Icon v-bind="getIconByName(item.filename)"/>
<span class="att-filename">{{ item.filename }}</span>
<span class="att-size">{{ formatBytes(item.size) }}</span>
<Icon style="cursor: pointer;" icon="material-symbols-light:close-rounded" @click="delAtt(index)"
width="22" height="22"/>
</div>
</div>
<div>
<el-button type="primary" @click="sendEmail" v-if="form.sendType === 'reply'">{{ $t('reply') }}</el-button>
<el-button type="primary" @click="sendEmail" v-else>{{ $t('send') }}</el-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import tinyEditor from '@/components/tiny-editor/index.vue'
import {h, nextTick, onMounted, onUnmounted, reactive, ref, toRaw} from "vue";
import {Icon} from "@iconify/vue";
import {useUserStore} from "@/store/user.js";
import {emailSend} from "@/request/email.js";
import {isEmail} from "@/utils/verify-utils.js";
import {useAccountStore} from "@/store/account.js";
import {useEmailStore} from "@/store/email.js";
import {fileToBase64, formatBytes} from "@/utils/file-utils.js";
import {getIconByName} from "@/utils/icon-utils.js";
import sendPercent from "@/components/send-percent/index.vue"
import {formatDetailDate} from "@/utils/day.js";
import {useSettingStore} from "@/store/setting.js";
import {userDraftStore} from "@/store/draft.js";
import db from "@/db/db.js";
import dayjs from "dayjs";
import {useI18n} from "vue-i18n";
defineExpose({
open,
openReply,
openDraft
})
const {t} = useI18n()
const draftStore = userDraftStore()
const settingStore = useSettingStore()
const emailStore = useEmailStore();
const accountStore = useAccountStore()
const editor = ref({})
const userStore = useUserStore();
const show = ref(false);
const percent = ref(0)
let percentMessage = null
let sending = false
const defValue = ref('')
const backReply = reactive({
receiveEmail: [],
subject: '',
content: '',
sendType: ''
})
const form = reactive({
sendEmail: '',
receiveEmail: [],
accountId: -1,
manyType: null,
name: '',
subject: '',
content: '',
sendType: '',
text: '',
emailId: 0,
attachments: [],
draftId: null,
})
function addTagChange(val) {
const emails = Array.from(new Set(
val.split(/[,]/).map(item => item.trim()).filter(item => item)
));
form.receiveEmail.splice(form.receiveEmail.length - 1, 1)
emails.forEach(email => {
if (isEmail(email) && !form.receiveEmail.includes(email)) {
form.receiveEmail.push(email)
}
})
}
function checkDistribute() {
form.manyType = form.manyType ? null : 'divide'
}
function clearContent() {
ElMessageBox.confirm(t('clearContentConfirm'), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
}).then(() => {
resetForm()
})
}
function delAtt(index) {
form.attachments.splice(index, 1);
}
function chooseFile() {
const doc = document.createElement("input")
doc.setAttribute("type", "file")
doc.click()
doc.onchange = async (e) => {
const file = e.target.files[0]
const size = file.size
const filename = file.name
const contentType = file.type
const TotalSize = form.attachments.reduce((acc, item) => acc + item.size, 0);
if ((TotalSize + size) > 29360128) {
ElMessage({
message: t('attLimitMsg'),
type: 'error',
plain: true,
})
return
}
const content = await fileToBase64(file)
form.attachments.push({content, filename, size, contentType})
}
}
async function sendEmail() {
if (form.receiveEmail.length === 0) {
ElMessage({
message: t('emptyRecipientMsg'),
type: 'error',
plain: true,
})
return
}
if (!form.subject) {
ElMessage({
message: t('emptySubjectMsg'),
type: 'error',
plain: true,
})
return
}
if (!form.content) {
ElMessage({
message: t('emptyContentMsg'),
type: 'error',
plain: true,
})
return
}
if (form.manyType === 'divide' && form.attachments.length > 0) {
ElMessage({
message: t('noSeparateSendMsg'),
type: 'error',
plain: true,
})
return
}
if (sending) {
ElMessage({
message: t('sendingErrorMsg'),
type: 'error',
plain: true,
})
return
}
percentMessage = ElMessage({
message: () => h(sendPercent, {value: percent.value, desc: t('sending')}),
dangerouslyUseHTMLString: true,
plain: true,
duration: 0,
customClass: 'message-bottom'
})
sending = true
show.value = false
emailSend(form, (e) => {
percent.value = Math.round((e.loaded * 98) / e.total)
}).then(emailList => {
const email = emailList[0]
emailList.forEach(item => {
emailStore.sendScroll?.addItem(item)
})
ElNotification({
title: t('sendSuccessMsg'),
type: "success",
message: h('span', {style: 'color: teal'}, email.subject),
position: 'bottom-right'
})
userStore.refreshUserInfo();
if (form.draftId) {
form.subject = ''
form.content = ''
form.receiveEmail = []
draftStore.setDraft = {...toRaw(form)}
}
resetForm()
show.value = false
}).catch((e) => {
ElNotification({
title: t('sendFailMsg'),
type: e.code === 403 ? 'warning' : 'error',
message: h('span', {style: 'color: teal'}, e.message),
position: 'bottom-right'
})
show.value = true
}).finally(() => {
percentMessage.close()
percent.value = 0
sending = false
})
}
function resetForm() {
form.receiveEmail = []
form.subject = ''
form.content = ''
form.manyType = null
form.attachments = []
form.sendType = ''
form.emailId = 0
form.draftId = null
backReply.content = ''
backReply.subject = ''
backReply.receiveEmail = []
backReply.sendType = ''
editor.value.clearEditor()
}
function change(content, text) {
form.content = content;
form.text = text
}
function openReply(email) {
resetForm();
email.subject = email.subject || ''
form.receiveEmail.push(email.sendEmail)
form.subject = (email.subject.startsWith('Re:') || email.subject.startsWith('回复:')) ? email.subject : 'Re: ' + email.subject
form.sendType = 'reply'
form.emailId = email.emailId
defValue.value = ''
setTimeout(() => {
defValue.value = `
<div></div>
<div>
<br>
${formatDetailDate(email.createTime)} ${email.name} &lt${email.sendEmail}&gt ${t('wrote')}:
</div>
<blockquote class="mceNonEditable" style="margin: 0 0 0 0.8ex;border-left: 1px solid rgb(204,204,204);padding-left: 1ex;">
<articl>
${formatImage(email.content) || `<pre style="font-family: inherit;word-break: break-word;white-space: pre-wrap;margin: 0">${email.text}</pre>`}
</article>
</blockquote>`
open()
nextTick(() => {
backReply.content = editor.value.getContent()
backReply.subject = form.subject
backReply.receiveEmail = form.receiveEmail
backReply.sendType = form.sendType
})
})
}
function formatImage(content) {
content = content || '';
const domain = settingStore.settings.r2Domain;
return content.replace(/{{domain}}/g, domain + '/');
}
function open() {
if (!accountStore.currentAccount.email) {
form.sendEmail = userStore.user.email;
form.accountId = userStore.user.accountId;
form.name = userStore.user.name;
} else {
form.sendEmail = accountStore.currentAccount.email;
form.accountId = accountStore.currentAccount.accountId;
form.name = accountStore.currentAccount.name;
}
show.value = true;
editor.value.focus()
}
function openDraft(draft) {
Object.assign(form, {...draft})
defValue.value = ''
setTimeout(() => defValue.value = form.content)
show.value = true;
editor.value.focus()
}
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
close()
}
};
onMounted(() => {
window.addEventListener('keydown', handleKeyDown);
});
onUnmounted(() => {
window.removeEventListener('keydown', handleKeyDown);
});
function close() {
if (form.draftId) {
draftStore.setDraft = {...toRaw(form)}
show.value = false
resetForm()
return;
}
if (!(form.content || form.subject || form.receiveEmail.length > 0)) {
show.value = false
resetForm()
return;
}
if (backReply.sendType === 'reply') {
let subjectFlag = form.subject === backReply.subject
let contentFlag = editor.value.getContent() === backReply.content
let receiveFlag = form.receiveEmail.length === 1 && form.receiveEmail[0] === backReply.receiveEmail[0]
if (subjectFlag && contentFlag && receiveFlag) {
resetForm();
close()
return;
}
}
ElMessageBox.confirm(t('saveDraftConfirm'), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning',
distinguishCancelAndClose: true
}).then(async () => {
const formData = {...toRaw(form)}
delete formData.draftId
delete formData.attachments
formData.createTime = dayjs().utc().format('YYYY-MM-DD HH:mm:ss');
const draftId = await db.value.draft.add({...formData})
db.value.att.add({draftId, attachments: toRaw(form.attachments)})
draftStore.refreshList++
show.value = false
resetForm()
}).catch((action) => {
if (action === 'cancel') {
show.value = false
resetForm()
}
})
}
</script>
<style scoped lang="scss">
.send {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.write-box {
background: var(--el-bg-color);
width: min(1367px, calc(100% - 80px));
box-shadow: var(--el-box-shadow-light);
border: 1px solid var(--el-border-color-light);
transition: var(--el-transition-duration);
padding: 15px;
border-radius: 8px;
display: grid;
grid-template-rows: auto 1fr;
overflow: hidden;
@media (max-width: 1024px) {
width: 100%;
height: 100%;
border-radius: 0;
border: 0;
padding-top: 10px;
}
@media (min-width: 1025px) {
height: min(800px, calc(100vh - 60px));
}
.title {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
.title-left {
align-items: center;
display: grid;
grid-template-columns: auto auto auto 1fr;
}
.title-text {
}
.sender {
margin-left: 8px;
}
.sender-name {
margin-left: 8px;
font-weight: bold;
}
.send-email {
color: #999896;
margin-left: 5px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
div {
display: flex;
align-items: center;
}
}
.container {
height: 100%;
display: grid;
grid-template-rows: auto auto 1fr auto;
gap: 15px;
.distribute {
color: var(--el-color-info);
background: var(--el-color-info-light-9);
border: var(--el-color-info-light-8);
border-radius: 4px;
font-size: 12px;
padding: 0 5px;
}
.distribute.checked {
background: var(--el-color-primary-light-9);
color: var(--el-color-primary) !important;
border-radius: 4px;
}
.distribute:hover {
background: var(--el-color-primary-light-9);
color: var(--el-color-primary) !important;
border-radius: 4px;
}
.item-title {
}
.button-item {
display: grid;
grid-template-columns: auto auto 1fr auto;
.att-add {
cursor: pointer;
}
.att-clear {
cursor: pointer;
margin-left: 10px;
}
.att-list {
display: grid;
gap: 5px;
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
padding-left: 10px;
padding-right: 10px;
max-height: 110px;
overflow-y: auto;
@media (max-width: 450px) {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
.att-item {
display: grid;
grid-template-columns: auto 1fr auto auto;
gap: 5px;
height: 32px;
font-size: 14px;
padding: 4px 5px;
background: var(--light-ill);
border-radius: 4px;
.att-filename {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
}
}
}
}
}
:deep(.el-input-tag__suffix) {
padding-right: 4px;
}
.icon {
cursor: pointer;
}
</style>