保存
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user