This commit is contained in:
eoao
2025-04-26 17:24:39 +08:00
commit 24ba4772e6
107 changed files with 13612 additions and 0 deletions

View File

@@ -0,0 +1,494 @@
<template>
<div class="email-container">
<div class="header-actions">
<el-checkbox
v-model="checkAll"
:indeterminate="isIndeterminate"
:disabled="!emailList.length"
@change="handleCheckAllChange"
>
</el-checkbox>
<Icon class="icon" icon="ion:reload" width="18" height="18" @click="refreshList" />
<Icon class="icon" icon="uiw:delete" width="16" height="16" v-if="getSelectedMailsIds().length > 0" @click="handleDelete" />
<Icon class="more-icon icon" width="16" height="16" icon="akar-icons:dot-grid-fill" @click="changeAccountShow" />
</div>
<div ref="scroll" class="scroll">
<el-scrollbar>
<div style="height: 100%;" :infinite-scroll-immediate="false" v-infinite-scroll="loadData" infinite-scroll-distance="600">
<div v-for="item in emailList" :key="item.emailId">
<div class="email-row"
:data-checked="item.checked"
@click="jumpDetails(item)"
>
<el-checkbox v-model="item.checked" @click.stop></el-checkbox>
<div @click.stop="starChange(item)" class="pc-star">
<Icon v-if="item.isStar" icon="fluent-color:star-16" width="20" height="20" />
<Icon v-else icon="solar:star-line-duotone" width="18" height="18" />
</div>
<div class="title" :class="accountShow ? 'title-column' : ''">
<div class="email-sender">{{ item.name }}</div>
<div class="email-subject">{{ item.subject}}</div>
</div>
<div class="email-right">
<span class="email-time">{{ fromNow(item.createTime) }}</span>
<div class="phone-star" :class="item.isStar ? 'star-pd' : ''" @click.stop="starChange(item)">
<Icon v-if="item.isStar" icon="fluent-color:star-16" width="20" height="20" />
<Icon v-else icon="solar:star-line-duotone" width="18" height="18" />
</div>
</div>
</div>
</div>
<div class="loading" v-if="loading">
<Loading />
</div>
<div class="follow-loading" v-if="emailList.length > 0 && !noLoading">
<Loading />
</div>
<div class="noLoading" v-if="noLoading && emailList.length > 0">
<div>没有更多数据了</div>
</div>
<div class="empty" v-if="noLoading && emailList.length === 0">
<el-empty description="没有任何邮件" />
</div>
</div>
</el-scrollbar>
</div>
</div>
</template>
<script setup>
import Loading from "@/components/loading/index.vue";
import {Icon} from "@iconify/vue";
import {computed, onActivated, reactive, ref, watch} from "vue";
import router from "@/router/index.js";
import {onBeforeRouteLeave} from "vue-router";
import {ElMessage, ElMessageBox} from "element-plus";
import {useEmailStore} from "@/store/email.js";
import {useUiStore} from "@/store/ui.js";
import {useSettingStore} from "@/store/setting.js";
import {fromNow} from "@/day/day.js";
const props = defineProps({
getEmailList: Function,
emailDelete: Function,
starAdd: Function,
starCancel: Function,
cancelSuccess: Function,
starSuccess: Function,
allowStar: {
type: Boolean,
default: true
}
})
const settingStore = useSettingStore()
const uiStore = useUiStore();
const emailStore = useEmailStore();
const loading = ref(false);
const followLoading = ref(false);
const noLoading = ref(false);
const emailList = reactive([])
const mailTotal = ref(0);
const checkAll = ref(false);
const isIndeterminate = ref(false);
const scroll = ref(null)
const firstLoad = ref(true)
let scrollTop = 0
const queryParam = reactive({
emailId: 0,
size: 30,
});
defineExpose({
refreshList,
deleteEmail,
addItemStar,
editEmailStar,
emailList,
firstLoad
})
onActivated(() => {
scroll.value.scrollTop = scrollTop
})
getEmailList()
onBeforeRouteLeave(() => {
scrollTop = scroll.value.scrollTop
})
watch(
() => emailList.map(item => item.checked),
() => {
if (emailList.length > 0) {
updateCheckStatus();
}
},
{ deep: true }
);
watch(() => emailStore.deleteIds, () => {
if (emailStore.deleteIds) {
deleteEmail(emailStore.deleteIds)
}
})
const accountShow = computed(() => {
return uiStore.accountShow && settingStore.settings.manyEmail === 0
})
function starChange(email) {
if (!email.isStar) {
if (!props.allowStar) return;
email.isStar = 1;
props.starAdd(email.emailId).then(() => {
email.isStar = 1;
props.starSuccess(email)
}).catch(() => {
email.isStar = 0
})
} else {
email.isStar = 0;
props.starCancel(email.emailId).then(() => {
email.isStar = 0;
props.cancelSuccess?.(email)
}).catch(() => {
email.isStar = 1;
})
}
}
function changeAccountShow() {
uiStore.accountShow = !uiStore.accountShow;
}
const handleDelete = () => {
ElMessageBox.confirm('确认批量删除这些邮件吗?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const emailIds = getSelectedMailsIds();
props.emailDelete(emailIds).then(() => {
ElMessage({
message: '删除成功',
type: 'success',
plain: true,
})
emailStore.deleteIds = emailIds;
})
})
}
function deleteEmail(emailIds) {
emailIds.forEach(emailId => {
emailList.forEach((item,index) => {
if (emailId === item.emailId) {
emailList.splice(index,1);
}
})
})
if (emailList.length < queryParam.size && !noLoading.value) {
getEemailList()
}
}
function addItemStar(email) {
const existIndex = emailList.findIndex(item => item.emailId === email.emailId)
if (existIndex > -1) {
return
}
const index = emailList.findIndex(item => item.emailId < email.emailId)
if (index !== -1) {
emailList.splice(index, 0, email);
} else {
if (noLoading.value) {
emailList.push(email)
}
}
}
function editEmailStar(emailId,isStar) {
const index = emailList.findIndex(item => item.emailId === emailId)
if (index !== -1) {
emailList[index].isStar = isStar
}
}
function handleCheckAllChange(val) {
emailList.forEach(item => item.checked = val);
isIndeterminate.value = false;
}
// 获取选中的邮件列表id
function getSelectedMailsIds() {
return emailList.filter(item => item.checked).map(item => item.emailId);
}
function updateCheckStatus() {
const checkedCount = emailList.filter(item => item.checked).length;
checkAll.value = checkedCount === emailList.length;
isIndeterminate.value = checkedCount > 0 && checkedCount < emailList.length;
}
function jumpDetails(email) {
emailStore.readEmail = email
router.push("/content");
}
function getEmailList() {
if (loading.value || followLoading.value || noLoading.value) return;
if (emailList.length === 0) {
loading.value = true
}else {
followLoading.value = true
}
props.getEmailList(queryParam.emailId, queryParam.size).then(data => {
if (emailList.length === 0) {
firstLoad.value = false
}
let list = data.list.map(item => ({
...item,
checked: false
}));
if (data.list.length < queryParam.size) {
noLoading.value = true
}
emailList.push(...list);
mailTotal.value = data.total;
queryParam.emailId = data.list.at(-1).emailId;
loading.value = false
followLoading.value = false
}).catch(() => {
loading.value = false
followLoading.value = false
})
}
function refreshList() {
emailList.splice(0);
noLoading.value = false;
mailTotal.value = 0;
checkAll.value = false;
firstLoad.value = true
isIndeterminate.value = false;
queryParam.emailId = 0;
getEmailList();
}
function loadData() {
getEmailList()
}
</script>
<style lang="scss" scoped>
.email-container {
background: #FFFFFF;
border-radius: 8px;
padding: 0;
font-size: 14px;
color: #2e2e2e;
overflow: hidden;
height: 100%;
}
.scroll {
margin: 0;
height: calc(100% - 38px);
overflow: auto;
.empty {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.noLoading {
display: flex;
justify-content: center;
align-items: center;
padding: 10px 0;
color: gray;
}
.follow-loading {
height: 60px;
display: flex;
justify-content: center;
align-items: center;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
}
.email-row {
display: flex;
align-items: center;
padding: 8px 0;
justify-content: space-between;
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
cursor: pointer;
position: relative;
transition: background 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
.el-checkbox{
width: 40px;
display: flex;
justify-content: center;
padding-left: 6px;
}
.title-column {
@media (max-width: 1200px) {
grid-template-columns: 1fr !important;
gap: 4px !important;
}
}
.title {
flex: 1;
display: grid;
gap: 20px;
grid-template-columns: 200px 1fr;
@media (max-width: 991px) {
grid-template-columns: 1fr;
gap: 4px;
}
.email-sender {
font-weight: bold;
color: #1a1a1a;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.email-subject {
color: #333;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.email-right {
align-self: center;
text-align: right;
font-size: 12px;
white-space: nowrap;
display: flex;
align-items: center;
color: #333;
padding-left: 20px;
}
&:hover {
box-shadow:
inset 1px 0 0 rgb(218, 220, 224),
inset -1px 0 0 rgb(218, 220, 224),
0 1px 2px 0 rgba(60, 64, 67, 0.3),
0 1px 3px 1px rgba(60, 64, 67, 0.15);
z-index: 2;
}
/*&[data-checked="true"] {
background-color: #c2dbff;
}*/
}
.phone-star {
display: none;
}
.pc-star {
padding-left: 8px;
display: flex;
width: 40px;
}
@media (max-width: 991px) {
.pc-star {
display: none;
}
.email-right {
display: flex;
flex-direction: column;
}
.phone-star {
display: block;
align-self: end;
padding-right: 16px;
padding-top: 8px;
}
.star-pd{
padding-top: 6px !important;
}
.title {
padding-left: 8px;
}
}
.email-time {
padding-right: 16px !important;
}
:deep(.el-scrollbar__view) {
height: 100%;
}
.header-actions {
display: flex;
align-items: center;
gap: 16px;
padding: 3px 16px;
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
.icon {
font-size: 18px;
margin-left: 16px;
cursor: pointer;
}
.more-icon {
margin-left: auto;
}
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
</style>

View File

@@ -0,0 +1,42 @@
<template>
<div style="padding: 0 15px;" @click="toggleClick">
<svg
:class="{'is-active':isActive}"
class="hamburger"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
fill="currentColor"
>
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
</svg>
</div>
</template>
<script setup>
defineProps({
isActive: {
type: Boolean,
default: false
}
})
const emit = defineEmits()
const toggleClick = () => {
emit('toggleClick');
}
</script>
<style scoped>
.hamburger {
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
}
.hamburger.is-active {
transform: rotate(180deg);
}
</style>

View File

@@ -0,0 +1,109 @@
<template>
<el-icon class="is-loading" :style="{ fontSize: `${size}px` }">
<svg class="circular" viewBox="0 0 20 20">
<g
class="path2 loading-path"
stroke-width="0"
style="animation: none; stroke: none"
>
<circle r="3.375" class="dot1" rx="0" ry="0" />
<circle r="3.375" class="dot2" rx="0" ry="0" />
<circle r="3.375" class="dot4" rx="0" ry="0" />
<circle r="3.375" class="dot3" rx="0" ry="0" />
</g>
</svg>
</el-icon>
</template>
<script>
export default {
props: {
size: {
type: Number,
default: 30
}
}
}
</script>
<style scoped>
.el-select-dropdown__loading {
display: flex;
justify-content: center;
align-items: center;
height: calc(v-bind(size) * 3.33px);
font-size: calc(v-bind(size) * 0.67px);
}
.circular {
display: inline;
height: v-bind(size) + 'px';
width: v-bind(size) + 'px';
animation: loading-rotate 2s linear infinite;
}
.path {
animation: loading-dash 1.5s ease-in-out infinite;
stroke-dasharray: 90, 150;
stroke-dashoffset: 0;
stroke-width: 2;
stroke: var(--el-color-primary);
stroke-linecap: round;
}
.loading-path .dot1 {
transform: translate(3.75px, 3.75px);
fill: var(--el-color-primary);
animation: custom-spin-move 1s infinite linear alternate;
opacity: 0.3;
}
.loading-path .dot2 {
transform: translate(calc(100% - 3.75px), 3.75px);
fill: var(--el-color-primary);
animation: custom-spin-move 1s infinite linear alternate;
opacity: 0.3;
animation-delay: 0.4s;
}
.loading-path .dot3 {
transform: translate(3.75px, calc(100% - 3.75px));
fill: var(--el-color-primary);
animation: custom-spin-move 1s infinite linear alternate;
opacity: 0.3;
animation-delay: 1.2s;
}
.loading-path .dot4 {
transform: translate(calc(100% - 3.75px), calc(100% - 3.75px));
fill: var(--el-color-primary);
animation: custom-spin-move 1s infinite linear alternate;
opacity: 0.3;
animation-delay: 0.8s;
}
@keyframes loading-rotate {
to {
transform: rotate(360deg);
}
}
@keyframes loading-dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -40px;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -120px;
}
}
@keyframes custom-spin-move {
to {
opacity: 1;
}
}
.is-loading {
display: inline-flex;
align-items: center;
justify-content: center;
}
</style>