新增邮件显示已读未读功能

This commit is contained in:
eoao
2025-11-18 22:08:00 +08:00
parent 0395992b5b
commit 9a089da3b7
12 changed files with 102 additions and 16 deletions

View File

@@ -18,10 +18,10 @@ http.interceptors.response.use((res) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const showMsg = res.config.noMsg; const noMsg = res.config.noMsg;
const data = res.data const data = res.data
if (showMsg) { if (noMsg) {
data.code === 200 ? resolve(data.data) : reject(data) data.code === 200 ? resolve(data.data) : reject(data)
@@ -76,9 +76,9 @@ http.interceptors.response.use((res) => {
return; return;
} }
const showMsg = error.config.noMsg; const noMsg = error.config.noMsg;
if (showMsg) { if (noMsg) {
return Promise.reject(error) return Promise.reject(error)
} else if (error.message.includes('Network Error')) { } else if (error.message.includes('Network Error')) {
ElMessage({ ElMessage({

View File

@@ -15,6 +15,9 @@
<Icon v-perm="'email:delete'" class="icon delete" icon="uiw:delete" width="16" height="16" <Icon v-perm="'email:delete'" class="icon delete" icon="uiw:delete" width="16" height="16"
v-if="getSelectedMailsIds().length > 0" v-if="getSelectedMailsIds().length > 0"
@click="handleDelete"/> @click="handleDelete"/>
<Icon v-perm="'email:delete'" class="icon delete" icon="fluent:mail-read-20-regular" width="21" height="21"
v-if="getSelectedMailsIds().length > 0"
@click="handleRead"/>
</div> </div>
<div class="header-right"> <div class="header-right">
@@ -42,11 +45,10 @@
<div v-if="!showStar"></div> <div v-if="!showStar"></div>
<div class="title" :class="accountShow ? 'title-column' : 'title-column'"> <div class="title" :class="accountShow ? 'title-column' : 'title-column'">
<div class="email-sender" :style=" showStatus ? 'gap: 10px;' : ''"> <div class="email-sender" :style=" (showStatus ? 'gap: 10px;' : '') + ((item.unread === EmailUnreadEnum.UNREAD && showUnread) ? 'font-weight: bold' : '')">
<div class="email-status" v-if="showStatus"> <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: #51C76B" width="20" height="20"/> <Icon icon="ic:round-mark-email-read" style="color: #51C76B" width="20" height="20"/>
/>
</el-tooltip> </el-tooltip>
<el-tooltip v-if="item.status === 1" effect="dark" :content="$t('sent')"> <el-tooltip v-if="item.status === 1" effect="dark" :content="$t('sent')">
<Icon icon="bi:send-arrow-up-fill" style="color: #51C76B" width="20" height="20"/> <Icon icon="bi:send-arrow-up-fill" style="color: #51C76B" width="20" height="20"/>
@@ -75,6 +77,7 @@
<div v-else></div> <div v-else></div>
<span class="name"> <span class="name">
<span> <span>
<div class="unread" v-if="isMobile && (item.unread === EmailUnreadEnum.UNREAD && showUnread) "/>
<slot name="name" :email="item"> {{ item.name }}</slot> <slot name="name" :email="item"> {{ item.name }}</slot>
</span> </span>
<span> <span>
@@ -85,8 +88,9 @@
</div> </div>
<div> <div>
<div class="email-text"> <div class="email-text">
<span class="email-subject"> <span class="email-subject" :style="(item.unread === EmailUnreadEnum.UNREAD && showUnread) ? 'font-weight: bold' : ''">
<slot name="subject" :email="item"> <div class="unread" v-if="!isMobile && (item.unread === EmailUnreadEnum.UNREAD && showUnread) "/>
<slot name="subject" :email="item" >
{{ item.subject || '\u200B' }} {{ item.subject || '\u200B' }}
</slot> </slot>
</span> </span>
@@ -109,7 +113,7 @@
</div> </div>
</div> </div>
<div class="email-right" :style="showUserInfo ? 'align-self: start;':''"> <div class="email-right" :style="showUserInfo ? 'align-self: start;':''">
<span class="email-time">{{ fromNow(item.createTime) }}</span> <span class="email-time" :style="(item.unread === EmailUnreadEnum.UNREAD && showUnread) ? 'font-weight: bold' : ''">{{ fromNow(item.createTime) }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -170,10 +174,12 @@ import {useSettingStore} from "@/store/setting.js";
import {sleep} from "@/utils/time-utils.js" import {sleep} from "@/utils/time-utils.js"
import {fromNow} from "@/utils/day.js"; import {fromNow} from "@/utils/day.js";
import {useI18n} from "vue-i18n"; import {useI18n} from "vue-i18n";
import {EmailUnreadEnum} from "@/enums/email-enum.js";
const props = defineProps({ const props = defineProps({
getEmailList: Function, getEmailList: Function,
emailDelete: Function, emailDelete: Function,
emailRead: Function,
starAdd: Function, starAdd: Function,
starCancel: Function, starCancel: Function,
cancelSuccess: Function, cancelSuccess: Function,
@@ -217,6 +223,10 @@ const props = defineProps({
showFirstLoading: { showFirstLoading: {
type: Boolean, type: Boolean,
default: true default: true
},
showUnread: {
type: Boolean,
default: false
} }
}) })
@@ -238,7 +248,7 @@ let scrollTop = 0
const latestEmail = ref(null) const latestEmail = ref(null)
const scrollbarRef = ref(null) const scrollbarRef = ref(null)
let reqLock = false let reqLock = false
let isMobile = innerWidth < 1367 let isMobile = ref(innerWidth < 1367)
let skeletonRows = 0 let skeletonRows = 0
const queryParam = reactive({ const queryParam = reactive({
emailId: 0, emailId: 0,
@@ -266,6 +276,10 @@ onBeforeRouteLeave(() => {
scrollTop = scroll.value.scrollTop scrollTop = scroll.value.scrollTop
}) })
window.onresize = () => {
isMobile = innerWidth < 1367
}
watch( watch(
() => emailList.map(item => item.checked), () => emailList.map(item => item.checked),
() => { () => {
@@ -372,6 +386,18 @@ function changeAccountShow() {
uiStore.accountShow = !uiStore.accountShow; uiStore.accountShow = !uiStore.accountShow;
} }
const handleRead = () => {
const emailIds = getSelectedMailsIds();
props.emailRead(emailIds);
emailIds.forEach(emailId => {
const index = emailList.findIndex(email => email.emailId === emailId);
if (index > -1) {
emailList[index].unread = EmailUnreadEnum.READ;
emailList[index].checked = false;
}
})
}
const handleDelete = () => { const handleDelete = () => {
ElMessageBox.confirm(t('delEmailsConfirm'), { ElMessageBox.confirm(t('delEmailsConfirm'), {
confirmButtonText: t('confirm'), confirmButtonText: t('confirm'),
@@ -701,7 +727,6 @@ function loadData() {
} }
.email-sender { .email-sender {
font-weight: bold;;
color: var(--el-text-color-primary); color: var(--el-text-color-primary);
display: grid; display: grid;
grid-template-columns: auto 1fr auto; grid-template-columns: auto 1fr auto;
@@ -875,7 +900,7 @@ function loadData() {
.header-actions { .header-actions {
display: grid; display: grid;
grid-template-columns: auto 1fr auto; grid-template-columns: auto 1fr auto auto;
align-items: center; align-items: center;
gap: 15px; gap: 15px;
padding: 3px 15px; padding: 3px 15px;
@@ -886,7 +911,7 @@ function loadData() {
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
position: relative; position: relative;
column-gap: 18px; column-gap: 20px;
row-gap: 8px; row-gap: 8px;
padding-left: 2px; padding-left: 2px;
color: var(--el-text-color-primary);; color: var(--el-text-color-primary);;
@@ -925,6 +950,17 @@ function loadData() {
bottom: 1px; bottom: 1px;
} }
.unread {
height: 6px;
width: 6px;
background: var(--el-color-primary);
margin-bottom: 2px;
margin-right: 5px;
border-radius: 50%;
display: inline-block;
justify-content: center;
}
ul { ul {
list-style: none; list-style: none;
padding: 0; padding: 0;

View File

@@ -0,0 +1,4 @@
export const EmailUnreadEnum = {
UNREAD: 0,
READ: 1
}

View File

@@ -12,6 +12,10 @@ export function emailLatest(emailId, accountId) {
return http.get('/email/latest', {params: {emailId, accountId}, noMsg: true }) return http.get('/email/latest', {params: {emailId, accountId}, noMsg: true })
} }
export function emailRead(emailIds) {
return http.put('/email/read', {emailIds}, {noMsg: true})
}
export function emailSend(form,progress) { export function emailSend(form,progress) {
return http.post('/email/send', form,{ return http.post('/email/send', form,{
onUploadProgress: (e) => { onUploadProgress: (e) => {

View File

@@ -12,6 +12,7 @@ export const useEmailStore = defineStore('email', {
delType: null, delType: null,
showStar: true, showStar: true,
showReply: true, showReply: true,
showUnread: false
}, },
sendScroll: null, sendScroll: null,
}), }),

View File

@@ -74,10 +74,10 @@
</template> </template>
<script setup> <script setup>
import ShadowHtml from '@/components/shadow-html/index.vue' import ShadowHtml from '@/components/shadow-html/index.vue'
import {reactive, ref, watch} from "vue"; import {reactive, ref, watch, onMounted, onUnmounted} from "vue";
import {useRouter} from 'vue-router' import {useRouter} from 'vue-router'
import {ElMessage, ElMessageBox} from 'element-plus' import {ElMessage, ElMessageBox} from 'element-plus'
import {emailDelete} from "@/request/email.js"; import {emailDelete, emailRead} from "@/request/email.js";
import {Icon} from "@iconify/vue"; import {Icon} from "@iconify/vue";
import {useEmailStore} from "@/store/email.js"; import {useEmailStore} from "@/store/email.js";
import {useAccountStore} from "@/store/account.js"; import {useAccountStore} from "@/store/account.js";
@@ -90,6 +90,7 @@ import {useSettingStore} from "@/store/setting.js";
import {allEmailDelete} from "@/request/all-email.js"; import {allEmailDelete} from "@/request/all-email.js";
import {useUiStore} from "@/store/ui.js"; import {useUiStore} from "@/store/ui.js";
import {useI18n} from "vue-i18n"; import {useI18n} from "vue-i18n";
import {EmailUnreadEnum} from "@/enums/email-enum.js";
const uiStore = useUiStore(); const uiStore = useUiStore();
const settingStore = useSettingStore(); const settingStore = useSettingStore();
@@ -105,6 +106,17 @@ watch(() => accountStore.currentAccountId, () => {
handleBack() handleBack()
}) })
onMounted(() => {
if (emailStore.contentData.showUnread && email.unread === EmailUnreadEnum.UNREAD) {
email.unread = EmailUnreadEnum.READ;
emailRead([email.emailId]);
}
})
onUnmounted(() => {
emailStore.contentData.showUnread = false;
})
function openReply() { function openReply() {
uiStore.writerRef.openReply(email) uiStore.writerRef.openReply(email)
} }

View File

@@ -7,6 +7,8 @@
:star-add="starAdd" :star-add="starAdd"
:star-cancel="starCancel" :star-cancel="starCancel"
:time-sort="params.timeSort" :time-sort="params.timeSort"
:email-read="emailRead"
:show-unread="true"
actionLeft="4px" actionLeft="4px"
@jump="jumpContent" @jump="jumpContent"
> >
@@ -25,7 +27,7 @@ import {useAccountStore} from "@/store/account.js";
import {useEmailStore} from "@/store/email.js"; import {useEmailStore} from "@/store/email.js";
import {useSettingStore} from "@/store/setting.js"; import {useSettingStore} from "@/store/setting.js";
import emailScroll from "@/components/email-scroll/index.vue" import emailScroll from "@/components/email-scroll/index.vue"
import {emailList, emailDelete, emailLatest} from "@/request/email.js"; import {emailList, emailDelete, emailLatest, emailRead} from "@/request/email.js";
import {starAdd, starCancel} from "@/request/star.js"; import {starAdd, starCancel} from "@/request/star.js";
import {defineOptions, onMounted, reactive, ref, watch} from "vue"; import {defineOptions, onMounted, reactive, ref, watch} from "vue";
import {sleep} from "@/utils/time-utils.js"; import {sleep} from "@/utils/time-utils.js";
@@ -62,6 +64,7 @@ function changeTimeSort() {
function jumpContent(email) { function jumpContent(email) {
emailStore.contentData.email = email emailStore.contentData.email = email
emailStore.contentData.delType = 'logic' emailStore.contentData.delType = 'logic'
emailStore.contentData.showUnread = true
emailStore.contentData.showStar = true emailStore.contentData.showStar = true
emailStore.contentData.showReply = true emailStore.contentData.showReply = true
router.push('/message') router.push('/message')

View File

@@ -29,3 +29,8 @@ app.post('/email/send', async (c) => {
return c.json(result.ok(email)); return c.json(result.ok(email));
}); });
app.put('/email/read', async (c) => {
await emailService.read(c, await c.req.json(), userContext.getUserId(c));
return c.json(result.ok());
})

View File

@@ -43,6 +43,10 @@ export const emailConst = {
SAVING: 6, SAVING: 6,
NOONE: 7, NOONE: 7,
FAILED: 8 FAILED: 8
},
unread: {
UNREAD: 0,
READ: 1
} }
} }

View File

@@ -21,6 +21,7 @@ export const email = sqliteTable('email', {
status: integer('status').default(0).notNull(), status: integer('status').default(0).notNull(),
resendEmailId: text('resend_email_id'), resendEmailId: text('resend_email_id'),
message: text('message'), message: text('message'),
unread: integer('unread').default(0).notNull(),
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(), createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(),
isDel: integer('is_del').default(0).notNull() isDel: integer('is_del').default(0).notNull()
}); });

View File

@@ -30,11 +30,22 @@ const init = {
}, },
async v2_5DB(c) { async v2_5DB(c) {
try { try {
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN email_prefix_filter text NOT NULL DEFAULT '';`).run(); await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN email_prefix_filter text NOT NULL DEFAULT '';`).run();
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
try {
await c.env.db.batch([
c.env.db.prepare(`ALTER TABLE email ADD COLUMN unread INTEGER NOT NULL DEFAULT 0;`).run(),
c.env.db.prepare(`UPDATE email SET unread = 1;`).run()
]);
} catch (e) {
console.error(e)
}
}, },
async v2_4DB(c) { async v2_4DB(c) {

View File

@@ -671,6 +671,11 @@ const emailService = {
async physicsDeleteByAccountId(c, accountId) { async physicsDeleteByAccountId(c, accountId) {
await attService.removeByAccountId(c, accountId); await attService.removeByAccountId(c, accountId);
await orm(c).delete(email).where(eq(email.accountId, accountId)).run(); await orm(c).delete(email).where(eq(email.accountId, accountId)).run();
},
async read(c, params, userId) {
const { emailIds } = params;
await orm(c).update(email).set({ unread: emailConst.unread.READ }).where(and(eq(email.userId, userId), inArray(email.emailId, emailIds)));
} }
}; };