feat(dashboard): 完善个人视角空状态

This commit is contained in:
kuaifan 2026-07-18 04:47:12 +00:00
parent 6666605f7d
commit 4b92ecb422
5 changed files with 315 additions and 42 deletions

View File

@ -2589,3 +2589,13 @@ AI任务分析
我的部门
待开始
暂无待开始任务
太棒了,任务全部清空
本周完成了 (*) 项任务
欢迎使用 DooTask
项目是任务与协作的起点,创建一个开始
创建第一个项目
已有团队?请同事把你加入项目即可
近期完成
本周 (*) 项
当前没有待处理任务
可以新建一项任务,或等待新的工作安排

View File

@ -89,8 +89,37 @@
</ul>
<div class="dashboard-card my-table">
<div v-if="listColumns.length === 0" class="table-all-empty">
<span class="empty-icon"><Icon type="md-checkmark"/></span>
<span>{{$L('太棒了,当前没有需要处理的任务')}}</span>
<div v-if="loadDashboardTasks !== false && cacheTasks.length === 0" class="empty-state-loading"><Loading/></div>
<div v-else class="empty-state-content">
<span class="empty-icon" :class="`empty-icon-${emptyState}`">
<Icon :type="emptyState === 'new' ? 'md-hand' : 'md-checkmark'"/>
</span>
<strong class="empty-state-title">{{emptyStateTitle}}</strong>
<span class="empty-state-summary">{{emptyStateSummary}}</span>
<div v-if="emptyState === 'new'" class="empty-state-actions">
<Button type="primary" icon="md-add" @click="createProject">{{$L('创建第一个项目')}}</Button>
</div>
<div v-else-if="emptyState === 'idle'" class="empty-state-actions">
<Button icon="md-add" @click="createTask">{{$L('新建任务')}}</Button>
</div>
<span v-if="emptyState === 'new'" class="empty-state-hint">{{$L('已有团队请同事把你加入项目即可')}}</span>
<div v-if="emptyState === 'completed'" class="recent-completed-list">
<div class="recent-completed-head">
<span>{{$L('近期完成')}}</span>
<em>{{$L('本周 (*) 项', weeklyCompletedCount)}}</em>
</div>
<div
v-for="task in recentCompleted"
:key="`recent-${task.id}`"
class="recent-completed-row"
@click="openTask(task)">
<Icon class="recent-completed-check" type="md-checkmark-circle"/>
<span class="recent-completed-name">{{task.name}}</span>
<span class="recent-completed-project">{{projectName(task)}}</span>
<span class="recent-completed-time">{{completedText(task.complete_at)}}</span>
</div>
</div>
</div>
</div>
<div v-else class="table-row table-thead">
<span></span>
@ -118,7 +147,7 @@
<span>{{listEmptyText(group.type)}}</span>
</div>
<div
v-for="item in groupList(group, groupLimit)"
v-for="item in groupList(group, listLimit(group))"
:key="`${group.type}-${item.id}`"
class="table-row"
:class="{complete: item.complete_at}"
@ -163,10 +192,10 @@
</ETooltip>
</div>
<div
v-if="group.list.length > groupLimit && !expandedGroups.includes(group.type)"
v-if="group.list.length > listLimit(group) && !expandedGroups.includes(group.type)"
:key="`more-${group.type}`"
class="card-more"
@click="expandedGroups.push(group.type)">{{$L('还有 (*) 项', group.list.length - groupLimit)}} </div>
@click="expandedGroups.push(group.type)">{{$L('还有 (*) 项', group.list.length - listLimit(group))}} </div>
</template>
</template>
</div>
@ -259,13 +288,11 @@ export default {
// /{type: 'open'|'close'}
collapsedOverrides: prefsCache.overrides,
strategySnapshot: null,
//
visibleSnapshot: null,
snapshotPending: false,
teamFocusPref: prefsCache.focus,
expandedGroups: [],
groupLimit: 10,
completedLimit: 5,
quadLimit: 5,
flashType: '',
flashTimer: null,
@ -297,12 +324,9 @@ export default {
},
activated() {
this.$store.dispatch("getTaskForDashboard", 600);
this.loadInterval(true);
this.loadLicense(true);
//
this.takeSnapshot();
this.snapshotPending = true;
},
deactivated() {
@ -390,12 +414,77 @@ export default {
return list;
},
//
listColumns({listAllColumns, visibleSnapshot}) {
if (visibleSnapshot === null) {
return listAllColumns.filter(group => group.count > 0);
// 线
currentListColumns({listAllColumns}) {
return listAllColumns.filter(group => group.list.length > 0);
},
//
weeklyCompleted({cacheTasks, taskCompleteTemps, nowTime}) {
const current = $A.daytz(nowTime);
const start = current.clone().startOf('day').subtract((current.day() + 6) % 7, 'day');
const next = start.clone().add(7, 'day');
return cacheTasks.filter(task => {
if (task.archived_at || !task.complete_at || task.owner != 1 || taskCompleteTemps.includes(task.id)) {
return false;
}
const complete = $A.dayjs(task.complete_at);
return complete.valueOf() >= start.valueOf() && complete.valueOf() < next.valueOf();
}).sort((a, b) => $A.sortDay(b.complete_at, a.complete_at));
},
weeklyCompletedCount({weeklyCompleted}) {
return weeklyCompleted.length;
},
recentCompleted({weeklyCompleted, completedLimit}) {
return weeklyCompleted.slice(0, completedLimit);
},
completedColumn({weeklyCompleted, weeklyCompletedCount}) {
return {
type: 'completed',
title: this.getTitle('completed'),
hidden: this.isCollapsed('completed'),
count: weeklyCompletedCount,
list: weeklyCompleted,
};
},
//
listColumns({currentListColumns, completedColumn}) {
if (currentListColumns.length === 0) {
return [];
}
return listAllColumns.filter(group => visibleSnapshot.includes(group.type));
return completedColumn.count > 0 ? [...currentListColumns, completedColumn] : currentListColumns;
},
emptyState({weeklyCompletedCount, cacheProjects, cacheTasks}) {
if (weeklyCompletedCount > 0) {
return 'completed';
}
const hasVisibleTask = cacheTasks.some(task => !task.archived_at);
return cacheProjects.length === 0 && !hasVisibleTask ? 'new' : 'idle';
},
emptyStateTitle({emptyState}) {
if (emptyState === 'completed') {
return this.$L('太棒了,任务全部清空');
}
if (emptyState === 'new') {
return this.$L('欢迎使用 DooTask');
}
return this.$L('当前没有待处理任务');
},
emptyStateSummary({emptyState, weeklyCompletedCount}) {
if (emptyState === 'completed') {
return this.$L('本周完成了 (*) 项任务', weeklyCompletedCount);
}
if (emptyState === 'new') {
return this.$L('项目是任务与协作的起点,创建一个开始');
}
return this.$L('可以新建一项任务,或等待新的工作安排');
},
overdueMaxDays({dashboardTask, nowTime}) {
@ -494,16 +583,6 @@ export default {
}
this.loadInterval(active)
this.loadLicense(active)
if (active) {
this.$store.dispatch("getTaskForDashboard", 600)
}
},
loadDashboardTasks(loading) {
if (!loading && this.snapshotPending) {
this.snapshotPending = false
this.takeSnapshot()
}
}
},
@ -520,6 +599,8 @@ export default {
return this.$L('待开始');
case 'assist':
return this.$L('我协助的');
case 'completed':
return this.$L('本周完成');
default:
return '';
}
@ -550,7 +631,6 @@ export default {
/**
* 策略快照行动优先
* - 空组不显示全部为空时列表整卡显示缺省态
* - 显示的组默认展开待完成量大且行动优先级低超期或今日到期有数据时默认收起
*/
takeSnapshot() {
@ -561,7 +641,6 @@ export default {
upcoming: this.upcomingTask.count,
assist: this.assistTask.length,
};
this.visibleSnapshot = ['overdue', 'today', 'todo', 'upcoming', 'assist'].filter(type => counts[type] > 0);
const collapsed = [];
if (counts.todo > 0 && (counts.overdue > 0 || counts.today > 0)) {
collapsed.push('todo');
@ -596,6 +675,10 @@ export default {
return group.list.slice(0, limit);
},
listLimit(group) {
return group.type === 'completed' ? this.completedLimit : this.groupLimit;
},
listEmptyText(type) {
switch (type) {
case 'overdue':
@ -633,10 +716,6 @@ export default {
if (!column || column.count === 0) {
return;
}
//
if (this.visibleSnapshot !== null && !this.visibleSnapshot.includes(type)) {
this.visibleSnapshot = [...this.visibleSnapshot, type];
}
if (this.isCollapsed(type)) {
this.toggleGroup(type);
}
@ -672,6 +751,29 @@ export default {
this.$store.dispatch("openTask", task)
},
createTask() {
this.$emit('on-click', 'addTask');
},
createProject() {
this.$emit('on-click', 'addProject');
},
completedText(completeAt) {
if (!completeAt) {
return '';
}
const date = $A.dayjs(completeAt);
const now = $A.daytz(this.nowTime);
if (date.format('YYYY-MM-DD') === now.format('YYYY-MM-DD')) {
return `${this.$L('今天')} ${date.format('HH:mm')}`;
}
if (date.format('YYYY-MM-DD') === now.clone().subtract(1, 'day').format('YYYY-MM-DD')) {
return `${this.$L('昨天')} ${date.format('HH:mm')}`;
}
return `${this.$L('(*)月(*)日', date.month() + 1, date.date())} ${date.format('HH:mm')}`;
},
openMenu(event, task) {
this.$store.state.taskOperation = {event, task}
},

View File

@ -1113,6 +1113,8 @@ export default {
*/
handleClearCache({state, dispatch}, userData) {
return new Promise(async resolve => {
state.loadDashboardTasks = null;
// localStorage
const keys = ['themeConf', 'languageName', 'keyboardConf'];
const savedData = keys.reduce((acc, key) => ({

View File

@ -75,7 +75,8 @@ export default {
// 加载状态
loads: [],
loadDashboardTasks: false,
// null: 尚未初始化true: 加载中false: 已完成首次加载
loadDashboardTasks: null,
loadUserBasic: false,
loadProjects: 0,
loadDialogs: 0,

View File

@ -656,6 +656,18 @@
color: #8ba3c9;
}
}
&.group-completed {
background: rgba(231, 245, 236, 0.72);
.group-dot {
background: #1e9e55;
}
.group-title {
color: #287a4b;
}
.group-chevron {
color: #8db89c;
}
}
&.group-flash {
box-shadow: inset 0 0 0 2px #1e9e55;
@ -668,21 +680,134 @@
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 60px 20px;
padding: 48px 30px 42px;
color: #a2a89f;
font-size: 13px;
.empty-state-loading {
min-height: 260px;
display: flex;
align-items: center;
justify-content: center;
}
.empty-state-content {
width: min(100%, 720px);
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.empty-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
width: 48px;
height: 48px;
border-radius: 50%;
background: #e7f5ec;
color: #1e9e55;
font-size: 20px;
font-size: 23px;
&.empty-icon-new {
background: #fff4e5;
color: #c47316;
}
}
.empty-state-title {
margin-top: 16px;
color: #22261f;
font-size: 18px;
font-weight: 600;
line-height: 1.4;
}
.empty-state-summary {
margin-top: 8px;
color: #7a8078;
font-size: 13px;
line-height: 1.6;
}
.empty-state-actions {
margin-top: 19px;
.ivu-btn {
min-width: 112px;
}
}
.empty-state-hint {
margin-top: 14px;
color: #a2a89f;
font-size: 12px;
}
.recent-completed-list {
width: min(100%, 680px);
margin-top: 30px;
border-top: 1px solid #ecefe9;
text-align: left;
}
.recent-completed-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 0 8px;
color: #6f776e;
font-size: 12px;
> em {
color: #a2a89f;
font-size: 11px;
font-style: normal;
}
}
.recent-completed-row {
display: grid;
grid-template-columns: 22px minmax(0, 1fr) 130px 82px;
align-items: center;
gap: 10px;
padding: 10px 0;
border-top: 1px solid #f0f2ee;
color: #4f584f;
cursor: pointer;
text-align: left;
&:hover {
color: #22261f;
}
}
.recent-completed-check {
color: $primary-color;
font-size: 18px;
}
.recent-completed-name,
.recent-completed-project,
.recent-completed-time {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.recent-completed-name {
font-size: 13px;
}
.recent-completed-project,
.recent-completed-time {
color: #a2a89f;
font-size: 11.5px;
}
.recent-completed-time {
text-align: right;
}
}
@ -1213,12 +1338,21 @@
}
.f-priority {
display: block;
display: flex;
align-items: center;
gap: 5px;
color: #7a8078;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
> i {
flex-shrink: 0;
}
> span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.f-status {
@ -1280,6 +1414,10 @@
display: none;
}
}
.recent-completed-list .recent-completed-row {
grid-template-columns: 22px minmax(0, 1fr) 100px 78px;
}
}
.dashboard-team {
@ -1312,6 +1450,14 @@
display: inline-block;
}
}
.recent-completed-list .recent-completed-row {
grid-template-columns: 22px minmax(0, 1fr) 78px;
.recent-completed-project {
display: none;
}
}
}
.dashboard-team {
@ -1438,6 +1584,18 @@ body.window-portrait {
}
}
.table-all-empty {
padding: 38px 14px 30px;
.empty-state-title {
font-size: 16px;
}
.recent-completed-list {
margin-top: 24px;
}
}
.table-group {
padding: 10px 14px;
}