feat: add admin account page with logout and responsive address bar (#803)

- Add admin account tab to display current login method
- Support logout for admin password login only
- Show login method (password/user admin/disabled check)
- Improve address bar responsive layout with auto-wrap
- Update changelog for new features

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Dream Hunter
2026-01-03 19:16:39 +08:00
committed by GitHub
co-authored by Claude Opus 4.5
parent a771446b9b
commit 8964d4461d
5 changed files with 83 additions and 6 deletions
+1
View File
@@ -14,6 +14,7 @@
### Features ### Features
- feat: |Admin| 新增管理员账号页面,显示当前登录方式并支持退出登录(仅限密码登录方式)
- feat: |邮件转发| 新增来源地址正则转发功能,支持按发件人地址过滤转发,完全向后兼容 - feat: |邮件转发| 新增来源地址正则转发功能,支持按发件人地址过滤转发,完全向后兼容
- feat: |地址来源| 新增地址来源追踪功能,记录地址创建来源(Web 记录 IP,Telegram 记录用户 IDAdmin 后台标记) - feat: |地址来源| 新增地址来源追踪功能,记录地址创建来源(Web 记录 IP,Telegram 记录用户 IDAdmin 后台标记)
- feat: |邮件过滤| 移除后端 keyword 参数,改为前端过滤当前页邮件,优化查询性能 - feat: |邮件过滤| 移除后端 keyword 参数,改为前端过滤当前页邮件,优化查询性能
+1
View File
@@ -14,6 +14,7 @@
### Features ### Features
- feat: |Admin| Add admin account page, display current login method and support logout (password login only)
- feat: |Email Forwarding| Add source address regex forwarding, filter by sender address, fully backward compatible - feat: |Email Forwarding| Add source address regex forwarding, filter by sender address, fully backward compatible
- feat: |Address Source| Add address source tracking feature, record address creation source (Web records IP, Telegram records user ID, Admin panel marked) - feat: |Address Source| Add address source tracking feature, record address creation source (Web records IP, Telegram records user ID, Admin panel marked)
- feat: |Email Filtering| Remove backend keyword parameter, switch to frontend filtering of current page emails, optimize query performance - feat: |Email Filtering| Remove backend keyword parameter, switch to frontend filtering of current page emails, optimize query performance
+4 -3
View File
@@ -230,7 +230,7 @@ watch([userJwt, isTelegram, () => settings.value.address], async () => {
</script> </script>
<template> <template>
<n-flex class="address-row" align="center" justify="center" :wrap="false"> <n-flex class="address-row" align="center" justify="center" :wrap="true">
<n-select v-model:value="addressValue" :options="addressOptions" :size="size" filterable <n-select v-model:value="addressValue" :options="addressOptions" :size="size" filterable
:loading="addressLoading" :placeholder="t('address')" @update:value="onAddressChange" :loading="addressLoading" :placeholder="t('address')" @update:value="onAddressChange"
class="address-select" /> class="address-select" />
@@ -244,16 +244,17 @@ watch([userJwt, isTelegram, () => settings.value.address], async () => {
<style scoped> <style scoped>
.address-row { .address-row {
width: 100%; width: 100%;
gap: 10px;
} }
.address-select { .address-select {
min-width: 220px; min-width: 220px;
max-width: 420px; max-width: 420px;
flex: 0 1 420px; flex: 1 1 220px;
} }
.address-copy { .address-copy {
margin-left: 10px;
flex: 0 0 auto; flex: 0 0 auto;
white-space: nowrap;
} }
</style> </style>
+75 -2
View File
@@ -1,9 +1,11 @@
<script setup> <script setup>
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useGlobalState } from '../store' import { useGlobalState } from '../store'
import { api } from '../api' import { api } from '../api'
import { getRouterPathWithLang } from '../utils'
import SenderAccess from './admin/SenderAccess.vue' import SenderAccess from './admin/SenderAccess.vue'
import Statistics from "./admin/Statistics.vue" import Statistics from "./admin/Statistics.vue"
@@ -30,9 +32,11 @@ import AiExtractSettings from './admin/AiExtractSettings.vue';
const { const {
adminAuth, showAdminAuth, adminTab, loading, adminAuth, showAdminAuth, adminTab, loading,
globalTabplacement, showAdminPage, userSettings globalTabplacement, showAdminPage, userSettings,
openSettings
} = useGlobalState() } = useGlobalState()
const message = useMessage() const message = useMessage()
const router = useRouter()
const SendMail = defineAsyncComponent(() => { const SendMail = defineAsyncComponent(() => {
loading.value = true; loading.value = true;
@@ -49,7 +53,20 @@ const authFunc = async () => {
} }
} }
const { t } = useI18n({ const showLogoutModal = ref(false)
const handleLogout = async () => {
// 清空管理员认证
adminAuth.value = '';
// 重置管理员相关状态
showAdminAuth.value = false;
adminTab.value = 'account';
// 显示成功提示并跳转
message.success(t('logoutSuccess'));
await router.push(getRouterPathWithLang('/', locale.value));
}
const { t, locale } = useI18n({
messages: { messages: {
en: { en: {
accessHeader: 'Admin Password', accessHeader: 'Admin Password',
@@ -80,6 +97,16 @@ const { t } = useI18n({
about: 'About', about: 'About',
ok: 'OK', ok: 'OK',
mailWebhook: 'Mail Webhook', mailWebhook: 'Mail Webhook',
adminAccount: 'Admin',
loginMethod: 'Login Method',
loginViaPassword: 'Admin Password Login',
loginViaUserAdmin: 'User Admin Permission',
loginViaDisabledCheck: 'Disabled Password Check',
logout: 'Logout',
logoutConfirmTitle: 'Confirm Logout',
logoutConfirmContent: 'Are you sure you want to logout from admin panel?',
confirm: 'Confirm',
logoutSuccess: 'Logout successful',
}, },
zh: { zh: {
accessHeader: 'Admin 密码', accessHeader: 'Admin 密码',
@@ -110,12 +137,36 @@ const { t } = useI18n({
about: '关于', about: '关于',
ok: '确定', ok: '确定',
mailWebhook: '邮件 Webhook', mailWebhook: '邮件 Webhook',
adminAccount: '管理员',
loginMethod: '登录方式',
loginViaPassword: 'Admin 密码登录',
loginViaUserAdmin: '用户管理员权限',
loginViaDisabledCheck: '已禁用密码检查',
logout: '退出登录',
logoutConfirmTitle: '确认退出',
logoutConfirmContent: '确定要退出管理员面板吗?',
confirm: '确认',
logoutSuccess: '退出成功',
} }
} }
}); });
const showAdminPasswordModal = computed(() => !showAdminPage.value || showAdminAuth.value) const showAdminPasswordModal = computed(() => !showAdminPage.value || showAdminAuth.value)
const tmpAdminAuth = ref('') const tmpAdminAuth = ref('')
// 判断是否通过 admin password 登录(而非用户管理员权限)
const isAdminPasswordLogin = computed(() => !!adminAuth.value)
// 获取当前登录方式
const currentLoginMethod = computed(() => {
if (adminAuth.value) {
return t('loginViaPassword');
} else if (userSettings.value.is_admin) {
return t('loginViaUserAdmin');
} else if (openSettings.value.disableAdminPasswordCheck) {
return t('loginViaDisabledCheck');
}
return '';
})
onMounted(async () => { onMounted(async () => {
// make sure user_id is fetched // make sure user_id is fetched
@@ -234,10 +285,32 @@ onMounted(async () => {
<n-tab-pane name="appearance" :tab="t('appearance')"> <n-tab-pane name="appearance" :tab="t('appearance')">
<Appearance /> <Appearance />
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="adminAccount" :tab="t('adminAccount')">
<div style="display: flex; justify-content: center; padding: 20px;">
<n-card style="width: 600px;">
<n-space vertical>
<n-text strong>{{ t('loginMethod') }}</n-text>
<n-text>{{ currentLoginMethod }}</n-text>
<n-divider v-if="isAdminPasswordLogin" />
<n-button v-if="isAdminPasswordLogin" type="warning" @click="showLogoutModal = true" block>
{{ t('logout') }}
</n-button>
</n-space>
</n-card>
</div>
</n-tab-pane>
<n-tab-pane name="about" :tab="t('about')"> <n-tab-pane name="about" :tab="t('about')">
<About /> <About />
</n-tab-pane> </n-tab-pane>
</n-tabs> </n-tabs>
<n-modal v-model:show="showLogoutModal" preset="dialog" :title="t('logoutConfirmTitle')">
<p>{{ t('logoutConfirmContent') }}</p>
<template #action>
<n-button :loading="loading" @click="handleLogout" size="small" tertiary type="warning">
{{ t('confirm') }}
</n-button>
</template>
</n-modal>
</div> </div>
</template> </template>
+2 -1
View File
@@ -150,6 +150,7 @@ onMounted(async () => {
} }
.address-manage { .address-manage {
margin-left: 10px; flex: 0 0 auto;
white-space: nowrap;
} }
</style> </style>