diff --git a/src/api/mail.ts b/src/api/mail.ts index b42fd27..cb200a1 100644 --- a/src/api/mail.ts +++ b/src/api/mail.ts @@ -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 => + del('/API/Mail/Contacts', data), + /** 管理员:全部邮箱列表 */ adminList: (): Promise<{ list: MailAdminItem[]; total: number }> => get<{ list: MailAdminItem[]; total: number }>('/API/Mail/AdminList'), diff --git a/src/utils/jmap.ts b/src/utils/jmap.ts index 98ff54e..826c67f 100644 --- a/src/utils/jmap.ts +++ b/src/utils/jmap.ts @@ -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 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() diff --git a/src/views/mail/Mailbox.vue b/src/views/mail/Mailbox.vue index 89cbaf6..2bacf3f 100644 --- a/src/views/mail/Mailbox.vue +++ b/src/views/mail/Mailbox.vue @@ -60,6 +60,12 @@ const sending = ref(false) // 联系人(云端 SQL 存储,发件时自动保存;写信输入匹配下拉) const contactOptions = ref([]) const contactsLoading = ref(false) +// 联系人管理视图(中栏切换) +const currentPane = ref<'mail' | 'contacts'>('mail') +const contactRows = ref([]) +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 { 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 { async function switchMailbox(id: string): Promise { 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 { + 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 { + 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 { + 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)" > {{ folderLabel(f) }} {{ f.unreadEmails > 99 ? '99+' : f.unreadEmails }} +
+ + 联系人 + {{ contactRows.length }} +
- +
+ + + +
@@ -831,6 +947,22 @@ function isSelected(id: string): boolean { 确认修改 + + + + + + + + + + + + + @@ -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;