feat(dashboard): 合并仪表盘视图切换为「列表|四象限|部门总览」

- 右上角两组切换合并为一个,部门视角选中后该项显示部门范围与成员数,再次点击选择团队范围
- 全选多个部门时显示「部门总览」,切换范围加载中成员数以 loading 占位
- 切换器宽度随内容平滑过渡,尊重减少动态效果设置;部门项与成员数补充悬停提示
- 移动端收起为图标按钮:部门负责人点击弹出列表/四象限/部门总览下拉,底部可选择团队范围并显示成员人数;普通用户点击在列表与四象限间切换
- 移动端部门视角图标带成员数角标(超过 99 显示 99+),头部右侧只占图标宽度
- 清理不再使用的文案与样式,同步 ai-kb 仪表盘相关说明
This commit is contained in:
kuaifan 2026-10-01 00:46:31 +00:00
parent 4115291bff
commit c06d61a11f
20 changed files with 340 additions and 139 deletions

View File

@ -2489,7 +2489,6 @@ AI任务分析
你撤回了一条消息
重新编辑
四象限
个人视角
部门任务总览
(*) 人
最久 (*) 天
@ -2537,7 +2536,6 @@ AI任务分析
没有超期任务,保持住
今天只有这一项,处理完就轻松了
数量较多,仅显示最近 (*) 项,展开其余 (*) 项
我的部门
待开始
暂无待开始任务
太棒了,任务全部清空
@ -2783,3 +2781,6 @@ WebDAV 路径冲突
请填写指派留言
任务负责人最多不能超过10个
展开显示更多
部门总览
全部部门
团队成员 (*) 人

View File

@ -37475,18 +37475,6 @@
"id": "Empat kuadran",
"ru": "Четыре квадранта"
},
{
"key": "个人视角",
"zh": "",
"zh-CHT": "個人視角",
"en": "Personal view",
"ko": "개인 보기",
"ja": "個人ビュー",
"de": "Persönliche Ansicht",
"fr": "Vue personnelle",
"id": "Tampilan pribadi",
"ru": "Личный вид"
},
{
"key": "部门任务总览",
"zh": "",
@ -37979,18 +37967,6 @@
"id": "Item banyak; menampilkan (%T1) terbaru, buka (%T2) sisanya",
"ru": "Элементов много: показаны последние (%T1), разверните остальные (%T2)"
},
{
"key": "我的部门",
"zh": "",
"zh-CHT": "我的部門",
"en": "My department",
"ko": "내 부서",
"ja": "自分の部署",
"de": "Meine Abteilung",
"fr": "Mon service",
"id": "Departemen saya",
"ru": "Мой отдел"
},
{
"key": "待开始",
"zh": "",
@ -40162,5 +40138,41 @@
"fr": "Afficher plus",
"id": "Tampilkan lebih banyak",
"ru": "Показать больше"
},
{
"key": "部门总览",
"zh": "",
"zh-CHT": "部門總覽",
"en": "Department overview",
"ko": "부서 개요",
"ja": "部門概要",
"de": "Abteilungsübersicht",
"fr": "Vue du service",
"id": "Ikhtisar departemen",
"ru": "Обзор отдела"
},
{
"key": "全部部门",
"zh": "",
"zh-CHT": "全部部門",
"en": "All departments",
"ko": "모든 부서",
"ja": "すべての部門",
"de": "Alle Abteilungen",
"fr": "Tous les services",
"id": "Semua departemen",
"ru": "Все отделы"
},
{
"key": "团队成员 (%T1) 人",
"zh": "",
"zh-CHT": "團隊成員 (%T1) 人",
"en": "(%T1) team members",
"ko": "팀원 (%T1)명",
"ja": "チームメンバー (%T1)人",
"de": "(%T1) Teammitglieder",
"fr": "(%T1) membres de l’équipe",
"id": "(%T1) anggota tim",
"ru": "Участников команды: (%T1)"
}
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -19,13 +19,13 @@ negative:
- 普通成员只有个人视角,不能查看部门负责人视角
- 部门负责人视角是只读任务总览,不会授予任务修改权限
- 仪表盘不支持用户自定义卡片或调整统计口径
last_verified: v1.8.69
last_verified: v1.9.36
---
# 仪表盘个人视角与部门负责人视角
## 定义
DooTask 仪表盘是登录后的任务工作台,入口为左侧栏「仪表盘」。所有用户都有个人视角;系统默认开启「部门负责人视角」(管理员可关闭),且当前用户负责或管理至少一个部门时,右上角还会出现「个人视角 / 部门负责人」切换。
DooTask 仪表盘是登录后的任务工作台,入口为左侧栏「仪表盘」。所有用户都有个人视角;系统默认开启「部门负责人视角」(管理员可关闭),且当前用户负责或管理至少一个部门时,右上角「列表 / 四象限」切换器末尾还会出现「部门总览」,点击进入部门负责人视角,再次点击该项可选择团队范围;再点「列表」或「四象限」即回到个人视角。
## 个人视角
- 顶部卡片:已超期、今日到期、待完成、我协助的;待完成卡片还会提示待开始数量。

