This commit is contained in:
eoao
2025-04-26 17:24:39 +08:00
commit 24ba4772e6
107 changed files with 13612 additions and 0 deletions

View File

@@ -0,0 +1,409 @@
<template>
<div class="account-box">
<div class="head-opt">
<Icon v-if="settingStore.settings.addEmail === 0" class="icon" icon="ion:add-outline" width="23" height="23" @click="add" />
<Icon class="icon" icon="ion:reload" width="18" height="18" @click="refresh" />
</div>
<el-scrollbar class="scrollbar">
<div v-infinite-scroll="getAccountList" :infinite-scroll-distance="600" :infinite-scroll-immediate="false">
<el-card class="item" :class="itemBg(item.accountId)" v-for="item in accounts" :key="item.accountId" @click="changeAccount(item)">
<div class="account">
{{ item.email }}
</div>
<div class="opt">
<div class="send-email" @click.stop>
<Icon icon="eva:email-fill" width="22" height="22" color="#fbbd08" />
</div>
<div class="settings" @click.stop>
<Icon v-if="item.accountId === userStore.user.accountId" icon="fluent:settings-24-filled" width="20" height="20" color="#909399" />
<el-dropdown v-else >
<Icon icon="fluent:settings-24-filled" width="20" height="20" color="#909399" />
<template #dropdown >
<el-dropdown-menu>
<el-dropdown-item @click="remove(item)">删除</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</el-card>
<!-- Initial Loading Skeleton -->
<template v-if="loading">
<el-skeleton v-for="i in 3" :key="i" animated>
<template #template>
<el-card class="item">
<el-skeleton-item variant="p" style="width: 70%; height: 20px; margin-bottom: 20px" />
<div style="display: flex; justify-content: space-between">
<el-skeleton-item variant="text" style="width: 20px" />
<el-skeleton-item variant="text" style="width: 20px" />
</div>
</el-card>
</template>
</el-skeleton>
</template>
<!-- Follow Loading Skeleton -->
<template v-if="accounts.length > 0 && !noLoading">
<el-skeleton animated>
<template #template>
<el-card class="item">
<el-skeleton-item variant="p" style="width: 70%; height: 20px; margin-bottom: 20px" />
<div style="display: flex; justify-content: space-between">
<el-skeleton-item variant="text" style="width: 20px" />
<el-skeleton-item variant="text" style="width: 20px" />
</div>
</el-card>
</template>
</el-skeleton>
</template>
<div class="noLoading" v-if="noLoading && accounts.length > 0">
<div>没有更多数据了</div>
</div>
<div class="empty" v-if="noLoading && accounts.length === 0">
<el-empty description="没有任何邮件" />
</div>
</div>
</el-scrollbar>
<el-dialog v-model="showAdd" title="添加邮箱" >
<form>
<div class="container">
<el-input v-model="addForm.email" type="text" placeholder="邮箱" autocomplete="off">
<template #append>
<div @click.stop="openSelect">
<el-select
ref="mySelect"
v-model="addForm.suffix"
placeholder="请选择"
class="select"
>
<el-option
v-for="item in domainList"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<div style="color: #333">
<span >{{addForm.suffix}}</span>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20" />
</div>
</div>
</template>
</el-input>
<el-button class="btn" type="primary" @click="submit" :loading="addLoading"
>添加
</el-button>
</div>
</form>
<div
class="add-email-turnstile"
:class="verifyShow ? 'turnstile-show' : 'turnstile-hide'"
:data-sitekey="settingStore.settings.siteKey"
data-callback="onTurnstileSuccess"
></div>
</el-dialog>
</div>
</template>
<script setup>
import {Icon} from "@iconify/vue";
import {nextTick, reactive, ref} from "vue";
import {accountList, accountAdd, accountDelete} from "@/request/account.js";
import {ElMessage, ElMessageBox} from "element-plus";
import {isEmail} from "@/utils/verify-utils.js";
import {useSettingStore} from "@/store/setting.js";
import {useAccountStore} from "@/store/account.js";
import {useUserStore} from "@/store/user.js";
const userStore = useUserStore();
const accountStore = useAccountStore();
const settingStore = useSettingStore();
const showAdd = ref(false)
const addLoading = ref(false);
const domainList = settingStore.domainList
const accounts = reactive([])
const noLoading = ref(false)
const loading = ref(false)
const followLoading = ref(false);
const verifyShow = ref(false)
let turnstileId = false
let verifyToken = ''
const addForm = reactive({
email: '',
suffix: settingStore.domainList[0]
})
const queryParams = {
accountId: 0,
size: 12
}
const mySelect = ref()
getAccountList()
const openSelect = () => {
mySelect.value.toggleMenu()
}
window.onTurnstileSuccess = (token) => {
verifyToken = token;
setTimeout(() => {
verifyShow.value = false
},1500)
};
function itemBg(accountId) {
return accountStore.currentAccountId === accountId ? 'item-choose' : ''
}
function remove(account) {
ElMessageBox.confirm(`确认删除${account.email}吗?`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
accountDelete(account.accountId).then(() => {
const index = accounts.findIndex(item => item.accountId === account.accountId);
accounts.splice(index, 1);
if (accounts.length < queryParams.size) {
getAccountList()
}
ElMessage({
message: '删除成功',
type: 'success',
plain: true,
})
})
});
}
function refresh() {
if (loading.value) {
return
}
loading.value = false
followLoading.value = false
noLoading.value = false
queryParams.accountId = 0
accounts.splice(0, accounts.length)
getAccountList()
}
function changeAccount(account) {
accountStore.currentAccountId = account.accountId
}
function add() {
showAdd.value = true
}
function getAccountList() {
if (loading.value || followLoading.value || noLoading.value) return;
if (accounts.length === 0) {
loading.value = true
}else {
followLoading.value = true
}
accountList(queryParams.accountId,queryParams.size).then(list => {
if (list.length < queryParams.size) {
noLoading.value = true
}
if (accounts.length === 0) {
accountStore.currentAccount = list[0].accountId
}
queryParams.accountId = list.at(-1).accountId
accounts.push(...list)
loading.value = false
followLoading.value = false
}).catch(() => {
loading.value = false
followLoading.value = false
})
}
function submit() {
if (!addForm.email){
ElMessage({
message: "邮箱不能为空",
type: "error",
plain: true
})
return
}
if (!isEmail(addForm.email+addForm.suffix)) {
ElMessage({
message: "非法邮箱",
type: "error",
plain: true
})
return
}
if (!verifyToken && settingStore.settings.addEmailVerify === 0) {
verifyShow.value = true
if (!turnstileId) {
nextTick(() => {
turnstileId = window.turnstile.render('.add-email-turnstile')
})
} else {
window.turnstile.reset(turnstileId)
}
return;
}
addLoading.value = true
accountAdd(addForm.email+addForm.suffix,verifyToken).then(account => {
addLoading.value = false
showAdd.value = false
addForm.email = ''
accounts.push(account)
verifyToken = ''
ElMessage({
message: "添加成功",
type: "success",
plain: true
})
}).catch(res => {
if (res.code === 400) {
verifyToken = ''
window.turnstile.reset(turnstileId)
verifyShow.value = true
}
addLoading.value = false
})
}
</script>
<style scoped lang="scss">
.account-box {
border-right: 1px solid var(--el-border-color) !important;
background-color: #FFF;
height: 100%;
overflow: hidden;
.head-opt {
display: flex;
align-items: center;
height: 38px;
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
.icon{
cursor: pointer;
margin-left: 10px;
}
.icon:nth-child(2) {
margin-left: 20px;
}
}
.scrollbar {
width: 100%;
height: calc(100% - 38px);
overflow: auto;
@media (max-width: 767px) {
height: calc(100% - 98px);
}
.empty {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.noLoading {
display: flex;
justify-content: center;
align-items: center;
padding: 10px 0;
color: gray;
}
}
.btn {
width: 100%;
margin-top: 15px;
}
.item {
background-color: #fff;
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
margin-left: 10px;
margin-right: 10px;
cursor: pointer;
.account {
font-weight: 600;
margin-bottom: 20px;
color: #333;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.opt {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #888;
}
:deep(.el-card__body) {
padding: 0;
}
}
.item:first-child{
margin-top: 10px ;
}
.item-choose {
background: var(--el-color-primary-light-8);
}
}
.setting-icon {
position: relative;
top: 6px;
}
:deep(.el-input-group__append) {
padding: 0 !important;
padding-left: 8px !important;
background: #FFFFFF;
}
:deep(.el-dialog) {
width: 400px !important;
@media (max-width: 440px) {
width: calc(100% - 40px) !important;
margin-right: 20px !important;
margin-left: 20px !important;
}
}
.select {
position: absolute;
right: 30px;
width: 100px;
opacity: 0;
pointer-events: none;
}
.add-email-turnstile {
margin-top: 15px;
}
.turnstile-show {
opacity: 1;
}
.turnstile-hide {
opacity: 0;
pointer-events: none;
position: fixed;
}
</style>

View File

@@ -0,0 +1,132 @@
<template>
<el-scrollbar class="scroll">
<div>
<div class="title" >
<Icon icon="mdi:email-outline" width="24" height="24" />
<div>{{settingStore.settings.title}}</div>
</div>
<el-menu :collapse="false" text-color="#fff" active-text-color="#fff" style="margin-top: 10px">
<el-menu-item @click="router.push('/email')" index="email"
:class="route.meta.name === 'email' ? 'choose-item' : ''">
<Icon icon="hugeicons:mailbox-01" width="20" height="20" />
<span class="menu-name" style="margin-left: 21px">收件箱</span>
</el-menu-item>
<el-menu-item @click="router.push('/star')" index="star"
:class="route.meta.name === 'star' ? 'choose-item' : ''">
<Icon icon="solar:star-line-duotone" width="20" height="20" />
<span class="menu-name" style="margin-left: 20px">星标邮件</span>
</el-menu-item>
<el-menu-item @click="router.push('/setting')" index="setting"
:class="route.meta.name === 'setting' ? 'choose-item' : ''">
<Icon icon="fluent:settings-48-regular" width="20" height="20" />
<span class="menu-name" style="margin-left: 20px">个人设置</span>
</el-menu-item>
<el-menu-item @click="router.push('/sys-setting')" index="sys-setting" v-if="userStore.user.type === 0"
:class="route.meta.name === 'sys-setting' ? 'choose-item' : ''">
<Icon icon="eos-icons:system-ok-outlined" width="18" height="18" />
<span class="menu-name" style="margin-left: 23px">系统设置</span>
</el-menu-item>
</el-menu>
</div>
</el-scrollbar>
</template>
<script setup>
import {useUserStore} from "@/store/user.js";
import router from "@/router/index.js";
import { useRoute } from "vue-router";
import {Icon} from "@iconify/vue";
import {useSettingStore} from "@/store/setting.js";
const settingStore = useSettingStore();
const userStore = useUserStore();
const route = useRoute();
</script>
<style lang="scss" scoped>
.title {
margin: 15px 10px;
height: 50px;
border-radius: 8px;
display: flex;
position: relative;
font-size: 16px;
font-weight: bold;
align-items: center;
justify-content: center;
gap: 5px;
color: #ffffff;
background: linear-gradient(135deg, #1890ff, #1c6dd0);
box-shadow: 0 0 12px rgba(24, 144, 255, 0.6), 0 0 20px rgba(24, 144, 255, 0.4);
transition: all 0.3s ease;
:deep(.el-icon) {
font-size: 20px;
}
.user-right-icon {
align-self: center;
position: absolute;
font-size: 12px;
right: 8px;
color: #ffffff;
}
}
.el-menu-item {
margin: 5px 10px !important;
border-radius: 8px;
height: 36px;
padding: 10px !important;
}
.choose-item {
font-weight: bold;
background: rgba(255, 255, 255, 0.08) !important;
backdrop-filter: blur(4px);
}
@media (hover: hover) {
.el-menu-item:hover {
background: rgba(255, 255, 255, 0.08) !important;
}
}
.menu-name {
user-select: none;
}
:deep(.el-scrollbar__wrap--hidden-default ) {
background: #001529 !important;
}
:deep(.el-menu-item) {
background: #001529;
}
:deep(.el-menu) {
background: #001529;
}
.el-menu {
border-right: 0;
width: 250px;
@media (max-width: 1199px) {
width: 200px;
}
}
:deep(.el-divider__text) {
background: #001529;
color: #FFFFFF;
}
.scroll {
box-shadow: 6px 0 20px rgba(0, 21, 41, 0.35);
}
</style>

View File

@@ -0,0 +1,131 @@
<template>
<div class="header">
<div class="btn">
<hanburger @click="changeAside"></hanburger>
<span class="breadcrumb-item">{{ route.meta.title }}</span>
</div>
<div class="toolbar">
<div class="email">{{ userStore.user.email }}</div>
<el-dropdown>
<div class="avatar">
<div class="avatar-text">
<div>{{ formatName(userStore.user.email) }}</div>
</div>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="24" height="24" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="router.push('/setting')">
个人设置
</el-dropdown-item>
<el-dropdown-item @click="clickLogout">
退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup>
import router from "@/router";
import hanburger from '@/components/hamburger/index.vue'
import { logout} from "@/request/login.js";
import {Icon} from "@iconify/vue";
import {useUiStore} from "@/store/ui.js";
import {useUserStore} from "@/store/user.js";
import { useRoute } from "vue-router";
const route = useRoute();
const userStore = useUserStore();
const uiStore = useUiStore();
function changeAside() {
uiStore.asideShow = !uiStore.asideShow
}
function clickLogout() {
logout().then(() => {
localStorage.removeItem("token")
router.push('/login')
})
}
function formatName(email) {
return email[0]?.toUpperCase() || ''
}
</script>
<style lang="scss" scoped>
.breadcrumb-item {
font-weight: bold;
font-size: 16px;
white-space: nowrap;
}
.setting-icon {
margin-right: 10px;
position: relative;
bottom: 10px;
}
.header {
text-align: right;
font-size: 12px;
display: grid;
height: 100%;
gap: 10px;
grid-template-columns: auto 1fr;
}
.btn {
display: inline-flex;
align-items: center;
height: 100%;
}
.toolbar {
display: grid;
grid-template-columns: 1fr auto;
margin-left: auto;
gap: 10px;
.email {
align-self: center;
font-size: 14px;
margin-right: 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: bold;
width: 100%;
}
.avatar {
display: flex;
align-items: center;
.avatar-text {
height: 30px;
width: 30px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
border: 1px solid #ccc;
}
.setting-icon {
position: relative;
top: 0;
}
}
}
.el-tooltip__trigger:first-child:focus-visible {
outline: unset;
}
</style>

View File

@@ -0,0 +1,121 @@
<template>
<el-container class="layout">
<el-aside
class="aside"
:class="uiStore.asideShow ? 'aside-show' : 'el-aside-hide'">
<Aside />
</el-aside>
<div
:class="(uiStore.asideShow && isMobile)? 'overlay-show':'overlay-hide'"
@click="uiStore.asideShow = false"
></div>
<el-container class="main-container">
<el-main>
<el-header>
<Header />
</el-header>
<Main />
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import Aside from '@/layout/aside/index.vue'
import Header from '@/layout/header/index.vue'
import Main from '@/layout/main/index.vue'
import { ref, onMounted, onBeforeUnmount } from 'vue'
import {useUiStore} from "@/store/ui.js";
const uiStore = useUiStore();
const isMobile = ref(window.innerWidth < 768)
const handleResize = () => {
isMobile.value = window.innerWidth < 768
uiStore.asideShow = window.innerWidth >= 992;
}
onMounted(() => {
window.addEventListener('resize', handleResize)
handleResize()
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
})
</script>
<style lang="scss" scoped>
.el-aside-hide {
position: fixed;
left: 0;
height: 100%;
z-index: 100;
transform: translateX(-100%);
transition: all 100ms ease;
}
.aside-show {
transform: translateX(0);
transition: all 100ms ease;
z-index: 101;
@media (max-width: 767px) {
position: fixed;
top: 0;
left: 0;
z-index: 101;
height: 100%;
background: #fff;
}
}
.el-aside {
width: auto;
transition: all 100ms ease;
}
.layout {
height: 100%;
position: fixed;
width: 100%;
top: 0;
left: 0;
overflow: hidden;
}
.main-container {
min-height: 100%;
background: #FFFFFF;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.el-main {
padding: 0;
}
.el-header {
background: #FFFFFF;
border-bottom: solid 1px var(--el-menu-border-color);
padding: 0 0 0 0;
}
.overlay-show {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.4);
z-index: 99;
transition: all 0.3s;
}
.overlay-hide {
display: flex;
pointer-events: none;
opacity: 0;
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<div :class=" accountShow ? 'main-box-show' : 'main-box-hide'">
<div :class="accountShow ? 'block-show' : 'block-hide'" @click="uiStore.accountShow = false"></div>
<account :class="accountShow ? 'show' : 'hide'" />
<router-view class="main-view" v-slot="{ Component,route }">
<keep-alive :include="['email','sys-setting','star']">
<component :is="Component" :key="route.name"/>
</keep-alive>
</router-view>
</div>
</template>
<script setup>
import account from '@/layout/account/index.vue'
import {useUiStore} from "@/store/ui.js";
import {useSettingStore} from "@/store/setting.js";
import {computed, onBeforeUnmount, onMounted} from "vue";
import { useRoute } from 'vue-router'
const props = defineProps({
openSend: Function
})
const settingStore = useSettingStore()
const uiStore = useUiStore();
const route = useRoute()
let innerWidth = window.innerWidth
const accountShow = computed(() => {
return uiStore.accountShow && settingStore.settings.manyEmail === 0
})
onMounted(() => {
window.addEventListener('resize', handleResize)
handleResize()
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
})
const handleResize = () => {
if (['content','email'].includes(route.meta.name)) {
if (innerWidth !== window.innerWidth) {
innerWidth = window.innerWidth;
uiStore.accountShow = window.innerWidth >= 768;
}
}
}
</script>
<style lang="scss" scoped>
.block-show {
position: fixed;
@media (max-width: 767px) {
position: absolute;
right: 0;
border: 0;
height: 100%;
width: 100%;
background: #000000;
opacity: 0.6;
z-index: 10;
transition: all 300ms;
}
}
.block-hide {
position: fixed;
pointer-events: none;
transition: all 300ms;
}
.show {
transition: all 100ms;
@media (max-width: 767px) {
position: fixed;
z-index: 100;
width: 199px;
}
}
.hide {
transition: all 100ms;
position: fixed;
transform: translateX(-100%);
opacity: 0;
@media (max-width: 767px) {
width: 199px;
z-index: 100;
}
}
.main-box-show {
display: grid;
grid-template-columns: 250px 1fr;
@media (max-width: 767px) {
grid-template-columns: 1fr;
}
height: calc(100% - 60px);
}
.main-box-hide {
display: grid;
grid-template-columns: 1fr;
height: calc(100% - 60px);
}
.main-view {
background: #FFFFFF;
}
.navigation {
height: 30px;
border-bottom: solid 1px var(--el-menu-border-color);
display: inline-flex;
justify-items: center;
align-items: center;
width: 100%;
.tag {
background: #FFFFFF;
margin-left: 5px;
}
}
</style>