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