View File

@ -18,13 +18,13 @@ negative:
- 布局切换只适用于个人视角
- 四象限不显示待开始和本周完成分组
- 用户不能自定义象限名称或顺序
last_verified: v1.9.18
last_verified: v1.9.36
---
# 切换仪表盘列表和四象限布局
## 入口
进入仪表盘个人视角,在右上角选择「列表」或「四象限」。所选布局保存在当前浏览器,下次进入时继续使用。
在仪表盘右上角切换器中选择「列表」或「四象限」。当前处于部门负责人视角(切换器选中部门项)时,点击「列表」或「四象限」会同时切回个人视角。移动端右上角为一个布局图标:普通成员点击即在列表和四象限之间来回切换;部门负责人点击后在下拉菜单中选择。所选布局保存在当前浏览器,下次进入时继续使用。
## 列表布局
- 顶部显示已超期、今日到期、待完成、我协助的四张摘要卡。

View File

@ -19,13 +19,13 @@ negative:
- 至少选择一个部门后才能确定
- 部门范围与项目负责人视角开关是两个独立状态
- 范围选择保存在当前浏览器,不跨设备同步
last_verified: v1.8.69
last_verified: v1.9.36
---
# 选择部门负责人仪表盘的团队范围
## 仪表盘入口
切换到「部门负责人」视角,点击右上角当前部门名称,打开「选择团队范围」。弹窗支持清空、全选、反选和逐项勾选;编辑时可以暂时清空,但没有选择任何部门时「确定」按钮不可用。
在仪表盘右上角切换器中选择「部门总览」进入部门负责人视角,该切换项保持选中:管理多个部门且选中全部时仍显示「部门总览」,只管理一个部门或只选部分部门时显示部门名称(多个部门显示为「研发部 +2」),后面附团队成员数。再次点击它打开「选择团队范围」。移动端点击右上角图标,在下拉菜单底部点击当前团队范围(全选多个部门时显示「全部部门」)打开同一弹窗。弹窗支持清空、全选、反选和逐项勾选;编辑时可以暂时清空,但没有选择任何部门时「确定」按钮不可用。
## 范围规则
- 首次默认选择全部可管理部门。

View File

@ -22,7 +22,7 @@ negative:
- 项目可单独关闭负责人视角可见性,关闭后不会被纳入
- 没有选择任何部门时不能保存范围
- 部门范围和项目列表开关保存在当前浏览器,不跨设备同步
last_verified: v1.9.18
last_verified: v1.9.36
---
# 设置项目负责人视角和共用部门范围
@ -30,7 +30,7 @@ last_verified: v1.9.18
## 入口
- 右上角头像菜单 →「负责人视角」。
- 项目列表搜索栏旁的负责人视角按钮。
- 仪表盘「部门负责人」视角 → 右上角部门名称;该入口只选择团队范围。
- 仪表盘右上角切换器选择「部门总览」→ 再次点击该切换项;该入口只选择团队范围。
## 项目负责人视角
从菜单或项目列表打开「项目负责人视角」弹窗:

