Smail/frontend/src/views/Editer/Draft.js
smail cccdcb30eb
Some checks are pending
Security Checks / security (push) Waiting to run
继续集成草稿编辑功能前保存检查点
2026-09-24 21:37:40 +08:00

162 lines
6.2 KiB
JavaScript

import {computed, onBeforeUnmount, ref, watch} from 'vue'
import {onBeforeRouteLeave, onBeforeRouteUpdate, useRoute} from 'vue-router'
import {ElMessage, ElMessageBox} from 'element-plus'
import {http} from '@/utils/axios'
import lang from '../../i18n/i18n'
export function useDraft({router, groupStore, ruleForm, valueHtml, fileList, editorRef, showCcBcc, isSending, pendingFiles}) {
const route = useRoute()
const draftID = ref(0)
const draftVersion = ref('')
const isSaving = ref(false)
const isLoading = ref(true)
const loadFailed = ref(false)
const replyTo = ref([])
const isBusy = computed(() => isSaving.value || isSending.value || isLoading.value || loadFailed.value || pendingFiles.value > 0)
let savedSnapshot = ''
let sent = false
let loadSequence = 0
let disposed = false
const buildMessage = () => ({
draft_id: draftID.value,
draft_version: draftVersion.value,
from: {name: ruleForm.nickName, email: `${ruleForm.sender}@${ruleForm.pickedDomain}`},
to: ruleForm.receivers.map(email => ({name: '', email})),
cc: ruleForm.cc.map(email => ({name: '', email})),
bcc: ruleForm.bcc.map(email => ({name: '', email})),
reply_to: replyTo.value,
subject: ruleForm.subject,
text: editorRef.value?.getText() || '',
html: valueHtml.value,
attrs: fileList.map(item => ({name: item.name, data: item.data}))
})
const snapshot = () => JSON.stringify({
from: [ruleForm.nickName, ruleForm.sender, ruleForm.pickedDomain],
to: ruleForm.receivers, cc: ruleForm.cc, bcc: ruleForm.bcc,
subject: ruleForm.subject, html: valueHtml.value, attrs: fileList
})
const isDirty = () => !sent && savedSnapshot !== '' && snapshot() !== savedSnapshot
const loadDraft = async () => {
const sequence = ++loadSequence
isLoading.value = true
loadFailed.value = false
const rawID = route.query.draft
if (rawID === undefined) {
draftID.value = 0
draftVersion.value = ''
replyTo.value = []
ruleForm.receivers = []
ruleForm.cc = []
ruleForm.bcc = []
ruleForm.subject = ''
valueHtml.value = '<p><br></p>'
fileList.splice(0)
showCcBcc.value = false
sent = false
savedSnapshot = snapshot()
isLoading.value = false
return
}
try {
const id = Number(rawID)
if (!Number.isSafeInteger(id) || id <= 0) throw new Error(lang.draft_load_failed)
const result = await http.post('/api/v1/email/draft/detail', {id})
if (disposed || sequence !== loadSequence) return
if (result.errorNo !== 0) throw new Error(result.errorMsg || lang.draft_load_failed)
const data = result.data
const message = data?.message
if (data?.id !== id || !data.version || !message?.from?.email) throw new Error(lang.draft_load_failed)
const split = message.from.email.lastIndexOf('@')
if (split <= 0) throw new Error(lang.draft_load_failed)
draftID.value = data.id
draftVersion.value = data.version
ruleForm.sender = message.from.email.slice(0, split)
ruleForm.pickedDomain = message.from.email.slice(split + 1)
ruleForm.nickName = message.from.name || ''
ruleForm.receivers = (message.to || []).map(item => item.email)
ruleForm.cc = (message.cc || []).map(item => item.email)
ruleForm.bcc = (message.bcc || []).map(item => item.email)
replyTo.value = message.reply_to || []
ruleForm.subject = message.subject || ''
valueHtml.value = message.html || '<p><br></p>'
fileList.splice(0, fileList.length, ...(message.attrs || []))
showCcBcc.value = ruleForm.cc.length > 0 || ruleForm.bcc.length > 0
sent = false
savedSnapshot = snapshot()
} catch (error) {
if (disposed || sequence !== loadSequence) return
loadFailed.value = true
ElMessage.error(error.message || lang.draft_load_failed)
} finally {
if (!disposed && sequence === loadSequence) isLoading.value = false
}
}
const saveDraft = async () => {
if (isBusy.value || !editorRef.value) return
isSaving.value = true
const submittedSnapshot = snapshot()
try {
const result = await http.post('/api/v1/email/draft/save', buildMessage())
if (result.errorNo !== 0) throw new Error(result.errorMsg || lang.fail)
if (!Number.isSafeInteger(result.data?.id) || result.data.id <= 0 || !result.data.version) throw new Error(lang.fail)
draftID.value = result.data.id
draftVersion.value = result.data.version
savedSnapshot = submittedSnapshot
groupStore.invalidate()
// 将已保存草稿写入地址,刷新后仍可恢复;先释放保存锁再更新路由。
isSaving.value = false
await router.replace({name: 'editer', query: {draft: String(draftID.value)}})
ElMessage.success(lang.draft_saved)
} catch (error) {
ElMessage.error(error.message || lang.fail)
} finally {
isSaving.value = false
}
}
const confirmLeave = async () => {
if (sent) return true
if (isSaving.value || isSending.value || pendingFiles.value > 0) return false
if (!isDirty()) return true
try {
await ElMessageBox.confirm(lang.draft_leave_warning, lang.draft_title, {
confirmButtonText: lang.draft_leave, cancelButtonText: lang.draft_stay, type: 'warning'
})
return true
} catch {
return false
}
}
onBeforeRouteLeave(confirmLeave)
onBeforeRouteUpdate((to, from) => to.query.draft === from.query.draft ? true : confirmLeave())
watch(() => route.query.draft, value => {
if (String(value || '') !== String(draftID.value || '')) loadDraft()
})
watch([editorRef, isBusy], ([editor, busy]) => {
if (!editor || editor.isDestroyed) return
if (busy) editor.disable()
else editor.enable()
})
const beforeUnload = event => {
if (isDirty() || isSaving.value || isSending.value || pendingFiles.value > 0) {
event.preventDefault()
event.returnValue = ''
}
}
window.addEventListener('beforeunload', beforeUnload)
onBeforeUnmount(() => {
disposed = true
loadSequence += 1
window.removeEventListener('beforeunload', beforeUnload)
})
const markSent = () => {
sent = true
groupStore.invalidate()
}
return {draftID, draftVersion, isSaving, isLoading, isBusy, loadFailed, loadDraft, saveDraft, buildMessage, markSent}
}