保存
This commit is contained in:
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
mail-vue/node_modules
|
||||||
|
mail-vue/dist
|
||||||
|
.wrangler
|
||||||
|
.venv
|
||||||
3
mail-vue/.env.dev
Normal file
3
mail-vue/.env.dev
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
NODE_ENV = 'dev'
|
||||||
|
VITE_APP_TITLE = '开发环境'
|
||||||
|
VITE_BASE_URL = 'http://127.0.0.1:8787/api'
|
||||||
3
mail-vue/.env.release
Normal file
3
mail-vue/.env.release
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
NODE_ENV = 'release'
|
||||||
|
VITE_APP_TITLE = '发布环境'
|
||||||
|
VITE_BASE_URL = '/api'
|
||||||
3
mail-vue/.env.remote
Normal file
3
mail-vue/.env.remote
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
NODE_ENV = 'remote'
|
||||||
|
VITE_APP_TITLE = '远程环境'
|
||||||
|
VITE_BASE_URL = 'xxxxxx'
|
||||||
14
mail-vue/index.html
Normal file
14
mail-vue/index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title></title>
|
||||||
|
<link rel="icon" href="./src/assets/favicon.svg" type="image/svg+xml">
|
||||||
|
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4107
mail-vue/package-lock.json
generated
Normal file
4107
mail-vue/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
mail-vue/package.json
Normal file
35
mail-vue/package.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "video-admin-vue",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --mode dev",
|
||||||
|
"remote": "vite --mode remote",
|
||||||
|
"build": "vite build --mode release",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@iconify/vue": "^4.3.0",
|
||||||
|
"@vueuse/core": "^12.0.0",
|
||||||
|
"@wangeditor/editor": "^5.1.23",
|
||||||
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
|
"axios": "^1.7.8",
|
||||||
|
"date-time-format-timezone": "^1.0.22",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
|
"element-plus": "^2.9.5",
|
||||||
|
"path": "^0.12.7",
|
||||||
|
"pinia": "^3.0.2",
|
||||||
|
"pinia-plugin-persistedstate": "^4.2.0",
|
||||||
|
"postal-mime": "^2.4.3",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-router": "^4.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"less": "^4.2.2",
|
||||||
|
"sass": "^1.82.0",
|
||||||
|
"terser": "^5.39.0",
|
||||||
|
"vite": "6.2.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
mail-vue/public/vite.svg
Normal file
1
mail-vue/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
31
mail-vue/src/App.vue
Normal file
31
mail-vue/src/App.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<router-view/>
|
||||||
|
<div class="loading" v-if="uiStore.init">
|
||||||
|
<loading/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import loading from "@/components/loading/index.vue";
|
||||||
|
import {useUiStore} from "@/store/ui.js";
|
||||||
|
import {useSettingStore} from "@/store/setting.js";
|
||||||
|
import {watch} from "vue";
|
||||||
|
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const uiStore = useUiStore();
|
||||||
|
|
||||||
|
watch(() => settingStore.settings.title,
|
||||||
|
(title) => document.title = title,
|
||||||
|
{immediate: true}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
mail-vue/src/assets/favicon.svg
Normal file
1
mail-vue/src/assets/favicon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><g fill="none"><path fill="#367af2" d="M2 10v12.5A4.5 4.5 0 0 0 6.5 27h19a4.5 4.5 0 0 0 4.5-4.5V10l-13.526 7.292a1 1 0 0 1-.948 0z"/><path fill="url(#fluentColorMail320)" d="M2 10v12.5A4.5 4.5 0 0 0 6.5 27h19a4.5 4.5 0 0 0 4.5-4.5V10l-13.526 7.292a1 1 0 0 1-.948 0z"/><path fill="url(#fluentColorMail321)" d="M2 10v12.5A4.5 4.5 0 0 0 6.5 27h19a4.5 4.5 0 0 0 4.5-4.5V10l-13.526 7.292a1 1 0 0 1-.948 0z"/><path fill="url(#fluentColorMail322)" fill-opacity="0.75" d="M2 10v12.5A4.5 4.5 0 0 0 6.5 27h19a4.5 4.5 0 0 0 4.5-4.5V10l-13.526 7.292a1 1 0 0 1-.948 0z"/><path fill="url(#fluentColorMail323)" fill-opacity="0.7" d="M2 10v12.5A4.5 4.5 0 0 0 6.5 27h19a4.5 4.5 0 0 0 4.5-4.5V10l-13.526 7.292a1 1 0 0 1-.948 0z"/><path fill="url(#fluentColorMail324)" d="M6.5 5A4.5 4.5 0 0 0 2 9.5v1.09l13.526 7.292a1 1 0 0 0 .948 0L30 10.59V9.5A4.5 4.5 0 0 0 25.5 5z"/><defs><linearGradient id="fluentColorMail320" x1="19.555" x2="26.862" y1="13.332" y2="27.873" gradientUnits="userSpaceOnUse"><stop offset=".199" stop-color="#0094f0" stop-opacity="0"/><stop offset=".431" stop-color="#0094f0"/></linearGradient><linearGradient id="fluentColorMail321" x1="12" x2="4.914" y1="11.79" y2="28.328" gradientUnits="userSpaceOnUse"><stop offset=".191" stop-color="#0094f0" stop-opacity="0"/><stop offset=".431" stop-color="#0094f0"/></linearGradient><linearGradient id="fluentColorMail322" x1="23.383" x2="24.532" y1="20.142" y2="28.575" gradientUnits="userSpaceOnUse"><stop stop-color="#2764e7" stop-opacity="0"/><stop offset="1" stop-color="#2764e7"/></linearGradient><linearGradient id="fluentColorMail323" x1="20.333" x2="22.43" y1="12.088" y2="29.25" gradientUnits="userSpaceOnUse"><stop offset=".533" stop-color="#ff6ce8" stop-opacity="0"/><stop offset="1" stop-color="#ff6ce8"/></linearGradient><linearGradient id="fluentColorMail324" x1="10.318" x2="18.903" y1=".976" y2="23.436" gradientUnits="userSpaceOnUse"><stop stop-color="#6ce0ff"/><stop offset=".462" stop-color="#29c3ff"/><stop offset="1" stop-color="#4894fe"/></linearGradient></defs></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
BIN
mail-vue/src/assets/fonts/HarmonyOS_Sans_SC_Regular.woff2
Normal file
BIN
mail-vue/src/assets/fonts/HarmonyOS_Sans_SC_Regular.woff2
Normal file
Binary file not shown.
80
mail-vue/src/axios/index.js
Normal file
80
mail-vue/src/axios/index.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import router from "@/router";
|
||||||
|
import {ElMessage} from 'element-plus';
|
||||||
|
|
||||||
|
let http = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_BASE_URL
|
||||||
|
});
|
||||||
|
|
||||||
|
http.interceptors.request.use(config => {
|
||||||
|
config.headers.Authorization = `${localStorage.getItem('token')}`
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
|
http.interceptors.response.use((res) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const data = res.data
|
||||||
|
if (data.code === 401) {
|
||||||
|
ElMessage({
|
||||||
|
message: data.message,
|
||||||
|
type: 'error',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
router.push('/login')
|
||||||
|
reject(data)
|
||||||
|
} else if (data.code === 403) {
|
||||||
|
ElMessage({
|
||||||
|
message: data.message,
|
||||||
|
type: 'error',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
reject(data)
|
||||||
|
} else if (data.code !== 200) {
|
||||||
|
ElMessage({
|
||||||
|
message: data.message,
|
||||||
|
type: 'error',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
reject(data)
|
||||||
|
}, 1)
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(data.data)
|
||||||
|
}, 1)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (error.message.includes('Network Error')) {
|
||||||
|
ElMessage({
|
||||||
|
message: '网络错误,请检查网络连接',
|
||||||
|
type: 'error',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
} else if (error.code === 'ECONNABORTED') {
|
||||||
|
ElMessage({
|
||||||
|
message: '请求超时,请稍后重试',
|
||||||
|
type: 'error',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
ElMessage.error('')
|
||||||
|
} else if (error.response) {
|
||||||
|
ElMessage({
|
||||||
|
message: `服务器繁忙`,
|
||||||
|
type: 'error',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
ElMessage({
|
||||||
|
message: '请求失败,请稍后再试',
|
||||||
|
type: 'error',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default http
|
||||||
|
|
||||||
|
|
||||||
494
mail-vue/src/components/email-scroll/index.vue
Normal file
494
mail-vue/src/components/email-scroll/index.vue
Normal file
@@ -0,0 +1,494 @@
|
|||||||
|
<template>
|
||||||
|
<div class="email-container">
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-checkbox
|
||||||
|
v-model="checkAll"
|
||||||
|
:indeterminate="isIndeterminate"
|
||||||
|
:disabled="!emailList.length"
|
||||||
|
@change="handleCheckAllChange"
|
||||||
|
>
|
||||||
|
</el-checkbox>
|
||||||
|
<Icon class="icon" icon="ion:reload" width="18" height="18" @click="refreshList" />
|
||||||
|
<Icon class="icon" icon="uiw:delete" width="16" height="16" v-if="getSelectedMailsIds().length > 0" @click="handleDelete" />
|
||||||
|
<Icon class="more-icon icon" width="16" height="16" icon="akar-icons:dot-grid-fill" @click="changeAccountShow" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div ref="scroll" class="scroll">
|
||||||
|
<el-scrollbar>
|
||||||
|
<div style="height: 100%;" :infinite-scroll-immediate="false" v-infinite-scroll="loadData" infinite-scroll-distance="600">
|
||||||
|
<div v-for="item in emailList" :key="item.emailId">
|
||||||
|
<div class="email-row"
|
||||||
|
:data-checked="item.checked"
|
||||||
|
@click="jumpDetails(item)"
|
||||||
|
>
|
||||||
|
<el-checkbox v-model="item.checked" @click.stop></el-checkbox>
|
||||||
|
<div @click.stop="starChange(item)" class="pc-star">
|
||||||
|
<Icon v-if="item.isStar" icon="fluent-color:star-16" width="20" height="20" />
|
||||||
|
<Icon v-else icon="solar:star-line-duotone" width="18" height="18" />
|
||||||
|
</div>
|
||||||
|
<div class="title" :class="accountShow ? 'title-column' : ''">
|
||||||
|
<div class="email-sender">{{ item.name }}</div>
|
||||||
|
<div class="email-subject">{{ item.subject}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="email-right">
|
||||||
|
<span class="email-time">{{ fromNow(item.createTime) }}</span>
|
||||||
|
<div class="phone-star" :class="item.isStar ? 'star-pd' : ''" @click.stop="starChange(item)">
|
||||||
|
<Icon v-if="item.isStar" icon="fluent-color:star-16" width="20" height="20" />
|
||||||
|
<Icon v-else icon="solar:star-line-duotone" width="18" height="18" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="loading" v-if="loading">
|
||||||
|
<Loading />
|
||||||
|
</div>
|
||||||
|
<div class="follow-loading" v-if="emailList.length > 0 && !noLoading">
|
||||||
|
<Loading />
|
||||||
|
</div>
|
||||||
|
<div class="noLoading" v-if="noLoading && emailList.length > 0">
|
||||||
|
<div>没有更多数据了</div>
|
||||||
|
</div>
|
||||||
|
<div class="empty" v-if="noLoading && emailList.length === 0">
|
||||||
|
<el-empty description="没有任何邮件" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Loading from "@/components/loading/index.vue";
|
||||||
|
import {Icon} from "@iconify/vue";
|
||||||
|
import {computed, onActivated, reactive, ref, watch} from "vue";
|
||||||
|
import router from "@/router/index.js";
|
||||||
|
import {onBeforeRouteLeave} from "vue-router";
|
||||||
|
import {ElMessage, ElMessageBox} from "element-plus";
|
||||||
|
import {useEmailStore} from "@/store/email.js";
|
||||||
|
import {useUiStore} from "@/store/ui.js";
|
||||||
|
import {useSettingStore} from "@/store/setting.js";
|
||||||
|
import {fromNow} from "@/day/day.js";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
getEmailList: Function,
|
||||||
|
emailDelete: Function,
|
||||||
|
starAdd: Function,
|
||||||
|
starCancel: Function,
|
||||||
|
cancelSuccess: Function,
|
||||||
|
starSuccess: Function,
|
||||||
|
allowStar: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const settingStore = useSettingStore()
|
||||||
|
const uiStore = useUiStore();
|
||||||
|
const emailStore = useEmailStore();
|
||||||
|
const loading = ref(false);
|
||||||
|
const followLoading = ref(false);
|
||||||
|
const noLoading = ref(false);
|
||||||
|
const emailList = reactive([])
|
||||||
|
const mailTotal = ref(0);
|
||||||
|
const checkAll = ref(false);
|
||||||
|
const isIndeterminate = ref(false);
|
||||||
|
const scroll = ref(null)
|
||||||
|
const firstLoad = ref(true)
|
||||||
|
let scrollTop = 0
|
||||||
|
const queryParam = reactive({
|
||||||
|
emailId: 0,
|
||||||
|
size: 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
refreshList,
|
||||||
|
deleteEmail,
|
||||||
|
addItemStar,
|
||||||
|
editEmailStar,
|
||||||
|
emailList,
|
||||||
|
firstLoad
|
||||||
|
})
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
scroll.value.scrollTop = scrollTop
|
||||||
|
})
|
||||||
|
|
||||||
|
getEmailList()
|
||||||
|
|
||||||
|
onBeforeRouteLeave(() => {
|
||||||
|
scrollTop = scroll.value.scrollTop
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => emailList.map(item => item.checked),
|
||||||
|
() => {
|
||||||
|
if (emailList.length > 0) {
|
||||||
|
updateCheckStatus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
watch(() => emailStore.deleteIds, () => {
|
||||||
|
if (emailStore.deleteIds) {
|
||||||
|
deleteEmail(emailStore.deleteIds)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const accountShow = computed(() => {
|
||||||
|
return uiStore.accountShow && settingStore.settings.manyEmail === 0
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function starChange(email) {
|
||||||
|
|
||||||
|
if (!email.isStar) {
|
||||||
|
|
||||||
|
if (!props.allowStar) return;
|
||||||
|
|
||||||
|
email.isStar = 1;
|
||||||
|
props.starAdd(email.emailId).then(() => {
|
||||||
|
email.isStar = 1;
|
||||||
|
props.starSuccess(email)
|
||||||
|
}).catch(() => {
|
||||||
|
email.isStar = 0
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
|
||||||
|
email.isStar = 0;
|
||||||
|
props.starCancel(email.emailId).then(() => {
|
||||||
|
email.isStar = 0;
|
||||||
|
props.cancelSuccess?.(email)
|
||||||
|
}).catch(() => {
|
||||||
|
email.isStar = 1;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeAccountShow() {
|
||||||
|
uiStore.accountShow = !uiStore.accountShow;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
ElMessageBox.confirm('确认批量删除这些邮件吗?', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
const emailIds = getSelectedMailsIds();
|
||||||
|
props.emailDelete(emailIds).then(() => {
|
||||||
|
ElMessage({
|
||||||
|
message: '删除成功',
|
||||||
|
type: 'success',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
emailStore.deleteIds = emailIds;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteEmail(emailIds) {
|
||||||
|
emailIds.forEach(emailId => {
|
||||||
|
emailList.forEach((item,index) => {
|
||||||
|
if (emailId === item.emailId) {
|
||||||
|
emailList.splice(index,1);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (emailList.length < queryParam.size && !noLoading.value) {
|
||||||
|
getEemailList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addItemStar(email) {
|
||||||
|
|
||||||
|
const existIndex = emailList.findIndex(item => item.emailId === email.emailId)
|
||||||
|
|
||||||
|
if (existIndex > -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = emailList.findIndex(item => item.emailId < email.emailId)
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
emailList.splice(index, 0, email);
|
||||||
|
} else {
|
||||||
|
if (noLoading.value) {
|
||||||
|
emailList.push(email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function editEmailStar(emailId,isStar) {
|
||||||
|
const index = emailList.findIndex(item => item.emailId === emailId)
|
||||||
|
if (index !== -1) {
|
||||||
|
emailList[index].isStar = isStar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCheckAllChange(val) {
|
||||||
|
emailList.forEach(item => item.checked = val);
|
||||||
|
isIndeterminate.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取选中的邮件列表id
|
||||||
|
function getSelectedMailsIds() {
|
||||||
|
return emailList.filter(item => item.checked).map(item => item.emailId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function updateCheckStatus() {
|
||||||
|
const checkedCount = emailList.filter(item => item.checked).length;
|
||||||
|
checkAll.value = checkedCount === emailList.length;
|
||||||
|
isIndeterminate.value = checkedCount > 0 && checkedCount < emailList.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jumpDetails(email) {
|
||||||
|
emailStore.readEmail = email
|
||||||
|
router.push("/content");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmailList() {
|
||||||
|
|
||||||
|
if (loading.value || followLoading.value || noLoading.value) return;
|
||||||
|
|
||||||
|
if (emailList.length === 0) {
|
||||||
|
loading.value = true
|
||||||
|
}else {
|
||||||
|
followLoading.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
props.getEmailList(queryParam.emailId, queryParam.size).then(data => {
|
||||||
|
|
||||||
|
if (emailList.length === 0) {
|
||||||
|
firstLoad.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let list = data.list.map(item => ({
|
||||||
|
...item,
|
||||||
|
checked: false
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (data.list.length < queryParam.size) {
|
||||||
|
noLoading.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
emailList.push(...list);
|
||||||
|
mailTotal.value = data.total;
|
||||||
|
queryParam.emailId = data.list.at(-1).emailId;
|
||||||
|
loading.value = false
|
||||||
|
followLoading.value = false
|
||||||
|
}).catch(() => {
|
||||||
|
loading.value = false
|
||||||
|
followLoading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function refreshList() {
|
||||||
|
emailList.splice(0);
|
||||||
|
noLoading.value = false;
|
||||||
|
mailTotal.value = 0;
|
||||||
|
checkAll.value = false;
|
||||||
|
firstLoad.value = true
|
||||||
|
isIndeterminate.value = false;
|
||||||
|
queryParam.emailId = 0;
|
||||||
|
getEmailList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadData() {
|
||||||
|
getEmailList()
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.email-container {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2e2e2e;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll {
|
||||||
|
margin: 0;
|
||||||
|
height: calc(100% - 38px);
|
||||||
|
overflow: auto;
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
.follow-loading {
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: background 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||||
|
.el-checkbox{
|
||||||
|
width: 40px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-column {
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
grid-template-columns: 1fr !important;
|
||||||
|
gap: 4px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
grid-template-columns: 200px 1fr;
|
||||||
|
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-sender {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1a1a1a;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-subject {
|
||||||
|
color: #333;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.email-right {
|
||||||
|
align-self: center;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #333;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow:
|
||||||
|
inset 1px 0 0 rgb(218, 220, 224),
|
||||||
|
inset -1px 0 0 rgb(218, 220, 224),
|
||||||
|
0 1px 2px 0 rgba(60, 64, 67, 0.3),
|
||||||
|
0 1px 3px 1px rgba(60, 64, 67, 0.15);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*&[data-checked="true"] {
|
||||||
|
background-color: #c2dbff;
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.phone-star {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-star {
|
||||||
|
padding-left: 8px;
|
||||||
|
display: flex;
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
.pc-star {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.email-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.phone-star {
|
||||||
|
display: block;
|
||||||
|
align-self: end;
|
||||||
|
padding-right: 16px;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
.star-pd{
|
||||||
|
padding-top: 6px !important;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-time {
|
||||||
|
padding-right: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-scrollbar__view) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 3px 16px;
|
||||||
|
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
|
||||||
|
.icon {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-left: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.more-icon {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
42
mail-vue/src/components/hamburger/index.vue
Normal file
42
mail-vue/src/components/hamburger/index.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div style="padding: 0 15px;" @click="toggleClick">
|
||||||
|
<svg
|
||||||
|
:class="{'is-active':isActive}"
|
||||||
|
class="hamburger"
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits()
|
||||||
|
const toggleClick = () => {
|
||||||
|
emit('toggleClick');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hamburger {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger.is-active {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
109
mail-vue/src/components/loading/index.vue
Normal file
109
mail-vue/src/components/loading/index.vue
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<el-icon class="is-loading" :style="{ fontSize: `${size}px` }">
|
||||||
|
<svg class="circular" viewBox="0 0 20 20">
|
||||||
|
<g
|
||||||
|
class="path2 loading-path"
|
||||||
|
stroke-width="0"
|
||||||
|
style="animation: none; stroke: none"
|
||||||
|
>
|
||||||
|
<circle r="3.375" class="dot1" rx="0" ry="0" />
|
||||||
|
<circle r="3.375" class="dot2" rx="0" ry="0" />
|
||||||
|
<circle r="3.375" class="dot4" rx="0" ry="0" />
|
||||||
|
<circle r="3.375" class="dot3" rx="0" ry="0" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.el-select-dropdown__loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: calc(v-bind(size) * 3.33px);
|
||||||
|
font-size: calc(v-bind(size) * 0.67px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.circular {
|
||||||
|
display: inline;
|
||||||
|
height: v-bind(size) + 'px';
|
||||||
|
width: v-bind(size) + 'px';
|
||||||
|
animation: loading-rotate 2s linear infinite;
|
||||||
|
}
|
||||||
|
.path {
|
||||||
|
animation: loading-dash 1.5s ease-in-out infinite;
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke: var(--el-color-primary);
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
.loading-path .dot1 {
|
||||||
|
transform: translate(3.75px, 3.75px);
|
||||||
|
fill: var(--el-color-primary);
|
||||||
|
animation: custom-spin-move 1s infinite linear alternate;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
.loading-path .dot2 {
|
||||||
|
transform: translate(calc(100% - 3.75px), 3.75px);
|
||||||
|
fill: var(--el-color-primary);
|
||||||
|
animation: custom-spin-move 1s infinite linear alternate;
|
||||||
|
opacity: 0.3;
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
.loading-path .dot3 {
|
||||||
|
transform: translate(3.75px, calc(100% - 3.75px));
|
||||||
|
fill: var(--el-color-primary);
|
||||||
|
animation: custom-spin-move 1s infinite linear alternate;
|
||||||
|
opacity: 0.3;
|
||||||
|
animation-delay: 1.2s;
|
||||||
|
}
|
||||||
|
.loading-path .dot4 {
|
||||||
|
transform: translate(calc(100% - 3.75px), calc(100% - 3.75px));
|
||||||
|
fill: var(--el-color-primary);
|
||||||
|
animation: custom-spin-move 1s infinite linear alternate;
|
||||||
|
opacity: 0.3;
|
||||||
|
animation-delay: 0.8s;
|
||||||
|
}
|
||||||
|
@keyframes loading-rotate {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes loading-dash {
|
||||||
|
0% {
|
||||||
|
stroke-dasharray: 1, 200;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: -40px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: -120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes custom-spin-move {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-loading {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
21
mail-vue/src/day/day.js
Normal file
21
mail-vue/src/day/day.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import dayjs from 'dayjs'
|
||||||
|
import 'dayjs/locale/zh-cn'
|
||||||
|
import utc from 'dayjs/plugin/utc'
|
||||||
|
import timezone from 'dayjs/plugin/timezone'
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime)
|
||||||
|
dayjs.extend(utc)
|
||||||
|
dayjs.extend(timezone)
|
||||||
|
dayjs.locale('zh-cn')
|
||||||
|
dayjs.tz.setDefault('Asia/Shanghai')
|
||||||
|
|
||||||
|
function day(time) {
|
||||||
|
return dayjs(time).add(8, 'hours')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fromNow = (time) => {
|
||||||
|
return time ? day(time).fromNow() : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export default day
|
||||||
409
mail-vue/src/layout/account/index.vue
Normal file
409
mail-vue/src/layout/account/index.vue
Normal 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>
|
||||||
132
mail-vue/src/layout/aside/index.vue
Normal file
132
mail-vue/src/layout/aside/index.vue
Normal 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>
|
||||||
131
mail-vue/src/layout/header/index.vue
Normal file
131
mail-vue/src/layout/header/index.vue
Normal 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>
|
||||||
121
mail-vue/src/layout/index.vue
Normal file
121
mail-vue/src/layout/index.vue
Normal 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>
|
||||||
129
mail-vue/src/layout/main/index.vue
Normal file
129
mail-vue/src/layout/main/index.vue
Normal 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>
|
||||||
20
mail-vue/src/main.js
Normal file
20
mail-vue/src/main.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import {createApp} from 'vue';
|
||||||
|
import App from './App.vue';
|
||||||
|
import router from './router';
|
||||||
|
import 'element-plus/dist/index.css';
|
||||||
|
import './style.css';
|
||||||
|
import ElementPlus from 'element-plus';
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
|
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
||||||
|
import piniaPersistedState from 'pinia-plugin-persistedstate';
|
||||||
|
const pinia = createPinia().use(piniaPersistedState)
|
||||||
|
const app = createApp(App).use(pinia)
|
||||||
|
|
||||||
|
app.use(ElementPlus, {
|
||||||
|
locale: zhCn,
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(router)
|
||||||
|
app.config.devtools = true;
|
||||||
|
|
||||||
|
app.mount('#app');
|
||||||
14
mail-vue/src/request/account.js
Normal file
14
mail-vue/src/request/account.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import http from '@/axios/index.js'
|
||||||
|
|
||||||
|
export function accountList(accountId, size) {
|
||||||
|
return http.get('/account/list', {params: {accountId, size}});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function accountAdd(email,token) {
|
||||||
|
return http.post('/account/add', {email,token})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function accountDelete(accountId) {
|
||||||
|
return http.delete('/account/delete', {params: {accountId}})
|
||||||
|
}
|
||||||
|
|
||||||
17
mail-vue/src/request/email.js
Normal file
17
mail-vue/src/request/email.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import http from '@/axios/index.js';
|
||||||
|
|
||||||
|
export function emailList(accountId, emailId, size) {
|
||||||
|
return http.get('/email/list', {params: {accountId, emailId, size}})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emailDelete(emailIds) {
|
||||||
|
return http.delete('/email/delete?emailIds=' + emailIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function attList(emailId) {
|
||||||
|
return http.get('/email/attList', {params: {emailId}})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emailLatest(emailId,accountId) {
|
||||||
|
return http.get('/email/latest',{params: {emailId,accountId}})
|
||||||
|
}
|
||||||
13
mail-vue/src/request/login.js
Normal file
13
mail-vue/src/request/login.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import http from '@/axios/index.js';
|
||||||
|
|
||||||
|
export function login(email, password) {
|
||||||
|
return http.post('/login', {email: email, password: password})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout() {
|
||||||
|
return http.delete('/logout')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function register(form) {
|
||||||
|
return http.post('/register', form)
|
||||||
|
}
|
||||||
13
mail-vue/src/request/send.js
Normal file
13
mail-vue/src/request/send.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import http from "@/axios/index.js";
|
||||||
|
|
||||||
|
export function sendEmail(form) {
|
||||||
|
return http.post('/send/sendEmail', form)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendList(accountId, sendId, size) {
|
||||||
|
return http.get('/send/list',{ params: {accountId, sendId, size}})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendDelete(sendIds) {
|
||||||
|
return http.delete('/send/delete?sendIds=' + sendIds);
|
||||||
|
}
|
||||||
9
mail-vue/src/request/setting.js
Normal file
9
mail-vue/src/request/setting.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import http from '@/axios/index.js';
|
||||||
|
|
||||||
|
export function settingSet(setting) {
|
||||||
|
return http.put('/setting/set',setting)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function settingQuery() {
|
||||||
|
return http.get('/setting/query')
|
||||||
|
}
|
||||||
13
mail-vue/src/request/star.js
Normal file
13
mail-vue/src/request/star.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import http from "@/axios/index.js";
|
||||||
|
|
||||||
|
export function starAdd(emailId) {
|
||||||
|
return http.post('/star/add', {emailId})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function starCancel(emailId) {
|
||||||
|
return http.delete('/star/cancel', {params: {emailId}})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function starList(emailId,size) {
|
||||||
|
return http.get('/star/list', {params: {emailId,size}})
|
||||||
|
}
|
||||||
13
mail-vue/src/request/user.js
Normal file
13
mail-vue/src/request/user.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import http from '@/axios/index.js';
|
||||||
|
|
||||||
|
export function loginUserInfo() {
|
||||||
|
return http.get('/user/loginUserInfo')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetPassword(password) {
|
||||||
|
return http.put('/user/resetPassword', {password})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userDelete() {
|
||||||
|
return http.delete('/user/delete')
|
||||||
|
}
|
||||||
184
mail-vue/src/router/index.js
Normal file
184
mail-vue/src/router/index.js
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import {createRouter, createWebHashHistory} from 'vue-router'
|
||||||
|
import {loginUserInfo} from "@/request/user.js";
|
||||||
|
import {useUiStore} from "@/store/ui.js";
|
||||||
|
import {useAccountStore} from "@/store/account.js";
|
||||||
|
import {useUserStore} from "@/store/user.js";
|
||||||
|
import {useSettingStore} from "@/store/setting.js";
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'layout',
|
||||||
|
redirect: '/email',
|
||||||
|
component: () => import('@/layout/index.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/email',
|
||||||
|
name: 'email',
|
||||||
|
component: () => import('@/views/email/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '收件箱',
|
||||||
|
name: 'email',
|
||||||
|
menu: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/content',
|
||||||
|
name: 'content',
|
||||||
|
component: () => import('@/views/content/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '邮件详情',
|
||||||
|
name: 'content',
|
||||||
|
menu: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/setting',
|
||||||
|
name: 'setting',
|
||||||
|
component: () => import('@/views/setting/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '个人设置',
|
||||||
|
name: 'setting',
|
||||||
|
menu: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/sys-setting',
|
||||||
|
name: 'sys-setting',
|
||||||
|
component: () => import('@/views/sys-setting/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '系统设置',
|
||||||
|
name: 'sys-setting',
|
||||||
|
menu: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/star',
|
||||||
|
name: 'star',
|
||||||
|
component: () => import('@/views/star/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '星标邮件',
|
||||||
|
name: 'star',
|
||||||
|
menu: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'login',
|
||||||
|
component: () => import('@/views/login/index.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/test',
|
||||||
|
name: 'test',
|
||||||
|
component: () => import('@/views/test/index.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
let firstLogin = true
|
||||||
|
|
||||||
|
router.beforeEach(async (to, from, next) => {
|
||||||
|
|
||||||
|
const uiStore = useUiStore()
|
||||||
|
|
||||||
|
if (uiStore.init && to.name === 'login') {
|
||||||
|
await initSetting()
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
|
||||||
|
if (!token && to.name !== 'login') {
|
||||||
|
return next({
|
||||||
|
name: 'login'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token && to.name === 'login') {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiStore.init && to.name !== 'login') {
|
||||||
|
await initSettingAndUserInfo()
|
||||||
|
firstLogin = false
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstLogin) {
|
||||||
|
await initUserInfo()
|
||||||
|
firstLogin = false
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
router.afterEach((to) => {
|
||||||
|
|
||||||
|
const uiStore = useUiStore()
|
||||||
|
if (to.meta.menu) {
|
||||||
|
if (['content', 'email'].includes(to.meta.name)) {
|
||||||
|
uiStore.accountShow = window.innerWidth > 767;
|
||||||
|
} else {
|
||||||
|
uiStore.accountShow = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (to.name === 'login') {
|
||||||
|
firstLogin = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
uiStore.asideShow = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
async function initSettingAndUserInfo() {
|
||||||
|
const uiStore = useUiStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const settingStore = useSettingStore()
|
||||||
|
const accountStore = useAccountStore()
|
||||||
|
const [setting,user] = await Promise.all([settingStore.initSetting(),loginUserInfo()])
|
||||||
|
uiStore.init = false
|
||||||
|
accountStore.currentAccountId = user.accountId
|
||||||
|
userStore.user = user
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function initUserInfo() {
|
||||||
|
const uiStore = useUiStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
try {
|
||||||
|
const user = await loginUserInfo()
|
||||||
|
const accountStore = useAccountStore()
|
||||||
|
accountStore.currentAccountId = user.accountId
|
||||||
|
userStore.user = user
|
||||||
|
uiStore.loginLoading = false
|
||||||
|
} catch (e) {
|
||||||
|
uiStore.loginLoading = false
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function initSetting() {
|
||||||
|
const settingStore = useSettingStore()
|
||||||
|
const uiStore = useUiStore()
|
||||||
|
try {
|
||||||
|
await settingStore.initSetting();
|
||||||
|
uiStore.init = false
|
||||||
|
} catch (e) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default router
|
||||||
7
mail-vue/src/store/account.js
Normal file
7
mail-vue/src/store/account.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useAccountStore = defineStore('account', {
|
||||||
|
state: () => ({
|
||||||
|
currentAccountId: 0,
|
||||||
|
})
|
||||||
|
})
|
||||||
13
mail-vue/src/store/email.js
Normal file
13
mail-vue/src/store/email.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useEmailStore = defineStore('email', {
|
||||||
|
state: () => ({
|
||||||
|
deleteIds: 0,
|
||||||
|
starScroll: null,
|
||||||
|
emailScroll: null,
|
||||||
|
readEmail: {},
|
||||||
|
}),
|
||||||
|
persist: {
|
||||||
|
pick: ['readEmail'],
|
||||||
|
},
|
||||||
|
})
|
||||||
7
mail-vue/src/store/send.js
Normal file
7
mail-vue/src/store/send.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useSendStore = defineStore('send', {
|
||||||
|
state: () => ({
|
||||||
|
deleteId: 0
|
||||||
|
})
|
||||||
|
})
|
||||||
21
mail-vue/src/store/setting.js
Normal file
21
mail-vue/src/store/setting.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { settingQuery} from "@/request/setting.js";
|
||||||
|
|
||||||
|
export const useSettingStore = defineStore('setting', {
|
||||||
|
state: () => ({
|
||||||
|
domainList: [],
|
||||||
|
settings: {
|
||||||
|
title: '-'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
async initSetting() {
|
||||||
|
if (this.domainList.length === 0) {
|
||||||
|
const data = await settingQuery()
|
||||||
|
this.domainList.push(...data.domainList)
|
||||||
|
delete data.domainList
|
||||||
|
this.settings = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
13
mail-vue/src/store/ui.js
Normal file
13
mail-vue/src/store/ui.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useUiStore = defineStore('ui', {
|
||||||
|
state: () => ({
|
||||||
|
asideShow: window.innerWidth > 991,
|
||||||
|
loginLoading: false,
|
||||||
|
accountShow: false,
|
||||||
|
init: true,
|
||||||
|
}),
|
||||||
|
persist: {
|
||||||
|
pick: ['accountShow'],
|
||||||
|
},
|
||||||
|
})
|
||||||
7
mail-vue/src/store/user.js
Normal file
7
mail-vue/src/store/user.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', {
|
||||||
|
state: () => ({
|
||||||
|
user: {},
|
||||||
|
})
|
||||||
|
})
|
||||||
7
mail-vue/src/store/writer.js
Normal file
7
mail-vue/src/store/writer.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', {
|
||||||
|
state: () => ({
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
79
mail-vue/src/style.css
Normal file
79
mail-vue/src/style.css
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a{
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'HarmonyOS';
|
||||||
|
src: url('@/assets/fonts/HarmonyOS_Sans_SC_Regular.woff2') format('woff2');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__inner:focus) {
|
||||||
|
background-color: transparent !important;
|
||||||
|
border-color: #dcdfe6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'HarmonyOS', -apple-system, BlinkMacSystemFont,
|
||||||
|
'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||||
|
'Noto Sans', sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #333;
|
||||||
|
background-color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, input, select, textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
*:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--el-color-primary: #1890ff;
|
||||||
|
--el-color-primary-dark-2: #1064c0;
|
||||||
|
--el-color-primary-light-3: #4dabff;
|
||||||
|
--el-color-primary-light-5: #69c0ff;
|
||||||
|
--el-color-primary-light-7: #91d5ff;
|
||||||
|
--el-color-primary-light-9: #e6f7ff;
|
||||||
|
--el-text-color-regular: #333;
|
||||||
|
}
|
||||||
6
mail-vue/src/utils/convert-utils.js
Normal file
6
mail-vue/src/utils/convert-utils.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import {useSettingStore} from "@/store/setting.js";
|
||||||
|
|
||||||
|
export function cvtR2Url(key) {
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
return 'https://' + settingStore.settings.r2Domain + '/' + key
|
||||||
|
}
|
||||||
13
mail-vue/src/utils/file-utils.js
Normal file
13
mail-vue/src/utils/file-utils.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export function getExtName(fileName) {
|
||||||
|
const index = fileName.lastIndexOf('.')
|
||||||
|
return index !== -1 ? fileName.slice(index + 1).toLowerCase() : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBytes(bytes) {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
const size = (bytes / Math.pow(k, i)).toFixed(2);
|
||||||
|
return `${size} ${units[i]}`;
|
||||||
|
}
|
||||||
3
mail-vue/src/utils/time-utils.js
Normal file
3
mail-vue/src/utils/time-utils.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
}
|
||||||
4
mail-vue/src/utils/verify-utils.js
Normal file
4
mail-vue/src/utils/verify-utils.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export function isEmail(email) {
|
||||||
|
const reg = /^[a-zA-Z0-9]+@([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]+$/;
|
||||||
|
return reg.test(email);
|
||||||
|
}
|
||||||
307
mail-vue/src/views/content/index.vue
Normal file
307
mail-vue/src/views/content/index.vue
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
<template>
|
||||||
|
<div class="box">
|
||||||
|
<div class="header-actions">
|
||||||
|
<Icon class="icon" icon="material-symbols-light:arrow-back-ios-new" width="20" height="20" @click="handleBack"/>
|
||||||
|
<Icon class="icon" icon="uiw:delete" width="16" height="16" @click="handleDelete"/>
|
||||||
|
<Icon class="icon" @click="changeStar" v-if="email.isStar" icon="fluent-color:star-16" width="20" height="20"/>
|
||||||
|
<Icon class="icon" @click="changeStar" v-else icon="solar:star-line-duotone" width="18" height="18"/>
|
||||||
|
</div>
|
||||||
|
<div></div>
|
||||||
|
<el-scrollbar class="scrollbar">
|
||||||
|
<div class="container">
|
||||||
|
<div class="email-title">
|
||||||
|
{{ email.subject }}
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="email-info">
|
||||||
|
<div>
|
||||||
|
<div class="send"><span class="send-source">发件人</span>
|
||||||
|
<div class="send-name">
|
||||||
|
<span class="send-name-title">{{ email.name }}</span>
|
||||||
|
<span><{{ email.sendEmail }}></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="receive"><span class="source">收件人</span><span>{{ email.receiveEmail }}</span></div>
|
||||||
|
<div class="date">
|
||||||
|
<div>{{ formatDetailDate(email.createTime) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="icon">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-scrollbar class="htm-scrollbar">
|
||||||
|
<div v-html="email.content"/>
|
||||||
|
</el-scrollbar>
|
||||||
|
<div class="att" v-if="atts.length > 0">
|
||||||
|
<div class="att-title">附件列表</div>
|
||||||
|
<div class="att-item" v-for="att in atts" :key="att.attId">
|
||||||
|
<img v-if="isImage(att.filename)" class="att-image" :src="cvtR2Url(att.key)"/>
|
||||||
|
<div class="att-icon" v-else>
|
||||||
|
<Icon :icon="toIcon(att.filename)" width="20" height="20"/>
|
||||||
|
</div>
|
||||||
|
<div class="att-name">
|
||||||
|
{{ att.filename }}
|
||||||
|
</div>
|
||||||
|
<div>{{ formatBytes(att.size) }}</div>
|
||||||
|
<div class="down-icon att-icon">
|
||||||
|
<a :href="cvtR2Url(att.key)" download>
|
||||||
|
<Icon icon="system-uicons:push-down" width="22" height="22"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import {reactive, watch} from "vue";
|
||||||
|
import {useRouter} from 'vue-router'
|
||||||
|
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||||
|
import {attList, emailDelete} from "@/request/email.js";
|
||||||
|
import {Icon} from "@iconify/vue";
|
||||||
|
import {useEmailStore} from "@/store/email.js";
|
||||||
|
import {useAccountStore} from "@/store/account.js";
|
||||||
|
import day from "@/day/day.js";
|
||||||
|
import {starAdd, starCancel} from "@/request/star.js";
|
||||||
|
import {getExtName, formatBytes} from "@/utils/file-utils.js";
|
||||||
|
import {cvtR2Url} from "@/utils/convert-utils.js";
|
||||||
|
|
||||||
|
const accountStore = useAccountStore();
|
||||||
|
|
||||||
|
const emailStore = useEmailStore();
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const email = emailStore.readEmail
|
||||||
|
const atts = reactive([])
|
||||||
|
|
||||||
|
attList(email.emailId).then(list => {
|
||||||
|
atts.push(...list)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => accountStore.currentAccountId, () => {
|
||||||
|
handleBack()
|
||||||
|
})
|
||||||
|
|
||||||
|
function toIcon(filename) {
|
||||||
|
const extName = getExtName(filename)
|
||||||
|
if (['zip', 'rar', '7z', 'tar', 'tgz'].includes(extName)) return 'hugeicons:file-zip';
|
||||||
|
if (['mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv'].includes(extName)) return 'fluent:video-clip-24-regular';
|
||||||
|
if (['txt', 'doc', 'docx', 'md'].includes(extName)) return 'hugeicons:google-doc'
|
||||||
|
if (['xls', 'csv', 'xlsx'].includes(extName)) return 'codicon:table';
|
||||||
|
if (['mp3', 'wav', 'aac', 'ogg', 'flac', 'm4a'].includes(extName)) return 'mynaui:music';
|
||||||
|
if (['.ppt', 'pptx', 'pps', 'potx', 'pot'].includes(extName)) return 'lsicon:file-ppt-filled'
|
||||||
|
if (extName === 'pdf') return 'hugeicons:pdf-02';
|
||||||
|
if (extName === 'apk') return 'proicons:android';
|
||||||
|
if (extName === 'exe') return 'bi:filetype-exe';
|
||||||
|
return "hugeicons:attachment-01"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function isImage(filename) {
|
||||||
|
return ['png', 'jpg', 'jpeg', 'bmp', 'gif'].includes(getExtName(filename))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const formatDetailDate = (time) => {
|
||||||
|
return day(time).format('YYYY年M月D日 ddd AH:mm')
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeStar() {
|
||||||
|
if (email.isStar) {
|
||||||
|
email.isStar = 0;
|
||||||
|
starCancel(email.emailId).then(() => {
|
||||||
|
email.isStar = 0;
|
||||||
|
emailStore.emailScroll?.editEmailStar(email.emailId, 0)
|
||||||
|
emailStore.starScroll?.deleteEmail([email.emailId])
|
||||||
|
}).catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
email.isStar = 1;
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
email.isStar = 1;
|
||||||
|
starAdd(email.emailId).then(() => {
|
||||||
|
email.isStar = 1;
|
||||||
|
emailStore.emailScroll?.editEmailStar(email.emailId, 1)
|
||||||
|
emailStore.starScroll?.addItemStar(email)
|
||||||
|
}).catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
email.isStar = 0;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
ElMessageBox.confirm('确认删除该邮件吗?', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
emailDelete(email.value.emailId).then(() => {
|
||||||
|
ElMessage({
|
||||||
|
message: '删除成功',
|
||||||
|
type: 'success',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
emailStore.deleteIds = [email.value.emailId]
|
||||||
|
})
|
||||||
|
router.back()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.box {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
padding: 9px 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 28px;
|
||||||
|
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
|
||||||
|
font-size: 18px;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.scrollbar {
|
||||||
|
height: calc(100% - 38px);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
font-size: 14px;
|
||||||
|
padding-left: 15px;
|
||||||
|
padding-right: 15px;
|
||||||
|
padding-top: 10px;
|
||||||
|
|
||||||
|
.email-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.htm-scrollbar {
|
||||||
|
padding-bottom: 15px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.att {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid #e7e9ec;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.att-title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.att-item {
|
||||||
|
div {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
align-self: start;
|
||||||
|
border: 1px solid #e7e9ec;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto 20px;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.att-icon {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.att-name {
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.att-image {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.down-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid #e7e9ec;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
/*.date {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-content: flex-start;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
.date {
|
||||||
|
color: #585d69;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
.send-name {
|
||||||
|
color: #585d69;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-name-title {
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.receive {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
span:nth-child(2) {
|
||||||
|
color: #585d69;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-source {
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source {
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
72
mail-vue/src/views/email/index.vue
Normal file
72
mail-vue/src/views/email/index.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<emailScroll ref="scroll"
|
||||||
|
:cancel-success="cancelStar"
|
||||||
|
:star-success="addStar"
|
||||||
|
:getEmailList="getEmailList"
|
||||||
|
:emailDelete="emailDelete"
|
||||||
|
:star-add="starAdd"
|
||||||
|
:star-cancel="starCancel"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useAccountStore} from "@/store/account.js";
|
||||||
|
import {useEmailStore} from "@/store/email.js";
|
||||||
|
import {useSettingStore} from "@/store/setting.js";
|
||||||
|
import emailScroll from "@/components/email-scroll/index.vue"
|
||||||
|
import {emailList, emailDelete, emailLatest} from "@/request/email.js";
|
||||||
|
import {starAdd, starCancel} from "@/request/star.js";
|
||||||
|
import {defineOptions, onMounted, ref, watch} from "vue";
|
||||||
|
import {sleep} from "@/utils/time-utils.js";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'email'
|
||||||
|
})
|
||||||
|
|
||||||
|
const emailStore = useEmailStore();
|
||||||
|
const accountStore = useAccountStore();
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const scroll = ref({})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
emailStore.emailScroll = scroll;
|
||||||
|
latest()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
watch(() => accountStore.currentAccountId, () => {
|
||||||
|
scroll.value.refreshList();
|
||||||
|
})
|
||||||
|
|
||||||
|
async function latest() {
|
||||||
|
while (true) {
|
||||||
|
const latestId = scroll.value.emailList[0]?.emailId ?? 0
|
||||||
|
|
||||||
|
if (!scroll.value.firstLoad && settingStore.settings.autoRefreshTime) {
|
||||||
|
try {
|
||||||
|
const accountId = accountStore.currentAccountId
|
||||||
|
const list = await emailLatest(latestId,accountId)
|
||||||
|
if (accountId === accountStore.currentAccountId) {
|
||||||
|
scroll.value.emailList.unshift(...list)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await sleep(settingStore.settings.autoRefreshTime * 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addStar(email) {
|
||||||
|
emailStore.starScroll?.addItemStar(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelStar(email) {
|
||||||
|
emailStore.starScroll?.deleteEmail([email.emailId])
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmailList(emailId, size) {
|
||||||
|
return emailList(accountStore.currentAccountId, emailId, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
451
mail-vue/src/views/login/index.vue
Normal file
451
mail-vue/src/views/login/index.vue
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
<template>
|
||||||
|
<div id="box">
|
||||||
|
<div id="background-wrap">
|
||||||
|
<div class="x1 cloud"></div>
|
||||||
|
<div class="x2 cloud"></div>
|
||||||
|
<div class="x3 cloud"></div>
|
||||||
|
<div class="x4 cloud"></div>
|
||||||
|
<div class="x5 cloud"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-wrapper">
|
||||||
|
<el-form autocomplete="off">
|
||||||
|
<div class="container" >
|
||||||
|
<span class="form-title">{{ settingStore.settings.title }}</span>
|
||||||
|
<div class="custom-style" v-if="settingStore.settings.register === 0">
|
||||||
|
<el-segmented v-model="show" :options="options" />
|
||||||
|
</div>
|
||||||
|
<div v-if="show === 'login'">
|
||||||
|
<el-input v-model="form.email" type="text" placeholder="邮箱" autocomplete="off">
|
||||||
|
<template #prefix>
|
||||||
|
<Icon icon="weui:email-outlined" width="22" height="22" />
|
||||||
|
</template>
|
||||||
|
<template #append>
|
||||||
|
<div @click.stop="openSelect">
|
||||||
|
<el-select
|
||||||
|
ref="mySelect"
|
||||||
|
v-model="suffix"
|
||||||
|
placeholder="请选择"
|
||||||
|
class="select"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in domainList"
|
||||||
|
:key="item"
|
||||||
|
:label="item"
|
||||||
|
:value="item"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<div style="color: #333">
|
||||||
|
<span >{{suffix}}</span>
|
||||||
|
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-input v-model="form.password" placeholder="密码" type="password" autocomplete="off">
|
||||||
|
<template #prefix>
|
||||||
|
<Icon icon="carbon:password" width="22" height="22" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-button class="btn" type="primary" @click="submit" :loading="uiStore.loginLoading"
|
||||||
|
>登录
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<el-input v-model="registerForm.email" type="text" placeholder="邮箱" autocomplete="off">
|
||||||
|
<template #prefix>
|
||||||
|
<Icon icon="weui:email-outlined" width="22" height="22" />
|
||||||
|
</template>
|
||||||
|
<template #append>
|
||||||
|
<div @click.stop="openSelect">
|
||||||
|
<el-select
|
||||||
|
ref="mySelect"
|
||||||
|
v-model="suffix"
|
||||||
|
placeholder="请选择"
|
||||||
|
class="select"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in domainList"
|
||||||
|
:key="item"
|
||||||
|
:label="item"
|
||||||
|
:value="item"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<div style="color: #333">
|
||||||
|
<span>{{suffix}}</span>
|
||||||
|
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-input v-model="registerForm.password" placeholder="密码" type="password" autocomplete="off">
|
||||||
|
<template #prefix>
|
||||||
|
<Icon icon="carbon:password" width="22" height="22" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-input v-model="registerForm.confirmPassword" placeholder="确认密码" type="password" autocomplete="off">
|
||||||
|
<template #prefix>
|
||||||
|
<Icon icon="carbon:password" width="22" height="22" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-button class="btn" type="primary" @click="submitRegister" :loading="registerLoading"
|
||||||
|
>注册
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<el-dialog
|
||||||
|
v-model="verifyShow"
|
||||||
|
title="验证你是不是人"
|
||||||
|
width="332"
|
||||||
|
align-center
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="register-turnstile"
|
||||||
|
:data-sitekey="settingStore.settings.siteKey"
|
||||||
|
data-callback="onTurnstileSuccess"
|
||||||
|
></div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import router from "@/router";
|
||||||
|
import {nextTick, reactive, ref} from "vue";
|
||||||
|
import {login} from "@/request/login.js";
|
||||||
|
import {register} from "@/request/login.js";
|
||||||
|
import {ElMessage} from 'element-plus'
|
||||||
|
import {isEmail} from "@/utils/verify-utils.js";
|
||||||
|
import {useUiStore} from "@/store/ui.js";
|
||||||
|
import {useSettingStore} from "@/store/setting.js";
|
||||||
|
import {Icon} from "@iconify/vue";
|
||||||
|
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const uiStore = useUiStore()
|
||||||
|
const show = ref('login')
|
||||||
|
const form = reactive({
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
|
||||||
|
});
|
||||||
|
const options = [{label: '登录', value: 'login'},{label: '注册', value: 'register'}];
|
||||||
|
const mySelect = ref()
|
||||||
|
const suffix = ref('')
|
||||||
|
const registerForm = reactive({
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
})
|
||||||
|
const domainList = settingStore.domainList;
|
||||||
|
const registerLoading = ref(false)
|
||||||
|
suffix.value = domainList[0]
|
||||||
|
const verifyShow = ref(false)
|
||||||
|
let verifyToken = ''
|
||||||
|
let turnstileId = ''
|
||||||
|
|
||||||
|
window.onTurnstileSuccess = (token) => {
|
||||||
|
verifyToken = token;
|
||||||
|
setTimeout(() => {
|
||||||
|
verifyShow.value = false
|
||||||
|
},1500)
|
||||||
|
};
|
||||||
|
|
||||||
|
const openSelect = () => {
|
||||||
|
mySelect.value.toggleMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
|
||||||
|
if (!form.email) {
|
||||||
|
ElMessage({
|
||||||
|
message: '邮箱不能为空',
|
||||||
|
type: 'error',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEmail(form.email + suffix.value)) {
|
||||||
|
ElMessage({
|
||||||
|
message: '输入的邮箱不合法',
|
||||||
|
type: 'error',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!form.password) {
|
||||||
|
ElMessage({
|
||||||
|
message: '密码不能为空',
|
||||||
|
type: 'error',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.password.length < 6) {
|
||||||
|
ElMessage({
|
||||||
|
message: '密码最少六位',
|
||||||
|
type: 'error',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uiStore.loginLoading = true
|
||||||
|
login(form.email+suffix.value, form.password).then(data => {
|
||||||
|
localStorage.setItem('token', data.token)
|
||||||
|
router.replace({name: 'layout'})
|
||||||
|
}).catch(() => {
|
||||||
|
uiStore.loginLoading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function submitRegister() {
|
||||||
|
|
||||||
|
if (!registerForm.email) {
|
||||||
|
ElMessage({
|
||||||
|
message: '邮箱不能为空',
|
||||||
|
type: 'error',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEmail(registerForm.email + suffix.value)) {
|
||||||
|
ElMessage({
|
||||||
|
message: '输入的邮箱不合法',
|
||||||
|
type: 'error',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!registerForm.password) {
|
||||||
|
ElMessage({
|
||||||
|
message: '密码不能为空',
|
||||||
|
type: 'error',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registerForm.password.length < 6) {
|
||||||
|
ElMessage({
|
||||||
|
message: '密码最少六位',
|
||||||
|
type: 'error',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registerForm.password !== registerForm.confirmPassword) {
|
||||||
|
|
||||||
|
ElMessage({
|
||||||
|
message: '两次密码输入不一致',
|
||||||
|
type: 'error',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verifyToken && settingStore.settings.registerVerify === 0) {
|
||||||
|
verifyShow.value = true
|
||||||
|
if (!turnstileId) {
|
||||||
|
nextTick(() => {
|
||||||
|
turnstileId = window.turnstile.render('.register-turnstile')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
nextTick(() => {
|
||||||
|
window.turnstile.reset(turnstileId);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerLoading.value = true
|
||||||
|
register({email: registerForm.email + suffix.value, password: registerForm.password,token: verifyToken}).then(() => {
|
||||||
|
show.value = 'login'
|
||||||
|
registerForm.email = ''
|
||||||
|
registerForm.password = ''
|
||||||
|
registerForm.confirmPassword = ''
|
||||||
|
registerLoading.value = false
|
||||||
|
verifyToken = ''
|
||||||
|
ElMessage({
|
||||||
|
message: '注册成功',
|
||||||
|
type: 'success',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
}).catch(res => {
|
||||||
|
if (res.code === 400) {
|
||||||
|
verifyToken = ''
|
||||||
|
window.turnstile.reset(turnstileId)
|
||||||
|
verifyShow.value = true
|
||||||
|
}
|
||||||
|
registerLoading.value = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.form-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 110;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: #FFFFFF;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
padding-top: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form{
|
||||||
|
max-width: 410px;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-icon {
|
||||||
|
position: relative;
|
||||||
|
top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input-group__append) {
|
||||||
|
padding: 0 !important;
|
||||||
|
padding-left: 8px !important;
|
||||||
|
background: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
position: absolute;
|
||||||
|
right: 30px;
|
||||||
|
width: 100px;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.custom-style {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.custom-style .el-segmented {
|
||||||
|
--el-border-radius-base: 8px;
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#box {
|
||||||
|
background: linear-gradient(to bottom, #2980b9, #6dd5fa, #fff);
|
||||||
|
color: #333;
|
||||||
|
font: 100% Arial, sans-serif;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#background-wrap {
|
||||||
|
height: 100%;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animateCloud {
|
||||||
|
0% {
|
||||||
|
margin-left: -500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
margin-left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.x1 {
|
||||||
|
animation: animateCloud 30s linear infinite;
|
||||||
|
transform: scale(0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.x2 {
|
||||||
|
animation: animateCloud 15s linear infinite;
|
||||||
|
transform: scale(0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.x3 {
|
||||||
|
animation: animateCloud 25s linear infinite;
|
||||||
|
transform: scale(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.x4 {
|
||||||
|
animation: animateCloud 13s linear infinite;
|
||||||
|
transform: scale(0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.x5 {
|
||||||
|
animation: animateCloud 20s linear infinite;
|
||||||
|
transform: scale(0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cloud {
|
||||||
|
background: linear-gradient(to bottom, #fff 5%, #f1f1f1 100%);
|
||||||
|
border-radius: 100px;
|
||||||
|
box-shadow: 0 8px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
height: 120px;
|
||||||
|
width: 350px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cloud:after,
|
||||||
|
.cloud:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
background: #fff;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cloud:after {
|
||||||
|
border-radius: 100px;
|
||||||
|
height: 100px;
|
||||||
|
left: 50px;
|
||||||
|
top: -50px;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cloud:before {
|
||||||
|
border-radius: 200px;
|
||||||
|
height: 180px;
|
||||||
|
width: 180px;
|
||||||
|
right: 50px;
|
||||||
|
top: -90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
169
mail-vue/src/views/setting/index.vue
Normal file
169
mail-vue/src/views/setting/index.vue
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<template>
|
||||||
|
<div class="box">
|
||||||
|
<div class="pass">
|
||||||
|
<div class="title">账户与密码</div>
|
||||||
|
<div class="pass-item">
|
||||||
|
<div>邮箱</div>
|
||||||
|
<div>{{ userStore.user.email }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="pass-item">
|
||||||
|
<div>密码</div>
|
||||||
|
<div>
|
||||||
|
<el-button type="primary" @click="pwdShow = true">修改密码</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="del-email">
|
||||||
|
<div class="title">删除账户</div>
|
||||||
|
<div style="color: #585d69;">
|
||||||
|
此操作将永久删除您的账户及其所有数据,无法恢复
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<el-button type="primary" @click="deleteConfirm">删除账户</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-dialog v-model="pwdShow" title="修改密码" width="340">
|
||||||
|
<form>
|
||||||
|
<div class="update-pwd">
|
||||||
|
<el-input type="password" placeholder="新的密码" v-model="form.password"/>
|
||||||
|
<el-input type="password" placeholder="确认密码" v-model="form.newPwd"/>
|
||||||
|
<el-button type="primary" :loading="setPwdLoading" @click="submitPwd">保存</el-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import {defineOptions} from "vue";
|
||||||
|
import {reactive, ref} from 'vue'
|
||||||
|
import {resetPassword, userDelete} from "@/request/user.js";
|
||||||
|
import {ElMessage, ElMessageBox} from "element-plus";
|
||||||
|
import {useUserStore} from "@/store/user.js";
|
||||||
|
import router from "@/router/index.js";
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const setPwdLoading = ref(false)
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'setting'
|
||||||
|
})
|
||||||
|
|
||||||
|
const pwdShow = ref(false)
|
||||||
|
const form = reactive({
|
||||||
|
password: '',
|
||||||
|
newPwd: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteConfirm = () => {
|
||||||
|
ElMessageBox.confirm('确认删除当前账号及所有数据吗?', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
userDelete().then(() => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
router.replace('/login');
|
||||||
|
ElMessage({
|
||||||
|
message: '删除成功',
|
||||||
|
type: 'success',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function submitPwd() {
|
||||||
|
|
||||||
|
if (!form.password) {
|
||||||
|
ElMessage({
|
||||||
|
message: '密码不能为空',
|
||||||
|
type: 'error',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.password.length < 6) {
|
||||||
|
ElMessage({
|
||||||
|
message: '密码不能小于6位',
|
||||||
|
type: 'error',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.password !== form.newPwd) {
|
||||||
|
ElMessage({
|
||||||
|
message: '两次密码输入不一致',
|
||||||
|
type: 'error',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setPwdLoading.value = true
|
||||||
|
resetPassword(form.password).then(() => {
|
||||||
|
ElMessage({
|
||||||
|
message: '修改成功',
|
||||||
|
type: 'success',
|
||||||
|
plain: true,
|
||||||
|
})
|
||||||
|
pwdShow.value = false
|
||||||
|
setPwdLoading.value = false
|
||||||
|
form.password = ''
|
||||||
|
form.newPwd = ''
|
||||||
|
}).catch(() => {
|
||||||
|
setPwdLoading.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.box {
|
||||||
|
padding: 40px 40px;
|
||||||
|
|
||||||
|
.update-pwd {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pass {
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
|
||||||
|
.pass-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 50px 1fr;
|
||||||
|
gap: 140px;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
gap: 80px;
|
||||||
|
}
|
||||||
|
div:first-child {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
div:last-child {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.del-email {
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
34
mail-vue/src/views/star/index.vue
Normal file
34
mail-vue/src/views/star/index.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<emailScroll type="star" ref="scroll"
|
||||||
|
:allow-star="false"
|
||||||
|
:cancel-success="cancelStar"
|
||||||
|
:getEmailList="starList"
|
||||||
|
:emailDelete="emailDelete"
|
||||||
|
:star-add="starAdd"
|
||||||
|
:star-cancel="starCancel"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import emailScroll from "@/components/email-scroll/index.vue"
|
||||||
|
import {emailDelete} from "@/request/email.js";
|
||||||
|
import {starAdd, starCancel, starList} from "@/request/star.js";
|
||||||
|
import {useEmailStore} from "@/store/email.js";
|
||||||
|
import {defineOptions, onMounted, ref} from "vue";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'star'
|
||||||
|
})
|
||||||
|
|
||||||
|
const scroll = ref({})
|
||||||
|
const emailStore = useEmailStore();
|
||||||
|
|
||||||
|
function cancelStar(email) {
|
||||||
|
emailStore.emailScroll?.editEmailStar(email.emailId, 0)
|
||||||
|
scroll.value.deleteEmail([email.emailId])
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
emailStore.starScroll = scroll
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
248
mail-vue/src/views/sys-setting/index.vue
Normal file
248
mail-vue/src/views/sys-setting/index.vue
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
<template>
|
||||||
|
<div class="box">
|
||||||
|
<div class="setting">
|
||||||
|
<div class="title">网站设置</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div><span>用户注册</span></div>
|
||||||
|
<div>
|
||||||
|
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
|
||||||
|
v-model="setting.register"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div><span>添加邮箱</span></div>
|
||||||
|
<div>
|
||||||
|
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
|
||||||
|
v-model="setting.addEmail"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div><span>邮件接收</span></div>
|
||||||
|
<div>
|
||||||
|
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
|
||||||
|
v-model="setting.receive"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div><span>注册验证</span></div>
|
||||||
|
<div>
|
||||||
|
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
|
||||||
|
v-model="setting.registerVerify"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div><span>添加验证</span></div>
|
||||||
|
<div>
|
||||||
|
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
|
||||||
|
v-model="setting.addEmailVerify"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div>
|
||||||
|
<span>多号模式</span>
|
||||||
|
<el-popover
|
||||||
|
content="开启后账号栏出现一个用户可以添加多个邮箱"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<Icon class="warning" icon="fe:warning" width="20" height="20" />
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
|
||||||
|
v-model="setting.manyEmail"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item ">
|
||||||
|
<div>
|
||||||
|
<span>轮询刷新</span>
|
||||||
|
<el-popover
|
||||||
|
|
||||||
|
content="轮询请求服务器获取最新邮件人多可能会超出免费额度"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<Icon class="warning" icon="fe:warning" width="20" height="20" />
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
</div>
|
||||||
|
<div class="item-select">
|
||||||
|
<el-select
|
||||||
|
@change="change"
|
||||||
|
style="width: 80px;"
|
||||||
|
v-model="setting.autoRefreshTime"
|
||||||
|
placeholder="Select"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in options"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="title-item"><span>网站标题</span></div>
|
||||||
|
<div class="email-title">
|
||||||
|
<div>{{ setting.title }}</div>
|
||||||
|
<div @click="editTitleShow = true">
|
||||||
|
<Icon icon="iconamoon:edit-light" width="24" height="24"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-dialog v-model="editTitleShow" title="修改标题" width="340" @close="editTitle = ''">
|
||||||
|
<form>
|
||||||
|
<el-input type="text" placeholder="网站标题" v-model="editTitle"/>
|
||||||
|
<el-button type="primary" :loading="editTitleLoading" @click="saveTitle">保存</el-button>
|
||||||
|
</form>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import {defineOptions} from "vue";
|
||||||
|
import {ref} from 'vue'
|
||||||
|
import {settingSet} from "@/request/setting.js";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
import {useSettingStore} from "@/store/setting.js";
|
||||||
|
import {useUserStore} from "@/store/user.js";
|
||||||
|
import {useAccountStore} from "@/store/account.js";
|
||||||
|
import {Icon} from "@iconify/vue";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'sys-setting'
|
||||||
|
})
|
||||||
|
|
||||||
|
const accountStore = useAccountStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const editTitleShow = ref(false)
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
let setting = ref(settingStore.settings)
|
||||||
|
const editTitle = ref('')
|
||||||
|
const editTitleLoading = ref(false)
|
||||||
|
let backup = {}
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
label: '关闭',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '3s',
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '5s',
|
||||||
|
value: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '7s',
|
||||||
|
value: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '10s',
|
||||||
|
value: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '15s',
|
||||||
|
value: 15,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '20s',
|
||||||
|
value: 20,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
function beforeChange() {
|
||||||
|
backup = JSON.stringify(setting.value)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function change() {
|
||||||
|
editSetting(setting)
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveTitle() {
|
||||||
|
backup = JSON.stringify(setting.value)
|
||||||
|
setting.value.title = editTitle.value
|
||||||
|
editSetting(setting)
|
||||||
|
}
|
||||||
|
|
||||||
|
function editSetting(setting) {
|
||||||
|
editTitleLoading.value = true
|
||||||
|
settingSet(setting.value).then(() => {
|
||||||
|
editTitleLoading.value = false
|
||||||
|
ElMessage({
|
||||||
|
message: "设置成功",
|
||||||
|
type: "success",
|
||||||
|
plain: true
|
||||||
|
})
|
||||||
|
if (setting.value.manyEmail === 1) {
|
||||||
|
accountStore.currentAccountId = userStore.user.accountId;
|
||||||
|
}
|
||||||
|
editTitleShow.value = false
|
||||||
|
}).catch(() => {
|
||||||
|
editTitleLoading.value = false
|
||||||
|
setting.value = JSON.parse(backup)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.box {
|
||||||
|
|
||||||
|
padding: 40px 40px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting {
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 140px;
|
||||||
|
font-weight: bold;
|
||||||
|
position: relative;
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
gap: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
>div:first-child {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
position: absolute;
|
||||||
|
left: 65px;
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-title {
|
||||||
|
font-weight: normal !important;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
div:first-child {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
margin-top: 15px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
29
mail-vue/src/views/test/index.vue
Normal file
29
mail-vue/src/views/test/index.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<el-scrollbar>
|
||||||
|
<div class="scrollbar-flex-content">
|
||||||
|
<p v-for="item in 1000" :key="item" class="scrollbar-demo-item">
|
||||||
|
{{ item }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.scrollbar-flex-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 200px 200px 200px 200px 200px 200px 200px 200px 200px 200px 200px;
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
.scrollbar-demo-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100px;
|
||||||
|
height: 50px;
|
||||||
|
margin: 10px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--el-color-danger-light-9);
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
31
mail-vue/vite.config.js
Normal file
31
mail-vue/vite.config.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {defineConfig,loadEnv} from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import path from 'path'
|
||||||
|
export default defineConfig(({mode}) => {
|
||||||
|
const env = loadEnv(mode, process.cwd(), 'VITE')
|
||||||
|
return {
|
||||||
|
server: {
|
||||||
|
host: true,
|
||||||
|
port: 3001,
|
||||||
|
hmr: true,
|
||||||
|
},
|
||||||
|
base: env.VITE_STATIC_URL || '/',
|
||||||
|
plugins: [vue()
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, 'src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: '../mail-worker/dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: () => 'all-in-one'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
assetsInclude: ['**/*.json'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
12
mail-worker/.editorconfig
Normal file
12
mail-worker/.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = tab
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.yml]
|
||||||
|
indent_style = space
|
||||||
6
mail-worker/.prettierrc
Normal file
6
mail-worker/.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 140,
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": true,
|
||||||
|
"useTabs": true
|
||||||
|
}
|
||||||
BIN
mail-worker/dist/assets/HarmonyOS_Sans_SC_Regular-D3EGA0gC.woff2
vendored
Normal file
BIN
mail-worker/dist/assets/HarmonyOS_Sans_SC_Regular-D3EGA0gC.woff2
vendored
Normal file
Binary file not shown.
1
mail-worker/dist/assets/favicon-C5dAZutX.svg
vendored
Normal file
1
mail-worker/dist/assets/favicon-C5dAZutX.svg
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><g fill="none"><path fill="#367af2" d="M2 10v12.5A4.5 4.5 0 0 0 6.5 27h19a4.5 4.5 0 0 0 4.5-4.5V10l-13.526 7.292a1 1 0 0 1-.948 0z"/><path fill="url(#fluentColorMail320)" d="M2 10v12.5A4.5 4.5 0 0 0 6.5 27h19a4.5 4.5 0 0 0 4.5-4.5V10l-13.526 7.292a1 1 0 0 1-.948 0z"/><path fill="url(#fluentColorMail321)" d="M2 10v12.5A4.5 4.5 0 0 0 6.5 27h19a4.5 4.5 0 0 0 4.5-4.5V10l-13.526 7.292a1 1 0 0 1-.948 0z"/><path fill="url(#fluentColorMail322)" fill-opacity="0.75" d="M2 10v12.5A4.5 4.5 0 0 0 6.5 27h19a4.5 4.5 0 0 0 4.5-4.5V10l-13.526 7.292a1 1 0 0 1-.948 0z"/><path fill="url(#fluentColorMail323)" fill-opacity="0.7" d="M2 10v12.5A4.5 4.5 0 0 0 6.5 27h19a4.5 4.5 0 0 0 4.5-4.5V10l-13.526 7.292a1 1 0 0 1-.948 0z"/><path fill="url(#fluentColorMail324)" d="M6.5 5A4.5 4.5 0 0 0 2 9.5v1.09l13.526 7.292a1 1 0 0 0 .948 0L30 10.59V9.5A4.5 4.5 0 0 0 25.5 5z"/><defs><linearGradient id="fluentColorMail320" x1="19.555" x2="26.862" y1="13.332" y2="27.873" gradientUnits="userSpaceOnUse"><stop offset=".199" stop-color="#0094f0" stop-opacity="0"/><stop offset=".431" stop-color="#0094f0"/></linearGradient><linearGradient id="fluentColorMail321" x1="12" x2="4.914" y1="11.79" y2="28.328" gradientUnits="userSpaceOnUse"><stop offset=".191" stop-color="#0094f0" stop-opacity="0"/><stop offset=".431" stop-color="#0094f0"/></linearGradient><linearGradient id="fluentColorMail322" x1="23.383" x2="24.532" y1="20.142" y2="28.575" gradientUnits="userSpaceOnUse"><stop stop-color="#2764e7" stop-opacity="0"/><stop offset="1" stop-color="#2764e7"/></linearGradient><linearGradient id="fluentColorMail323" x1="20.333" x2="22.43" y1="12.088" y2="29.25" gradientUnits="userSpaceOnUse"><stop offset=".533" stop-color="#ff6ce8" stop-opacity="0"/><stop offset="1" stop-color="#ff6ce8"/></linearGradient><linearGradient id="fluentColorMail324" x1="10.318" x2="18.903" y1=".976" y2="23.436" gradientUnits="userSpaceOnUse"><stop stop-color="#6ce0ff"/><stop offset=".462" stop-color="#29c3ff"/><stop offset="1" stop-color="#4894fe"/></linearGradient></defs></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
66
mail-worker/dist/assets/index-CBfzcbRL.js
vendored
Normal file
66
mail-worker/dist/assets/index-CBfzcbRL.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
mail-worker/dist/assets/index-Co3gDtCe.css
vendored
Normal file
1
mail-worker/dist/assets/index-Co3gDtCe.css
vendored
Normal file
File diff suppressed because one or more lines are too long
15
mail-worker/dist/index.html
vendored
Normal file
15
mail-worker/dist/index.html
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title></title>
|
||||||
|
<link rel="icon" href="/assets/favicon-C5dAZutX.svg" type="image/svg+xml">
|
||||||
|
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||||
|
<script type="module" crossorigin src="/assets/index-CBfzcbRL.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-Co3gDtCe.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
|
||||||
|
</body>
|
||||||
1
mail-worker/dist/vite.svg
vendored
Normal file
1
mail-worker/dist/vite.svg
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
4311
mail-worker/package-lock.json
generated
Normal file
4311
mail-worker/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
mail-worker/package.json
Normal file
24
mail-worker/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "mail-cf",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "wrangler dev --config wrangler-dev.toml",
|
||||||
|
"test": "wrangler deploy --config wrangler-test.toml",
|
||||||
|
"deploy": "wrangler deploy",
|
||||||
|
"start": "wrangler dev"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@cloudflare/vitest-pool-workers": "^0.7.5",
|
||||||
|
"vitest": "~3.0.7",
|
||||||
|
"wrangler": "^4.7.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@cloudflare/vite-plugin": "^1.0.5",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
|
"drizzle-orm": "^0.42.0",
|
||||||
|
"hono": "^4.7.5",
|
||||||
|
"postal-mime": "^2.4.3",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
mail-worker/src/api/account-api.js
Normal file
20
mail-worker/src/api/account-api.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import app from '../hono/hono';
|
||||||
|
import accountService from '../service/account-service';
|
||||||
|
import result from '../model/result';
|
||||||
|
import userContext from '../security/user-context';
|
||||||
|
|
||||||
|
app.get('/account/list', async (c) => {
|
||||||
|
const list = await accountService.list(c, c.req.query(), await userContext.getUserId(c));
|
||||||
|
return c.json(result.ok(list));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/account/delete', async (c) => {
|
||||||
|
await accountService.delete(c, c.req.query(), await userContext.getUserId(c));
|
||||||
|
return c.json(result.ok());
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/account/add', async (c) => {
|
||||||
|
const account = await accountService.add(c, await c.req.json(), await userContext.getUserId(c));
|
||||||
|
return c.json(result.ok(account));
|
||||||
|
});
|
||||||
|
|
||||||
26
mail-worker/src/api/email-api.js
Normal file
26
mail-worker/src/api/email-api.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import app from '../hono/hono';
|
||||||
|
import emailService from '../service/email-service';
|
||||||
|
import result from '../model/result';
|
||||||
|
import userContext from '../security/user-context';
|
||||||
|
import attService from '../service/att-service';
|
||||||
|
|
||||||
|
app.get('/email/list', async (c) => {
|
||||||
|
const data = await emailService.list(c, c.req.query(), await userContext.getUserId(c));
|
||||||
|
return c.json(result.ok(data));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/email/latest',async (c) => {
|
||||||
|
const list = await emailService.latest(c, c.req.query(), await userContext.getUserId(c));
|
||||||
|
return c.json(result.ok(list));
|
||||||
|
})
|
||||||
|
|
||||||
|
app.delete('/email/delete', async (c) => {
|
||||||
|
await emailService.delete(c, c.req.query(), await userContext.getUserId(c));
|
||||||
|
return c.json(result.ok());
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/email/attList', async (c) => {
|
||||||
|
const attList = await attService.list(c, c.req.query(), await userContext.getUserId(c));
|
||||||
|
return c.json(result.ok(attList));
|
||||||
|
})
|
||||||
|
|
||||||
19
mail-worker/src/api/login-api.js
Normal file
19
mail-worker/src/api/login-api.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import app from '../hono/hono';
|
||||||
|
import loginService from '../service/login-service';
|
||||||
|
import result from '../model/result';
|
||||||
|
import userContext from '../security/user-context';
|
||||||
|
|
||||||
|
app.post('/login', async (c) => {
|
||||||
|
const token = await loginService.login(c, await c.req.json());
|
||||||
|
return c.json(result.ok({ token: token }));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/register', async (c) => {
|
||||||
|
const jwt = await loginService.register(c, await c.req.json());
|
||||||
|
return c.json(result.ok(jwt));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/logout', async (c) => {
|
||||||
|
await loginService.logout(c, await userContext.getUserId(c));
|
||||||
|
return c.json(result.ok());
|
||||||
|
});
|
||||||
13
mail-worker/src/api/setting-api.js
Normal file
13
mail-worker/src/api/setting-api.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import app from '../hono/hono';
|
||||||
|
import result from '../model/result';
|
||||||
|
import settingService from '../service/setting-service';
|
||||||
|
|
||||||
|
app.put('/setting/set', async (c) => {
|
||||||
|
await settingService.set(c, await c.req.json());
|
||||||
|
return c.json(result.ok());
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/setting/query', async (c) => {
|
||||||
|
const setting = await settingService.query(c);
|
||||||
|
return c.json(result.ok(setting));
|
||||||
|
})
|
||||||
19
mail-worker/src/api/star-api.js
Normal file
19
mail-worker/src/api/star-api.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import app from '../hono/hono';
|
||||||
|
import startService from '../service/star-service';
|
||||||
|
import userContext from '../security/user-context';
|
||||||
|
import result from '../model/result';
|
||||||
|
|
||||||
|
app.post('/star/add', async (c) => {
|
||||||
|
await startService.add(c, await c.req.json(), await userContext.getUserId(c));
|
||||||
|
return c.json(result.ok());
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/star/list', async (c) => {
|
||||||
|
const data = await startService.list(c, c.req.query(), await userContext.getUserId(c));
|
||||||
|
return c.json(result.ok(data));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/star/cancel', async (c) => {
|
||||||
|
await startService.cancel(c, await c.req.query(), await userContext.getUserId(c));
|
||||||
|
return c.json(result.ok());
|
||||||
|
});
|
||||||
1
mail-worker/src/api/test-api.js
Normal file
1
mail-worker/src/api/test-api.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import app from '../hono/hono';
|
||||||
23
mail-worker/src/api/user-api.js
Normal file
23
mail-worker/src/api/user-api.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import app from '../hono/hono';
|
||||||
|
import userService from '../service/user-service';
|
||||||
|
import result from '../model/result';
|
||||||
|
import userContext from '../security/user-context';
|
||||||
|
|
||||||
|
app.get('/user/loginUserInfo', async (c) => {
|
||||||
|
const user = await userService.loginUserInfo(c, await userContext.getUserId(c));
|
||||||
|
return c.json(result.ok(user));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/user/resetPassword', async (c) => {
|
||||||
|
await userService.resetPassword(c, await c.req.json(), await userContext.getUserId(c));
|
||||||
|
return c.json(result.ok());
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/user/delete', async (c) => {
|
||||||
|
await userService.delete(c, await userContext.getUserId(c));
|
||||||
|
return c.json(result.ok());
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
10
mail-worker/src/const/constant.js
Normal file
10
mail-worker/src/const/constant.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const constant = {
|
||||||
|
|
||||||
|
TOKEN_HEADER: 'Authorization',
|
||||||
|
JWT_UID: 'user_id:',
|
||||||
|
JWT_TOKEN: 'token:',
|
||||||
|
TOKEN_EXPIRE: 60 * 60 * 24 * 30,
|
||||||
|
ATTACHMENT_PREFIX: 'attachments/'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default constant
|
||||||
42
mail-worker/src/const/entity-const.js
Normal file
42
mail-worker/src/const/entity-const.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export const userConst = {
|
||||||
|
type: {
|
||||||
|
COMMON: 1,
|
||||||
|
ADMIN: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settingConst = {
|
||||||
|
register: {
|
||||||
|
OPEN: 0,
|
||||||
|
CLOSE: 1,
|
||||||
|
},
|
||||||
|
receive: {
|
||||||
|
OPEN: 0,
|
||||||
|
CLOSE: 1,
|
||||||
|
},
|
||||||
|
send: {
|
||||||
|
OPEN: 0,
|
||||||
|
CLOSE: 1,
|
||||||
|
},
|
||||||
|
addEmail: {
|
||||||
|
OPEN: 0,
|
||||||
|
CLOSE: 1,
|
||||||
|
},
|
||||||
|
manyEmail: {
|
||||||
|
OPEN: 0,
|
||||||
|
CLOSE: 1,
|
||||||
|
},
|
||||||
|
registerVerify: {
|
||||||
|
OPEN: 0,
|
||||||
|
CLOSE: 1,
|
||||||
|
},
|
||||||
|
addEmailVerify: {
|
||||||
|
OPEN: 0,
|
||||||
|
CLOSE: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isDel = {
|
||||||
|
DELETE: 1,
|
||||||
|
NORMAL: 0
|
||||||
|
}
|
||||||
6
mail-worker/src/const/kv-const.js
Normal file
6
mail-worker/src/const/kv-const.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
const KvConst = {
|
||||||
|
AUTH_INFO: 'auth-uid:',
|
||||||
|
SETTING: 'setting:',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KvConst;
|
||||||
77
mail-worker/src/email/email.js
Normal file
77
mail-worker/src/email/email.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import PostalMime from 'postal-mime';
|
||||||
|
import emailService from '../service/email-service';
|
||||||
|
import accountService from '../service/account-service';
|
||||||
|
import { isDel } from '../const/entity-const';
|
||||||
|
import settingService from '../service/setting-service';
|
||||||
|
import attService from '../service/att-service';
|
||||||
|
import r2Service from '../service/r2-service';
|
||||||
|
import constant from '../const/constant';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import fileUtils from '../utils/file-utils';
|
||||||
|
|
||||||
|
export async function email(message, env, ctx) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
if (!await settingService.isReceive({ env })) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = await accountService.selectByEmailIncludeDel({ env: env }, message.to);
|
||||||
|
|
||||||
|
const reader = message.raw.getReader();
|
||||||
|
let content = '';
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
content += new TextDecoder().decode(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = await PostalMime.parse(content);
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
sendEmail: email.from.address,
|
||||||
|
name: email.from.name,
|
||||||
|
receiveEmail: message.to,
|
||||||
|
subject: email.subject,
|
||||||
|
content: email.html,
|
||||||
|
text: email.text,
|
||||||
|
userId: account.userId,
|
||||||
|
accountId: account.accountId
|
||||||
|
};
|
||||||
|
|
||||||
|
const emailRow = await emailService.receive({ env }, params);
|
||||||
|
|
||||||
|
|
||||||
|
if (email.attachments.length > 0) {
|
||||||
|
|
||||||
|
let attachments = email.attachments.map(item => {
|
||||||
|
let attachment = { ...item };
|
||||||
|
attachment.emailId = emailRow.emailId;
|
||||||
|
attachment.userId = emailRow.userId;
|
||||||
|
attachment.accountId = emailRow.accountId;
|
||||||
|
attachment.key = constant.ATTACHMENT_PREFIX + uuidv4() + fileUtils.getExtFileName(item.filename);
|
||||||
|
attachment.size = item.content.length ?? item.content.byteLength;
|
||||||
|
return attachment;
|
||||||
|
});
|
||||||
|
|
||||||
|
await attService.addAtt({ env }, attachments);
|
||||||
|
|
||||||
|
if (!env.r2) {
|
||||||
|
console.log('r2对象存储未配置, 附件取消保存');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let attachment of attachments) {
|
||||||
|
await r2Service.putObj({ env }, attachment.key, attachment.content, {
|
||||||
|
contentType: attachment.mimeType,
|
||||||
|
contentDisposition: `attachment; filename="${attachment.filename}"`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('邮件接收异常: ', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
mail-worker/src/entity/account.js
Normal file
12
mail-worker/src/entity/account.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { sqliteTable, text, integer} from 'drizzle-orm/sqlite-core';
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
export const account = sqliteTable('account', {
|
||||||
|
accountId: integer('account_id').primaryKey({ autoIncrement: true }),
|
||||||
|
email: text('email').notNull(),
|
||||||
|
status: integer('status').default(0).notNull(),
|
||||||
|
latestEmailTime: text('latest_email_time'),
|
||||||
|
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`),
|
||||||
|
userId: integer('user_id').notNull(),
|
||||||
|
isDel: integer('is_del').default(0).notNull(),
|
||||||
|
});
|
||||||
|
export default account
|
||||||
19
mail-worker/src/entity/att.js
Normal file
19
mail-worker/src/entity/att.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
|
||||||
|
export const att = sqliteTable('attachments', {
|
||||||
|
attId: integer('att_id').primaryKey({ autoIncrement: true }),
|
||||||
|
userId: integer('user_id').notNull(),
|
||||||
|
emailId: integer('email_id').notNull(),
|
||||||
|
accountId: integer('account_id').notNull(),
|
||||||
|
key: text('key').notNull(),
|
||||||
|
filename: text('filename'),
|
||||||
|
mimeType: text('mime_type'),
|
||||||
|
size: integer('size'),
|
||||||
|
disposition: text('disposition'),
|
||||||
|
related: text('related'),
|
||||||
|
contentId: text('content_id'),
|
||||||
|
encoding: text('encoding'),
|
||||||
|
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
16
mail-worker/src/entity/email.js
Normal file
16
mail-worker/src/entity/email.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { sqliteTable, text, integer} from 'drizzle-orm/sqlite-core';
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
export const email = sqliteTable('email', {
|
||||||
|
emailId: integer('email_id').primaryKey({ autoIncrement: true }),
|
||||||
|
sendEmail: text('send_email'),
|
||||||
|
name: text('name'),
|
||||||
|
receiveEmail: text('receive_email').notNull(),
|
||||||
|
accountId: integer('account_id').notNull(),
|
||||||
|
userId: integer('user_id').notNull(),
|
||||||
|
subject: text('subject'),
|
||||||
|
text: text('text'),
|
||||||
|
content: text('content'),
|
||||||
|
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||||
|
isDel: integer('is_del').default(0).notNull()
|
||||||
|
});
|
||||||
|
export default email
|
||||||
5
mail-worker/src/entity/orm.js
Normal file
5
mail-worker/src/entity/orm.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { drizzle } from 'drizzle-orm/d1';
|
||||||
|
|
||||||
|
export default function orm(c) {
|
||||||
|
return drizzle(c.env.db,{logger: false})
|
||||||
|
}
|
||||||
12
mail-worker/src/entity/setting.js
Normal file
12
mail-worker/src/entity/setting.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { sqliteTable, text, integer} from 'drizzle-orm/sqlite-core';
|
||||||
|
export const setting = sqliteTable('setting', {
|
||||||
|
register: integer('register').default(0).notNull(),
|
||||||
|
receive: integer('receive').default(0).notNull(),
|
||||||
|
title: text('title').default('').notNull(),
|
||||||
|
manyEmail: integer('many_email').default(1).notNull(),
|
||||||
|
addEmail: integer('add_email').default(0).notNull(),
|
||||||
|
autoRefreshTime: integer('auto_refresh_time').default(0).notNull(),
|
||||||
|
registerVerify: integer('register_verify').default(1).notNull(),
|
||||||
|
addEmailVerify: integer('add_email_verify').default(1).notNull()
|
||||||
|
});
|
||||||
|
export default setting
|
||||||
11
mail-worker/src/entity/star.js
Normal file
11
mail-worker/src/entity/star.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
|
||||||
|
export const star = sqliteTable('star', {
|
||||||
|
starId: integer('star_id').primaryKey({ autoIncrement: true }),
|
||||||
|
userId: integer('user_id').notNull(),
|
||||||
|
emailId: integer('email_id').notNull(),
|
||||||
|
createTime: text('create_time')
|
||||||
|
.notNull()
|
||||||
|
.default(sql`CURRENT_TIMESTAMP`),
|
||||||
|
});
|
||||||
14
mail-worker/src/entity/user.js
Normal file
14
mail-worker/src/entity/user.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { sqliteTable, text, integer} from 'drizzle-orm/sqlite-core';
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
const user = sqliteTable('user', {
|
||||||
|
userId: integer('user_id').primaryKey({ autoIncrement: true }),
|
||||||
|
email: text('email').notNull(),
|
||||||
|
type: integer('type').default(1).notNull(),
|
||||||
|
password: text('password').notNull(),
|
||||||
|
salt: text('salt').notNull(),
|
||||||
|
status: integer('status').default(0).notNull(),
|
||||||
|
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`),
|
||||||
|
activeTime: text('active_time'),
|
||||||
|
isDel: integer('is_del').default(0).notNull(),
|
||||||
|
});
|
||||||
|
export default user
|
||||||
9
mail-worker/src/error/biz-error.js
Normal file
9
mail-worker/src/error/biz-error.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
class BizError extends Error {
|
||||||
|
constructor(message, code) {
|
||||||
|
super(message);
|
||||||
|
this.code = code ? code : 501;
|
||||||
|
this.name = 'BizError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BizError;
|
||||||
50
mail-worker/src/hono/hono.js
Normal file
50
mail-worker/src/hono/hono.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Hono } from 'hono';
|
||||||
|
|
||||||
|
const app = new Hono();
|
||||||
|
import initDB from '../init/init-db';
|
||||||
|
import initCache from '../init/init-cache';
|
||||||
|
import verify from '../security/security';
|
||||||
|
import result from '../model/result';
|
||||||
|
import { cors } from 'hono/cors';
|
||||||
|
|
||||||
|
app.use('*', cors());
|
||||||
|
|
||||||
|
let initStatus = true;
|
||||||
|
app.use('*', async (c, next) => {
|
||||||
|
if (initStatus) {
|
||||||
|
await initDB(c);
|
||||||
|
initStatus = false;
|
||||||
|
}
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
let initCacheStatus = true;
|
||||||
|
app.use('*', async (c, next) => {
|
||||||
|
if (initCacheStatus) {
|
||||||
|
await initCache(c);
|
||||||
|
initCacheStatus = false;
|
||||||
|
}
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
app.use('*', async (c, next) => {
|
||||||
|
if(await verify(c)) {
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
app.onError((err, c) => {
|
||||||
|
if (err.name === 'BizError') {
|
||||||
|
console.log(err.message);
|
||||||
|
}else {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
return c.json(result.fail(err.message, err.code));
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
|
|
||||||
|
|
||||||
9
mail-worker/src/hono/webs.js
Normal file
9
mail-worker/src/hono/webs.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import app from './hono';
|
||||||
|
import '../api/email-api';
|
||||||
|
import '../api/user-api';
|
||||||
|
import '../api/login-api';
|
||||||
|
import '../api/setting-api';
|
||||||
|
import '../api/account-api';
|
||||||
|
import '../api/star-api';
|
||||||
|
import '../api/test-api';
|
||||||
|
export default app;
|
||||||
17
mail-worker/src/index.js
Normal file
17
mail-worker/src/index.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import app from './hono/webs';
|
||||||
|
import { email } from './email/email';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fetch(req, env, ctx) {
|
||||||
|
const url = new URL(req.url)
|
||||||
|
console.log(url.pathname)
|
||||||
|
if (url.pathname.startsWith('/api/')) {
|
||||||
|
url.pathname = url.pathname.replace('/api', '')
|
||||||
|
req = new Request(url.toString(), req)
|
||||||
|
return app.fetch(req, env, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return env.ASSETS.fetch(req);
|
||||||
|
},
|
||||||
|
email: email
|
||||||
|
};
|
||||||
4
mail-worker/src/init/init-cache.js
Normal file
4
mail-worker/src/init/init-cache.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import settingService from '../service/setting-service';
|
||||||
|
export default async function initCache(c) {
|
||||||
|
await settingService.refresh(c)
|
||||||
|
}
|
||||||
96
mail-worker/src/init/init-db.js
Normal file
96
mail-worker/src/init/init-db.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
export default async function initDB(c) {
|
||||||
|
await c.env.db.prepare(`
|
||||||
|
CREATE TABLE IF NOT EXISTS email (
|
||||||
|
email_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
send_email TEXT,
|
||||||
|
name TEXT,
|
||||||
|
receive_email TEXT NOT NULL,
|
||||||
|
account_id INTEGER NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
subject TEXT,
|
||||||
|
content TEXT,
|
||||||
|
text TEXT,
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
is_del INTEGER DEFAULT 0 NOT NULL
|
||||||
|
)
|
||||||
|
`).run();
|
||||||
|
|
||||||
|
await c.env.db.prepare(`
|
||||||
|
CREATE TABLE IF NOT EXISTS star (
|
||||||
|
star_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
email_id INTEGER NOT NULL,
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
)
|
||||||
|
`).run();
|
||||||
|
|
||||||
|
await c.env.db.prepare(`
|
||||||
|
CREATE TABLE IF NOT EXISTS attachments (
|
||||||
|
att_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
email_id INTEGER NOT NULL,
|
||||||
|
account_id INTEGER NOT NULL,
|
||||||
|
key TEXT NOT NULL,
|
||||||
|
filename TEXT,
|
||||||
|
mime_type TEXT,
|
||||||
|
size INTEGER,
|
||||||
|
disposition TEXT,
|
||||||
|
related TEXT,
|
||||||
|
content_id TEXT,
|
||||||
|
encoding TEXT,
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
)
|
||||||
|
`).run();
|
||||||
|
|
||||||
|
await c.env.db.prepare(`
|
||||||
|
CREATE TABLE IF NOT EXISTS user (
|
||||||
|
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
type INTEGER DEFAULT 1 NOT NULL,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
salt TEXT NOT NULL,
|
||||||
|
status INTEGER DEFAULT 0 NOT NULL,
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
active_time DATETIME,
|
||||||
|
is_del INTEGER DEFAULT 0 NOT NULL
|
||||||
|
)
|
||||||
|
`).run();
|
||||||
|
|
||||||
|
await c.env.db.prepare(`
|
||||||
|
CREATE TABLE IF NOT EXISTS account (
|
||||||
|
account_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
status INTEGER DEFAULT 0 NOT NULL,
|
||||||
|
latest_email_time DATETIME,
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
is_del INTEGER DEFAULT 0 NOT NULL
|
||||||
|
)
|
||||||
|
`).run();
|
||||||
|
|
||||||
|
await c.env.db.prepare(`
|
||||||
|
CREATE TABLE IF NOT EXISTS setting (
|
||||||
|
register INTEGER NOT NULL,
|
||||||
|
receive INTEGER NOT NULL,
|
||||||
|
add_email INTEGER NOT NULL,
|
||||||
|
many_email INTEGER NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
auto_refresh_time INTEGER NOT NULL,
|
||||||
|
register_verify INTEGER NOT NULL,
|
||||||
|
add_email_verify INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
`).run();
|
||||||
|
|
||||||
|
await c.env.db.prepare(`
|
||||||
|
INSERT INTO setting (register, receive,add_email,many_email,title,auto_refresh_time,register_verify,add_email_verify)
|
||||||
|
SELECT 0, 0, 0, 1, 'Cloud 邮箱', 0, 1, 1
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM setting)
|
||||||
|
`).run();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
9
mail-worker/src/model/result.js
Normal file
9
mail-worker/src/model/result.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const result = {
|
||||||
|
ok(data) {
|
||||||
|
return { code: 200, message: 'success', data: data ? data : null };
|
||||||
|
},
|
||||||
|
fail(message, code = 500) {
|
||||||
|
return { code, message };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default result;
|
||||||
60
mail-worker/src/security/security.js
Normal file
60
mail-worker/src/security/security.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import BizError from '../error/biz-error';
|
||||||
|
import constant from '../const/constant';
|
||||||
|
import jwtUtils from '../utils/jwt-utils';
|
||||||
|
import KvConst from '../const/kv-const';
|
||||||
|
import { userConst } from '../const/entity-const';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const exclude = [
|
||||||
|
'/login',
|
||||||
|
'/register',
|
||||||
|
'/setting/query'
|
||||||
|
];
|
||||||
|
|
||||||
|
const adminPerm = [
|
||||||
|
'/setting/set'
|
||||||
|
]
|
||||||
|
|
||||||
|
const verify = async (c) => {
|
||||||
|
|
||||||
|
if (c.req.path.startsWith('/test')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exclude.includes(c.req.path)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jwt = c.req.header(constant.TOKEN_HEADER);
|
||||||
|
|
||||||
|
const result = await jwtUtils.verifyToken(c, jwt)
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new BizError('身份认证失效,请重新登录',401)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {userId, token} = result
|
||||||
|
const authInfo = await c.env.kv.get(KvConst.AUTH_INFO + userId,{ type: 'json' });
|
||||||
|
|
||||||
|
if (!authInfo) {
|
||||||
|
throw new BizError('身份认证失效,请重新登录',401)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!authInfo.tokens.includes(token)) {
|
||||||
|
throw new BizError('身份认证失效,请重新登录',401)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (adminPerm.includes(c.req.path) && c.env.admin !== authInfo.user.email) {
|
||||||
|
throw new BizError('权限不足',403)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dayjs().diff(dayjs(authInfo.createTime), 'day') >= 1) {
|
||||||
|
authInfo.refreshTime = new Date().toISOString()
|
||||||
|
await c.env.kv.put(KvConst.AUTH_INFO + userId, JSON.stringify(authInfo), { expirationTtl: constant.TOKEN_EXPIRE });
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default verify;
|
||||||
16
mail-worker/src/security/user-context.js
Normal file
16
mail-worker/src/security/user-context.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import JwtUtils from '../utils/jwt-utils';
|
||||||
|
import constant from '../const/constant';
|
||||||
|
|
||||||
|
const userContext = {
|
||||||
|
async getUserId(c) {
|
||||||
|
const jwt = c.req.header(constant.TOKEN_HEADER);
|
||||||
|
const { userId } = await JwtUtils.verifyToken(c, jwt);
|
||||||
|
return Number(userId);
|
||||||
|
},
|
||||||
|
async getToken(c) {
|
||||||
|
const jwt = c.req.header(constant.TOKEN_HEADER);
|
||||||
|
const { token } = JwtUtils.verifyToken(c,jwt);
|
||||||
|
return token;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default userContext;
|
||||||
127
mail-worker/src/service/account-service.js
Normal file
127
mail-worker/src/service/account-service.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import BizError from '../error/biz-error';
|
||||||
|
import verifyUtils from '../utils/verify-utils';
|
||||||
|
import emailUtils from '../utils/email-utils';
|
||||||
|
import userService from './user-service';
|
||||||
|
import emailService from './email-service';
|
||||||
|
import orm from '../entity/orm';
|
||||||
|
import account from '../entity/account';
|
||||||
|
import { and, asc, eq, gt } from 'drizzle-orm';
|
||||||
|
import { isDel } from '../const/entity-const';
|
||||||
|
import settingService from './setting-service';
|
||||||
|
import turnstileService from './turnstile-service';
|
||||||
|
|
||||||
|
const accountService = {
|
||||||
|
|
||||||
|
async add(c, params, userId) {
|
||||||
|
|
||||||
|
|
||||||
|
if (!await settingService.isAddEmail(c)) {
|
||||||
|
throw new BizError('添加邮箱功能已关闭');
|
||||||
|
}
|
||||||
|
|
||||||
|
let { email, token } = params;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
throw new BizError('邮箱不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verifyUtils.isEmail(email)) {
|
||||||
|
throw new BizError('非法邮箱');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!c.env.domain.includes(emailUtils.getDomain(email))) {
|
||||||
|
throw new BizError('未配置改邮箱域名');
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountRow = await this.selectByEmailIncludeDel(c, email);
|
||||||
|
|
||||||
|
if (accountRow && accountRow.isDel === isDel.DELETE) {
|
||||||
|
throw new BizError('该邮箱已被注销');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accountRow) {
|
||||||
|
throw new BizError('该邮箱已被注册');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await settingService.isAddEmailVerify(c)) {
|
||||||
|
await turnstileService.verify(c, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return orm(c).insert(account).values({ email: email, userId: userId }).returning().get();
|
||||||
|
},
|
||||||
|
|
||||||
|
selectByEmailIncludeDel(c, email) {
|
||||||
|
return orm(c).select().from(account).where(eq(account.email, email)).get();
|
||||||
|
},
|
||||||
|
|
||||||
|
selectByEmail(c, email) {
|
||||||
|
return orm(c).select().from(account).where(
|
||||||
|
and(
|
||||||
|
eq(account.email, email),
|
||||||
|
eq(account.isDel, isDel.NORMAL)))
|
||||||
|
.get();
|
||||||
|
},
|
||||||
|
|
||||||
|
list(c, params, userId) {
|
||||||
|
|
||||||
|
let { accountId, size } = params;
|
||||||
|
|
||||||
|
accountId = Number(accountId);
|
||||||
|
size = Number(size);
|
||||||
|
|
||||||
|
if (size > 30) {
|
||||||
|
size = 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accountId) {
|
||||||
|
accountId = 0;
|
||||||
|
}
|
||||||
|
return orm(c).select().from(account).where(
|
||||||
|
and(
|
||||||
|
eq(account.userId, userId),
|
||||||
|
eq(account.isDel, isDel.NORMAL),
|
||||||
|
gt(account.accountId, accountId)))
|
||||||
|
.orderBy(asc(account.accountId))
|
||||||
|
.limit(size)
|
||||||
|
.all();
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(c, params, userId) {
|
||||||
|
|
||||||
|
let { accountId } = params;
|
||||||
|
|
||||||
|
const user = await userService.selectById(c, userId);
|
||||||
|
const accountRow = await this.selectById(c, accountId);
|
||||||
|
|
||||||
|
if (accountRow.email === user.email) {
|
||||||
|
throw new BizError('不可以删除自己的邮箱');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accountRow.userId !== user.userId) {
|
||||||
|
throw new BizError('该邮箱不属于当前用户');
|
||||||
|
}
|
||||||
|
|
||||||
|
await orm(c).update(account).set({ isDel: isDel.DELETE }).where(
|
||||||
|
and(eq(account.userId, userId),
|
||||||
|
eq(account.accountId, accountId)))
|
||||||
|
.run();
|
||||||
|
await emailService.removeByAccountId(c, accountId);
|
||||||
|
},
|
||||||
|
|
||||||
|
selectById(c, accountId) {
|
||||||
|
return orm(c).select().from(account).where(
|
||||||
|
and(eq(account.accountId, accountId),
|
||||||
|
eq(account.isDel, isDel.NORMAL)))
|
||||||
|
.get();
|
||||||
|
},
|
||||||
|
|
||||||
|
async insert(c, params) {
|
||||||
|
await orm(c).insert(account).values({ ...params }).returning();
|
||||||
|
},
|
||||||
|
|
||||||
|
async removeByUserId(c, userId) {
|
||||||
|
await orm(c).update(account).set({ isDel: isDel.DELETE }).where(eq(account.userId, userId)).run();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default accountService;
|
||||||
22
mail-worker/src/service/att-service.js
Normal file
22
mail-worker/src/service/att-service.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import orm from '../entity/orm';
|
||||||
|
import { att } from '../entity/att';
|
||||||
|
import { and, eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
const attService = {
|
||||||
|
async addAtt(c, params) {
|
||||||
|
await orm(c).insert(att).values(params).run();
|
||||||
|
},
|
||||||
|
async list(c, params, userId) {
|
||||||
|
const { emailId } = params;
|
||||||
|
|
||||||
|
const list = await orm(c).select().from(att).where(
|
||||||
|
and(
|
||||||
|
eq(att.emailId, emailId),
|
||||||
|
eq(att.userId, userId)))
|
||||||
|
.all();
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default attService;
|
||||||
102
mail-worker/src/service/email-service.js
Normal file
102
mail-worker/src/service/email-service.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import orm from '../entity/orm';
|
||||||
|
import email from '../entity/email';
|
||||||
|
import { isDel } from '../const/entity-const';
|
||||||
|
import { and, desc, eq, gt, inArray, lt, sql } from 'drizzle-orm';
|
||||||
|
import { star } from '../entity/star';
|
||||||
|
|
||||||
|
const emailService = {
|
||||||
|
|
||||||
|
async list(c, params, userId) {
|
||||||
|
|
||||||
|
let { emailId, accountId, size } = params;
|
||||||
|
size = Number(size);
|
||||||
|
emailId = Number(emailId);
|
||||||
|
|
||||||
|
if (size > 30) {
|
||||||
|
size = 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emailId) {
|
||||||
|
emailId = 9999999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = await orm(c)
|
||||||
|
.select({
|
||||||
|
...email,
|
||||||
|
starId: star.starId
|
||||||
|
})
|
||||||
|
.from(email)
|
||||||
|
.leftJoin(
|
||||||
|
star,
|
||||||
|
and(
|
||||||
|
eq(star.emailId, email.emailId),
|
||||||
|
eq(star.userId, userId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
lt(email.emailId, emailId),
|
||||||
|
eq(email.accountId, accountId),
|
||||||
|
eq(email.userId, userId),
|
||||||
|
eq(email.isDel, isDel.NORMAL)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(desc(email.emailId))
|
||||||
|
.limit(size)
|
||||||
|
.all();
|
||||||
|
|
||||||
|
const resultList = list.map(item => ({
|
||||||
|
...item,
|
||||||
|
isStar: item.starId != null ? 1 : 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { list: resultList };
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(c, params, userId) {
|
||||||
|
const { emailIds } = params;
|
||||||
|
const emailIdList = emailIds.split(',').map(Number);
|
||||||
|
await orm(c).update(email).set({ isDel: isDel.DELETE }).where(
|
||||||
|
and(
|
||||||
|
eq(email.userId, userId),
|
||||||
|
inArray(email.emailId, emailIdList)))
|
||||||
|
.run();
|
||||||
|
},
|
||||||
|
|
||||||
|
receive(c, params) {
|
||||||
|
return orm(c).insert(email).values({ ...params }).returning().get();
|
||||||
|
},
|
||||||
|
|
||||||
|
async removeByAccountId(c, accountId) {
|
||||||
|
await orm(c).update(email)
|
||||||
|
.set({ isDel: isDel.DELETE })
|
||||||
|
.where(eq(email.accountId, accountId))
|
||||||
|
.run();
|
||||||
|
},
|
||||||
|
|
||||||
|
async removeByUserId(c, userId) {
|
||||||
|
await orm(c).update(email).set({ isDel: isDel.DELETE }).where(eq(email.userId, userId)).run();
|
||||||
|
},
|
||||||
|
selectById(c, emailId) {
|
||||||
|
return orm(c).select().from(email).where(
|
||||||
|
and(eq(email.emailId, emailId),
|
||||||
|
eq(email.isDel, isDel.NORMAL)))
|
||||||
|
.get();
|
||||||
|
},
|
||||||
|
|
||||||
|
latest(c, params, userId) {
|
||||||
|
let { emailId,accountId } = params;
|
||||||
|
return orm(c).select().from(email).where(
|
||||||
|
and(
|
||||||
|
eq(email.userId, userId),
|
||||||
|
eq(email.isDel, isDel.NORMAL),
|
||||||
|
eq(email.accountId, accountId),
|
||||||
|
gt(email.emailId, emailId)
|
||||||
|
))
|
||||||
|
.orderBy(desc(email.emailId))
|
||||||
|
.limit(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default emailService;
|
||||||
113
mail-worker/src/service/login-service.js
Normal file
113
mail-worker/src/service/login-service.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import BizError from '../error/biz-error';
|
||||||
|
import userService from './user-service';
|
||||||
|
import emailUtils from '../utils/email-utils';
|
||||||
|
import { isDel, userConst } from '../const/entity-const';
|
||||||
|
import JwtUtils from '../utils/jwt-utils';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import KvConst from '../const/kv-const';
|
||||||
|
import constant from '../const/constant';
|
||||||
|
import userContext from '../security/user-context';
|
||||||
|
import verifyUtils from '../utils/verify-utils';
|
||||||
|
import accountService from './account-service';
|
||||||
|
import settingService from './setting-service';
|
||||||
|
import saltHashUtils from '../utils/crypto-utils';
|
||||||
|
import cryptoUtils from '../utils/crypto-utils';
|
||||||
|
import turnstileService from './turnstile-service';
|
||||||
|
|
||||||
|
|
||||||
|
const loginService = {
|
||||||
|
|
||||||
|
async register(c, params) {
|
||||||
|
|
||||||
|
const { email, password, token } = params;
|
||||||
|
|
||||||
|
if (!await settingService.isRegister(c)) {
|
||||||
|
throw new BizError('注册功能已关闭');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verifyUtils.isEmail(email)) {
|
||||||
|
throw new BizError('非法邮箱');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
throw new BizError('密码必须大于6位');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!c.env.domain.includes(emailUtils.getDomain(email))) {
|
||||||
|
throw new BizError('非法邮箱域名');
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRow = await userService.selectByEmailIncludeDel(c, email);
|
||||||
|
|
||||||
|
if (userRow && userRow.isDel === isDel.DELETE) {
|
||||||
|
throw new BizError('该邮箱已被注销');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userRow) {
|
||||||
|
throw new BizError('该邮箱已被注册');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await settingService.isRegisterVerify(c)) {
|
||||||
|
await turnstileService.verify(c,token)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { salt, hash } = await saltHashUtils.hashPassword(password);
|
||||||
|
|
||||||
|
const userId = await userService.insert(c, { email, password: hash, salt, type: userConst.type.COMMON });
|
||||||
|
await accountService.insert(c, { userId: userId, email });
|
||||||
|
},
|
||||||
|
|
||||||
|
async login(c, params) {
|
||||||
|
|
||||||
|
const { email, password } = params;
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
throw new BizError('邮箱和密码不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRow = await userService.selectByEmail(c, email);
|
||||||
|
|
||||||
|
if (!userRow) {
|
||||||
|
throw new BizError('该邮箱不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await cryptoUtils.verifyPassword(password, userRow.salt, userRow.password)) {
|
||||||
|
throw new BizError('密码输入错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
const uuid = uuidv4();
|
||||||
|
const jwt = await JwtUtils.generateToken(c,{ userId: userRow.userId, token: uuid });
|
||||||
|
|
||||||
|
let authInfo = await c.env.kv.get(KvConst.AUTH_INFO + userRow.userId, { type: 'json' });
|
||||||
|
|
||||||
|
if (authInfo) {
|
||||||
|
|
||||||
|
authInfo.tokens.push(uuid);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
authInfo = {
|
||||||
|
tokens: [],
|
||||||
|
user: userRow,
|
||||||
|
refreshTime: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
authInfo.tokens.push(uuid);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
await c.env.kv.put(KvConst.AUTH_INFO + userRow.userId, JSON.stringify(authInfo), { expirationTtl: constant.TOKEN_EXPIRE });
|
||||||
|
return jwt;
|
||||||
|
},
|
||||||
|
|
||||||
|
async logout(c, userId) {
|
||||||
|
const token = await userContext.getToken(c);
|
||||||
|
const authInfo = await c.env.kv.get(KvConst.AUTH_INFO + userId, { type: 'json' });
|
||||||
|
const index = authInfo.tokens.findIndex(item => item === token);
|
||||||
|
authInfo.tokens.splice(index, 1);
|
||||||
|
await c.env.kv.put(KvConst.AUTH_INFO + userId, JSON.stringify(authInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default loginService;
|
||||||
11
mail-worker/src/service/r2-service.js
Normal file
11
mail-worker/src/service/r2-service.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const r2Service = {
|
||||||
|
async putObj(c, key, content, metadata) {
|
||||||
|
const body = typeof content === 'string'
|
||||||
|
? new TextEncoder().encode(content)
|
||||||
|
: content;
|
||||||
|
await c.env.r2.put(key, body, {
|
||||||
|
httpMetadata: {...metadata}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default r2Service;
|
||||||
54
mail-worker/src/service/setting-service.js
Normal file
54
mail-worker/src/service/setting-service.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import KvConst from '../const/kv-const';
|
||||||
|
import setting from '../entity/setting';
|
||||||
|
import orm from '../entity/orm';
|
||||||
|
import { settingConst } from '../const/entity-const';
|
||||||
|
|
||||||
|
const settingService = {
|
||||||
|
|
||||||
|
async refresh(c) {
|
||||||
|
const settingRow = await orm(c).select().from(setting).get();
|
||||||
|
await c.env.kv.put(KvConst.SETTING, JSON.stringify(settingRow));
|
||||||
|
},
|
||||||
|
|
||||||
|
async query(c) {
|
||||||
|
const setting = await c.env.kv.get(KvConst.SETTING, { type: 'json' });
|
||||||
|
let domainList = c.env.domain;
|
||||||
|
domainList = domainList.map(item => '@' + item);
|
||||||
|
setting.domainList = domainList;
|
||||||
|
setting.siteKey = c.env.site_key;
|
||||||
|
setting.r2Domain = c.env.r2_domain;
|
||||||
|
return setting;
|
||||||
|
},
|
||||||
|
|
||||||
|
async set(c, params) {
|
||||||
|
await orm(c).update(setting).set({ ...params }).returning().get();
|
||||||
|
await this.refresh(c);
|
||||||
|
},
|
||||||
|
|
||||||
|
async isRegister(c) {
|
||||||
|
const { register } = await this.query(c);
|
||||||
|
return register === settingConst.register.OPEN;
|
||||||
|
},
|
||||||
|
|
||||||
|
async isReceive(c) {
|
||||||
|
const { receive } = await this.query(c);
|
||||||
|
return receive === settingConst.receive.OPEN;
|
||||||
|
},
|
||||||
|
|
||||||
|
async isAddEmail(c) {
|
||||||
|
const { addEmail, manyEmail } = await this.query(c);
|
||||||
|
return addEmail === settingConst.addEmail.OPEN && manyEmail === settingConst.manyEmail.OPEN;
|
||||||
|
},
|
||||||
|
|
||||||
|
async isRegisterVerify(c) {
|
||||||
|
const { registerVerify } = await this.query(c);
|
||||||
|
return registerVerify === settingConst.registerVerify.OPEN;
|
||||||
|
},
|
||||||
|
|
||||||
|
async isAddEmailVerify(c) {
|
||||||
|
const { addEmailVerify } = await this.query(c);
|
||||||
|
return addEmailVerify === settingConst.addEmailVerify.OPEN;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default settingService;
|
||||||
69
mail-worker/src/service/star-service.js
Normal file
69
mail-worker/src/service/star-service.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import orm from '../entity/orm';
|
||||||
|
import { star } from '../entity/star';
|
||||||
|
import emailService from './email-service';
|
||||||
|
import BizError from '../error/biz-error';
|
||||||
|
import { and, desc, eq, lt, sql } from 'drizzle-orm';
|
||||||
|
import email from '../entity/email';
|
||||||
|
import { isDel } from '../const/entity-const';
|
||||||
|
|
||||||
|
const startService = {
|
||||||
|
|
||||||
|
async add(c, params, userId) {
|
||||||
|
const { emailId } = params;
|
||||||
|
const email = await emailService.selectById(c, emailId);
|
||||||
|
if (!email) {
|
||||||
|
throw new BizError('星标的邮件不存在');
|
||||||
|
}
|
||||||
|
if (!email.userId === userId) {
|
||||||
|
throw new BizError('星标的邮件非当前用户所有');
|
||||||
|
}
|
||||||
|
const exist = await orm(c).select().from(star).where(
|
||||||
|
and(
|
||||||
|
eq(star.userId, userId),
|
||||||
|
eq(star.emailId, emailId)))
|
||||||
|
.get()
|
||||||
|
|
||||||
|
if (exist) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await orm(c).insert(star).values({ userId, emailId }).run();
|
||||||
|
},
|
||||||
|
|
||||||
|
async cancel(c, params, userId) {
|
||||||
|
const { emailId } = params;
|
||||||
|
await orm(c).delete(star).where(
|
||||||
|
and(
|
||||||
|
eq(star.userId, userId),
|
||||||
|
eq(star.emailId, emailId)))
|
||||||
|
.run();
|
||||||
|
},
|
||||||
|
|
||||||
|
async list(c, params, userId) {
|
||||||
|
let { emailId, size } = params;
|
||||||
|
emailId = Number(emailId);
|
||||||
|
size = Number(size);
|
||||||
|
|
||||||
|
if (!emailId) {
|
||||||
|
emailId = 9999999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = await orm(c).select({
|
||||||
|
isStar: sql`1`.as('isStar'),
|
||||||
|
starId: star.starId
|
||||||
|
, ...email
|
||||||
|
}).from(star)
|
||||||
|
.leftJoin(email, eq(email.emailId, star.emailId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(star.userId, userId),
|
||||||
|
eq(email.isDel, isDel.NORMAL),
|
||||||
|
lt(star.emailId, emailId)))
|
||||||
|
.orderBy(desc(star.emailId))
|
||||||
|
.limit(size)
|
||||||
|
.all();
|
||||||
|
return { list };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default startService;
|
||||||
30
mail-worker/src/service/turnstile-service.js
Normal file
30
mail-worker/src/service/turnstile-service.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import BizError from '../error/biz-error';
|
||||||
|
|
||||||
|
const turnstileService = {
|
||||||
|
|
||||||
|
async verify(c, token) {
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new BizError('验证token不能为空');
|
||||||
|
}
|
||||||
|
const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
secret: c.env.secret_key,
|
||||||
|
response: token,
|
||||||
|
remoteip: c.req.header('cf-connecting-ip')
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await res.json();
|
||||||
|
console.log(result)
|
||||||
|
if (!result.success) {
|
||||||
|
throw new BizError('人机验证失败,请重试',400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default turnstileService;
|
||||||
67
mail-worker/src/service/user-service.js
Normal file
67
mail-worker/src/service/user-service.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import BizError from '../error/biz-error';
|
||||||
|
import accountService from './account-service';
|
||||||
|
import orm from '../entity/orm';
|
||||||
|
import user from '../entity/user';
|
||||||
|
import { eq, and } from 'drizzle-orm';
|
||||||
|
import { isDel } from '../const/entity-const';
|
||||||
|
import kvConst from '../const/kv-const';
|
||||||
|
import cryptoUtils from '../utils/crypto-utils';
|
||||||
|
|
||||||
|
const userService = {
|
||||||
|
|
||||||
|
async loginUserInfo(c, userId) {
|
||||||
|
let user = await userService.selectById(c, userId);
|
||||||
|
let account = await accountService.selectByEmailIncludeDel(c, user.email);
|
||||||
|
delete user.password;
|
||||||
|
delete user.salt;
|
||||||
|
user.accountId = account.accountId;
|
||||||
|
user.type = c.env.admin === user.email ? 0 : 1;
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
|
||||||
|
async resetPassword(c, params, userId) {
|
||||||
|
|
||||||
|
const { password } = params;
|
||||||
|
|
||||||
|
if (password < 6) {
|
||||||
|
throw new BizError('密码不能小于6位');
|
||||||
|
}
|
||||||
|
const { salt, hash } = await cryptoUtils.hashPassword(password);
|
||||||
|
await orm(c).update(user).set({ password: hash, salt: salt }).where(eq(user.userId, userId)).run();
|
||||||
|
},
|
||||||
|
|
||||||
|
selectByEmail(c, email) {
|
||||||
|
return orm(c).select().from(user).where(
|
||||||
|
and(
|
||||||
|
eq(user.email, email),
|
||||||
|
eq(user.isDel, isDel.NORMAL)))
|
||||||
|
.get();
|
||||||
|
},
|
||||||
|
|
||||||
|
async insert(c, params) {
|
||||||
|
const { userId } = await orm(c).insert(user).values({ ...params }).returning().get();
|
||||||
|
return userId;
|
||||||
|
},
|
||||||
|
|
||||||
|
selectByEmailIncludeDel(c, email) {
|
||||||
|
return orm(c).select().from(user).where(eq(user.email, email)).get();
|
||||||
|
},
|
||||||
|
|
||||||
|
selectById(c, userId) {
|
||||||
|
return orm(c).select().from(user).where(
|
||||||
|
and(
|
||||||
|
eq(user.userId, userId),
|
||||||
|
eq(user.isDel, isDel.NORMAL)))
|
||||||
|
.get();
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(c, userId) {
|
||||||
|
await orm(c).update(user).set({ isDel: isDel.DELETE }).where(eq(user.userId, userId)).run();
|
||||||
|
await Promise.all([
|
||||||
|
c.env.kv.delete(kvConst.AUTH_INFO + userId),
|
||||||
|
accountService.removeByUserId(c, userId)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default userService;
|
||||||
31
mail-worker/src/utils/crypto-utils.js
Normal file
31
mail-worker/src/utils/crypto-utils.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
const saltHashUtils = {
|
||||||
|
|
||||||
|
generateSalt(length = 16) {
|
||||||
|
const array = new Uint8Array(length);
|
||||||
|
crypto.getRandomValues(array);
|
||||||
|
return btoa(String.fromCharCode(...array));
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
async hashPassword(password) {
|
||||||
|
const salt = this.generateSalt();
|
||||||
|
const hash = await this.genHashPassword(password, salt);
|
||||||
|
return { salt, hash };
|
||||||
|
},
|
||||||
|
|
||||||
|
async genHashPassword(password, salt) {
|
||||||
|
const data = encoder.encode(salt + password);
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
return btoa(String.fromCharCode(...hashArray));
|
||||||
|
},
|
||||||
|
|
||||||
|
async verifyPassword(inputPassword, salt, storedHash) {
|
||||||
|
const hash = await this.genHashPassword(inputPassword, salt);
|
||||||
|
return hash === storedHash;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default saltHashUtils;
|
||||||
9
mail-worker/src/utils/email-utils.js
Normal file
9
mail-worker/src/utils/email-utils.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const emailUtils = {
|
||||||
|
getDomain(email) {
|
||||||
|
if (typeof email !== 'string') return ''
|
||||||
|
const parts = email.split('@')
|
||||||
|
return parts.length === 2 ? parts[1] : ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default emailUtils
|
||||||
9
mail-worker/src/utils/file-utils.js
Normal file
9
mail-worker/src/utils/file-utils.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const fileUtils = {
|
||||||
|
getExtFileName(filename) {
|
||||||
|
const index = filename.lastIndexOf('.');
|
||||||
|
return index !== -1 ? filename.slice(index) : '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default fileUtils
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user