View File

@ -16,10 +16,10 @@ related_tools: [list_tasks]
related_pages: [dashboard]
prerequisites: []
negative:
- 普通成员看不到「部门负责人」切换
- 普通成员看不到「部门总览」切换项
- 仪表盘没有独立的权限配置,登录用户都能使用个人视角
- 部门负责人视角不会让用户获得系统管理员权限
last_verified: v1.8.69
last_verified: v1.9.36
---
# 仪表盘入口在哪
@ -32,9 +32,10 @@ last_verified: v1.8.69
## 进入后看到什么
- 所有登录用户默认可使用个人视角,查看自己负责和协助的任务。
- 个人视角右上角可以切换「列表 / 四象限」。
- 符合部门负责人条件时,右上角增加「个人视角 / 部门负责人」切换。
- 部门负责人视角顶部的部门按钮可打开「选择团队范围」弹窗。
- 右上角切换器可选「列表 / 四象限」,两者都是个人视角的布局。
- 符合部门负责人条件时,切换器末尾增加「部门总览」,点击进入部门负责人视角(部门任务总览)。
- 进入部门负责人视角后,该切换项保持选中:管理多个部门且选中全部时仍显示「部门总览」,只管理一个部门或只选部分部门时显示部门名称(多个部门显示为「研发部 +2」),后面附团队成员数;再次点击可打开「选择团队范围」弹窗。
- 移动端右上角三段切换收起为一个图标按钮:部门负责人点击后在下拉菜单中选择「列表 / 四象限 / 部门总览」,部门视角下菜单底部显示当前团队范围及成员人数(全选多个部门时为「全部部门」),点击可打开「选择团队范围」;图标在部门视角下带成员数角标,超过 99 显示「99+」。普通成员点击图标直接在列表和四象限之间切换。
## 部门负责人视角出现条件
1. 系统设置 `department_owner_project_view` 已开启(新安装或未设置时默认开启,管理员可手动关闭)。

View File

@ -17,7 +17,7 @@ prerequisites: []
negative:
- 普通成员不能切换到部门负责人视角
- 部门负责人视角只提供团队任务只读总览
last_verified: v1.8.69
last_verified: v1.9.36
---
# 仪表盘导航入口
@ -34,4 +34,4 @@ last_verified: v1.8.69
- 部门负责人视角:团队指标、成员任务分配、优先级分布、重点关注任务。
## 权限
所有登录用户可访问个人视角。系统默认开启负责人视角(管理员可关闭),且用户管理至少一个部门时,才显示「部门负责人」切换。详细说明见 [[dashboard.entry.menu-map]]。
所有登录用户可访问个人视角。系统默认开启负责人视角(管理员可关闭),且用户管理至少一个部门时,右上角「列表 / 四象限」切换器末尾才会出现「部门总览」。详细说明见 [[dashboard.entry.menu-map]]。

View File

