新增邮件发送和管理员功能
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user