This commit is contained in:
oneao committed 2025-06-10 21:27:20 +08:00
commit c570ec514c
550 files changed
+152597

No files matched your search

@@ -0,0 +1,34 @@
<template>
<div class="layout">
<div class="layout-topbar">
<Topbar />
</div>
<div class="layout-content">
<router-view v-slot="{ Component, route }">
<keep-alive>
<component :is="Component" :key="route.name" v-if="route.meta.keepAlive"></component>
</keep-alive>
<component :is="Component" :key="route.name" v-if="!route.meta.keepAlive"></component>
</router-view>
</div>
</div>
</template>
<script setup lang="ts">
import Topbar from "../topbar/index.vue";
</script>
<style lang="scss" scoped>
.layout {
display: flex;
flex-direction: column;
height: 100%;
}
.layout-topbar {
height: 56px;
}
.layout-content {
flex: 1;
overflow: hidden;
}
</style>
@@ -0,0 +1,66 @@
<template>
<div class="topbar">
<div class="topbar-left">
<div class="title" @click="routerStore.navigateTo('/')">Online-Email</div>
<a-divider direction="vertical" />
<PullEmail />
<WriteEmail />
<!-- <ReplyEmail />
<ReplyAllEmail />
<ForwardEmail /> -->
<DeleteEmail />
<MarkEmail />
<MoveEmail />
<!-- <PrintEmail /> -->
</div>
<div class="topbar-right">
<User />
</div>
</div>
</template>
<script setup lang="ts">
import User from "./modules/user.vue";
import PullEmail from "./modules/pull-email.vue";
import WriteEmail from "./modules/write-email.vue";
import ReplyEmail from "./modules/reply-email.vue";
import ReplyAllEmail from "./modules/reply-all-email.vue";
import DeleteEmail from "./modules/delete-email.vue";
import PrintEmail from "./modules/print-email.vue";
import ForwardEmail from "./modules/forward-email.vue";
import MarkEmail from "./modules/mark-email.vue";
import MoveEmail from "./modules/move-email.vue";
import { useRouterStore } from "@/store/modules/router";
const routerStore = useRouterStore();
</script>
<style lang="scss" scoped>
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 8px;
height: 100%;
background-color: #f7f9fc;
border-bottom: 1px solid #e0e0e0;
}
.topbar-left {
display: flex;
align-items: center;
gap: 8px;
.title {
font-weight: bold;
font-size: 18px;
color: #165dff;
cursor: pointer;
}
}
.topbar-right {
display: flex;
align-items: center;
gap: 16px;
}
</style>
@@ -0,0 +1,55 @@
<template>
<a-button size="small" type="primary" shape="round" :disabled="disabled" @click="handleDeleteEmail">
<template #icon><SvgIcon icon="line-md:remove" /></template>
删除
</a-button>
</template>
<script setup lang="ts">
import { useEmailStore } from "@/store/modules/email";
import { useRouterStore } from "@/store/modules/router";
import { Message } from "@arco-design/web-vue";
import { computed } from "vue";
import { deleteEmailApi } from "@/service/email";
import { useEmailListManager } from "@/hooks/useEmailListManager";
const { removeAndFill } = useEmailListManager();
const routerStore = useRouterStore();
const emailStore = useEmailStore();
const disabled = computed(() => {
if (!routerStore.isInHome) {
return true;
}
return false;
});
const handleDeleteEmail = () => {
if (!emailStore.isFolderSelected) {
Message.warning("请选择需要操作的文件夹!");
return;
}
if (!emailStore.isSelectedIds && !emailStore.isEmailSelected) {
Message.warning("请选择需要操作的数据!");
return;
}
const emailIds = emailStore.selectedIds.size > 0 ? Array.from(emailStore.selectedIds) : [emailStore.currentEmail.id];
deleteEmailApi({
folderId: emailStore.currentFolder.id,
emailIds
}).then((res) => {
Message.success(res.message);
if (emailStore.selectedIds.size > 0) {
removeAndFill(emailIds);
emailStore.selectedIds.clear();
} else {
removeAndFill([emailStore.currentEmail.id]);
emailStore.resetCurrentEmail();
}
});
};
</script>
@@ -0,0 +1,44 @@
<template>
<a-button-group>
<a-button size="small" type="primary" shape="round" :disabled="emailStore.currentFolder.category === EmailFolderCategoryEnum.DRAFT" @click="handleToWrite(false)">
<template #icon><SvgIcon icon="solar:multiple-forward-right-bold" /></template>
转发
</a-button>
<a-dropdown position="br" :popup-max-height="false">
<a-button size="small" type="primary" :disabled="emailStore.currentFolder.category === EmailFolderCategoryEnum.DRAFT">
<template #icon>
<icon-down />
</template>
</a-button>
<template #content>
<a-doption @click="handleToWrite(false)">HTML邮件</a-doption>
<a-doption @click="handleToWrite(true)">纯文本邮件</a-doption>
</template>
</a-dropdown>
</a-button-group>
</template>
<script setup lang="ts">
import { IconDown } from "@arco-design/web-vue/es/icon";
import { useRouterStore } from "@/store/modules/router";
import { useEmailStore } from "@/store/modules/email";
import { EmailFolderCategoryEnum } from "@/enums";
const routerStore = useRouterStore();
const emailStore = useEmailStore();
const handleToWrite = (isText: boolean) => {
if (routerStore.currentPath === "/write") {
return;
}
routerStore.navigateTo({
path: "/write",
query: {
isText: isText ? 1 : 0,
isForward: 1
}
});
};
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,82 @@
<template>
<a-dropdown>
<a-button size="small" type="primary" shape="round" :disabled="disabled">
<template #icon>
<SvgIcon icon="material-symbols:kid-star-outline" />
</template>
标记为
<IconDown style="margin-left: 4px" />
</a-button>
<template #content>
<a-doption @click="handleUpdateSeen(1)">已读</a-doption>
<a-doption @click="handleUpdateSeen(0)">未读</a-doption>
<a-doption @click="handleUpdateAllSeen(1)">全部设为已读</a-doption>
<a-doption @click="handleUpdateAllSeen(0)">全部设为未读</a-doption>
</template>
</a-dropdown>
</template>
<script setup lang="ts">
import { IconDown } from "@arco-design/web-vue/es/icon";
import { useEmailStore } from "@/store/modules/email";
import { useRouterStore } from "@/store/modules/router";
import { useEmailListManager } from "@/hooks/useEmailListManager";
import { computed } from "vue";
import { updateEmailSeenApi } from "@/service/email";
import { Message } from "@arco-design/web-vue";
const { emailList, cacheEmailList } = useEmailListManager();
const routerStore = useRouterStore();
const emailStore = useEmailStore();
const disabled = computed(() => {
if (!routerStore.isInHome) {
return true;
}
return false;
});
const handleUpdateSeen = (isSeen: 0 | 1) => {
if (!emailStore.isEmailSelected && !emailStore.isSelectedIds) {
Message.warning("请选中数据!");
return;
}
const folderId = emailStore.currentFolder.id;
let emailIds: string[] = [];
if (emailStore.selectedIds.size > 0) {
emailIds = Array.from(emailStore.selectedIds);
} else if (emailStore.isEmailSelected && emailStore.currentEmail?.id) {
emailIds = [emailStore.currentEmail.id];
}
if (emailIds.length === 0) return;
updateEmailSeenApi({ folderId, emailIds, isSeen }).then(() => {
emailList.value.forEach((item) => {
if (emailIds.includes(item.id)) {
item.isSeen = isSeen;
}
});
});
};
const handleUpdateAllSeen = (isSeen: 0 | 1) => {
const folderId = emailStore.currentFolder.id;
updateEmailSeenApi({
folderId,
emailIds: [],
isSeen: isSeen
}).then(() => {
emailList.value.forEach((item) => {
item.isSeen = isSeen;
});
cacheEmailList.value.data.forEach((item) => {
item.isSeen = isSeen;
});
});
};
</script>
@@ -0,0 +1,33 @@
<template>
<a-dropdown>
<a-button size="small" type="primary" shape="round" :disabled="disabled">
<template #icon>
<SvgIcon icon="ic:outline-drive-file-move" />
</template>
移动到
<IconDown style="margin-left: 4px" />
</a-button>
<template #content>
<a-doption>已读</a-doption>
<a-doption>未读</a-doption>
<a-doption>全部设为已读</a-doption>
</template>
</a-dropdown>
</template>
<script setup lang="ts">
import { IconDown } from "@arco-design/web-vue/es/icon";
import { useEmailStore } from "@/store/modules/email";
import { useRouterStore } from "@/store/modules/router";
import { computed } from "vue";
const routerStore = useRouterStore();
const emailStore = useEmailStore();
const disabled = computed(() => {
if (!routerStore.isInHome) {
return true;
}
return false;
});
</script>
@@ -0,0 +1,12 @@
<template>
<a-button size="small" type="primary" shape="round">
<template #icon><SvgIcon icon="material-symbols:print-outline-sharp" /></template>
打印
</a-button>
</template>
<script setup lang="ts">
import { useEmailStore } from "@/store/modules/email";
const emailStore = useEmailStore();
</script>
@@ -0,0 +1,108 @@
<template>
<a-button-group>
<a-button size="small" type="primary" shape="round">
<template #icon>
<SvgIcon icon="line-md:email-arrow-down" />
</template>
收信
</a-button>
<a-dropdown position="br" :popup-max-height="false" class="button-group">
<a-button size="small" type="primary">
<template #icon>
<icon-down />
</template>
</a-button>
<template #content>
<a-doption>
<template #icon>
<SvgIcon icon="material-symbols:download-2-outline-rounded" />
</template>
拉取所有文件夹
</a-doption>
<a-doption @click="handlePullData('0')">
<template #icon>
<SvgIcon icon="material-symbols:download-2-outline-rounded" />
</template>
拉取所有邮件
</a-doption>
<a-divider class="divider" />
<template v-for="item in emailStore.folderList" :key="item.name">
<a-dgroup v-if="item.children && item.children.length > 0" :key="item.id">
<template #title>
{{ item.label }}
</template>
<a-doption v-for="child in item.children" :key="child.id" @click="handlePullData(child.id)">
<template #icon><SvgIcon :icon="child.icon" /></template>
{{ child.label }}
</a-doption>
</a-dgroup>
<template v-else>
<a-doption :key="item.id" @click="handlePullData(item.id)">
<template #icon><SvgIcon :icon="item.icon" /></template>
{{ item.label }}
</a-doption>
</template>
</template>
</template>
</a-dropdown>
</a-button-group>
<EmailPullModal v-model:visible="visible" :title="streamData.folderContent" :content="streamData.emailContent" :percent="streamData.progress" />
</template>
<script setup lang="ts">
import EmailPullModal from "@/components/email-pull-modal.vue";
import { ref } from "vue";
import { IconDown } from "@arco-design/web-vue/es/icon";
import { useEmailStore } from "@/store/modules/email";
import { fetchGet } from "@/utils/fetch";
import { Message } from "@arco-design/web-vue";
import type { EmailPullData } from "@/types/email";
import { useEmailListManager } from "@/hooks/useEmailListManager";
const { resetData: resetEmailSummary, fetchData: loadEmailSummary } = useEmailListManager();
const emailStore = useEmailStore();
const visible = ref(false);
const streamData = ref<EmailPullData>({
folderContent: "正在初始化数据,请稍等 . . .",
emailContent: "正在处理邮件 . . .",
progress: 0
});
const handlePullData = (folderId: string) => {
visible.value = true;
fetchStreamData(folderId);
};
async function fetchStreamData(folderId: string) {
try {
await fetchGet<EmailPullData>(`/email/pull/${folderId}`, {
params: {}, // 示例参数
stream: true,
onStreamData: (res) => {
if (res.data !== null) {
streamData.value = res.data;
}
}
}).then(() => {
if (folderId === emailStore.currentFolder.id || folderId === "0") {
// 如果拉取的是当前文件夹或所有邮件,重置并加载邮件摘要
resetEmailSummary();
loadEmailSummary();
}
visible.value = false;
Message.success("拉取成功");
});
} catch (error) {
console.error("流式数据请求失败", error);
}
}
</script>
<style lang="scss" scoped>
.divider {
margin: 4px 0px !important;
}
</style>
@@ -0,0 +1,45 @@
<template>
<a-button-group>
<a-button size="small" type="primary" shape="round" :disabled="emailStore.currentFolder.category === EmailFolderCategoryEnum.DRAFT" @click="handleToWrite(false)">
<template #icon><SvgIcon icon="majesticons:reply-all" /></template>
回复全部
</a-button>
<a-dropdown position="br" :popup-max-height="false">
<a-button size="small" type="primary" :disabled="emailStore.currentFolder.category === EmailFolderCategoryEnum.DRAFT">
<template #icon>
<icon-down />
</template>
</a-button>
<template #content>
<a-doption @click="handleToWrite(false)">HTML邮件</a-doption>
<a-doption @click="handleToWrite(true)">纯文本邮件</a-doption>
</template>
</a-dropdown>
</a-button-group>
</template>
<script setup lang="ts">
import { IconDown } from "@arco-design/web-vue/es/icon";
import { useRouterStore } from "@/store/modules/router";
import { useEmailStore } from "@/store/modules/email";
import { EmailFolderCategoryEnum } from "@/enums";
const routerStore = useRouterStore();
const emailStore = useEmailStore();
const handleToWrite = (isText: boolean) => {
if (routerStore.currentPath === "/write") {
return;
}
routerStore.navigateTo({
path: "/write",
query: {
isText: isText ? 1 : 0,
isReplyAll: 1
}
});
};
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,44 @@
<template>
<a-button-group>
<a-button size="small" type="primary" shape="round" :disabled="emailStore.currentFolder.category === EmailFolderCategoryEnum.DRAFT" @click="handleToWrite(false)">
<template #icon><SvgIcon icon="majesticons:reply" /></template>
回复
</a-button>
<a-dropdown position="br" :popup-max-height="false">
<a-button size="small" type="primary" :disabled="emailStore.currentFolder.category === EmailFolderCategoryEnum.DRAFT">
<template #icon>
<icon-down />
</template>
</a-button>
<template #content>
<a-doption @click="handleToWrite(false)">HTML邮件</a-doption>
<a-doption @click="handleToWrite(true)">纯文本邮件</a-doption>
</template>
</a-dropdown>
</a-button-group>
</template>
<script setup lang="ts">
import { IconDown } from "@arco-design/web-vue/es/icon";
import { useRouterStore } from "@/store/modules/router";
import { useEmailStore } from "@/store/modules/email";
import { EmailFolderCategoryEnum } from "@/enums";
const routerStore = useRouterStore();
const emailStore = useEmailStore();
const handleToWrite = (isText: boolean) => {
if (routerStore.currentPath === "/write") {
return;
}
routerStore.navigateTo({
path: "/write",
query: {
isText: isText ? 1 : 0,
isReply: 1
}
});
};
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,60 @@
<template>
<a-dropdown v-if="userStore.isLogined">
<div class="user-message">
<EmailIcon :email="userStore.userInfo.email" />
<div class="user-message-name">{{ userStore.userInfo.displayName }}</div>
</div>
<template #content>
<a-doption @click="routerStore.navigateTo('/setting')">
<template #icon>
<SvgIcon icon="ic:outline-settings"></SvgIcon>
</template>
设置中心
</a-doption>
<a-doption @click="handleToLogin()">
<template #icon>
<SvgIcon icon="material-symbols:exit-to-app"></SvgIcon>
</template>
退出登录
</a-doption>
</template>
</a-dropdown>
</template>
<script setup lang="ts">
import { useUserStore } from "@/store/modules/user";
import { useRouterStore } from "@/store/modules/router";
const routerStore = useRouterStore();
const userStore = useUserStore();
function handleToLogin() {
userStore.clearUserInfo();
routerStore.navigateTo("/login");
}
</script>
<style lang="scss" scoped>
.user-message {
display: flex;
align-items: center;
gap: 10px;
height: 28px;
padding: 0px 12px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
color: #3c4a5d;
background-color: #ffffff;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
transition:
background-color 0.2s,
box-shadow 0.2s;
}
.user-message:hover {
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
}
</style>
@@ -0,0 +1,40 @@
<template>
<a-button-group>
<a-button size="small" type="primary" shape="round" @click="routerStore.navigateTo('/write')">
<template #icon><SvgIcon icon="line-md:edit" /></template>
写信
</a-button>
<a-dropdown position="br" :popup-max-height="false">
<a-button size="small" type="primary">
<template #icon>
<icon-down />
</template>
</a-button>
<template #content>
<a-doption @click="handleToWrite(false)">HTML邮件</a-doption>
<a-doption @click="handleToWrite(true)">纯文本邮件</a-doption>
</template>
</a-dropdown>
</a-button-group>
</template>
<script setup lang="ts">
import { IconDown } from "@arco-design/web-vue/es/icon";
import { useRouterStore } from "@/store/modules/router";
const routerStore = useRouterStore();
const handleToWrite = (isText: boolean) => {
if (routerStore.currentPath === "/write") {
return;
}
routerStore.navigateTo({
path: "/write",
query: {
isText: isText ? 1 : 0
}
});
};
</script>
<style lang="scss" scoped></style>