@ -56,19 +56,53 @@
<Icon type="ios-search"/>
<span>{{$L('搜索')}} ({{mateName}}+F)</span>
</div>
<div v-if="currentView === 'my'" class="header-seg">
<span :class="{'seg-active': layout === 'list'}" @click="setLayout('list')">{{$L('列表')}}</span>
<span :class="{'seg-active': layout === 'quad'}" @click="setLayout('quad')">{{$L('四象限')}}</span>
</div>
<div v-else class="header-dept" @click="deptViewShow = true">
<Icon type="md-people"/>
<span>{{deptLabel}}</span>
<em v-if="teamStats.member_count" class="dept-badge">{{teamStats.member_count}}</em>
</div>
<div v-if="teamEnabled" class="header-seg">
<span :class="{'seg-active': currentView === 'my'}" @click="setView('my')">{{$L('个人视角')}}</span>
<span :class="{'seg-active': currentView === 'team'}" @click="setView('team')">{{$L('部门负责人')}}</span>
<!--外层按内层实际宽度过渡,内容变化时宽度平滑伸缩-->
<div v-show="!windowPortrait" ref="headerSeg" class="header-seg">
<div ref="headerSegInner" class="seg-inner">
<span :class="{'seg-active': currentView === 'my' && layout === 'list'}" @click="setLayout('list')">{{$L('列表')}}</span>
<span :class="{'seg-active': currentView === 'my' && layout === 'quad'}" @click="setLayout('quad')">{{$L('四象限')}}</span>
<!--部门视角:未选中显示入口文案,选中后变为部门范围选择-->
<span v-if="teamEnabled && currentView === 'team'" class="seg-active seg-dept" :title="$L('选择团队范围')" @click="deptViewShow = true">
<em class="dept-name">{{deptLabel}}</em>
<em v-if="teamStats.member_count" class="dept-badge" :title="$L('团队成员 (*) 人', teamStats.member_count)">{{teamStats.member_count}}</em>
<div v-else-if="teamStatsLoading" class="dept-load"><Loading/></div>
</span>
<span v-else-if="teamEnabled" @click="setView('team')">{{$L('部门总览')}}</span>
</div>
</div>
<!--移动端:部门负责人用下拉切换,其余用户点击在列表/四象限间来回切换-->
<template v-if="windowPortrait">
<Dropdown v-if="teamEnabled" trigger="click" placement="bottom-end" transfer @on-click="onModeMenu">
<div class="header-mode">
<i class="taskfont" v-html="modeIcon"></i>
<template v-if="currentView === 'team'">
<em v-if="teamStats.member_count" class="mode-badge">{{teamStats.member_count > 99 ? '99+' : teamStats.member_count}}</em>
<div v-else-if="teamStatsLoading" class="mode-load"><Loading/></div>
</template>
</div>
<DropdownMenu slot="list">
<DropdownItem name="list" :selected="currentView === 'my' && layout === 'list'">
<div class="dashboard-mode-item"><i class="taskfont">&#xe894;</i>{{$L('列表')}}</div>
</DropdownItem>
<DropdownItem name="quad" :selected="currentView === 'my' && layout === 'quad'">
<div class="dashboard-mode-item"><i class="taskfont">&#xe746;</i>{{$L('四象限')}}</div>
</DropdownItem>
<DropdownItem name="team" :selected="currentView === 'team'">
<div class="dashboard-mode-item"><i class="taskfont">&#xe681;</i>{{$L('部门总览')}}</div>
</DropdownItem>
<DropdownItem v-if="currentView === 'team'" name="scope" divided>
<div class="dashboard-mode-item">
<span>{{deptScopeLabel}}</span>
<em v-if="teamStats.member_count" class="mode-count">{{$L('(*) 人', teamStats.member_count)}}</em>
<i class="taskfont mode-arrow">&#xe700;</i>
</div>
</DropdownItem>
</DropdownMenu>
</Dropdown>
<div v-else class="header-mode" @click="setLayout(layout === 'list' ? 'quad' : 'list')">
<i class="taskfont" v-html="modeIcon"></i>
</div>
</template>
</div>
</div>
<!--个人视角-->
@ -355,6 +389,16 @@ export default {
})
},
mounted() {
this.segObserver = new ResizeObserver(this.syncSegWidth);
this.segObserver.observe(this.$refs.headerSegInner);
},
beforeDestroy() {
this.segObserver?.disconnect();
this.segObserver = null;
},
activated() {
this.loadInterval(true);
this.loadLicense(true);
@ -577,12 +621,22 @@ export default {
return teamStats.generated_at ? $A.dayjs(teamStats.generated_at).format('HH:mm:ss') : '--'
},
deptLabel({userInfo, cacheDepartmentOwnerIds}) {
// 当前团队范围:未选择或选中全部可管理部门时 all 为 true
deptSelected({userInfo, cacheDepartmentOwnerIds}) {
const managed = (userInfo.managed_departments || []).map(item => ({...item, id: parseInt(item.id)}));
const ids = (cacheDepartmentOwnerIds || []).map(id => parseInt(id));
const selected = ids.length > 0 ? managed.filter(item => ids.includes(item.id)) : managed;
if (selected.length === 0 || selected.length === managed.length) {
return managed.length === 1 ? managed[0].name : this.$L('我的部门');
return {
managed,
selected,
all: selected.length === 0 || selected.length === managed.length,
};
},
deptLabel({deptSelected}) {
const {managed, selected, all} = deptSelected;
if (all) {
return managed.length === 1 ? managed[0].name : this.$L('部门总览');
}
if (selected.length === 1) {
return selected[0].name;
@ -590,6 +644,22 @@ export default {
return `${selected[0].name} +${selected.length - 1}`;
},
// 移动端下拉菜单里的部门范围:全选多个部门时显示「全部部门」,其余同 deptLabel
deptScopeLabel({deptSelected, deptLabel}) {
if (deptSelected.all && deptSelected.managed.length > 1) {
return this.$L('全部部门');
}
return deptLabel;
},
// 移动端切换图标跟随当前模式(taskfont 字符)
modeIcon({currentView, layout}) {
if (currentView === 'team') {
return '&#xe681;';
}
return layout === 'quad' ? '&#xe746;' : '&#xe894;';
},
dashboardHello({systemConfig, userInfo, nowTime}) {
if (systemConfig.system_welcome) {
return this.$L(systemConfig.system_welcome).replace(/\{username}/g, userInfo.nickname);
@ -647,7 +717,35 @@ export default {
$A.IDBSave("dashboardView", view);
},
/**
* 头部切换器宽度跟随内容:外层设置显式宽度才能触发 CSS 过渡
*/
syncSegWidth() {
const seg = this.$refs.headerSeg;
const inner = this.$refs.headerSegInner;
if (!seg || !inner || inner.offsetWidth === 0) {
return; // keep-alive 隐藏时宽度为 0,不同步,避免再次显示时从 0 展开
}
const style = window.getComputedStyle(seg);
const padding = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
seg.style.width = `${inner.offsetWidth + padding}px`;
},
onModeMenu(name) {
if (name === 'team') {
this.setView('team');
} else if (name === 'scope') {
this.deptViewShow = true;
} else {
this.setLayout(name);
}
},
// 列表/四象限属于个人视角,选中即切回个人视角
setLayout(layout) {
if (this.view !== 'my') {
this.setView('my');
}
this.layout = layout;
prefsCache.layout = layout;
$A.IDBSave("dashboardLayout", layout);

View File

@ -154,75 +154,81 @@
min-height: 40px;
flex-shrink: 0;
max-width: 100%;
overflow: hidden;
transition: width 0.2s ease;
> span {
padding: 0 16px;
min-width: 0;
min-height: 32px;
@media (prefers-reduced-motion: reduce) {
transition: none;
}
// 内层保持内容自然宽度,外层宽度由 syncSegWidth 同步后过渡
.seg-inner {
display: flex;
align-items: center;
border-radius: 8px;
font-size: 14px;
color: #999;
cursor: pointer;
white-space: nowrap;
overflow-wrap: anywhere;
text-align: center;
flex-shrink: 0;
width: max-content;
&.seg-active {
background: #ffffff;
color: #2a2b2d;
> span {
padding: 0 16px;
min-width: 0;
min-height: 32px;
display: flex;
align-items: center;
border-radius: 8px;
font-size: 14px;
color: #999;
cursor: pointer;
white-space: nowrap;
overflow-wrap: anywhere;
text-align: center;
&.seg-active {
background: #ffffff;
color: #2a2b2d;
}
// 部门视角选中后,该项变为部门范围选择
&.seg-dept {
gap: 6px;
max-width: 240px;
.dept-name {
font-style: normal;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
}
.dept-badge {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
min-width: 19px;
height: 19px;
border-radius: 10px;
padding: 0 5px;
background: #e6e8ea;
color: #5f665c;
font-style: normal;
font-size: 11px;
font-weight: 500;
font-variant-numeric: tabular-nums;
}
// 切换部门范围时成员数被清空,加载中占位
.dept-load {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 14px;
height: 14px;
padding: 1px;
}
}
}
}
}
// 部门范围选择(团队视角,占个人视角布局切换的同一槽位)
.header-dept {
display: flex;
align-items: center;
gap: 7px;
background: #f9f9f9;
border-radius: 12px;
height: 40px;
padding: 8px 12px;
max-width: 260px;
font-size: 13px;
color: #22261f;
cursor: pointer;
transition: background 0.2s;
> i {
color: #7a8078;
font-size: 17px;
}
> span {
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dept-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 19px;
height: 19px;
border-radius: 10px;
padding: 0 5px;
background: #e6e8ea;
color: #5f665c;
font-style: normal;
font-size: 11px;
font-weight: 500;
font-variant-numeric: tabular-nums;
}
&:hover {
background: #f4f5f7;
}
}
}
}
@ -1637,6 +1643,37 @@
}
}
// 移动端模式切换下拉项(transfer 到 body)
.dashboard-mode-item {
display: flex;
align-items: center;
gap: 8px;
min-width: 120px;
> i {
font-size: 16px;
}
> span {
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mode-count {
flex-shrink: 0;
color: #a2a89f;
font-style: normal;
font-size: 12px;
}
.mode-arrow {
margin-left: auto;
color: #a2a89f;
}
}
.dashboard-cache-popper {
.dashboard-cache-popover {
width: 260px;
@ -1892,13 +1929,17 @@ body.window-portrait {
.dashboard-header {
.header-left {
.header-hello,
.header-title > h2 {
// 右侧只剩图标按钮,取消最小宽度避免窄屏时右侧换行
min-width: 0;
.header-hello {
font-size: 20px;
}
}
.header-right {
// 只占图标实际宽度,余下空间留给标题
flex: none;
gap: 8px;
// 与右侧切换器等高对齐
@ -1915,22 +1956,66 @@ body.window-portrait {
}
}
// 移动端模式切换:图标跟随当前模式,部门视角带成员数角标
.header-mode {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: 12px;
background: #f9f9f9;
color: #7a8078;
cursor: pointer;
> i {
font-size: 20px;
transform: scale(0.86);
}
.mode-badge {
position: absolute;
top: -5px;
right: -6px;
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 8px;
background: $primary-color;
color: #ffffff;
font-style: normal;
font-size: 10px;
line-height: 16px;
text-align: center;
font-variant-numeric: tabular-nums;
}
.mode-load {
position: absolute;
top: -4px;
right: -4px;
width: 14px;
height: 14px;
padding: 1px;
border-radius: 50%;
background: #ffffff;
}
}
.header-seg {
min-height: 34px;
align-items: center;
> span {
.seg-inner > span {
padding: 4px 10px;
min-height: 26px;
font-size: 12px;
}
}
.header-dept {
height: 34px;
padding: 0 10px;
max-width: 180px;
font-size: 12px;
&.seg-dept {
max-width: 160px;
}
}
}
}
}
@ -2052,6 +2137,10 @@ body.window-portrait {
}
.team-focus {
.focus-chip {
padding: 4px 0;
}
.focus-row {
grid-template-columns: minmax(0, 1fr) 90px 96px;
gap: 8px;