mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-06 16:07:14 +08:00
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:
co-authored by
Claude Opus 4.5
parent
a771446b9b
commit
8964d4461d
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
- feat: |Admin| 新增管理员账号页面,显示当前登录方式并支持退出登录(仅限密码登录方式)
|
||||||
- feat: |邮件转发| 新增来源地址正则转发功能,支持按发件人地址过滤转发,完全向后兼容
|
- feat: |邮件转发| 新增来源地址正则转发功能,支持按发件人地址过滤转发,完全向后兼容
|
||||||
- feat: |地址来源| 新增地址来源追踪功能,记录地址创建来源(Web 记录 IP,Telegram 记录用户 ID,Admin 后台标记)
|
- feat: |地址来源| 新增地址来源追踪功能,记录地址创建来源(Web 记录 IP,Telegram 记录用户 ID,Admin 后台标记)
|
||||||
- feat: |邮件过滤| 移除后端 keyword 参数,改为前端过滤当前页邮件,优化查询性能
|
- feat: |邮件过滤| 移除后端 keyword 参数,改为前端过滤当前页邮件,优化查询性能
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.address-manage {
|
.address-manage {
|
||||||
margin-left: 10px;
|
flex: 0 0 auto;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user