feat(mail): 多邮箱选择登录与申请,展示已申请数量(N/5)

This commit is contained in:
TermiNexus committed 2026-08-13 01:00:43 +08:00
1 parent fde710918d
commit 1332f25326
2 files changed
+55 -21

No files matched your search

+7 -6
View File
@@ -17,7 +17,7 @@ export interface MailSessionConfig {
/** 我的邮箱接口响应 */
export interface MyMailboxResult {
Mailbox: MailboxInfo | null
MailboxList: MailboxInfo[]
Session: MailSessionConfig
}
@@ -66,12 +66,13 @@ export const mailApi = {
apply: (data: { MailboxName: string; Password: string; QuotaMB?: number }): Promise<{ MailboxName: string; QuotaMB: number }> =>
post<{ MailboxName: string; QuotaMB: number }>('/API/Mail/Apply', data),
/** 修改邮箱密码(需先验证当前密码) */
changePassword: (OldPassword: string, NewPassword: string): Promise<void> =>
post<void>('/API/Mail/ChangePassword', { OldPassword, NewPassword }),
/** 修改邮箱密码(需先验证当前密码;MailboxName 可选,多邮箱时指定,缺省取第一个) */
changePassword: (OldPassword: string, NewPassword: string, MailboxName?: string): Promise<void> =>
post<void>('/API/Mail/ChangePassword', { OldPassword, NewPassword, ...(MailboxName ? { MailboxName } : {}) }),
/** 邮箱登录:校验密码并换取会话 token(密码仅此一次传输,后端存内存;401 为密码错误,不触发系统登出) */
login: (Password: string): Promise<MailLoginResult> => post<MailLoginResult>('/API/Mail/Login', { Password }, { silent: true }),
/** 邮箱登录:校验密码并换取会话 token(密码仅此一次传输,后端存内存;401 为密码错误,不触发系统登出;MailboxName 可选,多邮箱时指定,缺省取第一个) */
login: (Password: string, MailboxName?: string): Promise<MailLoginResult> =>
post<MailLoginResult>('/API/Mail/Login', { Password, ...(MailboxName ? { MailboxName } : {}) }, { silent: true }),
/** 邮箱登出:吊销会话 token */
logout: (Token: string): Promise<void> => post<void>('/API/Mail/Logout', { Token }),
+48 -15
View File
@@ -26,7 +26,8 @@ import { JmapClient, JmapAuthError, type JmapMailbox, type JmapEmail } from '@/u
// === 状态 ===
const loading = ref(true)
const mailbox = ref<MailboxInfo | null>(null)
const myMailboxes = ref<MailboxInfo[]>([])
const selectedMailbox = ref<MailboxInfo | null>(null)
const session = ref<MailSessionConfig | null>(null)
// 申请表单
@@ -71,7 +72,9 @@ const contactSaving = ref(false)
const changePwdVisible = ref(false)
const changePwdForm = ref({ OldPassword: '', NewPassword: '', ConfirmPassword: '' })
const myAddress = computed(() => mailbox.value?.MailboxName || '')
const myAddress = computed(() => selectedMailbox.value?.MailboxName || '')
const mailboxCount = computed(() => myMailboxes.value.length)
const atMailboxLimit = computed(() => myMailboxes.value.length >= 5)
// === 本地已读覆盖 ===
// Stalwart v0.11 存在 JMAP keywords 写入已知 bug(GitHub #1139):setSeen 返回成功但已读随机回退未读,
@@ -107,7 +110,7 @@ const MAIL_NAME_KEY = 'gerp_mail_name'
function saveMailSession(token: string): void {
localStorage.setItem(MAIL_TOKEN_KEY, token)
localStorage.setItem(MAIL_NAME_KEY, mailbox.value?.MailboxName || '')
localStorage.setItem(MAIL_NAME_KEY, selectedMailbox.value?.MailboxName || '')
}
function clearMailSession(): void {
@@ -129,10 +132,10 @@ function handleMailSessionExpired(): void {
/** 恢复上次邮箱会话:localStorage 中的 token 直接连接 JMAP,失效则自动清除 */
async function restoreSession(): Promise<void> {
if (!mailbox.value || client.value) return
if (!selectedMailbox.value || client.value) return
const token = localStorage.getItem(MAIL_TOKEN_KEY)
const name = localStorage.getItem(MAIL_NAME_KEY)
if (!token || name !== mailbox.value.MailboxName) return
if (!token || name !== selectedMailbox.value.MailboxName) return
restoring.value = true
try {
const c = new JmapClient(name, token, handleMailSessionExpired)
@@ -151,7 +154,11 @@ async function restoreSession(): Promise<void> {
async function init(): Promise<void> {
try {
const result = await mailApi.myMailbox()
mailbox.value = result.Mailbox
myMailboxes.value = result.MailboxList || []
// 保留当前选中邮箱;无选中或已不在列表则取第一个启用邮箱
if (!selectedMailbox.value || !myMailboxes.value.some((m) => m.MailboxName === selectedMailbox.value?.MailboxName)) {
selectedMailbox.value = myMailboxes.value.find((m) => m.Status === 1) || myMailboxes.value[0] || null
}
session.value = result.Session
} catch {
// 拦截器已提示
@@ -194,7 +201,7 @@ async function submitApply(): Promise<void> {
// === 登录 JMAP ===
async function submitLogin(): Promise<void> {
if (!mailbox.value || !session.value) return
if (!selectedMailbox.value || !session.value) return
if (!loginForm.value.Password) {
ElMessage.warning('请输入邮箱密码')
return
@@ -202,7 +209,7 @@ async function submitLogin(): Promise<void> {
connecting.value = true
try {
// 后端校验密码并签发会话 token(密码仅此一次传输,后端存内存;后续 JMAP 调用只带 token)
const result = await mailApi.login(loginForm.value.Password)
const result = await mailApi.login(loginForm.value.Password, selectedMailbox.value.MailboxName)
const c = new JmapClient(result.MailboxName, result.Token, handleMailSessionExpired)
await c.discover()
client.value = c
@@ -617,7 +624,7 @@ async function submitChangePassword(): Promise<void> {
return
}
try {
await mailApi.changePassword(f.OldPassword, f.NewPassword)
await mailApi.changePassword(f.OldPassword, f.NewPassword, selectedMailbox.value?.MailboxName)
ElMessage.success('密码修改成功,请重新登录邮箱')
changePwdVisible.value = false
changePwdForm.value = { OldPassword: '', NewPassword: '', ConfirmPassword: '' }
@@ -704,12 +711,12 @@ function isSelected(id: string): boolean {
<div class="mail-page">
<div v-loading="loading" class="mail-container">
<!-- 未申请邮箱 -->
<el-empty v-if="!loading && !mailbox" description="您还没有企业邮箱">
<el-empty v-if="!loading && !myMailboxes.length" description="您还没有企业邮箱">
<el-button type="primary" @click="applyVisible = true">申请邮箱</el-button>
</el-empty>
<!-- 已申请:企业邮箱登录页 -->
<template v-else-if="mailbox && !client">
<template v-else-if="selectedMailbox && !client">
<div v-loading="restoring" class="mail-login-wrap">
<div class="mail-login-card">
<div class="login-brand">
@@ -717,7 +724,21 @@ function isSelected(id: string): boolean {
<div class="login-title">企业邮箱</div>
<div class="login-subtitle">GVSDS · 银河锋锐 企业级邮件服务</div>
</div>
<div class="login-address">
<el-select
v-if="myMailboxes.length > 1"
v-model="selectedMailbox"
class="login-address-select"
placeholder="选择要登录的邮箱"
>
<el-option
v-for="m in myMailboxes"
:key="m.MailboxName"
:value="m"
:label="m.MailboxName"
:disabled="m.Status !== 1"
/>
</el-select>
<div v-else class="login-address">
<el-icon><User /></el-icon>
<span>{{ myAddress }}</span>
</div>
@@ -737,16 +758,21 @@ function isSelected(id: string): boolean {
<div class="login-links">
<el-button text size="small" @click="changePwdVisible = true">修改密码</el-button>
<span class="link-divider" />
<el-button text size="small" :disabled="atMailboxLimit" @click="applyVisible = true">申请新邮箱</el-button>
<span class="link-divider" />
<el-button text size="small" type="danger" @click="logoutMail">退出当前邮箱</el-button>
</div>
<div class="login-tip">登录后即可网页收发邮件,也可在 Outlook 等客户端配置使用</div>
<div class="login-tip">
{{ mailboxCount >= 5 ? '已达邮箱数量上限(5 个)' : `已申请 ${mailboxCount} / 5 个邮箱` }}
· 登录后即可网页收发邮件,也可在 Outlook 等客户端配置使用
</div>
</div>
<div class="login-footer">© 2026 GVSDS 企业邮箱 · 银河锋锐</div>
</div>
</template>
<!-- 已登录 JMAP:三栏工作台 -->
<template v-else-if="mailbox && client">
<template v-else-if="selectedMailbox && client">
<div class="mail-shell">
<!-- 左栏:文件夹 -->
<aside class="mail-sidebar">
@@ -889,7 +915,11 @@ function isSelected(id: string): boolean {
</div>
<!-- 申请邮箱弹窗 -->
<el-dialog v-model="applyVisible" title="申请企业邮箱" width="420px">
<el-dialog
v-model="applyVisible"
:title="`申请企业邮箱${mailboxCount ? `(已申请 ${mailboxCount} / 5)` : ''}`"
width="420px"
>
<el-form label-width="80px">
<el-form-item label="邮箱名">
<el-input v-model="applyForm.MailboxName" placeholder="如 zhangsan" maxlength="63">
@@ -1058,6 +1088,9 @@ function isSelected(id: string): boolean {
text-overflow: ellipsis;
white-space: nowrap;
}
.login-address-select {
width: 100%;
}
.login-submit {
width: 100%;
height: 42px;