feat: UI: add user page: useIframeShowMail && mailboxSplitSize (#184)

This commit is contained in:
Dream Hunter
2024-05-01 18:09:21 +08:00
committed by GitHub
parent 32ce446a27
commit e81142f5ef
8 changed files with 375 additions and 276 deletions

View File

@@ -33,7 +33,7 @@ const props = defineProps({
},
})
const { themeSwitch } = useGlobalState()
const { themeSwitch, mailboxSplitSize, useIframeShowMail } = useGlobalState()
const autoRefresh = ref(false)
const autoRefreshInterval = ref(30)
const data = ref([])
@@ -146,6 +146,10 @@ const deleteMail = async () => {
}
};
const onSpiltSizeChange = (size) => {
mailboxSplitSize.value = size;
}
onMounted(async () => {
await refresh();
});
@@ -157,7 +161,8 @@ onBeforeUnmount(() => {
<template>
<div>
<n-split class="left" v-if="!isMobile" direction="horizontal" :max="0.75" :min="0.25" :default-size="0.3">
<n-split class="left" v-if="!isMobile" direction="horizontal" :max="0.75" :min="0.25"
:default-size="mailboxSplitSize" :on-update:size="onSpiltSizeChange">
<template #1>
<div class="center">
<div style="display: inline-block; margin-top: 10px; margin-bottom: 10px;">
@@ -230,8 +235,10 @@ onBeforeUnmount(() => {
{{ t('downloadMail') }}
</n-button>
</n-space>
<div v-html="curMail.message" style="margin-top: 10px;"></div>
<!-- <iframe :srcdoc="curMail.message" style="width: 100%; height: 100%;"></iframe> -->
<iframe v-if="useIframeShowMail" :srcdoc="curMail.message"
style="margin-top: 10px;width: 100%; height: 100%;">
</iframe>
<div v-else v-html="curMail.message" style="margin-top: 10px;"></div>
</n-card>
<n-card class="mail-item" v-else>
<n-result status="info" :title="t('pleaseSelectMail')">

View File

@@ -1,9 +1,8 @@
import { createRouter, createWebHistory } from 'vue-router'
import Index from '../views/Index.vue'
import Settings from '../views/Settings.vue'
import User from '../views/User.vue'
import SendMail from '../views/send/SendMail.vue'
import Admin from '../views/Admin.vue'
import SendBox from '../views/send/SendBox.vue'
const router = createRouter({
history: createWebHistory(),
@@ -13,17 +12,13 @@ const router = createRouter({
component: Index
},
{
path: '/settings',
component: Settings
path: '/user',
component: User
},
{
path: '/send',
component: SendMail
},
{
path: '/sendbox',
component: SendBox
},
{
path: '/admin',
component: Admin

View File

@@ -31,6 +31,7 @@ export const useGlobalState = createGlobalState(
}
})
const showAuth = ref(false);
const showPassword = ref(false);
const showAdminAuth = ref(false);
const auth = useStorage('auth', '');
const adminAuth = useStorage('adminAuth', '');
@@ -40,6 +41,8 @@ export const useGlobalState = createGlobalState(
const adminTab = ref("account");
const adminMailTabAddress = ref("");
const adminSendBoxTabAddress = ref("");
const mailboxSplitSize = useStorage('mailboxSplitSize', 0.25);
const useIframeShowMail = useStorage('useIframeShowMail', false);
return {
isDark,
toggleDark,
@@ -47,6 +50,7 @@ export const useGlobalState = createGlobalState(
settings,
openSettings,
showAuth,
showPassword,
auth,
jwt,
localeCache,
@@ -56,6 +60,8 @@ export const useGlobalState = createGlobalState(
adminTab,
adminMailTabAddress,
adminSendBoxTabAddress,
mailboxSplitSize,
useIframeShowMail,
}
},
)

View File

@@ -4,9 +4,13 @@ import { ref, h, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { useIsMobile } from '../utils/composables'
import { DarkModeFilled, LightModeFilled, MenuFilled, AdminPanelSettingsFilled, SendFilled } from '@vicons/material'
import {
DarkModeFilled, LightModeFilled, MenuFilled,
AdminPanelSettingsFilled, SendFilled
} from '@vicons/material'
import { GithubAlt, Language, User, Home, Copy } from '@vicons/fa'
import AdminContact from './admin/AdminContact.vue'
import Login from './Login.vue'
import { useGlobalState } from '../store'
import { api } from '../api'
@@ -14,42 +18,15 @@ const { toClipboard } = useClipboard()
const message = useMessage()
const {
jwt, localeCache, toggleDark, isDark,
jwt, localeCache, toggleDark, isDark, settings,
showAuth, adminAuth, auth, loading
} = useGlobalState()
const { openSettings, settings } = useGlobalState()
const route = useRoute()
const router = useRouter()
const isMobile = useIsMobile()
const isAdminRoute = computed(() => route.path.includes('admin'))
const showMobileMenu = ref(false)
const tabValue = ref('signin')
const showLogout = ref(false)
const showDelteAccount = ref(false)
const password = ref('')
const showPassword = ref(false)
const emailName = ref("")
const emailDomain = ref("")
const login = async () => {
if (!password.value) {
message.error(t('passwordInput'));
return;
}
try {
jwt.value = password.value;
await api.getSettings()
location.reload()
} catch (error) {
message.error(error.message || "error");
}
}
const logout = () => {
jwt.value = '';
location.reload()
}
const authFunc = async () => {
try {
@@ -71,73 +48,35 @@ const { t } = useI18n({
title: 'Cloudflare Temp Email',
dark: 'Dark',
light: 'Light',
login: 'Login',
logout: 'Logout',
logoutConfirm: 'Are you sure to logout?',
delteAccount: "Delete Account",
delteAccountConfirm: "Are you sure to delete your account and all emails for this account?",
accessHeader: 'Access Password',
accessTip: 'Please enter the correct password',
settings: 'Settings',
home: 'Home',
menu: 'Menu',
user: 'User',
sendbox: 'Send Box',
sendMail: 'Send Mail',
pleaseGetNewEmail: 'Please login or click "Get New Email" button to get a new email address',
getNewEmail: 'Get New Email',
getNewEmailTip1: 'Please input the email you want to use. only allow ., a-z, A-Z and 0-9',
getNewEmailTip2: 'Levaing it blank will generate a random email address.',
getNewEmailTip3: 'You can choose a domain from the dropdown list.',
yourAddress: 'Your email address is',
password: 'Password',
passwordTip: 'Please copy the password and you can use it to login to your email account.',
cancel: 'Cancel',
ok: 'OK',
copy: 'Copy',
copied: 'Copied',
showPassword: 'Show Password',
fetchAddressError: 'Login password is invalid or account not exist, it may be network connection issue, please try again later.',
mailV1Alert: 'You have some mails in v1, please click here to login and visit your history mails.',
generateName: 'Generate Fake Name',
help: 'Help',
passwordInput: 'Please input the password',
},
zh: {
title: 'Cloudflare 临时邮件',
dark: '暗色',
light: '亮色',
login: '登录',
logout: '退出登录',
logoutConfirm: '确定要退出登录吗?',
delteAccount: "删除账户",
delteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?",
accessHeader: '访问密码',
accessTip: '请输入站点访问密码',
settings: '设置',
home: '主页',
menu: '菜单',
user: '用户',
sendbox: '发件箱',
sendMail: '发送邮件',
pleaseGetNewEmail: '请"登录"或点击 "获取新邮箱" 按钮来获取一个新的邮箱地址',
getNewEmail: '注册新邮箱',
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许 ., a-z, A-Z, 0-9',
getNewEmailTip2: '留空将会生成一个随机的邮箱地址。',
getNewEmailTip3: '你可以从下拉列表中选择一个域名。',
yourAddress: '你的邮箱地址是',
password: '密码',
passwordTip: '请复制密码,你可以使用它登录你的邮箱。',
cancel: '取消',
ok: '确定',
copy: '复制',
copied: '已复制',
showPassword: '查看密码',
fetchAddressError: '登录密码无效或账号不存在,也可能是网络连接异常,请稍后再尝试。',
mailV1Alert: '你有一些 v1 版本的邮件,请点击此处登录查看。',
generateName: '生成随机名字',
help: '帮助',
passwordInput: '请输入密码',
}
}
});
@@ -185,6 +124,7 @@ const menuOptions = computed(() => [
text: true,
size: "small",
style: "width: 100%",
onClick: () => { router.push("/user"); showMobileMenu.value = false; }
},
{
default: () => t('user'),
@@ -193,75 +133,6 @@ const menuOptions = computed(() => [
),
show: showUserMenu.value,
key: "user",
children: [
{
label: () => h(
NButton,
{
text: true,
size: "small",
style: "width: 100%",
onClick: () => { router.push('/sendbox'); showMobileMenu.value = false; }
},
{ default: () => t('sendbox') }
),
key: "sendbox"
},
{
label: () => h(
NButton,
{
text: true,
size: "small",
style: "width: 100%",
onClick: () => { showPassword.value = true; showMobileMenu.value = false; }
},
{ default: () => t('showPassword') }
),
key: "showPassword"
},
{
label: () => h(
NButton,
{
text: true,
size: "small",
style: "width: 100%",
onClick: () => { router.push('/settings'); showMobileMenu.value = false; }
},
{ default: () => t('settings') }
),
show: openSettings.value.enableAutoReply,
key: "settings"
},
{
label: () => h(
NButton,
{
text: true,
size: "small",
style: "width: 100%",
onClick: () => { showLogout.value = true; showMobileMenu.value = false; }
},
{ default: () => t('logout') }
),
key: "logout"
},
{
label: () => h(
NButton,
{
text: true,
size: "small",
style: "width: 100%",
onClick: () => { showDelteAccount.value = true; showMobileMenu.value = false; }
},
{ default: () => t('delteAccount') }
),
show: openSettings.value.enableUserDeleteEmail,
key: "delte_account"
}
]
},
{
label: () => h(
@@ -331,53 +202,8 @@ const copy = async () => {
}
}
const generateNameLoading = ref(false);
const generateName = async () => {
try {
generateNameLoading.value = true;
const { faker } = await import('https://esm.sh/@faker-js/faker');
emailName.value = faker.person
.fullName()
.replace(/\s+/g, '.')
.replace(/[^a-zA-Z0-9.]/g, '')
.toLowerCase();
} catch (error) {
message.error(error.message || "error");
} finally {
generateNameLoading.value = false;
}
};
const newEmail = async () => {
try {
const res = await api.fetch(
`/api/new_address`
+ `?name=${emailName.value || ''}`
+ `&domain=${emailDomain.value || ''}`
);
jwt.value = res["jwt"];
await api.getSettings();
showPassword.value = true;
} catch (error) {
message.error(error.message || "error");
}
};
const deleteAccount = async () => {
try {
await api.fetch(`/api/delete_address`, {
method: 'DELETE'
});
jwt.value = '';
location.reload()
} catch (error) {
message.error(error.message || "error");
}
};
onMounted(async () => {
await api.getOpenSettings(message);
emailDomain.value = openSettings.value.domains ? openSettings.value.domains[0].value : "";
await api.getSettings();
});
</script>
@@ -439,84 +265,10 @@ onMounted(async () => {
<n-alert v-if="jwt" type="warning" show-icon>
<span>{{ t('fetchAddressError') }}</span>
</n-alert>
<n-tabs v-model:value="tabValue" size="large" justify-content="space-evenly">
<n-tab-pane name="signin" :tab="t('login')">
<n-form>
<n-form-item-row :label="t('password')" required>
<n-input v-model:value="password" type="textarea" :autosize="{ minRows: 3 }" />
</n-form-item-row>
<n-button @click="login" :loading="loading" type="primary" block secondary strong>
{{ t('login') }}
</n-button>
<n-button v-if="openSettings.enableUserCreateEmail" @click="tabValue = 'register'" block
secondary strong>
{{ t('getNewEmail') }}
</n-button>
</n-form>
</n-tab-pane>
<n-tab-pane v-if="openSettings.enableUserCreateEmail" name="register" :tab="t('getNewEmail')">
<n-spin :show="generateNameLoading">
<n-form>
<span>
<p>{{ t("getNewEmailTip1") }}</p>
<p>{{ t("getNewEmailTip2") }}</p>
<p>{{ t("getNewEmailTip3") }}</p>
</span>
<n-button @click="generateName" style="margin-bottom: 10px;">
{{ t('generateName') }}
</n-button>
<n-input-group>
<n-input-group-label v-if="openSettings.prefix">
{{ openSettings.prefix }}
</n-input-group-label>
<n-input v-model:value="emailName" />
<n-input-group-label>@</n-input-group-label>
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
:options="openSettings.domains" />
</n-input-group>
<n-button type="primary" block secondary strong @click="newEmail"
:loading="loading">
{{ t('ok') }}
</n-button>
</n-form>
</n-spin>
</n-tab-pane>
<n-tab-pane name="help" :tab="t('help')">
<n-alert type="info" show-icon>
<span>{{ t('pleaseGetNewEmail') }}</span>
</n-alert>
<AdminContact />
</n-tab-pane>
</n-tabs>
<Login />
</n-card>
</div>
</div>
<n-modal v-model:show="showPassword" preset="dialog" :title="t('password')">
<span>
<p>{{ t("passwordTip") }}</p>
</span>
<n-card>
<b>{{ jwt }}</b>
</n-card>
<template #action>
</template>
</n-modal>
<n-modal v-model:show="showLogout" preset="dialog" :title="t('logout')">
<p>{{ t('logoutConfirm') }}</p>
<template #action>
<n-button :loading="loading" @click="logout" size="small" tertiary type="primary">
{{ t('logout') }}
</n-button>
</template>
</n-modal>
<n-modal v-model:show="showDelteAccount" preset="dialog" :title="t('delteAccount')">
<p>{{ t('delteAccountConfirm') }}</p>
<template #action>
<n-button :loading="loading" @click="deleteAccount" size="small" tertiary type="error">
{{ t('delteAccount') }}
</n-button>
</template>
</n-modal>
<n-modal v-model:show="showAuth" :closable="false" :closeOnEsc="false" :maskClosable="false" preset="dialog"
:title="t('accessHeader')">
<p>{{ t('accessTip') }}</p>
@@ -537,11 +289,6 @@ onMounted(async () => {
justify-content: space-between;
}
.mobile-menu-button {
width: 100%;
text-align: left;
}
.n-alert {
margin-top: 10px;
margin-bottom: 10px;

View File

@@ -0,0 +1,166 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import AdminContact from './admin/AdminContact.vue'
import { useGlobalState } from '../store'
import { api } from '../api'
const message = useMessage()
const {
jwt, localeCache, loading, openSettings, showPassword
} = useGlobalState()
const tabValue = ref('signin')
const password = ref('')
const emailName = ref("")
const emailDomain = ref("")
const login = async () => {
if (!password.value) {
message.error(t('passwordInput'));
return;
}
try {
jwt.value = password.value;
await api.getSettings()
location.reload()
} catch (error) {
message.error(error.message || "error");
}
}
const { t } = useI18n({
locale: localeCache.value || 'zh',
messages: {
en: {
login: 'Login',
pleaseGetNewEmail: 'Please login or click "Get New Email" button to get a new email address',
getNewEmail: 'Get New Email',
getNewEmailTip1: 'Please input the email you want to use. only allow ., a-z, A-Z and 0-9',
getNewEmailTip2: 'Levaing it blank will generate a random email address.',
getNewEmailTip3: 'You can choose a domain from the dropdown list.',
password: 'Password',
ok: 'OK',
generateName: 'Generate Fake Name',
help: 'Help',
passwordInput: 'Please input the password',
},
zh: {
login: '登录',
pleaseGetNewEmail: '请"登录"或点击 "获取新邮箱" 按钮来获取一个新的邮箱地址',
getNewEmail: '注册新邮箱',
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许 ., a-z, A-Z, 0-9',
getNewEmailTip2: '留空将会生成一个随机的邮箱地址。',
getNewEmailTip3: '你可以从下拉列表中选择一个域名。',
password: '密码',
ok: '确定',
generateName: '生成随机名字',
help: '帮助',
passwordInput: '请输入密码',
}
}
});
const generateNameLoading = ref(false);
const generateName = async () => {
try {
generateNameLoading.value = true;
const { faker } = await import('https://esm.sh/@faker-js/faker');
emailName.value = faker.person
.fullName()
.replace(/\s+/g, '.')
.replace(/[^a-zA-Z0-9.]/g, '')
.toLowerCase();
} catch (error) {
message.error(error.message || "error");
} finally {
generateNameLoading.value = false;
}
};
const newEmail = async () => {
try {
const res = await api.fetch(
`/api/new_address`
+ `?name=${emailName.value || ''}`
+ `&domain=${emailDomain.value || ''}`
);
jwt.value = res["jwt"];
await api.getSettings();
showPassword.value = true;
} catch (error) {
message.error(error.message || "error");
}
};
onMounted(async () => {
emailDomain.value = openSettings.value.domains ? openSettings.value.domains[0].value : "";
});
</script>
<template>
<div>
<n-tabs v-model:value="tabValue" size="large" justify-content="space-evenly">
<n-tab-pane name="signin" :tab="t('login')">
<n-form>
<n-form-item-row :label="t('password')" required>
<n-input v-model:value="password" type="textarea" :autosize="{ minRows: 3 }" />
</n-form-item-row>
<n-button @click="login" :loading="loading" type="primary" block secondary strong>
{{ t('login') }}
</n-button>
<n-button v-if="openSettings.enableUserCreateEmail" @click="tabValue = 'register'" block secondary
strong>
{{ t('getNewEmail') }}
</n-button>
</n-form>
</n-tab-pane>
<n-tab-pane v-if="openSettings.enableUserCreateEmail" name="register" :tab="t('getNewEmail')">
<n-spin :show="generateNameLoading">
<n-form>
<span>
<p>{{ t("getNewEmailTip1") }}</p>
<p>{{ t("getNewEmailTip2") }}</p>
<p>{{ t("getNewEmailTip3") }}</p>
</span>
<n-button @click="generateName" style="margin-bottom: 10px;">
{{ t('generateName') }}
</n-button>
<n-input-group>
<n-input-group-label v-if="openSettings.prefix">
{{ openSettings.prefix }}
</n-input-group-label>
<n-input v-model:value="emailName" />
<n-input-group-label>@</n-input-group-label>
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
:options="openSettings.domains" />
</n-input-group>
<n-button type="primary" block secondary strong @click="newEmail" :loading="loading">
{{ t('ok') }}
</n-button>
</n-form>
</n-spin>
</n-tab-pane>
<n-tab-pane name="help" :tab="t('help')">
<n-alert type="info" show-icon>
<span>{{ t('pleaseGetNewEmail') }}</span>
</n-alert>
<AdminContact />
</n-tab-pane>
</n-tabs>
</div>
</template>
<style scoped>
.n-alert {
margin-top: 10px;
margin-bottom: 10px;
text-align: center;
}
.n-form .n-button {
margin-top: 10px;
}
</style>

View File

@@ -0,0 +1,46 @@
<script setup>
import { useI18n } from 'vue-i18n'
import { useStorage } from '@vueuse/core'
import { useGlobalState } from '../store'
import AutoReply from './user/AutoReply.vue';
import SendBox from './send/SendBox.vue';
import Account from './user/Account.vue';
const { localeCache, settings, openSettings } = useGlobalState()
const userTab = useStorage('userTab', 'account')
const { t } = useI18n({
locale: localeCache.value || 'zh',
messages: {
en: {
sendbox: 'Send Box',
auto_reply: 'Auto Reply',
account: 'Account',
},
zh: {
sendbox: '发件箱',
auto_reply: '自动回复',
account: '账户',
}
}
});
</script>
<template>
<div v-if="settings.address">
<n-tabs type="card" v-model:value="userTab">
<n-tab-pane name="account" :tab="t('account')">
<Account />
</n-tab-pane>
<n-tab-pane name="sendbox" :tab="t('sendbox')">
<SendBox />
</n-tab-pane>
<n-tab-pane v-if="openSettings.enableAutoReply" name="auto_reply" :tab="t('auto_reply')">
<AutoReply />
</n-tab-pane>
</n-tabs>
</div>
</template>

View File

@@ -0,0 +1,132 @@
<script setup>
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useGlobalState } from '../../store'
const {
jwt, localeCache, settings, showPassword, mailboxSplitSize, useIframeShowMail
} = useGlobalState()
const router = useRouter()
const showLogout = ref(false)
const showDelteAccount = ref(false)
const { t } = useI18n({
locale: localeCache.value || 'zh',
messages: {
en: {
mailboxSplitSize: 'Mailbox Split Size',
useIframeShowMail: 'Use iframe Show Mail',
logout: "Logout",
delteAccount: "Delete Account",
showPassword: 'Show Password',
password: 'Password',
passwordTip: 'Please copy the password and you can use it to login to your email account.',
logoutConfirm: 'Are you sure to logout?',
delteAccount: "Delete Account",
delteAccountConfirm: "Are you sure to delete your account and all emails for this account?",
},
zh: {
mailboxSplitSize: '邮箱界面分栏大小',
useIframeShowMail: '使用iframe显示邮件',
logout: '退出登录',
delteAccount: "删除账户",
showPassword: '查看密码',
password: '密码',
passwordTip: '请复制密码,你可以使用它登录你的邮箱。',
logoutConfirm: '确定要退出登录吗?',
delteAccount: "删除账户",
delteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?",
}
}
});
const logout = async () => {
jwt.value = '';
await router.push('/')
location.reload()
}
const deleteAccount = async () => {
try {
await api.fetch(`/api/delete_address`, {
method: 'DELETE'
});
jwt.value = '';
await router.push('/')
location.reload()
} catch (error) {
message.error(error.message || "error");
}
};
</script>
<template>
<div class="center" v-if="settings.address">
<n-card>
<n-card>
<n-form-item-row :label="t('mailboxSplitSize')">
<n-slider v-model:value="mailboxSplitSize" :min="0.25" :max="0.75" :step="0.01" :marks="{
0.25: '0.25',
0.5: '0.5',
0.75: '0.75'
}" />
</n-form-item-row>
<n-form-item-row :label="t('useIframeShowMail')">
<n-switch v-model:value="useIframeShowMail" :round="false" />
</n-form-item-row>
</n-card>
<n-button @click="showPassword = true" type="primary" secondary block strong>
{{ t('showPassword') }}
</n-button>
<n-button @click="showLogout = true" secondary block strong>
{{ t('logout') }}
</n-button>
<n-button @click="showDelteAccount = true" type="error" secondary block strong>
{{ t('delteAccount') }}
</n-button>
</n-card>
<n-modal v-model:show="showPassword" preset="dialog" :title="t('password')">
<span>
<p>{{ t("passwordTip") }}</p>
</span>
<n-card>
<b>{{ jwt }}</b>
</n-card>
</n-modal>
<n-modal v-model:show="showLogout" preset="dialog" :title="t('logout')">
<p>{{ t('logoutConfirm') }}</p>
<template #action>
<n-button :loading="loading" @click="logout" size="small" tertiary type="primary">
{{ t('logout') }}
</n-button>
</template>
</n-modal>
<n-modal v-model:show="showDelteAccount" preset="dialog" :title="t('delteAccount')">
<p>{{ t('delteAccountConfirm') }}</p>
<template #action>
<n-button :loading="loading" @click="deleteAccount" size="small" tertiary type="error">
{{ t('delteAccount') }}
</n-button>
</template>
</n-modal>
</div>
</template>
<style scoped>
.center {
display: flex;
justify-content: center;
}
.n-card {
max-width: 800px;
text-align: left;
}
.n-button {
margin-top: 10px;
}
</style>

View File

@@ -2,8 +2,8 @@
import { useI18n } from 'vue-i18n'
import { onMounted, ref } from 'vue'
import { useGlobalState } from '../store'
import { api } from '../api'
import { useGlobalState } from '../../store'
import { api } from '../../api'
const message = useMessage()
const sourcePrefix = ref("")