fix(mail): 修复邮件正文为空并新增联系人管理视图
- Email/get 补 bodyValues,正文按 partId 取值,HTML 去标签 - 左栏联系人入口 + 中栏 CRUD 管理(新增/编辑/删除) - 文件夹项选中态绑定当前视图,切换联系人时取消高亮
This commit is contained in:
1 parent
864a9fae3b
commit
5c320e0e4a
3 files changed
+188
-8
No files matched your search
+5
-1
@@ -1,4 +1,4 @@
|
||||
import { get, post } from './request'
|
||||
import { get, post, del } from './request'
|
||||
|
||||
/** 我的邮箱信息 */
|
||||
export interface MailboxInfo {
|
||||
@@ -87,6 +87,10 @@ export const mailApi = {
|
||||
addContact: (data: { ContactEmail: string; ContactName?: string }): Promise<{ Created: boolean }> =>
|
||||
post<{ Created: boolean }>('/API/Mail/Contacts', data),
|
||||
|
||||
/** 删除联系人(按 MailContactUUID 或 ContactEmail) */
|
||||
deleteContact: (data: { MailContactUUID?: string; ContactEmail?: string }): Promise<void> =>
|
||||
del<void>('/API/Mail/Contacts', data),
|
||||
|
||||
/** 管理员:全部邮箱列表 */
|
||||
adminList: (): Promise<{ list: MailAdminItem[]; total: number }> => get<{ list: MailAdminItem[]; total: number }>('/API/Mail/AdminList'),
|
||||
|
||||
|
||||
+2
-1
@@ -37,6 +37,7 @@ export interface JmapEmail {
|
||||
preview?: string
|
||||
textBody?: Array<{ type: string; partId?: string; value?: string; blobId?: string }>
|
||||
htmlBody?: Array<{ type: string; partId?: string; value?: string; blobId?: string }>
|
||||
bodyValues?: Record<string, { value?: string }>
|
||||
messageId: string[]
|
||||
inReplyTo?: string[] | null
|
||||
}
|
||||
@@ -153,7 +154,7 @@ export class JmapClient {
|
||||
ids,
|
||||
properties: [
|
||||
'id', 'subject', 'from', 'to', 'cc', 'receivedAt', 'size',
|
||||
'keywords', 'preview', 'textBody', 'htmlBody', 'messageId', 'inReplyTo',
|
||||
'keywords', 'preview', 'textBody', 'htmlBody', 'bodyValues', 'messageId', 'inReplyTo',
|
||||
],
|
||||
})
|
||||
const byId = new Map<string, JmapEmail>()
|
||||
|
||||
+181
-6
@@ -60,6 +60,12 @@ const sending = ref(false)
|
||||
// 联系人(云端 SQL 存储,发件时自动保存;写信输入匹配下拉)
|
||||
const contactOptions = ref<MailContact[]>([])
|
||||
const contactsLoading = ref(false)
|
||||
// 联系人管理视图(中栏切换)
|
||||
const currentPane = ref<'mail' | 'contacts'>('mail')
|
||||
const contactRows = ref<MailContact[]>([])
|
||||
const contactVisible = ref(false)
|
||||
const contactForm = ref({ ContactName: '', ContactEmail: '' })
|
||||
const contactSaving = ref(false)
|
||||
|
||||
// 改密
|
||||
const changePwdVisible = ref(false)
|
||||
@@ -268,6 +274,7 @@ async function loadMailboxes(): Promise<void> {
|
||||
const inbox = mailboxes.value.find((m) => m.role === 'inbox') || mailboxes.value[0]
|
||||
currentMailboxId.value = inbox?.id || ''
|
||||
await loadEmails()
|
||||
void refreshContacts()
|
||||
} catch (e) {
|
||||
if (e instanceof JmapAuthError) return
|
||||
ElMessage.error((e as Error).message || '加载邮箱失败')
|
||||
@@ -309,6 +316,7 @@ async function loadEmails(): Promise<void> {
|
||||
async function switchMailbox(id: string): Promise<void> {
|
||||
if (id === currentMailboxId.value) return
|
||||
currentMailboxId.value = id
|
||||
currentPane.value = 'mail'
|
||||
currentEmail.value = null
|
||||
await loadEmails()
|
||||
}
|
||||
@@ -422,6 +430,66 @@ function searchContacts(keyword: string): void {
|
||||
void loadContacts(keyword)
|
||||
}
|
||||
|
||||
// === 联系人管理(中栏 CRUD) ===
|
||||
|
||||
async function refreshContacts(): Promise<void> {
|
||||
try {
|
||||
const result = await mailApi.contacts()
|
||||
contactRows.value = result.list
|
||||
} catch {
|
||||
// 拦截器已提示
|
||||
}
|
||||
}
|
||||
|
||||
function switchPane(pane: 'mail' | 'contacts'): void {
|
||||
currentPane.value = pane
|
||||
if (pane === 'contacts') void refreshContacts()
|
||||
}
|
||||
|
||||
function openContactAdd(): void {
|
||||
contactForm.value = { ContactName: '', ContactEmail: '' }
|
||||
contactVisible.value = true
|
||||
}
|
||||
|
||||
function openContactEdit(c: MailContact): void {
|
||||
contactForm.value = { ContactName: c.ContactName, ContactEmail: c.ContactEmail }
|
||||
contactVisible.value = true
|
||||
}
|
||||
|
||||
async function saveContact(): Promise<void> {
|
||||
const email = contactForm.value.ContactEmail.trim().toLowerCase()
|
||||
if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[A-Za-z]{2,}$/.test(email)) {
|
||||
ElMessage.warning('请输入有效的邮箱地址')
|
||||
return
|
||||
}
|
||||
contactSaving.value = true
|
||||
try {
|
||||
await mailApi.addContact({ ContactEmail: email, ContactName: contactForm.value.ContactName.trim() })
|
||||
ElMessage.success('已保存')
|
||||
contactVisible.value = false
|
||||
await refreshContacts()
|
||||
} catch {
|
||||
// 拦截器已提示
|
||||
} finally {
|
||||
contactSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function removeContact(c: MailContact): Promise<void> {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除联系人 ${c.ContactName || c.ContactEmail} 吗?`, '删除确认', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await mailApi.deleteContact({ MailContactUUID: c.MailContactUUID })
|
||||
ElMessage.success('已删除')
|
||||
await refreshContacts()
|
||||
} catch {
|
||||
// 拦截器已提示
|
||||
}
|
||||
}
|
||||
|
||||
function openCompose(): void {
|
||||
composeForm.value = { ToList: [], Subject: '', TextBody: '' }
|
||||
composeInReplyTo.value = undefined
|
||||
@@ -432,10 +500,17 @@ function openCompose(): void {
|
||||
|
||||
function plainBody(e: JmapEmail | null): string {
|
||||
if (!e) return ''
|
||||
const textPart = e.textBody?.find((p) => p.value != null)
|
||||
if (textPart?.value) return textPart.value
|
||||
const htmlPart = e.htmlBody?.find((p) => p.value != null)
|
||||
if (htmlPart?.value) return htmlPart.value.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ')
|
||||
// 正文按 bodyValues 引用(JMAP 标准:textBody/htmlBody 仅含 partId,value 在 bodyValues 中)
|
||||
const partValue = (parts: JmapEmail['textBody']): string => {
|
||||
for (const p of parts || []) {
|
||||
if (p.partId && e.bodyValues?.[p.partId]?.value != null) return e.bodyValues[p.partId].value as string
|
||||
}
|
||||
return ''
|
||||
}
|
||||
const text = partValue(e.textBody)
|
||||
if (text) return text
|
||||
const html = partValue(e.htmlBody)
|
||||
if (html) return html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
return '(无正文内容)'
|
||||
}
|
||||
|
||||
@@ -671,13 +746,22 @@ function isSelected(id: string): boolean {
|
||||
v-for="f in displayFolders"
|
||||
:key="f.id"
|
||||
class="folder-item"
|
||||
:class="{ active: currentMailboxId === f.id }"
|
||||
:class="{ active: currentPane === 'mail' && currentMailboxId === f.id }"
|
||||
@click="switchMailbox(f.id)"
|
||||
>
|
||||
<el-icon class="folder-icon"><component :is="folderIcon(f)" /></el-icon>
|
||||
<span class="folder-name">{{ folderLabel(f) }}</span>
|
||||
<span v-if="f.unreadEmails > 0" class="folder-badge">{{ f.unreadEmails > 99 ? '99+' : f.unreadEmails }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="folder-item"
|
||||
:class="{ active: currentPane === 'contacts' }"
|
||||
@click="switchPane('contacts')"
|
||||
>
|
||||
<el-icon class="folder-icon"><User /></el-icon>
|
||||
<span class="folder-name">联系人</span>
|
||||
<span v-if="contactRows.length" class="folder-badge">{{ contactRows.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-footer">
|
||||
<el-button text size="small" @click="changePwdVisible = true">修改密码</el-button>
|
||||
@@ -685,8 +769,9 @@ function isSelected(id: string): boolean {
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 中栏:邮件列表 -->
|
||||
<!-- 中栏:邮件列表 / 联系人管理 -->
|
||||
<section class="mail-list-pane">
|
||||
<template v-if="currentPane === 'mail'">
|
||||
<div class="list-head">
|
||||
<span class="toolbar-title">{{ currentFolderName }}</span>
|
||||
<span v-if="selectedIds.length" class="toolbar-count">已选 {{ selectedIds.length }} 封</span>
|
||||
@@ -728,6 +813,37 @@ function isSelected(id: string): boolean {
|
||||
<el-button text size="small" :disabled="!selectedIds.length" @click="markSelected(false)">未读</el-button>
|
||||
<el-button text size="small" type="danger" :disabled="!selectedIds.length" @click="deleteSelected">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 联系人管理视图 -->
|
||||
<template v-else>
|
||||
<div class="list-head">
|
||||
<span class="toolbar-title">联系人</span>
|
||||
<span v-if="contactRows.length" class="toolbar-count">{{ contactRows.length }} 位</span>
|
||||
<div class="spacer" />
|
||||
<el-tooltip content="刷新">
|
||||
<el-button text size="small" :icon="Refresh" @click="refreshContacts" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="contacts-list">
|
||||
<div v-for="c in contactRows" :key="c.MailContactUUID" class="contact-row">
|
||||
<div class="contact-main">
|
||||
<div class="contact-name">{{ c.ContactName || c.ContactEmail }}</div>
|
||||
<div class="contact-email">{{ c.ContactEmail }}</div>
|
||||
</div>
|
||||
<div class="contact-actions">
|
||||
<el-button text size="small" @click="openContactEdit(c)">编辑</el-button>
|
||||
<el-button text size="small" type="danger" @click="removeContact(c)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="!contactRows.length" description="暂无联系人,发件后自动保存" :image-size="80" />
|
||||
</div>
|
||||
<div class="list-actions">
|
||||
<el-button type="primary" size="small" @click="openContactAdd">新增联系人</el-button>
|
||||
<div class="spacer" />
|
||||
<el-button text size="small" @click="switchPane('mail')">返回邮箱</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<!-- 右栏:阅读窗格 -->
|
||||
@@ -831,6 +947,22 @@ function isSelected(id: string): boolean {
|
||||
<el-button type="primary" @click="submitChangePassword">确认修改</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 联系人新增/编辑弹窗 -->
|
||||
<el-dialog v-model="contactVisible" :title="contactForm.ContactEmail ? '编辑联系人' : '新增联系人'" width="420px">
|
||||
<el-form label-width="70px">
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="contactForm.ContactName" placeholder="联系人姓名(可选)" maxlength="128" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="contactForm.ContactEmail" placeholder="name@example.com" maxlength="256" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="contactVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="contactSaving" @click="saveContact">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1109,6 +1241,49 @@ html.dark .mail-login-wrap {
|
||||
color: var(--el-text-color-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 联系人管理 */
|
||||
.contacts-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
.contact-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
transition: background 0.12s;
|
||||
}
|
||||
.contact-row:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
.contact-main {
|
||||
min-width: 0;
|
||||
}
|
||||
.contact-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.contact-email {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.contact-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.email-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
Reference in new issue
Block a user