邮件列表加载动画优化和打包拆分

This commit is contained in:
eoao
2025-08-26 23:10:55 +08:00
parent de0bb288ec
commit 01cef2355d
119 changed files with 732 additions and 330 deletions

View File

@@ -2,10 +2,10 @@
<div class="email-container">
<div class="header-actions">
<el-checkbox
v-model="checkAll"
:indeterminate="isIndeterminate"
:disabled="!emailList.length"
@change="handleCheckAllChange"
v-model="checkAll"
:indeterminate="isIndeterminate"
:disabled="!emailList.length || loading"
@change="handleCheckAllChange"
>
</el-checkbox>
<div class="header-left" :style="'padding-left:' + actionLeft">
@@ -18,22 +18,23 @@
</div>
<div class="header-right">
<span class="email-count" v-if="total">{{$t('emailCount', {total: total})}}</span>
<span class="email-count" v-if="total">{{ $t('emailCount', {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 ref="scrollbarRef" @scroll="handleScroll">
<el-scrollbar ref="scrollbarRef" style="height: 100%">
<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 v-if="(skeleton && !loading)" v-for="item in emailList" :key="item.emailId">
<div class="email-row"
:data-checked="item.checked"
@click="jumpDetails(item)"
>
<el-checkbox :class=" props.type === 'all-email' ? 'all-email-checkbox' : 'checkbox'" v-model="item.checked" @click.stop></el-checkbox>
<el-checkbox :class=" props.type === 'all-email' ? 'all-email-checkbox' : '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"/>
@@ -43,62 +44,33 @@
<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="$t('received')"
>
<el-tooltip v-if="item.status === 0" effect="dark" :content="$t('received')">
<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="$t('sent')"
>
<Icon icon="bi:send-arrow-up-fill" style="color: #67C23A" width="20" height="20"
/>
<el-tooltip v-if="item.status === 1" effect="dark" :content="$t('sent')">
<Icon icon="bi:send-arrow-up-fill" style="color: #67C23A" width="20" height="20"/>
</el-tooltip>
<el-tooltip v-if="item.status === 2"
effect="dark"
:content="$t('delivered')"
>
<Icon icon="bi:send-check-fill" style="color: #67C23A" width="20"
height="20"/>
<el-tooltip v-if="item.status === 2" effect="dark" :content="$t('delivered')">
<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="$t('bounced')"
>
<Icon icon="bi:send-x-fill" style="color: #F56C6C" width="20"
height="20"/>
<el-tooltip v-if="item.status === 3" effect="dark" :content="$t('bounced')">
<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="$t('complained')"
>
<Icon icon="bi:send-exclamation-fill" style="color:#FBBD08" width="20"
height="20"/>
<el-tooltip v-if="item.status === 4" effect="dark" :content="$t('complained')">
<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="$t('delayed')"
>
<Icon icon="bi:send-arrow-up-fill" style="color:#FBBD08" width="20"
height="20"/>
<el-tooltip v-if="item.status === 5" effect="dark" :content="$t('delayed')">
<Icon icon="bi:send-arrow-up-fill" style="color:#FBBD08" width="20" height="20"/>
</el-tooltip>
<el-tooltip v-if="item.status === 7"
effect="dark"
:content="$t('noRecipient')"
>
<Icon icon="ic:round-mark-email-read" style="color:#FBBD08" width="20"
height="20"/>
<el-tooltip v-if="item.status === 7" effect="dark" :content="$t('noRecipient')">
<Icon icon="ic:round-mark-email-read" style="color:#FBBD08" width="20" height="20"/>
</el-tooltip>
<div class="del-status" v-if="item.isDel">
<el-tooltip effect="dark" :content="$t('selectDeleted')">
<Icon class="icon" icon="mdi:email-remove" width="20" height="20"/>
</el-tooltip>
</div>
</div>
<div v-else></div>
<span class="name">
@@ -115,10 +87,10 @@
<div class="email-text">
<span class="email-subject">
<slot name="subject" :email="item">
{{ item.subject }}
{{ item.subject || '\u200B' }}
</slot>
</span>
<span class="email-content">{{ htmlToText(item) }}</span>
<span class="email-content">{{ htmlToText(item) || '\u200B' }}</span>
</div>
<div class="user-info" v-if="showUserInfo">
<div class="user">
@@ -133,9 +105,6 @@
</span>
<span>{{ item.type === 0 ? item.toEmail : item.sendEmail }}</span>
</div>
<div class="del-status" v-if="item.isDel">
<el-tag type="danger" size="small">{{$t('deleted')}}</el-tag>
</div>
</div>
</div>
</div>
@@ -144,21 +113,47 @@
</div>
</div>
</div>
<div class="loading" :class="loading ? 'loading-show' : 'loading-hide'" :style="firstLoad ? 'background: transparent' : ''">
<Loading/>
<template v-if="skeleton">
<skeletonBlock v-if="firstLoad && showFirstLoading"
:rows="20"
:showStar="showStar"
:accountShow="accountShow"
:showStatus="showStatus"
:showUserInfo="showUserInfo"
:type="type"/>
<skeletonBlock v-if="loading"
:rows="skeletonRows"
:showStar="showStar"
:accountShow="accountShow"
:showStatus="showStatus"
:showUserInfo="showUserInfo"
:type="type"/>
<skeletonBlock v-if="followLoading"
:rows="isMobile ? 1 : 2"
:showStar="showStar"
:accountShow="accountShow"
:showStatus="showStatus"
:showUserInfo="showUserInfo"
:type="type"/>
</template>
<template v-else>
<div></div>
<div class="loading" :class="loading ? 'loading-show' : 'loading-hide'"
:style="firstLoad ? 'background: transparent' : ''">
<Loading/>
</div>
<div class="follow-loading" v-if="followLoading">
<Loading/>
</div>
</template>
<div class="noLoading" v-if="noLoading && emailList.length > 0 && !(skeleton && loading)">
<div>{{ $t('noMoreData') }}</div>
</div>
<div class="follow-loading" v-if="followLoading">
<Loading/>
</div>
<div class="noLoading" v-if="noLoading && emailList.length > 0">
<div>{{$t('noMoreData')}}</div>
</div>
<div class="empty" v-if="noLoading && emailList.length === 0">
<div class="empty" v-if="noLoading && emailList.length === 0 && !(skeleton && loading)">
<el-empty :image-size="isMobile ? 120 : 0" :description="$t('noMessagesFound')"/>
</div>
</div>
</el-scrollbar>
</div>
</div>
</template>
@@ -166,11 +161,13 @@
<script setup>
import Loading from "@/components/loading/index.vue";
import {Icon} from "@iconify/vue";
import skeletonBlock from "@/components/email-scroll/skeleton/index.vue"
import {computed, onActivated, reactive, ref, watch} from "vue";
import {onBeforeRouteLeave} from "vue-router";
import {useEmailStore} from "@/store/email.js";
import {useUiStore} from "@/store/ui.js";
import {useSettingStore} from "@/store/setting.js";
import {sleep} from "@/utils/time-utils.js"
import {fromNow} from "@/utils/day.js";
import {useI18n} from "vue-i18n";
@@ -212,12 +209,19 @@ const props = defineProps({
type: {
type: String,
default: ''
},
skeleton: {
type: Boolean,
default: true
},
showFirstLoading: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['jump', 'refresh-before', 'delete-draft'])
const { t } = useI18n()
const {t} = useI18n()
const settingStore = useSettingStore()
const uiStore = useUiStore();
const emailStore = useEmailStore();
@@ -234,7 +238,8 @@ let scrollTop = 0
const latestEmail = ref(null)
const scrollbarRef = ref(null)
let reqLock = false
let isMobile = window.innerWidth < 1025
let isMobile = innerWidth < 1025
let skeletonRows = 0
const queryParam = reactive({
emailId: 0,
size: 30,
@@ -294,13 +299,16 @@ watch(() => emailStore.addStarEmailId, () => {
})
})
function getSkeletonRows() {
if (emailList.length > 20) return skeletonRows = 20
if (emailList.length === 0) return skeletonRows = 1
skeletonRows = emailList.length
}
const accountShow = computed(() => {
return uiStore.accountShow && settingStore.settings.manyEmail === 0
})
function handleScroll(e) {
}
function htmlToText(email) {
if (email.content) {
@@ -473,6 +481,7 @@ function getEmailList(refresh = false) {
}
} else {
getSkeletonRows()
loading.value = true
}
@@ -481,11 +490,13 @@ function getEmailList(refresh = false) {
} else {
followLoading.value = !refresh;
}
props.getEmailList(queryParam.emailId, queryParam.size).then(data => {
let start = Date.now();
props.getEmailList(queryParam.emailId, queryParam.size).then(async data => {
let end = Date.now();
let duration = end - start;
if (duration < 500 && !queryParam.emailId) {
await sleep(500 - duration)
}
firstLoad.value = false
let list = data.list.map(item => ({
@@ -516,6 +527,9 @@ function getEmailList(refresh = false) {
function refresh() {
emit('refresh-before')
if (props.skeleton) {
scrollbarRef.value.setScrollTop(0)
}
refreshList()
}
@@ -531,7 +545,6 @@ function loadData() {
}
</script>
<style lang="scss" scoped>
.email-container {
@@ -567,7 +580,7 @@ function loadData() {
justify-content: center;
align-items: center;
padding: 15px 0;
color: gray;
color: var(--secondary-text-color);
}
.follow-loading {
@@ -597,12 +610,12 @@ function loadData() {
.loading-hide {
pointer-events: none;
transition: all 200ms;
transition: var(--loading-hide-transition);
opacity: 0;
}
}
.email-row {
:deep(.email-row) {
display: flex;
padding: 8px 0;
justify-content: space-between;
@@ -611,16 +624,17 @@ function loadData() {
align-items: center;
position: relative;
transition: background 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
@media (max-width: 1199px) {
}
.user-info {
display: flex;
flex-wrap: wrap;
column-gap: 10px;
margin-top: 5px;
margin-bottom: 5px;
margin-bottom: 2px;
color: var(--email-scroll-content-color);
@media (max-width: 1199px) {
flex-direction: column;
}
.user, .account {
overflow: hidden;
@@ -628,7 +642,12 @@ function loadData() {
text-overflow: ellipsis;
transition: all 300ms;
line-height: 12px;
max-width: 290px;
max-width: 300px;
min-width: 0;
@media (max-width: 1223px) {
max-width: 280px;
}
span:first-child {
position: relative;
@@ -663,7 +682,7 @@ function loadData() {
}
.title-column {
@media (max-width: 1200px) {
@media (max-width: 1199px) {
grid-template-columns: 1fr !important;
gap: 4px !important;
}
@@ -689,7 +708,12 @@ function loadData() {
.email-status {
display: flex;
flex-direction: column;
align-content: center;
@media (max-width: 1199px) {
flex-direction: row;
gap: 5px;
}
}
.name {
@@ -708,6 +732,14 @@ function loadData() {
white-space: nowrap;
text-overflow: ellipsis;
}
.name-skeleton {
width: 150px;
height: 1rem;
@media (max-width: 767px) {
width: 130px;
}
}
}
.phone-time {
@@ -719,6 +751,30 @@ function loadData() {
}
}
.email-text-skeleton {
.text-skeleton-one {
width: 80%;
height: 16px;
@media (max-width: 1199px) {
width: 40%;
}
@media (max-width: 767px) {
width: 70%;
}
}
.text-skeleton-two {
width: min(300px, 100%);
height: 16px;
@media (min-width: 1200px) {
display: none;
}
@media (max-width: 1199px) {
width: 100%;
}
}
}
.email-text {
display: grid;
grid-template-columns: auto 1fr;
@@ -762,6 +818,12 @@ function loadData() {
}
}
.email-right-skeleton {
@media (max-width: 1199px) {
display: none;
}
}
&:hover {
background-color: var(--email-hover-background);
z-index: 0;
@@ -812,6 +874,7 @@ function loadData() {
gap: 15px;
padding: 3px 15px;
box-shadow: var(--header-actions-border);
.header-left {
display: flex;
flex-wrap: wrap;
@@ -828,6 +891,7 @@ function loadData() {
align-items: start;
height: 100%;
color: var(--el-text-color-primary);;
.email-count {
white-space: nowrap;
margin-top: 6px;
@@ -845,6 +909,15 @@ function loadData() {
}
}
.del-status {
color: var(--el-color-danger);
display: flex;
align-items: center;
justify-content: center;
position: relative;
bottom: 1px;
}
ul {
list-style: none;
padding: 0;

View File

@@ -0,0 +1,133 @@
<template>
<div v-for="item in rows" style="background: var(--el-bg-color)">
<div class="email-row">
<el-checkbox disabled :class=" props.type === 'all-email' ? 'all-email-checkbox' : 'checkbox'"
></el-checkbox>
<div class="pc-star" v-if="showStar">
<Icon style="color: var(--el-border-color)" icon="solar:star-line-duotone" width="18" height="18"/>
</div>
<div v-if="!showStar"></div>
<div class="title" :class="accountShow ? 'title-column' : 'title-column'">
<div class="email-sender">
<div class="email-status" v-if="showStatus">
</div>
<div v-else></div>
<span class="name">
<span>
<el-skeleton animated>
<template #template>
<el-skeleton-item variant="text" class="name-skeleton"/>
</template>
</el-skeleton>
</span>
<span></span>
</span>
<span class="phone-time">
<el-skeleton animated>
<template #template>
<el-skeleton-item variant="text" style="width: 50px;height: 1rem;"/>
</template>
</el-skeleton>
</span>
</div>
<div>
<div class="email-text-skeleton">
<el-skeleton animated>
<template #template>
<el-skeleton-item variant="text" class="text-skeleton-one"/>
<el-skeleton-item variant="text" class="text-skeleton-two"/>
</template>
</el-skeleton>
</div>
<div class="user-info" v-if="showUserInfo">
<div class="user">
<el-skeleton animated>
<template #template>
<el-skeleton-item variant="text"
style="width: 180px;margin-right: 15px;height: 1rem;margin-bottom: 4px;"/>
</template>
</el-skeleton>
</div>
<div class="account">
<el-skeleton animated>
<template #template>
<el-skeleton-item variant="text"
style="width: 180px;margin-right: 15px;height: 1rem;margin-bottom: 4px;"/>
</template>
</el-skeleton>
</div>
<div class="del-status" v-if="item.isDel">
<el-tag type="danger" size="small">{{ $t('deleted') }}</el-tag>
</div>
</div>
</div>
</div>
<div class="email-right-skeleton" :style="showUserInfo ? 'align-self: start;':''">
<el-skeleton animated>
<template #template>
<el-skeleton-item variant="text" style="width: 50px;margin-right: 15px;height: 1rem;"/>
</template>
</el-skeleton>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
rows: {
type: Number,
default: 1
},
showStar: {
type: Boolean,
default: true
},
accountShow: {
type: Boolean,
default: false
},
showStatus: {
type: Boolean,
default: false
},
showUserInfo: {
type: Boolean,
default: false
},
type: {
type: String,
default: ''
}
})
import {Icon} from "@iconify/vue";
</script>
<style scoped lang="scss">
.phone-star {
display: none;
}
.pc-star {
display: flex;
width: 40px;
}
@media (max-width: 1024px) {
.pc-star {
display: none;
}
.phone-star {
display: block;
align-self: end;
padding-right: 16px;
padding-top: 8px;
}
.star-pd {
padding-top: 6px !important;
}
}
</style>

View File

@@ -106,4 +106,4 @@ export default {
align-items: center;
justify-content: center;
}
</style>
</style>