新增邮件转发

This commit is contained in:
eoao
2026-02-07 12:24:45 +08:00
parent ee1b76cd1b
commit 24e37288e5
13 changed files with 79 additions and 25 deletions

View File

@@ -949,6 +949,11 @@ function loadData() {
@media (max-width: 1366px) { @media (max-width: 1366px) {
height: 83px; height: 83px;
} }
@media (pointer: coarse) {
/* 触屏 */
user-select: none;
}
&.all-email { &.all-email {
height: 65px; height: 65px;
@media (max-width: 1366px) { @media (max-width: 1366px) {

View File

@@ -8,7 +8,6 @@
<script setup> <script setup>
import {ref, onMounted, onBeforeUnmount, watch, nextTick, shallowRef, defineEmits, computed} from 'vue'; import {ref, onMounted, onBeforeUnmount, watch, nextTick, shallowRef, defineEmits, computed} from 'vue';
import loading from "@/components/loading/index.vue"; import loading from "@/components/loading/index.vue";
import {compressImage} from "@/utils/file-utils.js";
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {useUiStore} from '@/store/ui.js' import {useUiStore} from '@/store/ui.js'
import {useSettingStore} from '@/store/setting.js' import {useSettingStore} from '@/store/setting.js'
@@ -140,7 +139,6 @@ function initEditor() {
input.addEventListener('change', async (e) => { input.addEventListener('change', async (e) => {
let file = e.target.files[0]; let file = e.target.files[0];
file = await compressImage(file);
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
const id = 'blobid' + (new Date()).getTime(); const id = 'blobid' + (new Date()).getTime();

View File

@@ -184,6 +184,7 @@ const en = {
changeUserName: 'Change Username', changeUserName: 'Change Username',
send: 'Send', send: 'Send',
reply: 'Reply', reply: 'Reply',
forward: 'Forward',
confirm: 'Confirm', confirm: 'Confirm',
cancel: 'Cancel', cancel: 'Cancel',
delEmailConfirm: 'Confirm deleting this email?', delEmailConfirm: 'Confirm deleting this email?',
@@ -228,7 +229,6 @@ const en = {
banRestore: 'Confirm banning {msg}?', banRestore: 'Confirm banning {msg}?',
logOut: 'Sign out', logOut: 'Sign out',
clearContentConfirm: 'Are you sure to clear all content?', clearContentConfirm: 'Are you sure to clear all content?',
attLimitMsg: 'Attachment size limit: 28MB',
emptyRecipientMsg: 'Recipient email cannot be empty', emptyRecipientMsg: 'Recipient email cannot be empty',
emptySubjectMsg: 'Subject cannot be empty', emptySubjectMsg: 'Subject cannot be empty',
emptyContentMsg: 'Content cannot be empty', emptyContentMsg: 'Content cannot be empty',

View File

@@ -184,6 +184,7 @@ const zh = {
changeUserName: '修改用户名', changeUserName: '修改用户名',
send: '发送', send: '发送',
reply: '回复', reply: '回复',
forward: '转发',
confirm: '确定', confirm: '确定',
cancel: '取消', cancel: '取消',
delEmailConfirm: '确认删除该邮件吗?', delEmailConfirm: '确认删除该邮件吗?',
@@ -228,7 +229,6 @@ const zh = {
banRestore: '确认禁用 {msg} 吗?', banRestore: '确认禁用 {msg} 吗?',
logOut: '退出', logOut: '退出',
clearContentConfirm: '确定要清空所有内容吗?', clearContentConfirm: '确定要清空所有内容吗?',
attLimitMsg: '附件不能超过28MB',
emptyRecipientMsg: '收件人邮箱地址不能为空', emptyRecipientMsg: '收件人邮箱地址不能为空',
emptySubjectMsg: '主题不能为空', emptySubjectMsg: '主题不能为空',
emptyContentMsg: '邮件正文不能为空', emptyContentMsg: '邮件正文不能为空',

View File

@@ -61,6 +61,7 @@
</div> </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-if="form.sendType === 'reply'">{{ $t('reply') }}</el-button>
<el-button type="primary" @click="sendEmail" v-else-if="form.sendType === 'forward'">{{ $t('forward') }}</el-button>
<el-button type="primary" @click="sendEmail" v-else>{{ $t('send') }}</el-button> <el-button type="primary" @click="sendEmail" v-else>{{ $t('send') }}</el-button>
</div> </div>
</div> </div>
@@ -114,10 +115,11 @@ import router from "@/router/index.js";
defineExpose({ defineExpose({
open, open,
openReply, openReply,
openForward,
openDraft openDraft
}) })
const {t} = useI18n() const {t, locale} = useI18n()
const writerStore = useWriterStore(); const writerStore = useWriterStore();
const draftStore = userDraftStore() const draftStore = userDraftStore()
const settingStore = useSettingStore() const settingStore = useSettingStore()
@@ -207,7 +209,6 @@ function selectChange(value) {
function selectStatusChange(status) { function selectStatusChange(status) {
selectStatus = status selectStatus = status
ruleEmailsInputDesc.value = status ? '' : ruleEmailsInputDesc.value = t('ruleEmailsInputDesc')
} }
const openSelect = () => { const openSelect = () => {
@@ -264,25 +265,23 @@ function delAtt(index) {
function chooseFile() { function chooseFile() {
const doc = document.createElement("input") const doc = document.createElement("input")
doc.setAttribute("type", "file") doc.setAttribute("type", "file")
doc.multiple = true;
doc.click() doc.click()
doc.onchange = async (e) => { doc.onchange = async (e) => {
const file = e.target.files[0] const fileList = e.target.files;
const size = file.size
const filename = file.name for (const file of fileList) {
const contentType = file.type
const size = file.size
const filename = file.name
const contentType = file.type
const content = await fileToBase64(file)
form.attachments.push({content, filename, size, contentType})
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})
} }
} }
@@ -427,6 +426,35 @@ function focusChange() {
if (selectStatus) openSelect() if (selectStatus) openSelect()
} }
function openForward(email) {
resetForm();
email.subject = email.subject || ''
form.subject = email.subject
form.sendType = 'forward'
form.emailId = email.emailId
defValue.value = ''
setTimeout(() => {
defValue.value = `
<articl class="mceNonEditable" >
${formatImage(email.content) || `<pre style="font-family: inherit;word-break: break-word;white-space: pre-wrap;margin: 0">${email.text}</pre>`}
</article>
`
open()
nextTick(() => {
backReply.content = editor.value.getContent()
backReply.subject = form.subject
backReply.receiveEmail = form.receiveEmail
backReply.sendType = form.sendType
})
});
}
function openReply(email) { function openReply(email) {
resetForm(); resetForm();
@@ -527,7 +555,7 @@ function close() {
return; return;
} }
if (backReply.sendType === 'reply') { if (backReply.sendType === 'reply' || backReply.sendType === 'forward') {
let subjectFlag = form.subject === backReply.subject let subjectFlag = form.subject === backReply.subject
let contentFlag = editor.value.getContent() === backReply.content let contentFlag = editor.value.getContent() === backReply.content
let receiveFlag = form.receiveEmail.length === 1 && form.receiveEmail[0] === backReply.receiveEmail[0] let receiveFlag = form.receiveEmail.length === 1 && form.receiveEmail[0] === backReply.receiveEmail[0]

View File

@@ -7,7 +7,8 @@
<Icon class="icon" @click="changeStar" v-if="email.isStar" icon="fluent-color:star-16" width="20" height="20"/> <Icon class="icon" @click="changeStar" v-if="email.isStar" icon="fluent-color:star-16" width="20" height="20"/>
<Icon class="icon" @click="changeStar" v-else icon="solar:star-line-duotone" width="18" height="18"/> <Icon class="icon" @click="changeStar" v-else icon="solar:star-line-duotone" width="18" height="18"/>
</span> </span>
<Icon class="icon" v-if="emailStore.contentData.showReply" v-perm="'email:send'" @click="openReply" icon="la:reply" width="20" height="20" /> <Icon class="icon" v-if="emailStore.contentData.showReply" v-perm="'email:send'" @click="openReply" icon="la:reply" width="21" height="21" />
<Icon class="icon" v-if="emailStore.contentData.showReply" v-perm="'email:send'" @click="openForward" icon="tabler:location-share" width="19" height="19" />
</div> </div>
<div></div> <div></div>
<el-scrollbar class="scrollbar"> <el-scrollbar class="scrollbar">
@@ -121,6 +122,10 @@ function openReply() {
uiStore.writerRef.openReply(email) uiStore.writerRef.openReply(email)
} }
function openForward() {
uiStore.writerRef.openForward(email)
}
function toMessage(message) { function toMessage(message) {
return message ? JSON.parse(message).message : ''; return message ? JSON.parse(message).message : '';
} }

View File

@@ -57,7 +57,7 @@
</div> </div>
</div> </div>
<div class="empty" v-if="regKeyData.length === 0"> <div class="empty" v-if="regKeyData.length === 0">
<el-empty v-if="!regKeyFirst" :image-size="isMobile ? 120 : 0" :description="$t('noCodeFound')"/> <el-empty v-if="!regKeyFirst" :image-size="isMobile ? 120 : null" :description="$t('noCodeFound')"/>
</div> </div>
</el-scrollbar> </el-scrollbar>
<el-dialog v-model="showAdd" :title="$t('addRegKey')"> <el-dialog v-model="showAdd" :title="$t('addRegKey')">

View File

@@ -1273,6 +1273,13 @@ function adjustWidth() {
white-space: nowrap; white-space: nowrap;
} }
:deep(.el-table) {
@media (pointer: coarse) {
/* 触屏 */
user-select: none;
}
}
:deep(.el-table th.el-table__cell>.cell.highlight) { :deep(.el-table th.el-table__cell>.cell.highlight) {
color: #909399; color: #909399;
} }

View File

@@ -23,6 +23,8 @@ const en = {
noResendToken: 'Resend API token not configured', noResendToken: 'Resend API token not configured',
sendEmailNotCurUser: 'Sender email does not belong to current user', sendEmailNotCurUser: 'Sender email does not belong to current user',
notExistEmailReply: 'Mail does not exist and cannot be replied to', notExistEmailReply: 'Mail does not exist and cannot be replied to',
imageAttLimit: 'The maximum number of image attachments is 10',
attLimit: 'The maximum number of attachments is 10.',
pwdLengthLimit: 'Password length exceeds the limit', pwdLengthLimit: 'Password length exceeds the limit',
emailLengthLimit: 'Email length exceeds the limit', emailLengthLimit: 'Email length exceeds the limit',
minEmailPrefix: 'Email must be at least {{msg}} characters', minEmailPrefix: 'Email must be at least {{msg}} characters',

View File

@@ -23,6 +23,8 @@ const zh = {
noResendToken: 'resend密钥未配置', noResendToken: 'resend密钥未配置',
sendEmailNotCurUser: '发件人邮箱非当前用户所有', sendEmailNotCurUser: '发件人邮箱非当前用户所有',
notExistEmailReply: '邮件不存在无法回复', notExistEmailReply: '邮件不存在无法回复',
imageAttLimit: '图片不能超过10个',
attLimit: '附件不能超过10个',
pwdLengthLimit: '密码长度超出限制', pwdLengthLimit: '密码长度超出限制',
emailLengthLimit: '邮箱长度超出限制', emailLengthLimit: '邮箱长度超出限制',
minEmailPrefix: '邮箱名至少{{msg}}位', minEmailPrefix: '邮箱名至少{{msg}}位',

View File

@@ -179,6 +179,7 @@ const attService = {
cacheControl: `max-age=259200`, cacheControl: `max-age=259200`,
contentDisposition: `inline;filename=${attData.filename}` contentDisposition: `inline;filename=${attData.filename}`
}); });
delete attData.buff;
} }
await orm(c).insert(att).values(attDataList).run(); await orm(c).insert(att).values(attDataList).run();

View File

@@ -331,11 +331,17 @@ const emailService = {
//保存内嵌附件 //保存内嵌附件
if (imageDataList.length > 0) { if (imageDataList.length > 0) {
if (imageDataList.length > 10) {
throw new BizError(t('imageAttLimit'));
}
await attService.saveArticleAtt(c, imageDataList, userId, accountId, emailResult.emailId); await attService.saveArticleAtt(c, imageDataList, userId, accountId, emailResult.emailId);
} }
//保存普通附件 //保存普通附件
if (attachments?.length > 0) { if (attachments?.length > 0) {
if (attachments.length > 10) {
throw new BizError(t('attLimit'));
}
await attService.saveSendAtt(c, attachments, userId, accountId, emailResult.emailId); await attService.saveSendAtt(c, attachments, userId, accountId, emailResult.emailId);
} }

View File

@@ -77,10 +77,10 @@ const settingService = {
if (!showSiteKey) { if (!showSiteKey) {
settingRow.siteKey = settingRow.siteKey ? `${settingRow.siteKey.slice(0, 12)}******` : null; settingRow.siteKey = settingRow.siteKey ? `${settingRow.siteKey.slice(0, 6)}******` : null;
} }
settingRow.secretKey = settingRow.secretKey ? `${settingRow.secretKey.slice(0, 12)}******` : null; settingRow.secretKey = settingRow.secretKey ? `${settingRow.secretKey.slice(0, 6)}******` : null;
Object.keys(settingRow.resendTokens).forEach(key => { Object.keys(settingRow.resendTokens).forEach(key => {
settingRow.resendTokens[key] = `${settingRow.resendTokens[key].slice(0, 12)}******`; settingRow.resendTokens[key] = `${settingRow.resendTokens[key].slice(0, 12)}******`;