feat: 增加全宽列表视图功能 (#1079)

* feat(mailbox): add list view mode

- Add a toggleable list view for the mailbox, with a settings option and back button.
- deselect mail on second click in list view
- set current mail on row click in multi-action mode

* feat(mailbox): add configurable body preview line clamp

Allow users to set the number of preview lines (0–5) for mail body in the list view via a slider in Appearance settings. Includes i18n support for the new option and its "Off" state.

* chore: clarify some i18n message in settings

include the following changes:
- The original "Mailbox Split Size" to "Left list width in two-column mailbox view"
- The description of new feature "Full-width mailbox list view"
sync all languages with the updated message

* docs: update changelog with recent UI improvements

- Added mailbox full-width list view and body preview lines settings
- Extended left panel width ratio range to 0
- Included English changelog translations

* docs: fix CHANGELOG improvements types

* fix: enable mail list preview line clamp settings on mobile
This commit is contained in:
YewFence
2026-07-08 14:21:10 +08:00
committed by GitHub
parent 3f1d800e90
commit dbd1f8706d
11 changed files with 247 additions and 23 deletions
+153 -13
View File
@@ -60,7 +60,7 @@ const props = defineProps({
const localFilterKeyword = ref('')
const {
isDark, mailboxSplitSize, indexTab, loading, useUTCDate,
isDark, mailboxSplitSize, mailListView, mailListPreviewLineClamp, indexTab, loading, useUTCDate,
autoRefresh, configAutoRefreshInterval, sendMailModel
} = useGlobalState()
const autoRefreshInterval = ref(configAutoRefreshInterval.value)
@@ -71,6 +71,12 @@ const count = ref(0)
const page = ref(1)
const pageSize = ref(20)
const mailListPreviewLineClampValue = computed(() => {
const value = Number(mailListPreviewLineClamp.value)
if (!Number.isFinite(value)) return 0
return Math.min(5, Math.max(0, Math.round(value)))
})
// Computed property for filtered data (only filter current page)
const data = computed(() => {
if (!localFilterKeyword.value || localFilterKeyword.value.trim() === '') {
@@ -183,7 +189,7 @@ const refresh = async () => {
count.value = totalCount;
}
curMail.value = null;
if (!isMobile.value && data.value.length > 0) {
if (!isMobile.value && !mailListView.value && data.value.length > 0) {
curMail.value = data.value[0];
}
} catch (error) {
@@ -202,6 +208,11 @@ const backFirstPageAndRefresh = async () => {
const clickRow = async (row) => {
if (multiActionMode.value) {
row.checked = !row.checked;
curMail.value = row;
return;
}
if (mailListView.value && curMail.value?.id === row.id) {
curMail.value = null;
return;
}
curMail.value = row;
@@ -375,8 +386,13 @@ onBeforeUnmount(() => {
clearable />
</n-space>
</div>
<n-split class="left" direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize"
:on-update:size="onSpiltSizeChange">
<n-split class="left" direction="horizontal" :max="0.75" :min="0" :resize-trigger-size="8"
:default-size="mailboxSplitSize" :on-update:size="onSpiltSizeChange" v-if="!mailListView || curMail">
<template #resize-trigger>
<div class="split-handle">
<div class="split-handle__grip" />
</div>
</template>
<template #1>
<div style="overflow: auto; min-height: 60vh; max-height: 100vh;">
<n-list hoverable clickable>
@@ -412,15 +428,25 @@ onBeforeUnmount(() => {
</template>
<template #2>
<div v-if="curMail" style="margin: 8px;">
<n-flex justify="space-between">
<n-button @click="prevMail" :disabled="!canGoPrevMail" text size="small">
<template #icon>
<n-icon>
<ArrowBackIosNewFilled />
</n-icon>
</template>
{{ t('prevMail') }}
</n-button>
<n-flex justify="space-between" align="center">
<n-space :wrap="false" align="center">
<n-button v-if="mailListView" @click="curMail = null" text size="small">
<template #icon>
<n-icon>
<ArrowBackIosNewFilled />
</n-icon>
</template>
{{ t('backToList') }}
</n-button>
<n-button @click="prevMail" :disabled="!canGoPrevMail" text size="small">
<template #icon>
<n-icon>
<ArrowBackIosNewFilled />
</n-icon>
</template>
{{ t('prevMail') }}
</n-button>
</n-space>
<n-button @click="nextMail" :disabled="!canGoNextMail" text size="small" icon-placement="right">
<template #icon>
<n-icon>
@@ -446,6 +472,48 @@ onBeforeUnmount(() => {
</n-card>
</template>
</n-split>
<div v-else class="mail-list-scroll">
<n-list hoverable clickable>
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
:class="mailItemClass(row)">
<template #prefix v-if="multiActionMode">
<n-checkbox v-model:checked="row.checked" />
</template>
<n-thing class="mail-list-thing">
<template #header>
<n-ellipsis class="mail-list-title">
{{ row.subject }}
</n-ellipsis>
</template>
<template #description>
<div class="mail-list-meta">
<n-tag type="info">
ID: {{ row.id }}
</n-tag>
<n-tag type="info">
{{ utcToLocalDate(row.created_at, useUTCDate) }}
</n-tag>
<n-tag type="info">
<n-ellipsis class="mail-list-meta-text">
{{ showEMailTo ? "FROM: " + row.source : row.source }}
</n-ellipsis>
</n-tag>
<n-tag v-if="showEMailTo" type="info">
<n-ellipsis class="mail-list-meta-text">
TO: {{ row.address }}
</n-ellipsis>
</n-tag>
<AiExtractInfo :metadata="row.metadata" compact />
</div>
</template>
<n-ellipsis v-if="row.text && mailListPreviewLineClampValue > 0"
:line-clamp="mailListPreviewLineClampValue" class="mail-list-preview" :tooltip="false">
{{ row.text }}
</n-ellipsis>
</n-thing>
</n-list-item>
</n-list>
</div>
</div>
<div class="left" v-else>
<n-space justify="space-around" align="center" :wrap="false" style="display: flex; align-items: center;">
@@ -555,8 +623,80 @@ onBeforeUnmount(() => {
height: 100%;
}
.mail-list-scroll {
overflow-y: auto;
overflow-x: hidden;
min-height: 60vh;
max-height: 100vh;
}
.mail-list-thing,
.mail-list-title,
.mail-list-preview {
min-width: 0;
max-width: 100%;
}
.mail-list-thing,
.mail-list-preview {
width: 100%;
}
.mail-list-thing :deep(.n-thing-main),
.mail-list-thing :deep(.n-thing-header),
.mail-list-thing :deep(.n-thing-header__title),
.mail-list-thing :deep(.n-thing-main__description),
.mail-list-thing :deep(.n-thing-main__content) {
min-width: 0;
}
.mail-list-meta {
display: flex;
flex-wrap: wrap;
gap: 4px;
min-width: 0;
max-width: 100%;
}
.mail-list-meta :deep(.n-tag) {
max-width: 100%;
}
.mail-list-meta-text {
max-width: min(240px, 100%);
}
.mail-list-preview {
display: -webkit-box;
overflow-wrap: anywhere;
opacity: 0.7;
}
.mail-list-scroll :deep(.n-list-item__main) {
min-width: 0;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
.split-handle {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.split-handle__grip {
width: 4px;
height: 32px;
border-radius: 2px;
background-color: var(--n-resize-trigger-color);
transition: background-color 0.2s;
}
.split-handle:hover .split-handle__grip {
background-color: var(--n-resize-trigger-color-hover);
}
</style>
+26 -2
View File
@@ -210,8 +210,13 @@ onMounted(async () => {
</n-button>
</n-space>
</div>
<n-split direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize"
:on-update:size="onSpiltSizeChange">
<n-split direction="horizontal" :max="0.75" :min="0" :resize-trigger-size="8"
:default-size="mailboxSplitSize" :on-update:size="onSpiltSizeChange">
<template #resize-trigger>
<div class="split-handle">
<div class="split-handle__grip" />
</div>
</template>
<template #1>
<div style="overflow: auto; min-height: 60vh; max-height: 100vh;">
<n-list hoverable clickable>
@@ -379,4 +384,23 @@ pre {
white-space: pre-wrap;
word-wrap: break-word;
}
.split-handle {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.split-handle__grip {
width: 4px;
height: 32px;
border-radius: 2px;
background-color: var(--n-resize-trigger-color);
transition: background-color 0.2s;
}
.split-handle:hover .split-handle__grip {
background-color: var(--n-resize-trigger-color-hover);
}
</style>