新增邮件发送和管理员功能
This commit is contained in:
@@ -1,31 +1,10 @@
|
||||
<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>
|
||||
@@ -12,9 +12,17 @@ http.interceptors.request.use(config => {
|
||||
})
|
||||
|
||||
http.interceptors.response.use((res) => {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const showMsg = res.config.noMsg;
|
||||
const data = res.data
|
||||
if (data.code === 401) {
|
||||
|
||||
if (showMsg) {
|
||||
|
||||
data.code === 200 ? resolve(data.data) : reject(data)
|
||||
|
||||
} else if (data.code === 401) {
|
||||
ElMessage({
|
||||
message: data.message,
|
||||
type: 'error',
|
||||
@@ -26,7 +34,7 @@ http.interceptors.response.use((res) => {
|
||||
} else if (data.code === 403) {
|
||||
ElMessage({
|
||||
message: data.message,
|
||||
type: 'error',
|
||||
type: 'warning',
|
||||
plain: true,
|
||||
})
|
||||
reject(data)
|
||||
@@ -36,17 +44,19 @@ http.interceptors.response.use((res) => {
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
setTimeout(() => {
|
||||
reject(data)
|
||||
}, 1)
|
||||
|
||||
reject(data)
|
||||
}
|
||||
setTimeout(() => {
|
||||
resolve(data.data)
|
||||
}, 1)
|
||||
resolve(data.data)
|
||||
})
|
||||
},
|
||||
(error) => {
|
||||
if (error.message.includes('Network Error')) {
|
||||
|
||||
const showMsg = error.config.noMsg;
|
||||
|
||||
if (showMsg) {
|
||||
return Promise.reject(error)
|
||||
} else if (error.message.includes('Network Error')) {
|
||||
ElMessage({
|
||||
message: '网络错误,请检查网络连接',
|
||||
type: 'error',
|
||||
|
||||
@@ -8,50 +8,139 @@
|
||||
@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 class="header-left" :style="'padding-left:' + actionLeft">
|
||||
|
||||
<slot name="first"></slot>
|
||||
<Icon class="icon" icon="ion:reload" width="18" height="18" @click="refresh"/>
|
||||
<Icon v-perm="'email:delete'" class="icon" icon="uiw:delete" width="16" height="16"
|
||||
v-if="getSelectedMailsIds().length > 0"
|
||||
@click="handleDelete"/>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<span class="email-count" v-if="total">共 {{ total }} 封</span>
|
||||
<Icon v-if="showAccountIcon" class="more-icon icon" width="16" height="16" icon="akar-icons:dot-grid-fill"
|
||||
@click="changeAccountShow"/>
|
||||
</div>
|
||||
</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 ref="scroll" class="scroll">
|
||||
<el-scrollbar ref="scrollbarRef" @scroll="handleScroll" >
|
||||
<div class="scroll-box" :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" />
|
||||
<el-checkbox v-model="item.checked" @click.stop></el-checkbox>
|
||||
<div @click.stop="starChange(item)" class="pc-star" v-if="showStar">
|
||||
<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 v-if="!showStar"></div>
|
||||
<div class="title" :class="accountShow ? 'title-column' : 'title-column'">
|
||||
|
||||
<div class="email-sender" :style=" showStatus ? 'gap: 10px;' : ''">
|
||||
<div class="email-status" v-if="showStatus">
|
||||
<el-tooltip v-if="item.status === 0"
|
||||
effect="dark"
|
||||
content="已接收"
|
||||
>
|
||||
<Icon icon="ic:round-mark-email-read" style="color: #67C23A" width="20" height="20"/>
|
||||
/>
|
||||
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="item.status === 1"
|
||||
effect="dark"
|
||||
content="已发送"
|
||||
>
|
||||
<Icon icon="bi:send-arrow-up" style="color: #67C23A" width="20" height="20"
|
||||
/>
|
||||
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="item.status === 2"
|
||||
effect="dark"
|
||||
content="发送成功"
|
||||
>
|
||||
<Icon icon="bi:send-check-fill" style="color: #67C23A" width="20"
|
||||
height="20"/>
|
||||
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="item.status === 3"
|
||||
effect="dark"
|
||||
content="发送失败"
|
||||
>
|
||||
<Icon icon="bi:send-x-fill" style="color: #F56C6C" width="20"
|
||||
height="20"/>
|
||||
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="item.status === 4"
|
||||
effect="dark"
|
||||
content="被标记垃圾邮件"
|
||||
>
|
||||
<Icon icon="bi:send-exclamation-fill" style="color:#FBBD08" width="20"
|
||||
height="20"/>
|
||||
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="item.status === 5"
|
||||
effect="dark"
|
||||
content="发送延迟"
|
||||
>
|
||||
<Icon icon="quill:send-later" style="color:#FBBD08" width="20"
|
||||
height="20"/>
|
||||
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div v-else></div>
|
||||
<span class="name">
|
||||
<span>{{ item.name }}</span>
|
||||
<span>
|
||||
<Icon v-if="item.isStar" icon="fluent-color:star-16" width="18" height="18"/>
|
||||
</span>
|
||||
</span>
|
||||
<span class="phone-time">{{ fromNow(item.createTime) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="email-text">
|
||||
<span class="email-subject">{{ item.subject }}</span>
|
||||
<span class="email-content">{{ htmlToText(item) }}</span>
|
||||
</div>
|
||||
<div class="user-info" v-if="showUserInfo">
|
||||
<div class="user">
|
||||
<span>
|
||||
<Icon icon="mynaui:user" width="20" height="20"/>
|
||||
</span>
|
||||
<span>{{ item.userEmail }}</span>
|
||||
</div>
|
||||
<div class="account">
|
||||
<span>
|
||||
<Icon icon="mdi-light:email" width="20" height="20"/>
|
||||
</span>
|
||||
<span>{{ item.accountEmail }}</span>
|
||||
</div>
|
||||
<div class="del-status" v-if="item.isDel">
|
||||
<el-tag type="info" size="small">已删除</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="email-right" :style="showUserInfo ? 'align-self: start;':''">
|
||||
<span class="email-time">{{ fromNow(item.createTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="loading" v-if="loading">
|
||||
<Loading />
|
||||
<div class="loading" :class="loading ? 'loading-show' : 'loading-hide'">
|
||||
<Loading/>
|
||||
</div>
|
||||
<div class="follow-loading" v-if="emailList.length > 0 && !noLoading">
|
||||
<Loading />
|
||||
<div class="follow-loading" v-if="followLoading">
|
||||
<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="没有任何邮件" />
|
||||
<el-empty description="没有任何邮件"/>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
@@ -64,13 +153,12 @@
|
||||
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";
|
||||
import {fromNow} from "@/utils/day.js";
|
||||
|
||||
const props = defineProps({
|
||||
getEmailList: Function,
|
||||
@@ -79,6 +167,30 @@ const props = defineProps({
|
||||
starCancel: Function,
|
||||
cancelSuccess: Function,
|
||||
starSuccess: Function,
|
||||
actionLeft: {
|
||||
type: String,
|
||||
default: '0'
|
||||
},
|
||||
timeSort: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
showStatus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showAccountIcon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showUserInfo: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showStar: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
allowStar: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
@@ -86,6 +198,8 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
|
||||
const emit = defineEmits(['jump', 'refresh-before'])
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const uiStore = useUiStore();
|
||||
const emailStore = useEmailStore();
|
||||
@@ -93,12 +207,15 @@ const loading = ref(false);
|
||||
const followLoading = ref(false);
|
||||
const noLoading = ref(false);
|
||||
const emailList = reactive([])
|
||||
const mailTotal = ref(0);
|
||||
const total = ref(0);
|
||||
const checkAll = ref(false);
|
||||
const isIndeterminate = ref(false);
|
||||
const scroll = ref(null)
|
||||
const firstLoad = ref(true)
|
||||
let scrollTop = 0
|
||||
const latestEmail = ref(null)
|
||||
const scrollbarRef = ref(null)
|
||||
let reqLock = false
|
||||
const queryParam = reactive({
|
||||
emailId: 0,
|
||||
size: 30,
|
||||
@@ -107,10 +224,12 @@ const queryParam = reactive({
|
||||
defineExpose({
|
||||
refreshList,
|
||||
deleteEmail,
|
||||
addItemStar,
|
||||
editEmailStar,
|
||||
addItem,
|
||||
emailList,
|
||||
firstLoad
|
||||
firstLoad,
|
||||
latestEmail,
|
||||
noLoading,
|
||||
total
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
@@ -131,7 +250,7 @@ watch(
|
||||
updateCheckStatus();
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
{deep: true}
|
||||
);
|
||||
|
||||
|
||||
@@ -145,6 +264,28 @@ const accountShow = computed(() => {
|
||||
return uiStore.accountShow && settingStore.settings.manyEmail === 0
|
||||
})
|
||||
|
||||
function handleScroll(e) {
|
||||
}
|
||||
|
||||
function htmlToText(email) {
|
||||
if (email.content) {
|
||||
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = email.content;
|
||||
const scriptsAndStyles = tempDiv.querySelectorAll('script, style, title');
|
||||
scriptsAndStyles.forEach(el => el.remove());
|
||||
const text = tempDiv.textContent || tempDiv.innerText || '';
|
||||
return text.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
if (email.text) {
|
||||
return email.text
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function starChange(email) {
|
||||
|
||||
@@ -156,7 +297,8 @@ function starChange(email) {
|
||||
props.starAdd(email.emailId).then(() => {
|
||||
email.isStar = 1;
|
||||
props.starSuccess(email)
|
||||
}).catch(() => {
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
email.isStar = 0
|
||||
})
|
||||
} else {
|
||||
@@ -165,7 +307,8 @@ function starChange(email) {
|
||||
props.starCancel(email.emailId).then(() => {
|
||||
email.isStar = 0;
|
||||
props.cancelSuccess?.(email)
|
||||
}).catch(() => {
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
email.isStar = 1;
|
||||
})
|
||||
}
|
||||
@@ -187,7 +330,7 @@ const handleDelete = () => {
|
||||
ElMessage({
|
||||
message: '删除成功',
|
||||
type: 'success',
|
||||
plain: true,
|
||||
plain: true
|
||||
})
|
||||
emailStore.deleteIds = emailIds;
|
||||
})
|
||||
@@ -196,18 +339,18 @@ const handleDelete = () => {
|
||||
|
||||
function deleteEmail(emailIds) {
|
||||
emailIds.forEach(emailId => {
|
||||
emailList.forEach((item,index) => {
|
||||
emailList.forEach((item, index) => {
|
||||
if (emailId === item.emailId) {
|
||||
emailList.splice(index,1);
|
||||
emailList.splice(index, 1);
|
||||
}
|
||||
})
|
||||
})
|
||||
if (emailList.length < queryParam.size && !noLoading.value) {
|
||||
getEemailList()
|
||||
getEmailList()
|
||||
}
|
||||
}
|
||||
|
||||
function addItemStar(email) {
|
||||
function addItem(email) {
|
||||
|
||||
const existIndex = emailList.findIndex(item => item.emailId === email.emailId)
|
||||
|
||||
@@ -215,6 +358,18 @@ function addItemStar(email) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (props.timeSort) {
|
||||
if (noLoading.value) {
|
||||
emailList.push(email)
|
||||
latestEmail.value = email
|
||||
total.value++
|
||||
} else {
|
||||
total.value++
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const index = emailList.findIndex(item => item.emailId < email.emailId)
|
||||
|
||||
if (index !== -1) {
|
||||
@@ -224,13 +379,9 @@ function addItemStar(email) {
|
||||
emailList.push(email)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function editEmailStar(emailId,isStar) {
|
||||
const index = emailList.findIndex(item => item.emailId === emailId)
|
||||
if (index !== -1) {
|
||||
emailList[index].isStar = isStar
|
||||
}
|
||||
latestEmail.value = email
|
||||
total.value++
|
||||
}
|
||||
|
||||
function handleCheckAllChange(val) {
|
||||
@@ -251,56 +402,75 @@ function updateCheckStatus() {
|
||||
}
|
||||
|
||||
function jumpDetails(email) {
|
||||
emailStore.readEmail = email
|
||||
router.push("/content");
|
||||
emit('jump', email)
|
||||
}
|
||||
|
||||
function getEmailList() {
|
||||
|
||||
if (loading.value || followLoading.value || noLoading.value) return;
|
||||
function getEmailList(refresh = false) {
|
||||
|
||||
if (reqLock) return;
|
||||
|
||||
reqLock = true
|
||||
|
||||
if (!refresh) {
|
||||
|
||||
if (loading.value || noLoading.value) {
|
||||
reqLock = false
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
loading.value = true
|
||||
}
|
||||
|
||||
if (emailList.length === 0) {
|
||||
loading.value = true
|
||||
}else {
|
||||
followLoading.value = true
|
||||
} else {
|
||||
followLoading.value = !refresh;
|
||||
}
|
||||
|
||||
|
||||
props.getEmailList(queryParam.emailId, queryParam.size).then(data => {
|
||||
|
||||
if (emailList.length === 0) {
|
||||
firstLoad.value = false
|
||||
}
|
||||
|
||||
firstLoad.value = false
|
||||
|
||||
let list = data.list.map(item => ({
|
||||
...item,
|
||||
checked: false
|
||||
}));
|
||||
|
||||
if (data.list.length < queryParam.size) {
|
||||
noLoading.value = true
|
||||
|
||||
if (refresh) {
|
||||
emailList.length = 0
|
||||
}
|
||||
|
||||
latestEmail.value = data.latestEmail
|
||||
emailList.push(...list);
|
||||
mailTotal.value = data.total;
|
||||
queryParam.emailId = data.list.at(-1).emailId;
|
||||
|
||||
if (refresh) scrollbarRef.value?.setScrollTop(0);
|
||||
|
||||
noLoading.value = data.list.length < queryParam.size;
|
||||
followLoading.value = data.list.length >= queryParam.size;
|
||||
|
||||
total.value = data.total;
|
||||
queryParam.emailId = data.list.length > 0 ? data.list.at(-1).emailId : 0
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
followLoading.value = false
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
followLoading.value = false
|
||||
reqLock = false
|
||||
})
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
emit('refresh-before')
|
||||
refreshList()
|
||||
}
|
||||
|
||||
function refreshList() {
|
||||
emailList.splice(0);
|
||||
noLoading.value = false;
|
||||
mailTotal.value = 0;
|
||||
checkAll.value = false;
|
||||
firstLoad.value = true
|
||||
isIndeterminate.value = false;
|
||||
queryParam.emailId = 0;
|
||||
getEmailList();
|
||||
getEmailList(true);
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
@@ -312,8 +482,9 @@ function loadData() {
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.email-container {
|
||||
background: #FFFFFF;
|
||||
border-radius: 8px;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
color: #2e2e2e;
|
||||
@@ -323,49 +494,106 @@ function loadData() {
|
||||
|
||||
.scroll {
|
||||
margin: 0;
|
||||
height: calc(100% - 38px);
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.scroll-box {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.noLoading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
padding: 15px 0;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.follow-loading {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.loading-show {
|
||||
transition: all 200ms ease 200ms;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.loading-hide {
|
||||
pointer-events: none;
|
||||
transition: all 200ms;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
transition: background 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
.el-checkbox{
|
||||
width: 40px;
|
||||
@media (max-width: 1199px) {
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 10px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
color: rgba(25, 41, 59, 0.4);
|
||||
|
||||
.user, .account {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
transition: all 300ms;
|
||||
line-height: 12px;
|
||||
max-width: 290px;
|
||||
|
||||
span:first-child {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
span:last-child {
|
||||
margin-left: 5px;
|
||||
position: relative;
|
||||
bottom: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-checkbox {
|
||||
display: flex;
|
||||
padding-left: 15px;
|
||||
padding-right: 20px;
|
||||
justify-content: center;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.title-column {
|
||||
@@ -378,9 +606,10 @@ function loadData() {
|
||||
.title {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
grid-template-columns: 200px 1fr;
|
||||
|
||||
grid-template-columns: 220px 1fr;
|
||||
@media (max-width: 1199px) {
|
||||
padding-right: 15px;
|
||||
}
|
||||
@media (max-width: 991px) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 4px;
|
||||
@@ -389,38 +618,90 @@ function loadData() {
|
||||
.email-sender {
|
||||
font-weight: bold;
|
||||
color: #1a1a1a;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
|
||||
.email-status {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
grid-template-columns: auto 1fr;
|
||||
@media (min-width: 992px) {
|
||||
grid-template-columns: 1fr;
|
||||
> span:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> span:first-child {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.phone-time {
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
color: #333 !important;
|
||||
@media (min-width: 1200px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.email-subject {
|
||||
.email-text {
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
@media (max-width: 1199px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.email-subject {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.email-content {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
padding-left: 10px;
|
||||
color: rgba(25, 41, 59, 0.4);
|
||||
@media (max-width: 1199px) {
|
||||
padding-left: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.email-right {
|
||||
align-self: center;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
padding-left: 15px;
|
||||
align-items: center;
|
||||
color: #333;
|
||||
padding-left: 20px;
|
||||
@media (max-width: 1199px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&: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;
|
||||
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: 0;
|
||||
}
|
||||
|
||||
/*&[data-checked="true"] {
|
||||
@@ -434,7 +715,6 @@ function loadData() {
|
||||
}
|
||||
|
||||
.pc-star {
|
||||
padding-left: 8px;
|
||||
display: flex;
|
||||
width: 40px;
|
||||
}
|
||||
@@ -443,22 +723,15 @@ function loadData() {
|
||||
.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{
|
||||
.star-pd {
|
||||
padding-top: 6px !important;
|
||||
}
|
||||
.title {
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.email-time {
|
||||
@@ -470,18 +743,43 @@ function loadData() {
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 3px 16px;
|
||||
gap: 15px;
|
||||
padding: 3px 15px;
|
||||
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
|
||||
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
column-gap: 15px;
|
||||
row-gap: 8px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
align-items: start;
|
||||
height: 100%;
|
||||
|
||||
.email-count {
|
||||
white-space: nowrap;
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 18px;
|
||||
margin-left: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.more-icon {
|
||||
margin-left: auto;
|
||||
margin-top: 8px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div style="padding: 0 15px;" @click="toggleClick">
|
||||
<div style="padding: 0 15px;cursor: pointer" @click="toggleClick">
|
||||
<svg
|
||||
:class="{'is-active':isActive}"
|
||||
class="hamburger"
|
||||
|
||||
23
mail-vue/src/components/send-percent/index.vue
Normal file
23
mail-vue/src/components/send-percent/index.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="percent">
|
||||
<Icon icon="line-md:loading-loop" width="18" height="18" />
|
||||
<div>
|
||||
<span>{{value}}%</span>
|
||||
<span style="margin-left: 5px">邮件正在发送中</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {Icon} from "@iconify/vue";
|
||||
|
||||
defineProps({
|
||||
value: [Number,String]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.percent {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
133
mail-vue/src/components/shadow-html/index.vue
Normal file
133
mail-vue/src/components/shadow-html/index.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div class="content-box" ref="contentBox">
|
||||
<div ref="container" class="content-html"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
html: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const container = ref(null)
|
||||
const contentBox = ref(null)
|
||||
let shadowRoot = null
|
||||
|
||||
// 确保字体在 Shadow DOM 中可用
|
||||
function loadFontInShadow() {
|
||||
const style = document.createElement('style')
|
||||
style.textContent = `
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS';
|
||||
src: url('@/assets/fonts/HarmonyOS_Sans_SC_Regular.woff2') format('woff2');
|
||||
font-display: swap;
|
||||
}
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
|
||||
function updateContent() {
|
||||
if (!shadowRoot) return
|
||||
|
||||
shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
all: initial;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: 'HarmonyOS', -apple-system, BlinkMacSystemFont,
|
||||
'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #13181D;
|
||||
}
|
||||
|
||||
.shadow-content {
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto !important;
|
||||
display: block;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: inherit;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #409EFF !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
<div class="shadow-content">
|
||||
${props.html}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function autoScale() {
|
||||
if (!shadowRoot || !contentBox.value) return
|
||||
|
||||
const parent = contentBox.value
|
||||
const shadowContent = shadowRoot.querySelector('.shadow-content')
|
||||
|
||||
if (!shadowContent) return
|
||||
|
||||
const parentWidth = parent.offsetWidth
|
||||
const parentHeight = parent.offsetHeight
|
||||
|
||||
const childWidth = shadowContent.scrollWidth
|
||||
const childHeight = shadowContent.scrollHeight
|
||||
|
||||
if (childWidth === 0 || childHeight === 0) return
|
||||
|
||||
const scaleX = parentWidth / childWidth
|
||||
const scaleY = parentHeight / childHeight
|
||||
const scale = Math.min(scaleX, scaleY)
|
||||
|
||||
const hostElement = shadowRoot.host
|
||||
hostElement.style.transform = `scale(${scale})`
|
||||
hostElement.style.transformOrigin = 'top left'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadFontInShadow() // 预加载字体
|
||||
shadowRoot = container.value.attachShadow({ mode: 'open' })
|
||||
updateContent()
|
||||
autoScale()
|
||||
})
|
||||
|
||||
watch(() => props.html, () => {
|
||||
updateContent()
|
||||
autoScale()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-family: 'HarmonyOS', -apple-system, BlinkMacSystemFont,
|
||||
'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.content-html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
178
mail-vue/src/components/tiny-editor/index.vue
Normal file
178
mail-vue/src/components/tiny-editor/index.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<div class="editor-box" :class="showLoading ? 'editor-box-loading' : ''">
|
||||
<loading class="loading" v-if="showLoading" />
|
||||
<textarea v-else style="outline: none" :id="editorId" ref="editorRef"></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, onMounted, onBeforeUnmount, watch} from 'vue';
|
||||
import loading from "@/components/loading/index.vue";
|
||||
import {compressImage} from "@/utils/file-utils.js";
|
||||
defineExpose({
|
||||
clearEditor
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
editorId: {
|
||||
type: String,
|
||||
default: () => `editor-${Date.now()}`
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
const editor = ref(null);
|
||||
const isInitialized = ref(false);
|
||||
const editorRef = ref(null);
|
||||
const showLoading = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
initTinyMCE();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
destroyEditor();
|
||||
});
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (editor.value && editor.value.getContent() !== newValue) {
|
||||
editor.value.setContent(newValue);
|
||||
}
|
||||
});
|
||||
|
||||
function clearEditor() {
|
||||
if (editor.value) {
|
||||
editor.value.setContent('');
|
||||
}
|
||||
}
|
||||
|
||||
function initTinyMCE() {
|
||||
if (window.tinymce) {
|
||||
initEditor();
|
||||
} else {
|
||||
showLoading.value = true;
|
||||
const script = document.createElement('script');
|
||||
script.src = '/tinymce/tinymce.min.js';
|
||||
script.onload = () => initEditor();
|
||||
document.head.appendChild(script);
|
||||
showLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function initEditor() {
|
||||
window.tinymce.init({
|
||||
selector: `#${props.editorId}`,
|
||||
statusbar: false,
|
||||
height: "100%",
|
||||
plugins: 'link image advlist lists emoticons fullscreen table preview code',
|
||||
toolbar: 'bold emoticons forecolor fontsize | alignleft aligncenter alignright alignjustify | outdent indent | bullist numlist | link image | table code preview fullscreen',
|
||||
toolbar_mode: 'scrolling',
|
||||
mobile: {
|
||||
toolbar: 'fullscreen bold emoticons forecolor fontsize | alignleft aligncenter alignright alignjustify | outdent indent | bullist numlist | link image | table code preview ',
|
||||
},
|
||||
emoticons_search: false,
|
||||
language: 'zh_CN',
|
||||
language_url: '/tinymce/langs/zh_CN.js',
|
||||
menubar: false,
|
||||
license_key: 'gpl',
|
||||
content_style: " .tox-dialog__body-content { margin: 0 !important; } img { max-width: 100%; height: auto; }",
|
||||
setup: (ed) => {
|
||||
editor.value = ed;
|
||||
ed.on('init', () => {
|
||||
ed.setContent(props.modelValue);
|
||||
isInitialized.value = true;
|
||||
});
|
||||
ed.on('input change', () => {
|
||||
const content = ed.getContent();
|
||||
const text = ed.getContent({ format: 'text' });
|
||||
emit('change', content, text);
|
||||
});
|
||||
},
|
||||
branding: false,
|
||||
file_picker_types: 'image',
|
||||
image_dimensions: false,
|
||||
image_description: false,
|
||||
link_title: false,
|
||||
dialog_type: 'none',
|
||||
file_picker_callback: (callback, value, meta) => {
|
||||
const input = document.createElement('input');
|
||||
input.setAttribute('type', 'file');
|
||||
input.setAttribute('accept', 'image/*');
|
||||
|
||||
input.addEventListener('change', async (e) => {
|
||||
let file = e.target.files[0];
|
||||
file = await compressImage(file);
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const id = 'blobid' + (new Date()).getTime();
|
||||
const blobCache = tinymce.activeEditor.editorUpload.blobCache;
|
||||
const base64 = reader.result.split(',')[1];
|
||||
const blobInfo = blobCache.create(id, file, base64);
|
||||
blobCache.add(blobInfo);
|
||||
|
||||
callback(blobInfo.blobUri(), { title: file.name });
|
||||
}
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
input.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function destroyEditor() {
|
||||
if (editor.value) {
|
||||
editor.value.destroy();
|
||||
editor.value = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.editor-box {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loading {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.editor-box-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:deep(.tox-tbtn.tox-tbtn--select.tox-tbtn--bespoke) {
|
||||
width: 80px !important;
|
||||
}
|
||||
|
||||
:deep(.tox.tox-tinymce.tox-fullscreen) {
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
padding-bottom: 15px;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
:deep(.tox-tinymce) {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
:deep(.tox-toolbar__group) {
|
||||
padding-left: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.tox-tbtn) {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,21 +0,0 @@
|
||||
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
|
||||
21
mail-vue/src/directives/perm.js
Normal file
21
mail-vue/src/directives/perm.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useUserStore } from "@/store/user.js";
|
||||
|
||||
export default {
|
||||
mounted(el, binding) {
|
||||
const userStore = useUserStore();
|
||||
const permKeys = userStore.user.permKeys;
|
||||
const value = binding.value;
|
||||
|
||||
if (permKeys.includes('*')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasPermission = Array.isArray(value)
|
||||
? value.some(key => permKeys.includes(key))
|
||||
: permKeys.includes(value);
|
||||
|
||||
if (!hasPermission) {
|
||||
el.parentNode && el.parentNode.removeChild(el);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<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" />
|
||||
<div class="head-opt" >
|
||||
<Icon v-if="settingStore.settings.addEmail === 0" v-perm="'account:add'" 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">
|
||||
@@ -15,7 +15,7 @@
|
||||
<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" />
|
||||
<Icon v-if="item.accountId === userStore.user.accountId || !hasPerm('account:delete')" 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 >
|
||||
@@ -68,7 +68,6 @@
|
||||
|
||||
</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>
|
||||
@@ -97,7 +96,6 @@
|
||||
>添加
|
||||
</el-button>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
class="add-email-turnstile"
|
||||
:class="verifyShow ? 'turnstile-show' : 'turnstile-hide'"
|
||||
@@ -116,6 +114,7 @@ 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";
|
||||
import hasPerm from "@/utils/perm.js";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const accountStore = useAccountStore();
|
||||
@@ -136,12 +135,15 @@ const addForm = reactive({
|
||||
})
|
||||
const queryParams = {
|
||||
accountId: 0,
|
||||
size: 12
|
||||
size: 20
|
||||
}
|
||||
|
||||
const mySelect = ref()
|
||||
|
||||
getAccountList()
|
||||
if (hasPerm('account:query')) {
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
|
||||
const openSelect = () => {
|
||||
mySelect.value.toggleMenu()
|
||||
@@ -192,6 +194,7 @@ function refresh() {
|
||||
}
|
||||
function changeAccount(account) {
|
||||
accountStore.currentAccountId = account.accountId
|
||||
accountStore.currentAccount = account
|
||||
}
|
||||
|
||||
function add() {
|
||||
@@ -270,6 +273,7 @@ function submit() {
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
userStore.refreshUserInfo()
|
||||
}).catch(res => {
|
||||
if (res.code === 400) {
|
||||
verifyToken = ''
|
||||
@@ -292,12 +296,13 @@ function submit() {
|
||||
align-items: center;
|
||||
height: 38px;
|
||||
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
.icon{
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.icon:nth-child(2) {
|
||||
margin-left: 20px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
.scrollbar {
|
||||
@@ -364,6 +369,7 @@ function submit() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.setting-icon {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
@@ -392,6 +398,11 @@ function submit() {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.el-pagination .el-select) {
|
||||
width: 100px;
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
.add-email-turnstile {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
@@ -6,22 +6,45 @@
|
||||
<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"
|
||||
<el-menu-item @click="router.push({name: '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"
|
||||
<el-menu-item @click="router.push({name: 'send'})" index="send"
|
||||
:class="route.meta.name === 'send' ? 'choose-item' : ''">
|
||||
<Icon icon="cil:send" width="20" height="20" />
|
||||
<span class="menu-name" style="margin-left: 21px">已发送</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item @click="router.push({name: '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"
|
||||
<el-menu-item @click="router.push({name: '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"
|
||||
<div class="manage-title" v-perm="['user:query','role:query','setting:query']">
|
||||
<div>管理</div>
|
||||
</div>
|
||||
<el-menu-item @click="router.push({name: 'user'})" index="setting" v-perm="'user:query'"
|
||||
:class="route.meta.name === 'user' ? 'choose-item' : ''">
|
||||
<Icon icon="iconoir:user" width="24" height="24" />
|
||||
<span class="menu-name" style="margin-left: 16px">用户列表</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item @click="router.push({name: 'sys-email'})" index="sys-email" v-perm="'sys-email:query'"
|
||||
:class="route.meta.name === 'sys-email' ? 'choose-item' : ''">
|
||||
<Icon icon="fluent:mail-list-28-regular" width="22" height="22" />
|
||||
<span class="menu-name" style="margin-left: 18px">邮件列表</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item @click="router.push({name: 'role'})" index="setting" v-perm="'role:query'"
|
||||
:class="route.meta.name === 'role' ? 'choose-item' : ''">
|
||||
<Icon icon="hugeicons:key-02" width="22" height="22" />
|
||||
<span class="menu-name" style="margin-left: 18px">权限控制</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item @click="router.push({name: 'sys-setting'})" index="sys-setting" v-perm="'setting:query'"
|
||||
: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>
|
||||
@@ -37,15 +60,14 @@
|
||||
</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>
|
||||
@@ -63,7 +85,6 @@ const route = useRoute();
|
||||
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) {
|
||||
@@ -81,6 +102,11 @@ const route = useRoute();
|
||||
}
|
||||
|
||||
|
||||
.manage-title {
|
||||
margin-top: 10px;
|
||||
padding-left: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.el-menu-item {
|
||||
margin: 5px 10px !important;
|
||||
@@ -122,7 +148,7 @@ const route = useRoute();
|
||||
border-right: 0;
|
||||
width: 250px;
|
||||
@media (max-width: 1199px) {
|
||||
width: 200px;
|
||||
width: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +158,7 @@ const route = useRoute();
|
||||
}
|
||||
|
||||
.scroll {
|
||||
box-shadow: 6px 0 20px rgba(0, 21, 41, 0.35);
|
||||
|
||||
}
|
||||
|
||||
.github {
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
<template>
|
||||
<div class="header">
|
||||
<div class="btn">
|
||||
<div class="header-btn">
|
||||
<hanburger @click="changeAside"></hanburger>
|
||||
<span class="breadcrumb-item">{{ route.meta.title }}</span>
|
||||
</div>
|
||||
<div class="writer-box" @click="openSend">
|
||||
<div class="writer" >
|
||||
<Icon icon="material-symbols:edit-outline-sharp" width="22" height="22" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<div class="email">{{ userStore.user.email }}</div>
|
||||
<el-dropdown>
|
||||
<div class="email">
|
||||
<span>{{ userStore.user.email }}</span>
|
||||
</div>
|
||||
<el-dropdown :teleported="false" popper-class="detail-dropdown" >
|
||||
<div class="avatar">
|
||||
<div class="avatar-text">
|
||||
<div>{{ formatName(userStore.user.email) }}</div>
|
||||
@@ -14,14 +21,38 @@
|
||||
<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>
|
||||
<div class="user-details">
|
||||
<div class="details-avatar">
|
||||
{{ formatName(userStore.user.email) }}
|
||||
</div>
|
||||
<div class="detail-email">
|
||||
{{ userStore.user.email }}
|
||||
</div>
|
||||
<div class="detail-user-type">
|
||||
<el-tag type="warning">{{userStore.user.role.name}}</el-tag>
|
||||
</div>
|
||||
<div class="action-info">
|
||||
<div>
|
||||
<span style="margin-right: 10px">邮件发送 :</span>
|
||||
<span style="margin-right: 10px">邮箱添加 :</span>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<span v-if="sendCount !== null" style="margin-right: 5px">{{ sendCount }}</span>
|
||||
<el-tag v-if="!hasPerm('email:send')" type="warning">{{sendType}}</el-tag>
|
||||
<el-tag v-else type="success">{{sendType}}</el-tag>
|
||||
</div>
|
||||
<div>
|
||||
<span v-if="accountCount && hasPerm('account:add')" style="margin-right: 5px">{{ accountCount }}个</span>
|
||||
<el-tag v-if="!accountCount && hasPerm('account:add')" type="success">无限制</el-tag>
|
||||
<el-tag v-if="!hasPerm('account:add')" type="warning">无权限</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="logout">
|
||||
<el-button type="primary" :loading="logoutLoading" @click="clickLogout">退出</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
@@ -36,18 +67,58 @@ import {Icon} from "@iconify/vue";
|
||||
import {useUiStore} from "@/store/ui.js";
|
||||
import {useUserStore} from "@/store/user.js";
|
||||
import { useRoute } from "vue-router";
|
||||
import {computed, ref} from "vue";
|
||||
import hasPerm from "@/utils/perm.js";
|
||||
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
const uiStore = useUiStore();
|
||||
const logoutLoading = ref(false)
|
||||
|
||||
const accountCount = computed(() => {
|
||||
return userStore.user.role.accountCount
|
||||
})
|
||||
|
||||
const sendType = computed(() => {
|
||||
|
||||
if (!hasPerm('email:send')) {
|
||||
return '无权限'
|
||||
}
|
||||
if (userStore.user.role.sendType === 'day') {
|
||||
return '每天'
|
||||
}
|
||||
if (userStore.user.role.sendType === 'count') {
|
||||
return '次数'
|
||||
}
|
||||
})
|
||||
|
||||
const sendCount = computed(() => {
|
||||
|
||||
if (!hasPerm('email:send')) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!userStore.user.role.sendCount) {
|
||||
return '无限制'
|
||||
}
|
||||
return userStore.user.sendCount + '/' + userStore.user.role.sendCount
|
||||
})
|
||||
|
||||
function openSend() {
|
||||
uiStore.writerRef.open()
|
||||
}
|
||||
|
||||
function changeAside() {
|
||||
uiStore.asideShow = !uiStore.asideShow
|
||||
}
|
||||
|
||||
function clickLogout() {
|
||||
logoutLoading.value = true
|
||||
logout().then(() => {
|
||||
localStorage.removeItem("token")
|
||||
router.push('/login')
|
||||
}).finally(() => {
|
||||
logoutLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -61,7 +132,7 @@ function formatName(email) {
|
||||
|
||||
.breadcrumb-item {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -71,16 +142,113 @@ function formatName(email) {
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-popper.is-pure) {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
width: 250px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
justify-items: center;
|
||||
|
||||
.detail-user-type {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.action-info {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
margin-top: 10px;
|
||||
>div:first-child {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
>div:last-child {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
text-align: center;
|
||||
>div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-email {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
margin-top: 10px;
|
||||
width: 250px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
}
|
||||
.logout {
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-bottom: 10px;
|
||||
.el-button {
|
||||
height: 26px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.details-avatar {
|
||||
margin-top: 20px;
|
||||
height: 45px;
|
||||
width: 45px;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.header {
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
display: grid;
|
||||
height: 100%;
|
||||
gap: 10px;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-columns: auto auto 1fr;
|
||||
}
|
||||
|
||||
.btn {
|
||||
.writer-box {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 5px;
|
||||
.writer {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #1890ff, #1c6dd0);
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.writer-text {
|
||||
margin-left: 15px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
@@ -102,12 +270,13 @@ function formatName(email) {
|
||||
text-overflow: ellipsis;
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
cursor: pointer;
|
||||
.avatar-text {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
:class="(uiStore.asideShow && isMobile)? 'overlay-show':'overlay-hide'"
|
||||
@click="uiStore.asideShow = false"
|
||||
></div>
|
||||
|
||||
<el-container class="main-container">
|
||||
<el-main>
|
||||
<el-header>
|
||||
@@ -19,6 +18,7 @@
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
<writer ref="writerRef" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -27,17 +27,20 @@ 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";
|
||||
import writer from '@/layout/write/index.vue'
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const isMobile = ref(window.innerWidth < 768)
|
||||
|
||||
|
||||
const writerRef = ref({})
|
||||
const isMobile = ref(window.innerWidth < 992)
|
||||
const handleResize = () => {
|
||||
isMobile.value = window.innerWidth < 768
|
||||
uiStore.asideShow = window.innerWidth >= 992;
|
||||
isMobile.value = window.innerWidth < 992
|
||||
uiStore.asideShow = window.innerWidth >= 991;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
uiStore.writerRef = writerRef
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
handleResize()
|
||||
})
|
||||
@@ -58,10 +61,12 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.aside-show {
|
||||
-webkit-box-shadow: 3px 0 5px rgba(0, 21, 41, .35);
|
||||
box-shadow: 3px 0 5px rgba(0, 21, 41, 0.35);
|
||||
transform: translateX(0);
|
||||
transition: all 100ms ease;
|
||||
z-index: 101;
|
||||
@media (max-width: 767px) {
|
||||
@media (max-width: 991px) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<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'" />
|
||||
<div :class="accountShow && hasPerm('account:query') ? 'main-box-show' : 'main-box-hide'">
|
||||
<div :class="accountShow && hasPerm('account:query') ? 'block-show' : 'block-hide'" @click="uiStore.accountShow = false"></div>
|
||||
<account :class="accountShow && hasPerm('account:query') ? 'show' : 'hide'" />
|
||||
<router-view class="main-view" v-slot="{ Component,route }">
|
||||
<keep-alive :include="['email','sys-setting','star']">
|
||||
<keep-alive :include="['email','sys-email','send','sys-setting','star','user','role']">
|
||||
<component :is="Component" :key="route.name"/>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
@@ -15,6 +15,7 @@ import {useUiStore} from "@/store/ui.js";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
import {computed, onBeforeUnmount, onMounted} from "vue";
|
||||
import { useRoute } from 'vue-router'
|
||||
import hasPerm from "@/utils/perm.js";
|
||||
|
||||
const props = defineProps({
|
||||
openSend: Function
|
||||
@@ -39,10 +40,10 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
|
||||
const handleResize = () => {
|
||||
if (['content','email'].includes(route.meta.name)) {
|
||||
if (['content','email','send'].includes(route.meta.name)) {
|
||||
if (innerWidth !== window.innerWidth) {
|
||||
innerWidth = window.innerWidth;
|
||||
uiStore.accountShow = window.innerWidth >= 768;
|
||||
uiStore.accountShow = window.innerWidth >= 767;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +78,7 @@ const handleResize = () => {
|
||||
@media (max-width: 767px) {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
width: 199px;
|
||||
width: 229px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,8 +87,8 @@ const handleResize = () => {
|
||||
position: fixed;
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
@media (max-width: 767px) {
|
||||
width: 199px;
|
||||
@media (max-width: 991px) {
|
||||
width: 229px;
|
||||
z-index: 100;
|
||||
}
|
||||
}
|
||||
@@ -96,10 +97,13 @@ const handleResize = () => {
|
||||
.main-box-show {
|
||||
display: grid;
|
||||
grid-template-columns: 250px 1fr;
|
||||
height: calc(100% - 60px);
|
||||
@media (max-width: 1200px) {
|
||||
grid-template-columns: 229px 1fr;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
height: calc(100% - 60px);
|
||||
}
|
||||
|
||||
.main-box-hide {
|
||||
|
||||
378
mail-vue/src/layout/write/index.vue
Normal file
378
mail-vue/src/layout/write/index.vue
Normal file
@@ -0,0 +1,378 @@
|
||||
<template>
|
||||
<div class="send" v-show="show">
|
||||
<div class="write-box">
|
||||
<div class="title">
|
||||
<div class="title-left">
|
||||
<span class="title-text">写邮件</span>
|
||||
</div>
|
||||
<div @click="close" style="cursor: pointer;">
|
||||
<Icon icon="material-symbols-light:close-rounded" width="22" height="22"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<el-input v-model="form.sendEmail" disabled placeholder="">
|
||||
<template #prefix>
|
||||
<div class="item-title">发件人 :</div>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input v-model="form.receiveEmail" placeholder="收件人邮箱地址">
|
||||
<template #prefix>
|
||||
<div class="item-title">收件人 :</div>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input v-model="form.name" placeholder="发件人名字,不填则默认使用邮箱名">
|
||||
<template #prefix>
|
||||
<div class="item-title">名字 :</div>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input v-model="form.subject" placeholder="邮件主题">
|
||||
<template #prefix>
|
||||
<div class="item-title">主题 :</div>
|
||||
</template>
|
||||
</el-input>
|
||||
<tinyEditor ref="editor" @change="change"/>
|
||||
<div class="button-item">
|
||||
<div class="att-add" @click="chooseFile">
|
||||
<Icon icon="iconamoon:attachment-fill" width="26" height="26"/>
|
||||
</div>
|
||||
<div class="att-clear" @click="clearContent">
|
||||
<Icon icon="icon-park-outline:clear-format" width="26" height="26"/>
|
||||
</div>
|
||||
<div class="att-list">
|
||||
<div class="att-item" v-for="(item,index) in form.attachments" :key="index">
|
||||
<Icon :icon="getIconByName(item.filename)" width="20" height="20"/>
|
||||
<span class="att-filename">{{ item.filename }}</span>
|
||||
<span class="att-size">{{ formatBytes(item.size) }}</span>
|
||||
<Icon style="cursor: pointer;" icon="material-symbols-light:close-rounded" @click="delAtt(index)"
|
||||
width="22" height="22"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="primary" @click="sendEmail">发送</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import tinyEditor from '@/components/tiny-editor/index.vue'
|
||||
import {h, onMounted, onUnmounted, reactive, ref} from "vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {useUserStore} from "@/store/user.js";
|
||||
import {emailSend} from "@/request/email.js";
|
||||
import {isEmail} from "@/utils/verify-utils.js";
|
||||
import {ElMessage, ElMessageBox, ElNotification} from "element-plus";
|
||||
import {useAccountStore} from "@/store/account.js";
|
||||
import {useEmailStore} from "@/store/email.js";
|
||||
import {fileToBase64, formatBytes} from "@/utils/file-utils.js";
|
||||
import {getIconByName} from "@/utils/icon-utils.js";
|
||||
import sendPercent from "@/components/send-percent/index.vue"
|
||||
import hasPerm from "@/utils/perm.js";
|
||||
|
||||
defineExpose({
|
||||
open
|
||||
})
|
||||
|
||||
const emailStore = useEmailStore();
|
||||
const accountStore = useAccountStore()
|
||||
const editor = ref({})
|
||||
const userStore = useUserStore();
|
||||
const show = ref(false);
|
||||
const percent = ref(0)
|
||||
let percentMessage = null
|
||||
let sending = false
|
||||
const form = reactive({
|
||||
sendEmail: '',
|
||||
receiveEmail: '',
|
||||
accountId: -1,
|
||||
name: '',
|
||||
subject: '',
|
||||
content: '',
|
||||
text: '',
|
||||
attachments: []
|
||||
})
|
||||
|
||||
function clearContent() {
|
||||
ElMessageBox.confirm('确定要清空邮件吗?', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
resetForm()
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function delAtt(index) {
|
||||
form.attachments.splice(index, 1);
|
||||
}
|
||||
|
||||
function chooseFile() {
|
||||
const doc = document.createElement("input")
|
||||
doc.setAttribute("type", "file")
|
||||
doc.click()
|
||||
doc.onchange = async (e) => {
|
||||
|
||||
const file = e.target.files[0]
|
||||
const size = file.size
|
||||
const filename = file.name
|
||||
const contentType = file.type
|
||||
|
||||
const TotalSize = form.attachments.reduce((acc, item) => acc + item.size, 0);
|
||||
if ((TotalSize + size) > 29360128) {
|
||||
ElMessage({
|
||||
message: '附件文件大小限制28mb',
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const content = await fileToBase64(file)
|
||||
form.attachments.push({content, filename, size, contentType})
|
||||
}
|
||||
}
|
||||
|
||||
async function sendEmail() {
|
||||
|
||||
if (!form.receiveEmail) {
|
||||
ElMessage({
|
||||
message: '收件人邮箱地址不能为空',
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!isEmail(form.receiveEmail)) {
|
||||
ElMessage({
|
||||
message: '请输入正确的收件人邮箱',
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.subject) {
|
||||
ElMessage({
|
||||
message: '主题不能为空',
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.content) {
|
||||
ElMessage({
|
||||
message: '正文不能为空',
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (sending) {
|
||||
ElMessage({
|
||||
message: '邮件正在发送中',
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
percentMessage = ElMessage({
|
||||
message: () => h(sendPercent, { value: percent.value }),
|
||||
dangerouslyUseHTMLString: true,
|
||||
plain: true,
|
||||
duration: 0,
|
||||
customClass: 'message-bottom'
|
||||
})
|
||||
|
||||
sending = true
|
||||
close()
|
||||
emailSend(form, (e) => {
|
||||
percent.value = Math.round((e.loaded * 98) / e.total)
|
||||
}).then(email => {
|
||||
emailStore.sendScroll?.addItem(email)
|
||||
resetForm()
|
||||
show.value = false
|
||||
ElNotification({
|
||||
title: '邮件已发送',
|
||||
type: "success",
|
||||
message: h('span', { style: 'color: teal' }, email.subject),
|
||||
position: 'bottom-right'
|
||||
})
|
||||
userStore.refreshUserInfo();
|
||||
}).catch((e) => {
|
||||
ElNotification({
|
||||
title: '发送失败',
|
||||
type: e.code === 403 ? 'warning' : 'error',
|
||||
message: h('span', { style: 'color: teal' }, e.message),
|
||||
position: 'bottom-right'
|
||||
})
|
||||
}).finally(() => {
|
||||
percentMessage.close()
|
||||
percent.value = 0
|
||||
sending = false
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function resetForm() {
|
||||
form.receiveEmail = ''
|
||||
form.name = ''
|
||||
form.subject = ''
|
||||
form.content = ''
|
||||
form.attachments = []
|
||||
editor.value.clearEditor()
|
||||
}
|
||||
|
||||
function change(content, text) {
|
||||
form.content = content;
|
||||
form.text = text
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (!accountStore.currentAccount.email) {
|
||||
form.sendEmail = userStore.user.email;
|
||||
form.accountId = userStore.user.accountId;
|
||||
} else {
|
||||
form.sendEmail = accountStore.currentAccount.email;
|
||||
form.accountId = accountStore.currentAccount.accountId;
|
||||
}
|
||||
show.value = true;
|
||||
}
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
close()
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
|
||||
function close() {
|
||||
show.value = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.send {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.write-box {
|
||||
background: #FFFFFF;
|
||||
width: 900px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
transition: var(--el-transition-duration);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@media (max-width: 991px) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
height: min(800px, calc(100vh - 60px));
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
.title-left {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.title-text {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
|
||||
.item-title {
|
||||
color: #333;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.button-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto 1fr auto;
|
||||
|
||||
.att-add {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.att-clear {
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.att-list {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
max-height: 110px;
|
||||
overflow-y: auto;
|
||||
@media (max-width: 450px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
}
|
||||
|
||||
.att-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto auto;
|
||||
gap: 5px;
|
||||
height: 32px;
|
||||
font-size: 14px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
padding: 5px 5px;
|
||||
border-radius: 4px;
|
||||
|
||||
.att-filename {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -4,17 +4,21 @@ import router from './router';
|
||||
import 'element-plus/dist/index.css';
|
||||
import './style.css';
|
||||
import ElementPlus from 'element-plus';
|
||||
import VueCropper from 'vue-cropper';
|
||||
import 'vue-cropper/dist/index.css'
|
||||
import { init } from '@/utils/init.js';
|
||||
import { createPinia } from 'pinia';
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
||||
import piniaPersistedState from 'pinia-plugin-persistedstate';
|
||||
import perm from "@/directives/perm.js";
|
||||
const pinia = createPinia().use(piniaPersistedState)
|
||||
const app = createApp(App).use(pinia)
|
||||
|
||||
app.use(ElementPlus, {
|
||||
locale: zhCn,
|
||||
});
|
||||
await init()
|
||||
|
||||
app.use(router)
|
||||
app.use(router).use(VueCropper).directive('perm',perm)
|
||||
app.config.devtools = true;
|
||||
|
||||
app.mount('#app');
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import http from '@/axios/index.js';
|
||||
|
||||
export function emailList(accountId, emailId, size) {
|
||||
return http.get('/email/list', {params: {accountId, emailId, size}})
|
||||
export function emailList(accountId, emailId, timeSort, size, type) {
|
||||
return http.get('/email/list', {params: {accountId, emailId, timeSort, size, type}})
|
||||
}
|
||||
|
||||
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}, noMsg: true })
|
||||
}
|
||||
|
||||
export function emailLatest(emailId,accountId) {
|
||||
return http.get('/email/latest',{params: {emailId,accountId}})
|
||||
export function emailSend(form,progress) {
|
||||
return http.post('/email/send', form,{
|
||||
onUploadProgress: (e) => {
|
||||
progress(e)
|
||||
},
|
||||
noMsg: true
|
||||
})
|
||||
}
|
||||
14
mail-vue/src/request/my.js
Normal file
14
mail-vue/src/request/my.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import http from '@/axios/index.js';
|
||||
|
||||
export function loginUserInfo() {
|
||||
return http.get('/my/loginUserInfo')
|
||||
}
|
||||
|
||||
export function resetPassword(password) {
|
||||
return http.put('/my/resetPassword', {password})
|
||||
}
|
||||
|
||||
export function userDelete() {
|
||||
return http.delete('/my/delete')
|
||||
}
|
||||
|
||||
30
mail-vue/src/request/role.js
Normal file
30
mail-vue/src/request/role.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import http from '@/axios/index.js';
|
||||
|
||||
export function roleAdd(params) {
|
||||
return http.post('/role/add',params)
|
||||
}
|
||||
|
||||
export function rolePermTree() {
|
||||
return http.get('/role/permTree')
|
||||
}
|
||||
|
||||
export function roleRoleList() {
|
||||
return http.get('/role/list')
|
||||
}
|
||||
|
||||
export function roleSet(params) {
|
||||
return http.put('/role/set',params)
|
||||
}
|
||||
|
||||
export function roleDelete(roleId) {
|
||||
return http.delete('/role/delete',{params:{roleId}})
|
||||
}
|
||||
|
||||
export function roleSetDef(roleId) {
|
||||
return http.put('/role/setDefault',{roleId})
|
||||
}
|
||||
|
||||
|
||||
export function roleSelectUse() {
|
||||
return http.get('/role/selectUse')
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -6,4 +6,12 @@ export function settingSet(setting) {
|
||||
|
||||
export function settingQuery() {
|
||||
return http.get('/setting/query')
|
||||
}
|
||||
|
||||
export function setBackground(background) {
|
||||
return http.put('/setting/setBackground',{background})
|
||||
}
|
||||
|
||||
export function physicsDeleteAll() {
|
||||
return http.delete('/setting/physicsDeleteAll')
|
||||
}
|
||||
9
mail-vue/src/request/sys-email.js
Normal file
9
mail-vue/src/request/sys-email.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import http from '@/axios/index.js';
|
||||
|
||||
export function sysEmailAll(params) {
|
||||
return http.get('/sys-email/list', {params: {...params}})
|
||||
}
|
||||
|
||||
export function sysEmailDelete(emailIds) {
|
||||
return http.delete('/sys-email/delete?emailIds=' + emailIds)
|
||||
}
|
||||
@@ -1,13 +1,35 @@
|
||||
import http from '@/axios/index.js';
|
||||
import http from '@/axios/index.js'
|
||||
|
||||
export function loginUserInfo() {
|
||||
return http.get('/user/loginUserInfo')
|
||||
|
||||
export function userList(params) {
|
||||
return http.get('/user/list', {params: {...params}})
|
||||
}
|
||||
|
||||
export function resetPassword(password) {
|
||||
return http.put('/user/resetPassword', {password})
|
||||
export function userSetPwd(params) {
|
||||
return http.put('/user/setPwd', params)
|
||||
}
|
||||
|
||||
export function userDelete() {
|
||||
return http.delete('/user/delete')
|
||||
export function userSetStatus(params) {
|
||||
return http.put('/user/setStatus', params)
|
||||
}
|
||||
|
||||
export function userSetType(params) {
|
||||
return http.put('/user/setType', params)
|
||||
}
|
||||
|
||||
|
||||
export function userDelete(userId) {
|
||||
return http.delete('/user/delete', {params:{userId}})
|
||||
}
|
||||
|
||||
export function userAdd(form) {
|
||||
return http.post('/user/add', form)
|
||||
}
|
||||
|
||||
export function userRestSendCount(userId) {
|
||||
return http.put('/user/resetSendCount', {userId})
|
||||
}
|
||||
|
||||
export function userRestore(userId,type) {
|
||||
return http.put('/user/restore', {userId,type})
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
import {createRouter, createWebHashHistory} from 'vue-router'
|
||||
import {loginUserInfo} from "@/request/user.js";
|
||||
import {createRouter, createWebHistory} from 'vue-router'
|
||||
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 = [
|
||||
{
|
||||
@@ -22,6 +18,16 @@ const routes = [
|
||||
menu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/send',
|
||||
name: 'send',
|
||||
component: () => import('@/views/send/index.vue'),
|
||||
meta: {
|
||||
title: '已发送',
|
||||
name: 'send',
|
||||
menu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/content',
|
||||
name: 'content',
|
||||
@@ -42,16 +48,6 @@ const routes = [
|
||||
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',
|
||||
@@ -74,28 +70,26 @@ const routes = [
|
||||
path: '/test',
|
||||
name: 'test',
|
||||
component: () => import('@/views/test/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: '404',
|
||||
component: () => import('@/views/404/index.vue')
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
history: createWebHistory(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'
|
||||
})
|
||||
@@ -105,18 +99,6 @@ router.beforeEach(async (to, from, next) => {
|
||||
return next()
|
||||
}
|
||||
|
||||
if (uiStore.init && to.name !== 'login') {
|
||||
await initSettingAndUserInfo()
|
||||
firstLogin = false
|
||||
return next()
|
||||
}
|
||||
|
||||
if (firstLogin) {
|
||||
await initUserInfo()
|
||||
firstLogin = false
|
||||
return next()
|
||||
}
|
||||
|
||||
next()
|
||||
|
||||
})
|
||||
@@ -125,60 +107,16 @@ router.afterEach((to) => {
|
||||
|
||||
const uiStore = useUiStore()
|
||||
if (to.meta.menu) {
|
||||
if (['content', 'email'].includes(to.meta.name)) {
|
||||
if (['content', 'email', 'send'].includes(to.meta.name)) {
|
||||
uiStore.accountShow = window.innerWidth > 767;
|
||||
} else {
|
||||
uiStore.accountShow = false
|
||||
}
|
||||
}
|
||||
|
||||
if (to.name === 'login') {
|
||||
firstLogin = true
|
||||
}
|
||||
|
||||
if (window.innerWidth < 768) {
|
||||
if (window.innerWidth < 992) {
|
||||
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
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
export const useAccountStore = defineStore('account', {
|
||||
state: () => ({
|
||||
currentAccountId: 0,
|
||||
currentAccount: {}
|
||||
})
|
||||
})
|
||||
@@ -5,9 +5,14 @@ export const useEmailStore = defineStore('email', {
|
||||
deleteIds: 0,
|
||||
starScroll: null,
|
||||
emailScroll: null,
|
||||
readEmail: {},
|
||||
contentData: {
|
||||
email: null,
|
||||
delType: null,
|
||||
showStar: true
|
||||
},
|
||||
sendScroll: null,
|
||||
}),
|
||||
persist: {
|
||||
pick: ['readEmail'],
|
||||
pick: ['contentData'],
|
||||
},
|
||||
})
|
||||
|
||||
12
mail-vue/src/store/role.js
Normal file
12
mail-vue/src/store/role.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useRoleStore = defineStore('role', {
|
||||
state: () => ({
|
||||
refresh: 0,
|
||||
}),
|
||||
actions: {
|
||||
refreshSelect() {
|
||||
this.refresh ++
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,21 +1,13 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { settingQuery} from "@/request/setting.js";
|
||||
|
||||
export const useSettingStore = defineStore('setting', {
|
||||
state: () => ({
|
||||
domainList: [],
|
||||
settings: {
|
||||
title: '-'
|
||||
r2Domain: ''
|
||||
}
|
||||
}),
|
||||
actions: {
|
||||
async initSetting() {
|
||||
if (this.domainList.length === 0) {
|
||||
const data = await settingQuery()
|
||||
this.domainList.push(...data.domainList)
|
||||
delete data.domainList
|
||||
this.settings = data
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,9 +3,14 @@ import { defineStore } from 'pinia'
|
||||
export const useUiStore = defineStore('ui', {
|
||||
state: () => ({
|
||||
asideShow: window.innerWidth > 991,
|
||||
loginLoading: false,
|
||||
accountShow: false,
|
||||
init: true,
|
||||
backgroundLoading: true,
|
||||
writerRef: null,
|
||||
asideCount: {
|
||||
email: 0,
|
||||
send: 0,
|
||||
sysEmail: 0
|
||||
}
|
||||
}),
|
||||
persist: {
|
||||
pick: ['accountShow'],
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import {loginUserInfo} from "@/request/my.js";
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
user: {},
|
||||
})
|
||||
}),
|
||||
actions: {
|
||||
refreshUserInfo() {
|
||||
loginUserInfo().then(user => {
|
||||
this.user = user
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -64,6 +64,26 @@ button, input, select, textarea {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tox .tox-dialog--width-lg {
|
||||
@media (min-width: 992px) {
|
||||
height: 850px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tox .tox-dialog__body-content {
|
||||
max-height: min(850px, calc(100vh - 110px)) !important;
|
||||
@media (min-width: 992px) {
|
||||
max-height: min(850px, calc(100vh - 110px));
|
||||
}
|
||||
}
|
||||
|
||||
.tox .tox-dialog__body-content {
|
||||
@media (min-width: 992px) {
|
||||
max-height: min(850px, calc(100vh - 110px)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
:root {
|
||||
--el-color-primary: #1890ff;
|
||||
--el-color-primary-dark-2: #1064c0;
|
||||
@@ -73,3 +93,32 @@ button, input, select, textarea {
|
||||
--el-color-primary-light-9: #e6f7ff;
|
||||
--el-text-color-regular: #333;
|
||||
}
|
||||
|
||||
.message-bottom {
|
||||
top: auto !important;
|
||||
bottom: 20px !important;
|
||||
.el-icon {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (pointer: fine) and (hover: hover) {
|
||||
/* 整个滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px; /* 垂直滚动条宽度 */
|
||||
height: 6px; /* 水平滚动条高度 */
|
||||
}
|
||||
|
||||
/* 滚动条轨道 */
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* 滚动条滑块 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
|
||||
export function cvtR2Url(key) {
|
||||
const settingStore = useSettingStore();
|
||||
return 'https://' + settingStore.settings.r2Domain + '/' + key
|
||||
return settingStore.settings.r2Domain + '/' + key
|
||||
}
|
||||
43
mail-vue/src/utils/day.js
Normal file
43
mail-vue/src/utils/day.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
|
||||
export function fromNow(date) {
|
||||
const d = dayjs.utc(date).tz('Asia/Shanghai');
|
||||
const now = dayjs();
|
||||
const diffSeconds = now.diff(d, 'second');
|
||||
const diffMinutes = now.diff(d, 'minute');
|
||||
const diffHours = now.diff(d, 'hour');
|
||||
const isToday = now.isSame(d, 'day');
|
||||
|
||||
if (isToday) {
|
||||
if (diffSeconds < 60) return `几秒前`;
|
||||
if (diffMinutes < 60) return `${diffMinutes}分钟前`;
|
||||
if (diffHours >= 1 && diffHours < 2) return '1小时前';
|
||||
return d.format('HH:mm');
|
||||
}
|
||||
else if (now.subtract(1, 'day').isSame(d, 'day')) {
|
||||
return `昨天 ${d.format('HH:mm')}`;
|
||||
}
|
||||
else if (now.subtract(2, 'day').isSame(d, 'day')) {
|
||||
return `前天 ${d.format('HH:mm')}`;
|
||||
}
|
||||
return d.year() === now.year()
|
||||
? d.format('M月D日')
|
||||
: d.format('YYYY/M/D');
|
||||
}
|
||||
|
||||
|
||||
export function formatDetailDate(time) {
|
||||
return dayjs.utc(time).format('YYYY年M月D日 ddd AH:mm')
|
||||
}
|
||||
|
||||
export function tzDayjs(time) {
|
||||
return dayjs.utc(time).tz('Asia/Shanghai')
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import Compressor from "compressorjs";
|
||||
|
||||
export function getExtName(fileName) {
|
||||
const index = fileName.lastIndexOf('.')
|
||||
return index !== -1 ? fileName.slice(index + 1).toLowerCase() : ''
|
||||
@@ -10,4 +12,38 @@ export function formatBytes(bytes) {
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
const size = (bytes / Math.pow(k, i)).toFixed(2);
|
||||
return `${size} ${units[i]}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function fileToBase64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => {
|
||||
const base64 = reader.result.split(',')[1];
|
||||
resolve(base64);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
});
|
||||
}
|
||||
|
||||
export function base64Size(base64String) {
|
||||
const padding = (base64String.match(/=*$/) || [''])[0].length;
|
||||
const base64Length = base64String.length;
|
||||
return (base64Length * 3) / 4 - padding;
|
||||
}
|
||||
|
||||
export function compressImage(file, quality = 0.6) {
|
||||
return new Promise((resolve, reject) => {
|
||||
new Compressor(file, {
|
||||
quality,
|
||||
mimeType: 'image/jpeg',
|
||||
convertSize: 1024 * 1024,
|
||||
success(result) {
|
||||
resolve(result);
|
||||
},
|
||||
error(err) {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
16
mail-vue/src/utils/icon-utils.js
Normal file
16
mail-vue/src/utils/icon-utils.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import {getExtName} from "@/utils/file-utils.js";
|
||||
|
||||
export function getIconByName(filename) {
|
||||
const extName = getExtName(filename)
|
||||
if (['zip', 'rar', '7z', 'tar', 'tgz'].includes(extName)) return 'octicon:file-zip-24';
|
||||
if (['png', 'jpg', 'jpeg','gif','webp'].includes(extName)) return 'mingcute:pic-line';
|
||||
if (['mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv'].includes(extName)) return 'fluent:video-clip-24-regular';
|
||||
if (['txt', 'doc', 'docx', 'md','ini','conf'].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"
|
||||
}
|
||||
71
mail-vue/src/utils/init.js
Normal file
71
mail-vue/src/utils/init.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import { useUserStore } from "@/store/user.js";
|
||||
import { useSettingStore } from "@/store/setting.js";
|
||||
import { useAccountStore } from "@/store/account.js";
|
||||
import { loginUserInfo } from "@/request/my.js";
|
||||
import { permsToRouter } from "@/utils/perm.js";
|
||||
import router from "@/router";
|
||||
import { settingQuery } from "@/request/setting.js";
|
||||
import {cvtR2Url} from "@/utils/convert.js";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
export async function init() {
|
||||
document.title = '-'
|
||||
|
||||
const settingStore = useSettingStore();
|
||||
const userStore = useUserStore();
|
||||
const accountStore = useAccountStore();
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
let setting = null;
|
||||
|
||||
if (token) {
|
||||
const userPromise = loginUserInfo().catch(e => {
|
||||
console.error(e);
|
||||
return null;
|
||||
});
|
||||
|
||||
const [s, user] = await Promise.all([settingQuery(), userPromise]);
|
||||
setting = s;
|
||||
settingStore.settings = setting;
|
||||
settingStore.domainList = setting.domainList;
|
||||
document.title = setting.title;
|
||||
|
||||
if (user) {
|
||||
accountStore.currentAccountId = user.accountId;
|
||||
userStore.user = user;
|
||||
|
||||
const routers = permsToRouter(user.permKeys);
|
||||
routers.forEach(routerData => {
|
||||
router.addRoute('layout', routerData);
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
setting = await settingQuery();
|
||||
settingStore.settings = setting;
|
||||
settingStore.domainList = setting.domainList;
|
||||
document.title = setting.title;
|
||||
}
|
||||
|
||||
const loading = document.getElementById('loading-first');
|
||||
|
||||
if (!setting.background) {
|
||||
loading.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
const img = new Image();
|
||||
img.src = cvtR2Url(setting.background);
|
||||
img.onload = () => {
|
||||
loading.remove();
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
|
||||
console.warn('背景图片加载失败:', img.src);
|
||||
loading.remove();
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
60
mail-vue/src/utils/perm.js
Normal file
60
mail-vue/src/utils/perm.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import {useUserStore} from "@/store/user.js";
|
||||
|
||||
export default function hasPerm(permKey) {
|
||||
const {permKeys} = useUserStore().user;
|
||||
return permKeys.includes('*') || permKeys.includes(permKey);
|
||||
}
|
||||
|
||||
|
||||
export function permsToRouter(permKeys) {
|
||||
const routerList = []
|
||||
Object.keys(routers).forEach(perm => {
|
||||
if (permKeys.includes(perm) || permKeys.includes('*')) {
|
||||
routerList.push(routers[perm])
|
||||
}
|
||||
})
|
||||
return routerList;
|
||||
}
|
||||
|
||||
const routers = {
|
||||
'user:query': {
|
||||
path: '/sys/user',
|
||||
name: 'user',
|
||||
component: () => import('@/views/user/index.vue'),
|
||||
meta: {
|
||||
title: '用户列表',
|
||||
name: 'user',
|
||||
menu: true
|
||||
}
|
||||
},
|
||||
'role:query': {
|
||||
path: '/sys/role',
|
||||
name: 'role',
|
||||
component: () => import('@/views/role/index.vue'),
|
||||
meta: {
|
||||
title: '权限控制',
|
||||
name: 'role',
|
||||
menu: true
|
||||
}
|
||||
},
|
||||
'setting:query': {
|
||||
path: '/sys/setting',
|
||||
name: 'sys-setting',
|
||||
component: () => import('@/views/sys-setting/index.vue'),
|
||||
meta: {
|
||||
title: '系统设置',
|
||||
name: 'sys-setting',
|
||||
menu: true
|
||||
}
|
||||
},
|
||||
'sys-email:query': {
|
||||
path: '/sys/email',
|
||||
name: 'sys-email',
|
||||
component: () => import('@/views/sys-email/index.vue'),
|
||||
meta: {
|
||||
title: '邮箱列表',
|
||||
name: 'sys-email',
|
||||
menu: true
|
||||
}
|
||||
}
|
||||
}
|
||||
21
mail-vue/src/views/404/index.vue
Normal file
21
mail-vue/src/views/404/index.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import router from "@/router/index.js";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="box-404">
|
||||
<el-empty class="empty" description="404错误, 找不到页面" >
|
||||
<el-button type="primary" @click="router.replace({name: 'layout'})">返回首页</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.box-404 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -2,9 +2,11 @@
|
||||
<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"/>
|
||||
<Icon v-perm="'email:delete'" class="icon" icon="uiw:delete" width="16" height="16" @click="handleDelete"/>
|
||||
<template v-if="emailStore.contentData.showStar">
|
||||
<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"/>
|
||||
</template>
|
||||
</div>
|
||||
<div></div>
|
||||
<el-scrollbar class="scrollbar">
|
||||
@@ -26,95 +28,112 @@
|
||||
<div>{{ formatDetailDate(email.createTime) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="icon">
|
||||
|
||||
</div>
|
||||
<el-alert v-if="email.status === 3" :closable="false" :title="'发送失败: ' + toMessage(email.message)" class="email-msg" type="error" show-icon />
|
||||
<el-alert v-if="email.status === 4" :closable="false" title="被标记为垃圾邮件" class="email-msg" type="warning" show-icon />
|
||||
<el-alert v-if="email.status === 5" :closable="false" title="邮件发送被延迟" class="email-msg" type="warning" show-icon />
|
||||
</div>
|
||||
<el-scrollbar class="htm-scrollbar">
|
||||
<div v-html="email.content"/>
|
||||
<el-scrollbar class="htm-scrollbar" :class="email.attList.length === 0 ? 'bottom-distance' : ''">
|
||||
<ShadowHtml :html="formatImage(email)" />
|
||||
</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 style="color: #333" :href="cvtR2Url(att.key)" download>
|
||||
<Icon icon="system-uicons:push-down" width="22" height="22"/>
|
||||
</a>
|
||||
<div class="att" v-if="email.attList.length > 0">
|
||||
<div class="att-title">
|
||||
<span>附件列表</span>
|
||||
<span>共 {{email.attList.length}} 个</span>
|
||||
</div>
|
||||
<div class="att-box">
|
||||
|
||||
<div class="att-item" v-for="att in email.attList" :key="att.attId">
|
||||
<div class="att-icon" @click="showImage(att.key)">
|
||||
<Icon :icon="getIconByName(att.filename)" width="20" height="20"/>
|
||||
</div>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="att.filename"
|
||||
>
|
||||
<div class="att-name" @click="showImage(att.key)">
|
||||
{{ att.filename }}
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<div style="color: rgba(24, 36, 48, 0.6);">{{ formatBytes(att.size) }}</div>
|
||||
<div class="opt-icon att-icon">
|
||||
<Icon v-if="isImage(att.filename)" icon="hugeicons:view" width="22" height="22" @click="showImage(att.key)"/>
|
||||
<a :href="cvtR2Url(att.key)" download>
|
||||
<Icon icon="system-uicons:push-down" width="22" height="22"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<el-image-viewer
|
||||
v-if="showPreview"
|
||||
:url-list="srcList"
|
||||
show-progress
|
||||
@close="showPreview = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {reactive, watch} from "vue";
|
||||
import ShadowHtml from '@/components/shadow-html/index.vue'
|
||||
import {reactive, ref, watch} from "vue";
|
||||
import {useRouter} from 'vue-router'
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import {attList, emailDelete} from "@/request/email.js";
|
||||
import {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 {formatDetailDate} from "@/utils/day.js";
|
||||
import {starAdd, starCancel} from "@/request/star.js";
|
||||
import {getExtName, formatBytes} from "@/utils/file-utils.js";
|
||||
import {cvtR2Url} from "@/utils/convert-utils.js";
|
||||
import {cvtR2Url} from "@/utils/convert.js";
|
||||
import {getIconByName} from "@/utils/icon-utils.js";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
import {sysEmailDelete} from "@/request/sys-email.js";
|
||||
|
||||
const settingStore = useSettingStore();
|
||||
const accountStore = useAccountStore();
|
||||
|
||||
const emailStore = useEmailStore();
|
||||
|
||||
const router = useRouter()
|
||||
const email = emailStore.readEmail
|
||||
const atts = reactive([])
|
||||
|
||||
attList(email.emailId).then(list => {
|
||||
atts.push(...list)
|
||||
})
|
||||
const email = emailStore.contentData.email
|
||||
const showPreview = ref(false)
|
||||
const srcList = reactive([])
|
||||
|
||||
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 toMessage(message) {
|
||||
return message ? JSON.parse(message).message : '';
|
||||
}
|
||||
|
||||
function formatImage(email) {
|
||||
let content = email.content || email.text || ''
|
||||
const domain = settingStore.settings.r2Domain;
|
||||
return content.replace('{{domain}}', domain + '/');
|
||||
}
|
||||
|
||||
function showImage(key) {
|
||||
if (!isImage(key)) return;
|
||||
const url = cvtR2Url(key)
|
||||
srcList.length = 0
|
||||
srcList.push(url)
|
||||
showPreview.value = true
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -124,8 +143,7 @@ function changeStar() {
|
||||
email.isStar = 1;
|
||||
starAdd(email.emailId).then(() => {
|
||||
email.isStar = 1;
|
||||
emailStore.emailScroll?.editEmailStar(email.emailId, 1)
|
||||
emailStore.starScroll?.addItemStar(email)
|
||||
emailStore.starScroll?.addItem(email)
|
||||
}).catch((e) => {
|
||||
console.error(e)
|
||||
email.isStar = 0;
|
||||
@@ -143,14 +161,27 @@ const handleDelete = () => {
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
emailDelete(email.value.emailId).then(() => {
|
||||
ElMessage({
|
||||
message: '删除成功',
|
||||
type: 'success',
|
||||
plain: true,
|
||||
if (emailStore.contentData.delType === 'logic') {
|
||||
emailDelete(email.emailId).then(() => {
|
||||
ElMessage({
|
||||
message: '删除成功',
|
||||
type: 'success',
|
||||
plain: true,
|
||||
})
|
||||
emailStore.deleteIds = [email.emailId]
|
||||
})
|
||||
emailStore.deleteIds = [email.value.emailId]
|
||||
})
|
||||
} else {
|
||||
|
||||
sysEmailDelete(email.emailId).then(() => {
|
||||
ElMessage({
|
||||
message: '删除成功',
|
||||
type: 'success',
|
||||
plain: true,
|
||||
})
|
||||
emailStore.deleteIds = [email.emailId]
|
||||
})
|
||||
}
|
||||
|
||||
router.back()
|
||||
})
|
||||
}
|
||||
@@ -165,7 +196,7 @@ const handleDelete = () => {
|
||||
padding: 9px 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 28px;
|
||||
gap: 20px;
|
||||
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
|
||||
font-size: 18px;
|
||||
|
||||
@@ -193,8 +224,6 @@ const handleDelete = () => {
|
||||
}
|
||||
|
||||
.htm-scrollbar {
|
||||
padding-bottom: 15px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -202,28 +231,41 @@ const handleDelete = () => {
|
||||
flex-direction: column;
|
||||
|
||||
.att {
|
||||
margin-top: 20px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #e7e9ec;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #e4e7ed;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
width: fit-content;
|
||||
.att-box {
|
||||
min-width: min(410px,calc(100vw - 53px));
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.att-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
span:first-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.att-item {
|
||||
cursor: pointer;
|
||||
div {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
padding: 10px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 4px;
|
||||
align-self: start;
|
||||
border: 1px solid #e7e9ec;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto 20px;
|
||||
grid-template-columns: auto 1fr auto auto;
|
||||
gap: 10px;
|
||||
|
||||
.att-icon {
|
||||
@@ -231,7 +273,7 @@ const handleDelete = () => {
|
||||
}
|
||||
|
||||
.att-name {
|
||||
|
||||
margin-right: 10px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -244,29 +286,36 @@ const handleDelete = () => {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.down-icon {
|
||||
.opt-icon {
|
||||
color: rgba(24, 36, 48, 0.6);
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
a {
|
||||
color: rgba(24, 36, 48, 0.6);
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.email-msg {
|
||||
max-width: 400px;
|
||||
width: fit-content;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.send {
|
||||
display: flex;
|
||||
margin-bottom: 6px;
|
||||
@@ -278,7 +327,7 @@ const handleDelete = () => {
|
||||
}
|
||||
|
||||
.send-name-title {
|
||||
padding-right: 10px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,4 +354,9 @@ const handleDelete = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-distance {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -5,18 +5,33 @@
|
||||
:getEmailList="getEmailList"
|
||||
:emailDelete="emailDelete"
|
||||
:star-add="starAdd"
|
||||
:star-cancel="starCancel"/>
|
||||
:star-cancel="starCancel"
|
||||
:time-sort="params.timeSort"
|
||||
actionLeft="4px"
|
||||
@jump="jumpContent"
|
||||
>
|
||||
<template #first>
|
||||
<Icon class="icon" @click="changeTimeSort" icon="material-symbols-light:timer-arrow-down-outline"
|
||||
v-if="params.timeSort === 0" width="28" height="28"/>
|
||||
<Icon class="icon" @click="changeTimeSort" icon="material-symbols-light:timer-arrow-up-outline" v-else
|
||||
width="28" height="28"/>
|
||||
</template>
|
||||
|
||||
</emailScroll>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useAccountStore} from "@/store/account.js";
|
||||
import {useEmailStore} from "@/store/email.js";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
import {useUiStore} from "@/store/ui.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 {defineOptions, onMounted, reactive, ref, watch} from "vue";
|
||||
import {sleep} from "@/utils/time-utils.js";
|
||||
import router from "@/router/index.js";
|
||||
import {Icon} from "@iconify/vue";
|
||||
|
||||
defineOptions({
|
||||
name: 'email'
|
||||
@@ -26,6 +41,9 @@ const emailStore = useEmailStore();
|
||||
const accountStore = useAccountStore();
|
||||
const settingStore = useSettingStore();
|
||||
const scroll = ref({})
|
||||
const params = reactive({
|
||||
timeSort: 0,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
emailStore.emailScroll = scroll;
|
||||
@@ -37,16 +55,39 @@ watch(() => accountStore.currentAccountId, () => {
|
||||
scroll.value.refreshList();
|
||||
})
|
||||
|
||||
function changeTimeSort() {
|
||||
params.timeSort = params.timeSort ? 0 : 1
|
||||
scroll.value.refreshList();
|
||||
}
|
||||
|
||||
function jumpContent(email) {
|
||||
emailStore.contentData.email = email
|
||||
emailStore.contentData.delType = 'logic'
|
||||
emailStore.contentData.showStar = true
|
||||
router.push('/content')
|
||||
}
|
||||
|
||||
const existIds = new Set();
|
||||
|
||||
async function latest() {
|
||||
while (true) {
|
||||
const latestId = scroll.value.emailList[0]?.emailId ?? 0
|
||||
const latestId = scroll.value.latestEmail?.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)
|
||||
const curTimeSort = params.timeSort
|
||||
const list = await emailLatest(latestId, accountId)
|
||||
if (accountId === accountStore.currentAccountId && params.timeSort === curTimeSort) {
|
||||
if (list.length > 0) {
|
||||
|
||||
list.forEach(email => {
|
||||
existIds.add(email.emailId)
|
||||
scroll.value.addItem(email)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
@@ -57,7 +98,7 @@ async function latest() {
|
||||
}
|
||||
|
||||
function addStar(email) {
|
||||
emailStore.starScroll?.addItemStar(email)
|
||||
emailStore.starScroll?.addItem(email)
|
||||
}
|
||||
|
||||
function cancelStar(email) {
|
||||
@@ -65,8 +106,12 @@ function cancelStar(email) {
|
||||
}
|
||||
|
||||
function getEmailList(emailId, size) {
|
||||
return emailList(accountStore.currentAccountId, emailId, size)
|
||||
return emailList(accountStore.currentAccountId, emailId, params.timeSort, size, 0)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div id="box">
|
||||
<div id="background-wrap">
|
||||
<div id="login-box" :style="background">
|
||||
<div id="background-wrap" v-if="!settingStore.settings.background">
|
||||
<div class="x1 cloud"></div>
|
||||
<div class="x2 cloud"></div>
|
||||
<div class="x3 cloud"></div>
|
||||
@@ -8,19 +8,18 @@
|
||||
<div class="x5 cloud"></div>
|
||||
</div>
|
||||
<div class="form-wrapper">
|
||||
<el-form autocomplete="off">
|
||||
<div class="container" >
|
||||
<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" />
|
||||
<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" />
|
||||
<Icon icon="weui:email-outlined" width="22" height="22"/>
|
||||
</template>
|
||||
<template #append>
|
||||
<div @click.stop="openSelect">
|
||||
<div @click.stop="openSelect">
|
||||
<el-select
|
||||
ref="mySelect"
|
||||
v-model="suffix"
|
||||
@@ -35,28 +34,28 @@
|
||||
/>
|
||||
</el-select>
|
||||
<div style="color: #333">
|
||||
<span >{{suffix}}</span>
|
||||
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20" />
|
||||
<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" />
|
||||
<Icon icon="carbon:password" width="22" height="22"/>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button class="btn" type="primary" @click="submit" :loading="uiStore.loginLoading"
|
||||
<el-button class="btn" type="primary" @click="submit" :loading="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" />
|
||||
<Icon icon="weui:email-outlined" width="22" height="22"/>
|
||||
</template>
|
||||
<template #append>
|
||||
<div @click.stop="openSelect">
|
||||
<div @click.stop="openSelect">
|
||||
<el-select
|
||||
ref="mySelect"
|
||||
v-model="suffix"
|
||||
@@ -71,20 +70,20 @@
|
||||
/>
|
||||
</el-select>
|
||||
<div style="color: #333">
|
||||
<span>{{suffix}}</span>
|
||||
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20" />
|
||||
<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" />
|
||||
<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" />
|
||||
<Icon icon="carbon:password" width="22" height="22"/>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button class="btn" type="primary" @click="submitRegister" :loading="registerLoading"
|
||||
@@ -92,7 +91,6 @@
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="verifyShow"
|
||||
@@ -111,24 +109,30 @@
|
||||
|
||||
<script setup>
|
||||
import router from "@/router";
|
||||
import {nextTick, reactive, ref} from "vue";
|
||||
import {computed, 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 {useAccountStore} from "@/store/account.js";
|
||||
import {useUserStore} from "@/store/user.js";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {cvtR2Url} from "@/utils/convert.js";
|
||||
import {loginUserInfo} from "@/request/my.js";
|
||||
import {permsToRouter} from "@/utils/perm.js";
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const userStore = useUserStore();
|
||||
const settingStore = useSettingStore();
|
||||
const uiStore = useUiStore()
|
||||
const loginLoading = ref(false)
|
||||
const show = ref('login')
|
||||
const form = reactive({
|
||||
email: '',
|
||||
password: '',
|
||||
|
||||
});
|
||||
const options = [{label: '登录', value: 'login'},{label: '注册', value: 'register'}];
|
||||
const options = [{label: '登录', value: 'login'}, {label: '注册', value: 'register'}];
|
||||
const mySelect = ref()
|
||||
const suffix = ref('')
|
||||
const registerForm = reactive({
|
||||
@@ -136,18 +140,28 @@ const registerForm = reactive({
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
const domainList = settingStore.domainList;
|
||||
const domainList = settingStore.domainList;
|
||||
const registerLoading = ref(false)
|
||||
suffix.value = domainList[0]
|
||||
const verifyShow = ref(false)
|
||||
let verifyToken = ''
|
||||
let turnstileId = ''
|
||||
|
||||
const background = computed(() => {
|
||||
|
||||
return settingStore.settings.background ? {
|
||||
'background-image': `url(${cvtR2Url(settingStore.settings.background)})`,
|
||||
'background-repeat': 'no-repeat',
|
||||
'background-size': 'cover',
|
||||
'background-position': 'center'
|
||||
} : ''
|
||||
})
|
||||
|
||||
window.onTurnstileSuccess = (token) => {
|
||||
verifyToken = token;
|
||||
setTimeout(() => {
|
||||
verifyShow.value = false
|
||||
},1500)
|
||||
}, 1500)
|
||||
};
|
||||
|
||||
const openSelect = () => {
|
||||
@@ -184,21 +198,19 @@ const submit = () => {
|
||||
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 => {
|
||||
loginLoading.value = true
|
||||
login(form.email + suffix.value, form.password).then(async data => {
|
||||
localStorage.setItem('token', data.token)
|
||||
router.replace({name: 'layout'})
|
||||
}).catch(() => {
|
||||
uiStore.loginLoading = false
|
||||
const user = await loginUserInfo();
|
||||
accountStore.currentAccountId = user.accountId;
|
||||
userStore.user = user;
|
||||
const routers = permsToRouter(user.permKeys);
|
||||
routers.forEach(routerData => {
|
||||
router.addRoute('layout', routerData);
|
||||
});
|
||||
await router.replace({name: 'layout'})
|
||||
}).finally(() => {
|
||||
loginLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -255,7 +267,7 @@ function submitRegister() {
|
||||
verifyShow.value = true
|
||||
if (!turnstileId) {
|
||||
nextTick(() => {
|
||||
turnstileId = window.turnstile.render('.register-turnstile')
|
||||
turnstileId = window.turnstile.render('.register-turnstile')
|
||||
})
|
||||
} else {
|
||||
nextTick(() => {
|
||||
@@ -266,7 +278,7 @@ function submitRegister() {
|
||||
}
|
||||
|
||||
registerLoading.value = true
|
||||
register({email: registerForm.email + suffix.value, password: registerForm.password,token: verifyToken}).then(() => {
|
||||
register({email: registerForm.email + suffix.value, password: registerForm.password, token: verifyToken}).then(() => {
|
||||
show.value = 'login'
|
||||
registerForm.email = ''
|
||||
registerForm.password = ''
|
||||
@@ -305,14 +317,17 @@ function submitRegister() {
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 380px;
|
||||
background: #FFFFFF;
|
||||
padding: 20px;
|
||||
margin: 0 20px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
border: 1px solid #e4e7ed;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
.switch {
|
||||
padding-top: 10px;
|
||||
font-size: 14px;
|
||||
@@ -334,10 +349,6 @@ function submitRegister() {
|
||||
}
|
||||
}
|
||||
|
||||
form{
|
||||
max-width: 410px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.setting-icon {
|
||||
position: relative;
|
||||
@@ -357,15 +368,17 @@ form{
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.custom-style {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.custom-style .el-segmented {
|
||||
--el-border-radius-base: 8px;
|
||||
--el-border-radius-base: 6px;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
#box {
|
||||
#login-box {
|
||||
background: linear-gradient(to bottom, #2980b9, #6dd5fa, #fff);
|
||||
color: #333;
|
||||
font: 100% Arial, sans-serif;
|
||||
@@ -375,6 +388,7 @@ form{
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
|
||||
#background-wrap {
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
|
||||
411
mail-vue/src/views/role/index.vue
Normal file
411
mail-vue/src/views/role/index.vue
Normal file
@@ -0,0 +1,411 @@
|
||||
<template>
|
||||
<div class="perm-box">
|
||||
<div class="header-actions">
|
||||
<Icon class="icon" icon="ion:add-outline" width="23" height="23" @click="openAddRole" />
|
||||
<Icon class="icon" icon="ion:reload" width="18" height="18" @click="refresh" />
|
||||
</div>
|
||||
<div class="loading" v-if="tableLoading">
|
||||
<loading/>
|
||||
</div>
|
||||
<el-scrollbar v-else class="perm-scrollbar">
|
||||
<div>
|
||||
<el-table
|
||||
:data="roles"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-table-column width="10" />
|
||||
<el-table-column label="身份名称" prop="name" :min-width="roleWidth">
|
||||
<template #default="props">
|
||||
<div class="role-name">
|
||||
<span >{{props.row.name}}</span>
|
||||
<span v-if="props.row.isDefault"><el-tag class="def-tag" type="success">默认</el-tag></span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" :width="sortWidth" prop="sort"/>
|
||||
<el-table-column v-if="desShow" label="描述" min-width="200" prop="description" >
|
||||
<template #default="props">
|
||||
<div class="description">
|
||||
<span >{{props.row.description}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" :width="settingWidth">
|
||||
<template #default="props">
|
||||
<el-dropdown trigger="click">
|
||||
<el-button size="small" type="primary">操作</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="openRoleSet(props.row)">修改</el-dropdown-item>
|
||||
<el-dropdown-item @click="setDef(props.row)">默认</el-dropdown-item>
|
||||
<el-dropdown-item @click="delRole(props.row)">删除</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<el-dialog class="dialog" v-model="roleFormShow" :title="dialogType.title" @closed="resetForm">
|
||||
<div class="dialog-box">
|
||||
<el-input class="dialog-input" v-model="form.name" type="text" :maxlength="12" placeholder="身份名称" autocomplete="off" />
|
||||
<el-input class="dialog-input" v-model="form.description" :maxlength="30" type="text" placeholder="描述" autocomplete="off" />
|
||||
<div class="dialog-input">
|
||||
<el-input-number placeholder="排序" :min="0" :max="9999" v-model.number="form.sort" controls-position="right" autocomplete="off" />
|
||||
</div>
|
||||
<el-segmented size="small" class="perm-expand" @change="expandChange" v-model="expand" :options="options" />
|
||||
<el-tree
|
||||
:expand-on-click-node="false"
|
||||
:check-on-click-node="false"
|
||||
ref="tree"
|
||||
:data="treeList"
|
||||
show-checkbox
|
||||
node-key="permId"
|
||||
:default-expand-all="expand"
|
||||
:props="{
|
||||
label: 'name'
|
||||
}"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div>
|
||||
<span>{{node.label}}</span>
|
||||
<span class="send-num" v-if="data.permKey === 'email:send'" @click.stop>
|
||||
<el-input-number v-model="form.sendCount" controls-position="right" :min="0" :max="99999" size="small" placeholder="数量" >
|
||||
</el-input-number>
|
||||
<el-select v-model="form.sendType" placeholder="Select" size="small" style="width: 60px;margin-left: 5px;">
|
||||
<el-option label="总数" value="count" />
|
||||
<el-option label="每天" value="day" />
|
||||
</el-select>
|
||||
</span>
|
||||
<span class="send-num" v-if="data.permKey === 'account:add'" @click.stop>
|
||||
<el-input-number v-model="form.accountCount" controls-position="right" :min="0" :max="99999" size="small" placeholder="数量" >
|
||||
</el-input-number>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
<el-button class="btn" type="primary" :loading="permLoading" @click="roleFormClick"
|
||||
>保存
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {defineOptions, nextTick, reactive, ref} from "vue";
|
||||
import {roleAdd, roleDelete, rolePermTree, roleRoleList, roleSet, roleSetDef} from "@/request/role.js";
|
||||
import {ElMessage, ElMessageBox} from "element-plus";
|
||||
import loading from '@/components/loading/index.vue';
|
||||
import {useRoleStore} from "@/store/role.js";
|
||||
|
||||
defineOptions({
|
||||
name: 'role'
|
||||
})
|
||||
|
||||
const roleStore = useRoleStore();
|
||||
const roleFormShow = ref(false)
|
||||
const treeList = reactive([])
|
||||
const roles = ref([])
|
||||
const tree = ref({})
|
||||
const permLoading = ref(false)
|
||||
const tableLoading = ref(false)
|
||||
const desShow = ref(true)
|
||||
const settingWidth = ref(null)
|
||||
const sortWidth = ref(null)
|
||||
const roleWidth = ref(200)
|
||||
|
||||
const dialogType = reactive({
|
||||
title: '',
|
||||
type: ''
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
name: null,
|
||||
description: null,
|
||||
sendType: 'count',
|
||||
sendCount: '',
|
||||
accountCount: '',
|
||||
sort: 0,
|
||||
isDefault: 0,
|
||||
})
|
||||
|
||||
const expand = ref(false)
|
||||
|
||||
const options = [{ label: '展开',value: true }, { label:'收起',value: false}]
|
||||
|
||||
let chooseRole = {}
|
||||
|
||||
refresh()
|
||||
|
||||
rolePermTree().then(tree => {
|
||||
treeList.push(...tree)
|
||||
})
|
||||
|
||||
function roleFormClick() {
|
||||
if (dialogType.type === 'add') {
|
||||
addRole()
|
||||
} else {
|
||||
setRole()
|
||||
}
|
||||
}
|
||||
|
||||
function setDef(role) {
|
||||
roleSetDef(role.roleId).then(() => {
|
||||
ElMessage({
|
||||
message: "设置成功",
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
getRoleList()
|
||||
})
|
||||
}
|
||||
|
||||
function delRole(role) {
|
||||
ElMessageBox.confirm(`确认删除 ${role.name} 吗?`, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
roleDelete(role.roleId).then(() => {
|
||||
ElMessage({
|
||||
message: "删除成功",
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
getRoleList()
|
||||
roleStore.refreshSelect()
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function expandChange(e) {
|
||||
if (e) {
|
||||
const nodes = tree.value?.store.nodesMap;
|
||||
for (const key in nodes) {
|
||||
nodes[key].expanded = true;
|
||||
}
|
||||
} else {
|
||||
const nodes = tree.value?.store.nodesMap;
|
||||
for (const key in nodes) {
|
||||
nodes[key].expanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function setRole() {
|
||||
|
||||
if (!form.name) {
|
||||
ElMessage({
|
||||
message: "身份名不能为空",
|
||||
type: "error",
|
||||
plain: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const params = {...form,roleId: chooseRole.roleId}
|
||||
const checkedId = tree.value.getCheckedKeys()
|
||||
const halfId = tree.value.getHalfCheckedKeys()
|
||||
params.permIds = [...checkedId, ...halfId]
|
||||
|
||||
permLoading.value = true
|
||||
roleSet(params).then(() => {
|
||||
ElMessage({
|
||||
message: "修改成功",
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
roleFormShow.value = false
|
||||
getRoleList()
|
||||
}).finally(() => {
|
||||
permLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.name = null
|
||||
form.description = null
|
||||
form.sort = 0
|
||||
form.sendType = 'count'
|
||||
form.sendCount = ''
|
||||
form.accountCount = ''
|
||||
tree.value.setCheckedKeys([])
|
||||
}
|
||||
|
||||
function openRoleSet(role) {
|
||||
chooseRole = role
|
||||
dialogType.title = '修改身份'
|
||||
dialogType.type = 'set'
|
||||
roleFormShow.value = true
|
||||
form.sort = role.sort
|
||||
form.name = role.name
|
||||
form.sendType = role.sendType
|
||||
form.sendCount = role.sendCount
|
||||
form.accountCount = role.accountCount
|
||||
nextTick(() => {
|
||||
tree.value.setCheckedKeys(role.permIds)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function openAddRole() {
|
||||
dialogType.title = '添加身份'
|
||||
dialogType.type = 'add'
|
||||
roleFormShow.value = true
|
||||
}
|
||||
|
||||
function addRole() {
|
||||
const params = {...form}
|
||||
const checkedId = tree.value.getCheckedKeys()
|
||||
const halfId = tree.value.getHalfCheckedKeys()
|
||||
params.permIds = [...checkedId, ...halfId]
|
||||
|
||||
permLoading.value = true
|
||||
roleAdd(params).then(() => {
|
||||
ElMessage({
|
||||
message: "添加成功",
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
roleFormShow.value = false
|
||||
getRoleList()
|
||||
roleStore.refreshSelect()
|
||||
}).finally(() => {
|
||||
permLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function refresh() {
|
||||
tableLoading.value = true
|
||||
roles.length = 0
|
||||
getRoleList()
|
||||
}
|
||||
|
||||
function getRoleList() {
|
||||
roleRoleList().then(list => {
|
||||
roles.value = list
|
||||
}).finally(() => {
|
||||
tableLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function adjustWidth() {
|
||||
desShow.value = window.innerWidth > 767
|
||||
settingWidth.value = window.innerWidth < 480 ? 75 : null
|
||||
sortWidth.value = window.innerWidth < 480 ? 75 : null
|
||||
roleWidth.value = window.innerWidth < 480 ? 180 : 200
|
||||
}
|
||||
|
||||
adjustWidth()
|
||||
|
||||
window.onresize = () => {
|
||||
adjustWidth()
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
|
||||
.perm-box {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
.perm-scrollbar {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.send-num {
|
||||
margin-left: 10px;
|
||||
.el-input-number {
|
||||
width: 95px;
|
||||
}
|
||||
}
|
||||
|
||||
.def-tag {
|
||||
margin-left: 10px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
padding: 9px 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
|
||||
font-size: 18px;
|
||||
.search {
|
||||
:deep(.el-input-group) {
|
||||
height: 28px;
|
||||
}
|
||||
:deep(.el-input__inner) {
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.description) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.loading {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.role-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.description {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
||||
:deep(.el-segmented--small .el-segmented__item) {
|
||||
border-radius: 8px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dialog-box {
|
||||
.dialog-input {
|
||||
margin-bottom: 15px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.perm-expand {
|
||||
margin-bottom: 10px;
|
||||
--el-border-radius-base: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-dialog) {
|
||||
margin-top: 15vh !important;
|
||||
margin-bottom: 20px !important;
|
||||
width: 400px !important;
|
||||
@media (max-width: 440px) {
|
||||
width: calc(100% - 40px) !important;
|
||||
margin-right: 20px !important;
|
||||
margin-left: 20px !important;
|
||||
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
}
|
||||
</style>
|
||||
82
mail-vue/src/views/send/index.vue
Normal file
82
mail-vue/src/views/send/index.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<emailScroll ref="sendScroll"
|
||||
:cancel-success="cancelStar"
|
||||
:star-success="addStar"
|
||||
:getEmailList="getEmailList"
|
||||
:emailDelete="emailDelete"
|
||||
:star-add="starAdd"
|
||||
show-status
|
||||
actionLeft="4px"
|
||||
:star-cancel="starCancel"
|
||||
@jump="jumpContent"
|
||||
:time-sort="params.timeSort"
|
||||
>
|
||||
<template #first>
|
||||
<Icon class="icon" @click="changeTimeSort" icon="material-symbols-light:timer-arrow-down-outline"
|
||||
v-if="params.timeSort === 0" width="28" height="28"/>
|
||||
<Icon class="icon" @click="changeTimeSort" icon="material-symbols-light:timer-arrow-up-outline" v-else
|
||||
width="28" height="28"/>
|
||||
</template>
|
||||
</emailScroll>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useAccountStore} from "@/store/account.js";
|
||||
import {useEmailStore} from "@/store/email.js";
|
||||
import emailScroll from "@/components/email-scroll/index.vue"
|
||||
import {emailList, emailDelete} from "@/request/email.js";
|
||||
import {starAdd, starCancel} from "@/request/star.js";
|
||||
import {defineOptions, onMounted, reactive, ref, watch} from "vue";
|
||||
import router from "@/router/index.js";
|
||||
import {Icon} from "@iconify/vue";
|
||||
|
||||
defineOptions({
|
||||
name: 'send'
|
||||
})
|
||||
|
||||
const emailStore = useEmailStore();
|
||||
const accountStore = useAccountStore();
|
||||
const sendScroll = ref({})
|
||||
const params = reactive({
|
||||
timeSort: 0,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
emailStore.sendScroll = sendScroll;
|
||||
})
|
||||
|
||||
watch(() => accountStore.currentAccountId, () => {
|
||||
sendScroll.value.refreshList();
|
||||
})
|
||||
|
||||
function changeTimeSort() {
|
||||
params.timeSort = params.timeSort ? 0 : 1
|
||||
sendScroll.value.refreshList();
|
||||
}
|
||||
|
||||
function jumpContent(email) {
|
||||
emailStore.contentData.email = email
|
||||
emailStore.contentData.delType = 'logic'
|
||||
emailStore.contentData.showStar = true
|
||||
router.push('/content')
|
||||
}
|
||||
|
||||
function addStar(email) {
|
||||
emailStore.starScroll?.addItem(email)
|
||||
}
|
||||
|
||||
function cancelStar(email) {
|
||||
emailStore.starScroll?.deleteEmail([email.emailId])
|
||||
}
|
||||
|
||||
function getEmailList(emailId, size) {
|
||||
return emailList(accountStore.currentAccountId, emailId, params.timeSort, size, 1)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="del-email">
|
||||
<div class="del-email" v-perm="'my:delete'">
|
||||
<div class="title">删除账户</div>
|
||||
<div style="color: #585d69;">
|
||||
此操作将永久删除您的账户及其所有数据,无法恢复
|
||||
@@ -23,20 +23,18 @@
|
||||
</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 {resetPassword, userDelete} from "@/request/my.js";
|
||||
import {ElMessage, ElMessageBox} from "element-plus";
|
||||
import {useUserStore} from "@/store/user.js";
|
||||
import router from "@/router/index.js";
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
:getEmailList="starList"
|
||||
:emailDelete="emailDelete"
|
||||
:star-add="starAdd"
|
||||
:star-cancel="starCancel"/>
|
||||
:star-cancel="starCancel"
|
||||
@jump="jumpContent"
|
||||
actionLeft="6px"
|
||||
:show-account-icon="false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -14,6 +18,7 @@ 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";
|
||||
import router from "@/router/index.js";
|
||||
|
||||
defineOptions({
|
||||
name: 'star'
|
||||
@@ -22,8 +27,14 @@ defineOptions({
|
||||
const scroll = ref({})
|
||||
const emailStore = useEmailStore();
|
||||
|
||||
function jumpContent(email) {
|
||||
emailStore.contentData.email = email
|
||||
emailStore.contentData.delType = 'logic'
|
||||
emailStore.contentData.showStar = true
|
||||
router.push('/content')
|
||||
}
|
||||
|
||||
function cancelStar(email) {
|
||||
emailStore.emailScroll?.editEmailStar(email.emailId, 0)
|
||||
scroll.value.deleteEmail([email.emailId])
|
||||
}
|
||||
|
||||
|
||||
216
mail-vue/src/views/sys-email/index.vue
Normal file
216
mail-vue/src/views/sys-email/index.vue
Normal file
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<div class="email-list-box">
|
||||
<emailScroll ref="sysEmailScroll"
|
||||
:get-emailList="getEmailList"
|
||||
:email-delete="sysEmailDelete"
|
||||
:star-add="starAdd"
|
||||
:star-cancel="starCancel"
|
||||
:show-star="false"
|
||||
show-user-info
|
||||
show-status
|
||||
:show-account-icon="false"
|
||||
@jump="jumpContent"
|
||||
@refresh-before="refreshBefore"
|
||||
|
||||
>
|
||||
<template #first>
|
||||
<el-input
|
||||
v-model="searchValue"
|
||||
placeholder="输入内容查询"
|
||||
class="search-input"
|
||||
>
|
||||
<template #prefix>
|
||||
<div @click.stop="openSelect">
|
||||
<el-select
|
||||
ref="mySelect"
|
||||
v-model="searchType"
|
||||
placeholder="请选择"
|
||||
class="select"
|
||||
>
|
||||
<el-option key="1" label="用户" :value="'user'"/>
|
||||
<el-option key="2" label="邮箱" :value="'account'"/>
|
||||
<el-option key="3" label="发件人" :value="'name'"/>
|
||||
<el-option key="4" label="主题" :value="'subject'"/>
|
||||
</el-select>
|
||||
<div style="color: #333;display: flex;">
|
||||
<span>{{ selectTitle }}</span>
|
||||
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-select v-model="params.type" placeholder="Select" class="status-select">
|
||||
<el-option key="1" label="全部" value="all"/>
|
||||
<el-option key="2" label="发件" value="send"/>
|
||||
<el-option key="3" label="收件" value="receive"/>
|
||||
<el-option key="4" label="删除" value="delete"/>
|
||||
</el-select>
|
||||
<Icon class="icon" icon="iconoir:search" @click="search" width="20" height="20"/>
|
||||
<Icon class="icon" @click="changeTimeSort" icon="material-symbols-light:timer-arrow-down-outline"
|
||||
v-if="params.timeSort === 0" width="28" height="28"/>
|
||||
<Icon class="icon" @click="changeTimeSort" icon="material-symbols-light:timer-arrow-up-outline" v-else
|
||||
width="28" height="28"/>
|
||||
</template>
|
||||
</emailScroll>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {starAdd, starCancel} from "@/request/star.js";
|
||||
import emailScroll from "@/components/email-scroll/index.vue"
|
||||
import {computed, defineOptions, reactive, ref} from "vue";
|
||||
import {useEmailStore} from "@/store/email.js";
|
||||
import {
|
||||
sysEmailAll,
|
||||
sysEmailDelete
|
||||
} from "@/request/sys-email.js";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import router from "@/router/index.js";
|
||||
|
||||
defineOptions({
|
||||
name: 'sys-email'
|
||||
})
|
||||
|
||||
const emailStore = useEmailStore();
|
||||
const sysEmailScroll = ref({})
|
||||
const searchType = ref('user')
|
||||
const searchValue = ref('')
|
||||
const mySelect = ref()
|
||||
|
||||
const openSelect = () => {
|
||||
mySelect.value.toggleMenu()
|
||||
}
|
||||
|
||||
const params = reactive({
|
||||
timeSort: 0,
|
||||
type: 'all',
|
||||
userEmail: null,
|
||||
accountEmail: null,
|
||||
name: null,
|
||||
subject: null
|
||||
})
|
||||
|
||||
|
||||
const selectTitle = computed(() => {
|
||||
if (searchType.value === 'user') return '用户'
|
||||
if (searchType.value === 'account') return '邮箱'
|
||||
if (searchType.value === 'name') return '发件人'
|
||||
if (searchType.value === 'subject') return '主题'
|
||||
})
|
||||
|
||||
function refreshBefore() {
|
||||
searchValue.value = null
|
||||
params.timeSort = 0
|
||||
params.type = 'all'
|
||||
params.userEmail = null
|
||||
params.accountEmail = null
|
||||
params.name = null
|
||||
params.subject = null
|
||||
|
||||
}
|
||||
|
||||
function search() {
|
||||
|
||||
params.userEmail = null
|
||||
params.accountEmail = null
|
||||
params.name = null
|
||||
params.subject = null
|
||||
|
||||
if (searchType.value === 'user') {
|
||||
params.userEmail = searchValue.value
|
||||
}
|
||||
|
||||
if (searchType.value === 'account') {
|
||||
params.accountEmail = searchValue.value
|
||||
}
|
||||
|
||||
if (searchType.value === 'name') {
|
||||
params.name = searchValue.value
|
||||
}
|
||||
|
||||
if (searchType.value === 'subject') {
|
||||
params.subject = searchValue.value
|
||||
}
|
||||
|
||||
sysEmailScroll.value.refreshList();
|
||||
}
|
||||
|
||||
function changeTimeSort() {
|
||||
params.timeSort = params.timeSort ? 0 : 1
|
||||
sysEmailScroll.value.refreshList();
|
||||
}
|
||||
|
||||
function jumpContent(email) {
|
||||
emailStore.contentData.email = email
|
||||
emailStore.contentData.delType = 'physics'
|
||||
emailStore.contentData.showStar = false
|
||||
router.push('/content')
|
||||
}
|
||||
|
||||
|
||||
function getEmailList(emailId, size) {
|
||||
return sysEmailAll({emailId, size, ...params})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.email-list-box {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.search {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.select {
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.header-actions) {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
|
||||
width: min(280px,calc(100vw - 140px));
|
||||
height: 28px;
|
||||
.setting-icon {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.status-select {
|
||||
margin-bottom: 2px;
|
||||
width: 80px;
|
||||
:deep(.el-select__wrapper) {
|
||||
min-height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-with-select {
|
||||
max-width: 200px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
:deep(.input-with-select .el-input-group__append) {
|
||||
background-color: var(--el-fill-color-blank);
|
||||
}
|
||||
|
||||
:deep(.el-select__wrapper) {
|
||||
padding: 2px 10px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -1,177 +1,486 @@
|
||||
<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 class="settings-container">
|
||||
<el-scrollbar class="scroll">
|
||||
<div class="card-grid">
|
||||
<!-- Website Settings Card -->
|
||||
<div class="settings-card">
|
||||
<div class="card-title">网站设置</div>
|
||||
<div class="card-content">
|
||||
<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>
|
||||
<el-tooltip effect="dark" content="开启后账号栏出现一个用户可以添加多个邮箱">
|
||||
<Icon class="warning" icon="fe:warning" width="20" height="20"/>
|
||||
</el-tooltip>
|
||||
</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-tooltip effect="dark" content="轮询请求服务器获取最新邮件">
|
||||
<Icon class="warning" icon="fe:warning" width="20" height="20"/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<span>物理清空数据</span>
|
||||
<el-tooltip effect="dark" content="该操作会物理清空所有已被删除的数据">
|
||||
<Icon class="warning" icon="fe:warning" width="20" height="20"/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<el-button class="opt-button" style="margin-top: 0" @click="physicsDeleteAllData" size="small"
|
||||
type="primary">
|
||||
<Icon icon="material-symbols:delete-outline-rounded" width="16" height="16"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Personalization Settings Card -->
|
||||
<div class="settings-card">
|
||||
<div class="card-title">个性化设置</div>
|
||||
<div class="card-content">
|
||||
<div class="setting-item">
|
||||
<div class="title-item"><span>网站标题</span></div>
|
||||
<div class="email-title">
|
||||
<span>{{ setting.title }}</span>
|
||||
<el-button class="opt-button" size="small" type="primary" @click="editTitleShow = true">
|
||||
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item personalized">
|
||||
<div><span>登录背景</span></div>
|
||||
<div>
|
||||
<el-image
|
||||
class="background"
|
||||
:src="cvtR2Url(setting.background)"
|
||||
:preview-src-list="[cvtR2Url(setting.background)]"
|
||||
show-progress
|
||||
fit="cover"
|
||||
>
|
||||
<template #error>
|
||||
<div class="error-image" @click="openCut">
|
||||
<Icon icon="ph:image" width="24" height="24"/>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div class="background-btn">
|
||||
<el-button class="opt-button" size="small" type="primary" @click="openCut">
|
||||
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
|
||||
</el-button>
|
||||
<el-button class="opt-button" size="small" type="primary" @click="delBackground">
|
||||
<Icon icon="material-symbols:delete-outline-rounded" width="16" height="16"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email Sending Settings Card -->
|
||||
<div class="settings-card">
|
||||
<div class="card-title">邮件设置</div>
|
||||
<div class="card-content">
|
||||
<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.send"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>添加resend令牌</span></div>
|
||||
<div>
|
||||
<el-button class="opt-button" style="margin-top: 0" @click="openResendForm" size="small" type="primary">
|
||||
<Icon icon="material-symbols:add-rounded" width="16" height="16"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item token-item" v-for="(value, key, index) in setting.resendTokens" :key="index">
|
||||
<div><span>{{ key }}</span></div>
|
||||
<div><span>{{ value }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- R2 Object Storage Card -->
|
||||
<div class="settings-card">
|
||||
<div class="card-title">R2对象存储</div>
|
||||
<div class="card-content">
|
||||
<div class="setting-item">
|
||||
<div><span>访问域名</span></div>
|
||||
<div class="r2domain">
|
||||
<span>{{ setting.r2Domain || '空' }}</span>
|
||||
<el-button class="opt-button" size="small" type="primary" @click="r2DomainShow = true">
|
||||
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Turnstile Verification Card -->
|
||||
<div class="settings-card">
|
||||
<div class="card-title">Turnstile 人机验证</div>
|
||||
<div class="card-content">
|
||||
<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>siteKey</span></div>
|
||||
<div class="bot-verify">
|
||||
<span>{{ setting.siteKey || '空' }}</span>
|
||||
<el-button class="opt-button" size="small" type="primary" @click="turnstileShow = true">
|
||||
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>secretKey</span></div>
|
||||
<div class="bot-verify">
|
||||
<span> {{ setting.secretKey || '空' }} </span>
|
||||
<el-button class="opt-button" size="small" type="primary" @click="turnstileShow = true">
|
||||
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="card-title">关于</div>
|
||||
<div class="card-content">
|
||||
<div class="concerning-item">
|
||||
<span>版本:</span>
|
||||
<span>v1.1.0</span>
|
||||
</div>
|
||||
<div class="concerning-item">
|
||||
<span>交流:</span>
|
||||
<el-button @click="jumpTelegram">
|
||||
telegram
|
||||
<template #icon>
|
||||
<Icon icon="logos:telegram" width="30" height="30"/>
|
||||
</template>
|
||||
</el-button>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
|
||||
<!-- Dialogs remain the same -->
|
||||
<el-dialog v-model="editTitleShow" title="修改标题" width="340" @closed="editTitle = ''">
|
||||
<form>
|
||||
<el-input type="text" placeholder="网站标题" v-model="editTitle"/>
|
||||
<el-button type="primary" :loading="settingLoading" @click="saveTitle">保存</el-button>
|
||||
</form>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="resendTokenFormShow" title="添加resend令牌" width="340" @closed="cleanResendTokenForm">
|
||||
<form>
|
||||
<el-select style="margin-bottom: 15px" v-model="resendTokenForm.domain" placeholder="Select">
|
||||
<el-option
|
||||
v-for="item in settingStore.domainList"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input type="text" placeholder="令牌" v-model="resendTokenForm.token"/>
|
||||
<el-button type="primary" :loading="settingLoading" @click="saveResendToken">保存</el-button>
|
||||
</form>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="r2DomainShow" title="添加R2访问域名" width="340" @closed="r2DomainInput = ''">
|
||||
<form>
|
||||
<el-input type="text" placeholder="R2访问域名" v-model="r2DomainInput"/>
|
||||
<el-button type="primary" :loading="settingLoading" @click="saveR2domain">保存</el-button>
|
||||
</form>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="turnstileShow" title="添加Turnstile密钥" width="340"
|
||||
@closed="turnstileForm.secretKey = '';turnstileForm.siteKey = ''">
|
||||
<form>
|
||||
<el-input type="text" placeholder="siteKey" v-model="turnstileForm.siteKey"/>
|
||||
<el-input type="text" style="margin-top: 15px" placeholder="secretKey" v-model="turnstileForm.secretKey"/>
|
||||
<el-button type="primary" :loading="settingLoading" @click="saveTurnstileKey">保存</el-button>
|
||||
</form>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="cutShow"
|
||||
title="背景截图"
|
||||
class="cut-dialog"
|
||||
>
|
||||
<div class="cropper">
|
||||
<vueCropper
|
||||
ref="cropper"
|
||||
:img="cutImage"
|
||||
:fixedNumber="[16,9]"
|
||||
outputType="jpeg"
|
||||
:fixed="true"
|
||||
:centerBox="true"
|
||||
:full="true"
|
||||
:autoCrop="true"
|
||||
:outputSize="0.9"
|
||||
></vueCropper>
|
||||
</div>
|
||||
<div class="cut-button">
|
||||
<el-button type="primary" :loading="settingLoading" @click="saveBackground">保存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {defineOptions} from "vue";
|
||||
import {ref} from 'vue'
|
||||
import {settingSet} from "@/request/setting.js";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {defineOptions, nextTick, onMounted, reactive, ref} from "vue";
|
||||
import {physicsDeleteAll, setBackground, settingQuery, settingSet} from "@/request/setting.js";
|
||||
import {ElMessage, ElMessageBox} 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";
|
||||
import {compressImage} from "@/utils/file-utils.js";
|
||||
import {cvtR2Url} from "@/utils/convert.js";
|
||||
import {storeToRefs} from "pinia";
|
||||
|
||||
defineOptions({
|
||||
name: 'sys-setting'
|
||||
})
|
||||
|
||||
const cropper = ref()
|
||||
const cutImage = ref('')
|
||||
const cutShow = ref(false)
|
||||
const accountStore = useAccountStore();
|
||||
const userStore = useUserStore();
|
||||
const editTitleShow = ref(false)
|
||||
const resendTokenFormShow = ref(false)
|
||||
const r2DomainShow = ref(false)
|
||||
const turnstileShow = ref(false)
|
||||
const settingStore = useSettingStore();
|
||||
let setting = ref(settingStore.settings)
|
||||
const {settings: setting} = storeToRefs(settingStore);
|
||||
const editTitle = ref('')
|
||||
const editTitleLoading = ref(false)
|
||||
const settingLoading = ref(false)
|
||||
const r2DomainInput = ref('')
|
||||
let backup = {}
|
||||
const resendTokenForm = reactive({
|
||||
domain: '',
|
||||
token: '',
|
||||
})
|
||||
const turnstileForm = reactive({
|
||||
siteKey: '',
|
||||
secretKey: ''
|
||||
})
|
||||
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,
|
||||
}
|
||||
{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() {
|
||||
onMounted(() => {
|
||||
resendTokenForm.domain = settingStore.domainList[0];
|
||||
})
|
||||
|
||||
function physicsDeleteAllData() {
|
||||
ElMessageBox.prompt('此操作不可逆转, 输入 <b style="font-weight: bold">确认删除</b> 继续操作', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
dangerouslyUseHTMLString: true,
|
||||
title: '警告',
|
||||
type: 'warning',
|
||||
inputPattern: /^确认删除$/,
|
||||
inputErrorMessage: '请输入确认删除',
|
||||
}).then(() => {
|
||||
physicsDeleteAll().then(() => {
|
||||
ElMessage({
|
||||
message: "删除成功",
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function delBackground() {
|
||||
ElMessageBox.confirm('确定要删除这个背景吗?', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
setting.value.background = null
|
||||
editSetting({background: null})
|
||||
})
|
||||
}
|
||||
|
||||
function saveTurnstileKey() {
|
||||
const settingForm = {}
|
||||
settingForm.siteKey = turnstileForm.siteKey
|
||||
settingForm.secretKey = turnstileForm.secretKey
|
||||
editSetting(settingForm)
|
||||
}
|
||||
|
||||
function saveBackground() {
|
||||
settingLoading.value = true
|
||||
cropper.value.getCropData(data => {
|
||||
setBackground(data).then(key => {
|
||||
setting.value.background = key
|
||||
cutShow.value = false
|
||||
ElMessage({
|
||||
message: "设置成功",
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
}).finally(() => {
|
||||
settingLoading.value = false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function openCut() {
|
||||
const doc = document.createElement('input')
|
||||
doc.setAttribute('type', 'file')
|
||||
doc.setAttribute('accept', 'image/*')
|
||||
doc.click()
|
||||
doc.onchange = async (e) => {
|
||||
const image = await compressImage(e.target.files[0], 0.9)
|
||||
cutImage.value = URL.createObjectURL(image)
|
||||
cutShow.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function saveR2domain() {
|
||||
const settingForm = {r2Domain: r2DomainInput.value}
|
||||
if (settingForm.r2Domain && !settingForm.r2Domain.startsWith('http')) {
|
||||
ElMessage({
|
||||
message: "域名必须以http或https开头",
|
||||
type: "error",
|
||||
plain: true
|
||||
})
|
||||
return;
|
||||
}
|
||||
editSetting(settingForm)
|
||||
}
|
||||
|
||||
function openResendForm() {
|
||||
resendTokenFormShow.value = true
|
||||
}
|
||||
|
||||
function saveResendToken() {
|
||||
const settingForm = {
|
||||
resendTokens: {}
|
||||
}
|
||||
const domain = resendTokenForm.domain.slice(1)
|
||||
settingForm.resendTokens[domain] = resendTokenForm.token
|
||||
editSetting(settingForm)
|
||||
}
|
||||
|
||||
function backupSetting() {
|
||||
const settingForm = {...setting.value}
|
||||
delete settingForm.resendTokens
|
||||
delete settingForm.siteKey
|
||||
delete settingForm.secretKey
|
||||
backup = JSON.stringify(setting.value)
|
||||
}
|
||||
|
||||
function cleanResendTokenForm() {
|
||||
resendTokenForm.token = ''
|
||||
}
|
||||
|
||||
function beforeChange() {
|
||||
if (settingLoading.value) return false
|
||||
backupSetting()
|
||||
return true
|
||||
}
|
||||
|
||||
function change() {
|
||||
editSetting(setting)
|
||||
function change(e) {
|
||||
const settingForm = {...setting.value}
|
||||
delete settingForm.siteKey
|
||||
delete settingForm.secretKey
|
||||
delete settingForm.resendTokens
|
||||
editSetting(settingForm, false)
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
settingQuery().then(setting => {
|
||||
settingStore.settings = setting;
|
||||
settingStore.domainList = setting.domainList;
|
||||
})
|
||||
}
|
||||
|
||||
function saveTitle() {
|
||||
backup = JSON.stringify(setting.value)
|
||||
setting.value.title = editTitle.value
|
||||
editSetting(setting)
|
||||
editSetting({title: editTitle.value})
|
||||
}
|
||||
|
||||
function editSetting(setting) {
|
||||
editTitleLoading.value = true
|
||||
settingSet(setting.value).then(() => {
|
||||
editTitleLoading.value = false
|
||||
function jumpTelegram() {
|
||||
const doc = document.createElement('a')
|
||||
doc.href = 'https://t.me/cloud_mail_tg'
|
||||
doc.target = '_blank'
|
||||
doc.click()
|
||||
}
|
||||
|
||||
|
||||
function editSetting(settingForm, refreshStatus = true) {
|
||||
if (settingLoading.value) return
|
||||
settingLoading.value = true
|
||||
settingSet(settingForm).then(() => {
|
||||
settingLoading.value = false
|
||||
ElMessage({
|
||||
message: "设置成功",
|
||||
type: "success",
|
||||
@@ -180,69 +489,256 @@ function editSetting(setting) {
|
||||
if (setting.value.manyEmail === 1) {
|
||||
accountStore.currentAccountId = userStore.user.accountId;
|
||||
}
|
||||
if (refreshStatus) {
|
||||
refresh()
|
||||
}
|
||||
editTitleShow.value = false
|
||||
r2DomainShow.value = false
|
||||
resendTokenFormShow.value = false
|
||||
turnstileShow.value = false
|
||||
}).catch(() => {
|
||||
editTitleLoading.value = false
|
||||
setting.value = JSON.parse(backup)
|
||||
setting.value = {...setting.value, ...JSON.parse(backup)}
|
||||
}).finally(() => {
|
||||
settingLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.box {
|
||||
.settings-container {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
padding: 40px 40px;
|
||||
.scroll {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
.card-grid {
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
@media (max-width: 500px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.background {
|
||||
width: 230px;
|
||||
height: 120px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
@media (max-width: 500px) {
|
||||
width: 140px;
|
||||
height: 79px;
|
||||
}
|
||||
}
|
||||
|
||||
.background-btn {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
transition: all 300ms;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
padding: 10px 20px;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 10px;
|
||||
font-weight: bold;
|
||||
|
||||
> div:first-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.setting {
|
||||
font-size: 14px;
|
||||
> div:last-child {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
justify-items: flex-end;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.warning {
|
||||
margin-left: 5px;
|
||||
color: gray;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cropper {
|
||||
height: 397px;
|
||||
width: 705px;
|
||||
@media (max-width: 767px) {
|
||||
width: calc(100vw - 60px);
|
||||
height: calc((100vw - 60px) * 9 / 16);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.cut-dialog.el-dialog) {
|
||||
width: fit-content !important;
|
||||
height: fit-content !important;
|
||||
}
|
||||
|
||||
.error-image {
|
||||
background: #f5f7fa;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cut-button {
|
||||
padding-top: 15px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
|
||||
.el-button {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.bot-verify {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 40px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
gap: 140px;
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
@media (max-width: 767px) {
|
||||
gap: 80px;
|
||||
}
|
||||
.el-button {
|
||||
width: 48px;
|
||||
margin: 0 0 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
>div:first-child {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.opt-button {
|
||||
width: fit-content !important;
|
||||
}
|
||||
|
||||
.r2domain {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
width: 48px;
|
||||
margin: 0 0 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.personalized {
|
||||
align-items: start;
|
||||
|
||||
> div:last-child {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
|
||||
.el-button {
|
||||
margin-left: 10px;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.warning {
|
||||
position: absolute;
|
||||
left: 65px;
|
||||
color: gray;
|
||||
.concerning-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
:deep(.el-button) {
|
||||
padding: 0 10px;
|
||||
|
||||
i {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
> span:first-child {
|
||||
font-weight: bold;
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.email-title {
|
||||
font-weight: normal !important;
|
||||
display: flex;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
div:first-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-template-columns: 1fr auto;
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
.token-item {
|
||||
padding-top: 0;
|
||||
|
||||
div:last-child {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
form .el-button {
|
||||
margin-top: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-switch {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
|
||||
:deep(.el-button--small) {
|
||||
margin-top: 2px !important;
|
||||
margin-bottom: 2px !important;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
:deep(.el-select__wrapper) {
|
||||
min-height: 28px;
|
||||
}
|
||||
</style>
|
||||
973
mail-vue/src/views/user/index.vue
Normal file
973
mail-vue/src/views/user/index.vue
Normal file
@@ -0,0 +1,973 @@
|
||||
<template>
|
||||
<div class="user-box">
|
||||
<div class="header-actions">
|
||||
<Icon class="icon" icon="ion:add-outline" width="23" height="23" @click="openAdd"/>
|
||||
<div class="search">
|
||||
<el-input
|
||||
v-model="params.email"
|
||||
class="search-input"
|
||||
placeholder="输入邮箱名搜索"
|
||||
>
|
||||
</el-input>
|
||||
</div>
|
||||
<el-select v-model="params.status" placeholder="Select" class="status-select">
|
||||
<el-option :key="-1" label="全部" :value="-1"/>
|
||||
<el-option :key="0" label="正常" :value="0"/>
|
||||
<el-option :key="1" label="封禁" :value="1"/>
|
||||
<el-option :key="-2" label="删除" :value="-2"/>
|
||||
</el-select>
|
||||
<Icon class="icon" icon="iconoir:search" @click="search" width="20" height="20"/>
|
||||
<Icon class="icon" @click="changeTimeSort" icon="material-symbols-light:timer-arrow-down-outline"
|
||||
v-if="params.timeSort === 1" width="28" height="28"/>
|
||||
<Icon class="icon" @click="changeTimeSort" icon="material-symbols-light:timer-arrow-up-outline" v-else width="28"
|
||||
height="28"/>
|
||||
<Icon class="icon" icon="ion:reload" width="18" height="18" @click="refresh"/>
|
||||
<Icon class="icon" icon="pepicons-pencil:expand" width="26" height="26" @click="changeExpand"/>
|
||||
</div>
|
||||
<el-scrollbar ref="scrollbarRef" class="scrollbar">
|
||||
<div>
|
||||
<div class="loading" :class="tableLoading ? 'loading-show' : 'loading-hide'">
|
||||
<loading/>
|
||||
</div>
|
||||
<el-table
|
||||
@filter-change="tableFilter"
|
||||
:empty-text="first ? '' : null"
|
||||
:default-expand-all="expandStatus"
|
||||
:data="users"
|
||||
:preserve-expanded-content="preserveExpanded"
|
||||
style="width: 100%;"
|
||||
:key="key"
|
||||
>
|
||||
<el-table-column :width="expandWidth" type="expand">
|
||||
<template #default="props">
|
||||
<div class="details">
|
||||
<div v-if="!sendNumShow"><span class="details-item-title">发件数量:</span>{{ props.row.sendEmailCount }}
|
||||
</div>
|
||||
<div v-if="!accountNumShow"><span class="details-item-title">邮箱数量:</span>{{
|
||||
props.row.accountCount
|
||||
}}
|
||||
</div>
|
||||
<div v-if="!createTimeShow"><span class="details-item-title">注册时间:</span>{{
|
||||
tzDayjs(props.row.createTime).format('YYYY-MM-DD HH:mm:ss')
|
||||
}}
|
||||
</div>
|
||||
<div v-if="!typeShow"><span class="details-item-title">身份类型:</span> {{ toRoleName(props.row.type) }}
|
||||
</div>
|
||||
<div v-if="!statusShow">
|
||||
<span class="details-item-title">状态:</span>
|
||||
<el-tag disable-transitions v-if="props.row.isDel === 1" type="info">删除</el-tag>
|
||||
<el-tag disable-transitions v-else-if="props.row.status === 0" type="primary">正常</el-tag>
|
||||
<el-tag disable-transitions v-else-if="props.row.status === 1" type="danger">封禁</el-tag>
|
||||
</div>
|
||||
<div><span class="details-item-title">注册IP:</span>{{ props.row.createIp || '未知' }}</div>
|
||||
<div><span class="details-item-title">近期IP:</span>{{ props.row.activeIp || '未知' }}</div>
|
||||
<div><span class="details-item-title">近期活动:</span>{{
|
||||
props.row.activeTime ? tzDayjs(props.row.activeTime).format('YYYY-MM-DD') : '未知'
|
||||
}}
|
||||
</div>
|
||||
<div><span class="details-item-title">登录设备:</span>{{ props.row.device || '未知' }}</div>
|
||||
<div><span class="details-item-title">登录系统:</span>{{ props.row.os || '未知' }}</div>
|
||||
<div><span class="details-item-title">登录浏览器:</span>{{ props.row.browser || '未知' }}</div>
|
||||
<div>
|
||||
<span class="details-item-title">发件次数:</span>
|
||||
<span>{{ formatSendCount(props.row) }}</span>
|
||||
<el-tag style="margin-left: 10px" v-if="props.row.sendAction.hasPerm" type="success">
|
||||
{{ formatSendType(props.row) }}
|
||||
</el-tag>
|
||||
<el-button size="small" style="margin-left: 10px"
|
||||
v-if="props.row.sendAction.hasPerm && props.row.sendAction.sendCount"
|
||||
@click="resetSendCount(props.row)" type="primary">重置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column show-overflow-tooltip :tooltip-formatter="tableRowFormatter" label="用户邮箱" :min-width="emailWidth">
|
||||
<template #default="props">
|
||||
<div class="email-row">{{ props.row.email }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :formatter="formatterReceive" label-class-name="receive" column-key="receive"
|
||||
:filtered-value="filteredValue" :filters="filters" :width="receiveWidth" label="收件数量"
|
||||
prop="receiveEmailCount"/>
|
||||
<el-table-column :formatter="formatterSend" label-class-name="send" column-key="send"
|
||||
:filtered-value="filteredValue" :filters="filters" v-if="sendNumShow" label="发件数量"
|
||||
prop="sendEmailCount"/>
|
||||
<el-table-column :formatter="formatterAccount" label-class-name="account" column-key="account"
|
||||
:filtered-value="filteredValue" :filters="filters" v-if="accountNumShow" label="邮箱数量"
|
||||
prop="accountCount"/>
|
||||
<el-table-column v-if="createTimeShow" label="注册时间" min-width="160" prop="createTime">
|
||||
<template #default="props">
|
||||
{{ tzDayjs(props.row.createTime).format('YYYY-MM-DD HH:mm:ss') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="statusShow" min-width="60px" label="状态" prop="status">
|
||||
<template #default="props">
|
||||
<el-tag disable-transitions v-if="props.row.isDel === 1" type="info">删除</el-tag>
|
||||
<el-tag disable-transitions v-else-if="props.row.status === 0" type="primary">正常</el-tag>
|
||||
<el-tag disable-transitions v-else-if="props.row.status === 1" type="danger">封禁</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="typeShow" label="身份类型" min-width="140" prop="type">
|
||||
<template #default="props">
|
||||
<div class="type">
|
||||
{{ toRoleName(props.row.type) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="设置" :width="settingWidth">
|
||||
<template #default="props">
|
||||
<el-dropdown trigger="click">
|
||||
<el-button size="small" type="primary">操作</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="openSetPwd(props.row)">改密</el-dropdown-item>
|
||||
<el-dropdown-item @click="openSetType(props.row)">权限</el-dropdown-item>
|
||||
<el-dropdown-item v-if="props.row.isDel !== 1" @click="setStatus(props.row)">
|
||||
{{ setStatusName(props.row) }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-else @click="restore(props.row)">恢复</el-dropdown-item>
|
||||
<el-dropdown-item @click="delUser(props.row)">删除</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination" v-if="total > 10">
|
||||
<el-pagination
|
||||
:size="pageSize"
|
||||
:current-page="params.num"
|
||||
:page-size="params.size"
|
||||
:pager-count="pagerCount"
|
||||
:page-sizes="[10, 15, 20, 25, 30, 50]"
|
||||
background
|
||||
:layout="layout"
|
||||
:total="total"
|
||||
@size-change="sizeChange"
|
||||
@current-change="numChange"
|
||||
/>
|
||||
<el-pagination
|
||||
v-if="phonePageShow"
|
||||
:size="pageSize"
|
||||
:current-page="params.num"
|
||||
:page-size="params.size"
|
||||
:pager-count="pagerCount"
|
||||
:page-sizes="[10, 15, 20, 25, 30, 50]"
|
||||
background
|
||||
layout="sizes, total"
|
||||
:total="total"
|
||||
@size-change="sizeChange"
|
||||
@current-change="numChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<el-dialog class="dialog" v-model="setPwdShow" title="修改密码" @closed="resetUserForm">
|
||||
<div class="dialog-box">
|
||||
<el-input v-model="userForm.password" type="password" placeholder="密码" autocomplete="off">
|
||||
</el-input>
|
||||
<el-button class="btn" type="primary" :loading="settingLoading" @click="updatePwd"
|
||||
>保存
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-dialog class="dialog" v-model="setTypeShow" title="设置权限" @closed="resetUserForm">
|
||||
<div class="dialog-box">
|
||||
<el-select v-model="userForm.type" placeholder="Select">
|
||||
<el-option v-for="item in roleList" :label="item.name" :value="item.roleId" :key="item.roleId"/>
|
||||
</el-select>
|
||||
<el-button class="btn" :loading="settingLoading" type="primary" @click="setType"
|
||||
>保存
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="showAdd" title="添加用户">
|
||||
<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-input type="password" v-model="addForm.password" placeholder="密码"/>
|
||||
<el-select v-model="addForm.type" placeholder="身份类型">
|
||||
<el-option v-for="item in roleList" :label="item.name" :value="item.roleId" :key="item.roleId"/>
|
||||
</el-select>
|
||||
<el-button class="btn" type="primary" @click="submit" :loading="addLoading"
|
||||
>添加
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {defineOptions, h, reactive, ref, watch} from 'vue'
|
||||
import {
|
||||
userList,
|
||||
userDelete,
|
||||
userSetPwd,
|
||||
userSetStatus,
|
||||
userSetType,
|
||||
userAdd,
|
||||
userRestSendCount, userRestore
|
||||
} from '@/request/user.js'
|
||||
import {roleSelectUse} from "@/request/role.js";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {ElMessage, ElMessageBox, ElRadio, ElRadioGroup} from "element-plus";
|
||||
import loading from "@/components/loading/index.vue";
|
||||
import {tzDayjs} from "@/utils/day.js";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
import {isEmail} from "@/utils/verify-utils.js";
|
||||
import {useRoleStore} from "@/store/role.js";
|
||||
|
||||
defineOptions({
|
||||
name: 'user'
|
||||
})
|
||||
|
||||
const roleStore = useRoleStore()
|
||||
const settingStore = useSettingStore()
|
||||
const filteredValue = ['normal', 'del']
|
||||
const filters = [{text: '正常', value: 'normal'}, {text: '删除', value: 'del'}]
|
||||
const preserveExpanded = ref(false)
|
||||
const emailWidth = ref(230)
|
||||
const expandWidth = ref(40)
|
||||
const settingWidth = ref(null)
|
||||
const sendNumShow = ref(true)
|
||||
const accountNumShow = ref(true)
|
||||
const createTimeShow = ref(true)
|
||||
const statusShow = ref(true)
|
||||
const typeShow = ref(true)
|
||||
const receiveWidth = ref(null)
|
||||
const phonePageShow = ref(false)
|
||||
const layout = ref('prev, pager, next, sizes, total')
|
||||
const pageSize = ref('')
|
||||
const expandStatus = ref(false)
|
||||
const users = ref([])
|
||||
const total = ref(0)
|
||||
const first = ref(true)
|
||||
const scrollbarRef = ref(null)
|
||||
|
||||
const domainList = settingStore.domainList
|
||||
|
||||
const addForm = reactive({
|
||||
email: '',
|
||||
suffix: settingStore.domainList[0],
|
||||
password: '',
|
||||
type: null,
|
||||
})
|
||||
|
||||
const params = reactive({
|
||||
email: '',
|
||||
num: 1,
|
||||
size: 15,
|
||||
timeSort: 0,
|
||||
status: -1
|
||||
})
|
||||
let chooseUser = {}
|
||||
const userForm = reactive({
|
||||
password: null,
|
||||
type: -1,
|
||||
userId: 0,
|
||||
})
|
||||
|
||||
const showAdd = ref(false)
|
||||
const addLoading = ref(false);
|
||||
const setTypeShow = ref(false)
|
||||
const setPwdShow = ref(false)
|
||||
const pagerCount = ref(10)
|
||||
const settingLoading = ref(false)
|
||||
const tableLoading = ref(true)
|
||||
const roleList = reactive([])
|
||||
const mySelect = ref({})
|
||||
const key = ref(0)
|
||||
|
||||
roleSelectUse().then(list => {
|
||||
roleList.length = 0
|
||||
roleList.push(...list)
|
||||
})
|
||||
|
||||
|
||||
watch(() => roleStore.refresh, () => {
|
||||
roleSelectUse().then(list => {
|
||||
roleList.length = 0
|
||||
roleList.push(...list)
|
||||
})
|
||||
})
|
||||
|
||||
getUserList()
|
||||
|
||||
const filterItem = reactive({
|
||||
send: ['normal', 'del'],
|
||||
account: ['normal', 'del'],
|
||||
receive: ['normal', 'del']
|
||||
})
|
||||
|
||||
function tableFilter(e) {
|
||||
|
||||
if (e.send) filterItem.send = e.send
|
||||
if (e.account) filterItem.account = e.account
|
||||
if (e.receive) filterItem.receive = e.receive
|
||||
|
||||
}
|
||||
|
||||
function formatterSend(e) {
|
||||
|
||||
if (filterItem.send.length === 2) {
|
||||
return e.sendEmailCount + e.delSendEmailCount
|
||||
}
|
||||
|
||||
if (filterItem.send.includes('normal')) {
|
||||
return e.sendEmailCount
|
||||
}
|
||||
|
||||
if (filterItem.send.includes('del')) {
|
||||
return e.delSendEmailCount
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
function formatterAccount(e) {
|
||||
|
||||
if (filterItem.account.length === 2) {
|
||||
return e.accountCount + e.delAccountCount
|
||||
}
|
||||
|
||||
if (filterItem.account.includes('normal')) {
|
||||
return e.accountCount
|
||||
}
|
||||
|
||||
if (filterItem.account.includes('del')) {
|
||||
return e.delAccountCount
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
function formatterReceive(e) {
|
||||
|
||||
|
||||
if (filterItem.receive.length === 2) {
|
||||
return e.receiveEmailCount + e.delReceiveEmailCount
|
||||
}
|
||||
|
||||
if (filterItem.receive.includes('normal')) {
|
||||
return e.receiveEmailCount
|
||||
}
|
||||
|
||||
if (filterItem.receive.includes('del')) {
|
||||
return e.delReceiveEmailCount
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
function setStatusName(user) {
|
||||
if (user.isDel === 1) return '恢复'
|
||||
if (user.status === 0) return '禁用'
|
||||
if (user.status === 1) return '启用'
|
||||
}
|
||||
|
||||
const tableRowFormatter = (data) => {
|
||||
return data.row.email
|
||||
}
|
||||
|
||||
function changeExpand() {
|
||||
expandStatus.value = !expandStatus.value
|
||||
key.value++
|
||||
}
|
||||
|
||||
const openSelect = () => {
|
||||
mySelect.value.toggleMenu()
|
||||
}
|
||||
|
||||
function resetAddForm() {
|
||||
addForm.email = ''
|
||||
addForm.suffix = settingStore.domainList[0]
|
||||
addForm.type = null
|
||||
addForm.password = ''
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
showAdd.value = true
|
||||
}
|
||||
|
||||
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 (!addForm.password) {
|
||||
ElMessage({
|
||||
message: "密码不能为空",
|
||||
type: "error",
|
||||
plain: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (addForm.password.length < 6) {
|
||||
ElMessage({
|
||||
message: "密码至少六位",
|
||||
type: "error",
|
||||
plain: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!addForm.type) {
|
||||
ElMessage({
|
||||
message: "身份类型不能为空",
|
||||
type: "error",
|
||||
plain: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
addLoading.value = true
|
||||
const form = {...addForm}
|
||||
form.email = form.email + form.suffix
|
||||
userAdd(form).then(() => {
|
||||
addLoading.value = false
|
||||
showAdd.value = false
|
||||
ElMessage({
|
||||
message: "添加成功",
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
resetAddForm()
|
||||
getUserList(false)
|
||||
}).finally(res => {
|
||||
addLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function formatSendType(user) {
|
||||
if (user.sendAction.sendType === 'day') return '每天'
|
||||
if (user.sendAction.sendType === 'count') return '总数'
|
||||
}
|
||||
|
||||
function formatSendCount(user) {
|
||||
|
||||
if (!user.sendAction.hasPerm) {
|
||||
return '无权限'
|
||||
}
|
||||
|
||||
if (!user.sendAction.sendCount) {
|
||||
return '无限制';
|
||||
}
|
||||
|
||||
return user.sendCount + '/' + user.sendAction.sendCount + '次'
|
||||
}
|
||||
|
||||
function toRoleName(type) {
|
||||
|
||||
if (type === 0) {
|
||||
return '超级管理员'
|
||||
}
|
||||
|
||||
const index = roleList.findIndex(role => role.roleId === type)
|
||||
if (index > -1) {
|
||||
return roleList[index].name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function resetSendCount(user) {
|
||||
|
||||
ElMessageBox.confirm(`确认重置${user.email}发件次数吗?`, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
userRestSendCount(user.userId).then(() => {
|
||||
ElMessage({
|
||||
message: "重置成功",
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
user.sendCount = 0
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function delUser(user) {
|
||||
ElMessageBox.confirm(`确认删除${user.email}吗?`, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
userDelete(user.userId).then(() => {
|
||||
ElMessage({
|
||||
message: "删除成功",
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
getUserList(false)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function restore(user) {
|
||||
|
||||
const type = ref(0)
|
||||
|
||||
ElMessageBox.confirm( null, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
message: () => h('div', [
|
||||
h('div', { class: 'mb-2' }, `确认要恢复 ${user.email}`),
|
||||
h(ElRadioGroup, {
|
||||
modelValue: type.value,
|
||||
'onUpdate:modelValue': (val) => (type.value = val),
|
||||
}, [
|
||||
h(ElRadio, { label: 'option1', value: 0 }, '普通恢复'),
|
||||
h(ElRadio, { label: 'option2', value: 1 }, '包括已删除的数据'),
|
||||
])
|
||||
]),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
userRestore(user.userId,type.value).then(() => {
|
||||
user.isDel = 0
|
||||
ElMessage({
|
||||
message: "恢复成功",
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function setStatus(user) {
|
||||
|
||||
if (user.status === 0) {
|
||||
ElMessageBox.confirm(`确认禁用 ${user.email} 吗?`, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
httpSetStatus(user)
|
||||
});
|
||||
} else {
|
||||
httpSetStatus(user)
|
||||
}
|
||||
}
|
||||
|
||||
function httpSetStatus(user) {
|
||||
let status = user.status ? 0 : 1
|
||||
userSetStatus({status: status, userId: user.userId}).then(() => {
|
||||
user.status = status
|
||||
ElMessage({
|
||||
message: "设置成功",
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function setType() {
|
||||
settingLoading.value = true
|
||||
userSetType({type: userForm.type, userId: userForm.userId}).then(() => {
|
||||
chooseUser.type = userForm.type
|
||||
setTypeShow.value = false
|
||||
ElMessage({
|
||||
message: "设置成功",
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
|
||||
}).finally(() => {
|
||||
settingLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function resetUserForm() {
|
||||
userForm.password = null
|
||||
userForm.userId = 0
|
||||
userForm.type = 0
|
||||
}
|
||||
|
||||
function search() {
|
||||
params.num = 1
|
||||
getUserList()
|
||||
}
|
||||
|
||||
function updatePwd() {
|
||||
|
||||
if (userForm.password.length < 6) {
|
||||
ElMessage({
|
||||
message: '密码不能为空',
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (userForm.password.length < 6) {
|
||||
ElMessage({
|
||||
message: '密码最少六位',
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
settingLoading.value = true
|
||||
userSetPwd({password: userForm.password, userId: userForm.userId}).then(() => {
|
||||
setPwdShow.value = false
|
||||
ElMessage({
|
||||
message: "设置成功",
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
}).finally(() => {
|
||||
settingLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function openSetType(user) {
|
||||
chooseUser = user
|
||||
userForm.userId = user.userId
|
||||
userForm.type = user.type
|
||||
setTypeShow.value = true
|
||||
}
|
||||
|
||||
function openSetPwd(user) {
|
||||
userForm.userId = user.userId
|
||||
setPwdShow.value = true
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
params.email = ''
|
||||
params.num = 1
|
||||
params.status = -1
|
||||
params.timeSort = 0
|
||||
getUserList();
|
||||
roleSelectUse().then(list => {
|
||||
roleList.length = 0
|
||||
roleList.push(...list)
|
||||
})
|
||||
}
|
||||
|
||||
function changeTimeSort() {
|
||||
params.num = 1
|
||||
params.timeSort = params.timeSort ? 0 : 1
|
||||
getUserList()
|
||||
}
|
||||
|
||||
function numChange(num) {
|
||||
params.num = num
|
||||
getUserList()
|
||||
}
|
||||
|
||||
function sizeChange(size) {
|
||||
params.size = size
|
||||
getUserList()
|
||||
}
|
||||
|
||||
function getUserList(loading = true) {
|
||||
|
||||
tableLoading.value = loading
|
||||
const newParams = {...params}
|
||||
|
||||
if (newParams.status === -2) {
|
||||
delete newParams.status
|
||||
newParams.isDel = 1
|
||||
}
|
||||
userList(newParams).then(data => {
|
||||
users.value = data.list
|
||||
total.value = data.total
|
||||
scrollbarRef.value?.setScrollTop(0);
|
||||
}).finally(() => {
|
||||
tableLoading.value = false
|
||||
first.value = false
|
||||
})
|
||||
}
|
||||
|
||||
window.onresize = () => {
|
||||
adjustWidth()
|
||||
};
|
||||
|
||||
adjustWidth()
|
||||
|
||||
function adjustWidth() {
|
||||
const width = window.innerWidth
|
||||
statusShow.value = width > 1060
|
||||
createTimeShow.value = width > 1200
|
||||
accountNumShow.value = width > 650
|
||||
sendNumShow.value = width > 685
|
||||
typeShow.value = width > 767
|
||||
emailWidth.value = width > 480 ? 230 : null
|
||||
settingWidth.value = width < 480 ? 75 : null
|
||||
expandWidth.value = width < 480 ? 25 : 40
|
||||
pagerCount.value = width < 768 ? 7 : 11
|
||||
receiveWidth.value = width < 480 ? 90 : null
|
||||
layout.value = width < 768 ? 'pager' : 'prev, pager, next,sizes, total'
|
||||
phonePageShow.value = width < 768
|
||||
pageSize.value = width < 380 ? 'small' : ''
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.el-message-box__container {
|
||||
align-items: start !important;
|
||||
}
|
||||
|
||||
.el-message-box__message {
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.user-box {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-dialog) {
|
||||
width: 400px !important;
|
||||
@media (max-width: 440px) {
|
||||
width: calc(100% - 40px) !important;
|
||||
margin-right: 20px !important;
|
||||
margin-left: 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
padding: 9px 15px;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
|
||||
font-size: 18px;
|
||||
|
||||
.search-input {
|
||||
width: min(200px, calc(100vw - 140px));
|
||||
}
|
||||
|
||||
.search {
|
||||
:deep(.el-input-group) {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.type {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.choose-star {
|
||||
color: var(--el-color-primary)
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
height: calc(100% - 50px);
|
||||
@media (max-width: 464px) {
|
||||
height: calc(100% - 90px);
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
padding: 20px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
|
||||
.details-item-title {
|
||||
white-space: pre;
|
||||
color: #909399;
|
||||
font-weight: bold;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 20px;
|
||||
padding-right: 30px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: end;
|
||||
gap: 10px;
|
||||
@media (max-width: 767px) {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.el-pagination {
|
||||
align-self: end;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.email-row {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.status-select {
|
||||
width: 80px;
|
||||
|
||||
:deep(.el-select__wrapper) {
|
||||
min-height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog {
|
||||
.dialog-box {
|
||||
.el-button {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.select {
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
width: 100px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-show {
|
||||
transition: all 200ms ease 200ms;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.loading-hide {
|
||||
pointer-events: none;
|
||||
transition: all 200ms;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.setting-icon {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-pagination .el-select) {
|
||||
width: 100px;
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
: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;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.cell) {
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
:deep(.receive .cell) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:deep(.send .cell) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:deep(.account .cell) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:deep(.el-table th.el-table__cell>.cell.highlight) {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
:deep(.el-table__inner-wrapper:before) {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
:deep(.el-message-box__container) {
|
||||
align-items: start;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user