feat: 完善管理后台团队仪表盘

This commit is contained in:
kuaifan 2026-07-18 01:13:43 +00:00
parent 844db09c12
commit 6666605f7d
7 changed files with 2698 additions and 459 deletions

View File

@ -12,6 +12,7 @@
"allowJs": true,
"checkJs": false,
"jsx": "preserve",
"ignoreDeprecations": "6.0",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,

View File

@ -2534,3 +2534,58 @@ AI任务分析
服务器返回错误HTTP (*)
你撤回了一条消息
重新编辑
四象限
个人视角
部门任务总览
(*) 人
最久 (*) 天
今天 (*) 项到期
还有 (*) 项
待完成
我协助的
(*)月(*)日
有 (*) 项已超期,建议先处理
有 (*) 项今日到期
今日无到期任务
团队有 (*) 项已超期,涉及 (*) 人
团队有 (*) 项已超期
团队本周已完成 (*) 项
查看任务
涉及 (*) 人
3 天内到期
本周完成
较上周
与上周持平
成员任务分配
(*) 项
未分配
查看全部 (*) 人
优先级分布
未完成 (*) 项
点击任一档位可下钻到任务列表
高优先级
重点关注任务
未分配负责人
待分配
全部显示完毕
逾期 (*) 天
逾期 (*) 小时
夜深了,{username}
早上好,{username}
中午好,{username}
下午好,{username}
晚上好,{username}
演示数据,接口对接后可打开任务详情
服务器时间
暂无已超期任务
暂无今日到期任务
暂无待完成任务
暂无协助的任务
没有超期任务,保持住
今天只有这一项,处理完就轻松了
数量较多,仅显示最近 (*) 项,展开其余 (*) 项
太棒了,当前没有需要处理的任务
我的部门
待开始
暂无待开始任务

View File

@ -0,0 +1,379 @@
<template>
<div class="dashboard-team">
<!--核心指标-->
<ul class="dashboard-cards">
<li @click="onBlock('uncompleted')">
<div class="card-label">{{$L('未完成')}}</div>
<div class="card-data">
<span class="card-num">{{blocks.uncompleted || 0}}</span>
<span class="card-sub">{{$L('项')}}</span>
</div>
<div class="card-link">{{$L('查看任务')}} </div>
</li>
<li @click="onBlock('overdue')">
<div class="card-label">{{$L('已超期')}}</div>
<div class="card-data">
<span class="card-num num-red">{{blocks.overdue || 0}}</span>
<span class="card-sub">{{$L('项')}}<template v-if="blocks.overdue_owner_count"> · {{$L('涉及 (*) ', blocks.overdue_owner_count)}}</template></span>
</div>
<div class="card-link link-red">{{$L('查看任务')}} </div>
</li>
<li @click="onBlock('soon')">
<div class="card-label">{{$L('3 天内到期')}}</div>
<div class="card-data">
<span class="card-num num-orange">{{blocks.due_soon || 0}}</span>
<span class="card-sub">{{$L('项')}}</span>
</div>
<div class="card-link link-orange">{{$L('查看任务')}} </div>
</li>
<li class="card-static">
<div class="card-label">{{$L('本周完成')}}</div>
<div class="card-data">
<span class="card-num num-green">{{blocks.week_completed || 0}}</span>
<span class="card-sub">{{$L('项')}}<template v-if="weekDiffText"> · {{weekDiffText}}</template></span>
</div>
</li>
</ul>
<!--成员任务分配 + 优先级分布-->
<div class="team-duo">
<div class="dashboard-card team-members">
<div class="card-head">
<span class="head-title">{{$L('成员任务分配')}}</span>
<span class="head-legend">
<span><i style="background:#1e9e55"></i>{{$L('进行中')}}</span>
<span><i style="background:#bfe3cd"></i>{{$L('待处理')}}</span>
<span v-if="hasTestSegment"><i style="background:#6ba7d8"></i>{{$L('待测试')}}</span>
<span><i style="background:#d94f46"></i>{{$L('已超期')}}</span>
</span>
</div>
<ul class="member-list">
<li v-for="member in visibleMembers" :key="member.nickname" class="member-row" @click="onMember(member)">
<span class="mock-avatar" :style="{backgroundColor: member.color}">{{member.nickname.substring(0, 1)}}</span>
<span class="member-name">{{member.nickname}}</span>
<span class="member-track">
<span class="member-bar" :style="{width: memberBarWidth(member)}">
<i v-if="member.segments.progress > 0" :style="{flex: member.segments.progress, background: '#1e9e55'}"></i>
<i v-if="member.segments.start > 0" :style="{flex: member.segments.start, background: '#bfe3cd'}"></i>
<i v-if="member.segments.test > 0" :style="{flex: member.segments.test, background: '#6ba7d8'}"></i>
<i v-if="member.overdue > 0" :style="{flex: member.overdue, background: '#d94f46'}"></i>
</span>
</span>
<span class="member-num">
<em v-if="member.overdue > 0" class="num-overdue">{{member.overdue}} {{$L('超期')}} · </em>{{$L('(*) ', member.total)}}
</span>
</li>
<li v-if="blocks.no_owner > 0" class="member-row member-noowner" @click="onBlock('noowner')">
<span class="mock-avatar avatar-none">?</span>
<span class="member-name">{{$L('未分配')}}</span>
<span class="member-track">
<span class="member-bar bar-hatched" :style="{width: memberBarWidth({total: blocks.no_owner})}"></span>
</span>
<span class="member-num num-noowner">{{$L('(*) 项', blocks.no_owner)}}</span>
</li>
</ul>
<div v-if="members.length > memberLimit" class="card-more" @click="memberExpand = !memberExpand">
{{memberExpand ? $L('收起') : $L('查看全部 (*) 人', members.length) + ' →'}}
</div>
</div>
<div class="dashboard-card team-priority">
<div class="card-head">
<span class="head-title">{{$L('优先级分布')}}</span>
<span class="head-note">{{$L('未完成 (*) 项', blocks.uncompleted || 0)}}</span>
</div>
<div class="priority-list">
<div v-for="item in priorityList" :key="item.level" class="priority-row" @click="onPriority(item)">
<div class="priority-head">
<span class="priority-name">{{item.name}}</span>
<span class="priority-num">{{$L('(*) 项', item.num)}} · {{item.percent}}%</span>
</div>
<div class="priority-bar">
<i :style="{width: item.percent + '%', background: item.color}"></i>
</div>
</div>
<div class="priority-tip">{{$L('点击任一档位可下钻到任务列表')}}</div>
</div>
</div>
</div>
<!--重点关注任务-->
<div ref="focusCard" class="dashboard-card team-focus">
<div class="card-head focus-head">
<span class="head-title">{{$L('重点关注任务')}}</span>
<span
v-for="chip in chips"
:key="chip.type"
class="focus-chip"
:class="[`chip-${chip.type}`, {'chip-active': !extraFilter && focus === chip.type}]"
@click="setFocus(chip.type)">{{chip.label}} {{chip.num}}</span>
<span v-if="extraFilter" class="focus-chip chip-extra chip-active" @click="clearFilter">
{{extraFilter.label}}<i>×</i>
</span>
</div>
<div class="focus-table">
<div class="focus-row focus-thead">
<span>{{$L('任务')}}</span>
<span class="f-project">{{$L('项目')}}</span>
<span class="f-owner">{{$L('负责人')}}</span>
<span class="f-priority">{{$L('优先级')}}</span>
<span class="f-status">{{$L('状态')}}</span>
<span class="f-end">{{$L('截止时间')}}</span>
</div>
<div v-for="item in currentList.list" :key="item.id" class="focus-row" @click="onTask(item)">
<span class="f-name">{{item.name}}</span>
<span class="f-project">{{item.project_name}}</span>
<span class="f-owner">
<template v-if="item.owner">
<span class="mock-avatar" :style="{backgroundColor: item.owner.color}">{{item.owner.nickname.substring(0, 1)}}</span>
<span class="owner-name">{{item.owner.nickname}}</span>
</template>
<template v-else>
<span class="mock-avatar avatar-none">?</span><em class="owner-none">{{$L('待分配')}}</em>
</template>
</span>
<span class="f-priority">
<template v-if="item.p_name">
<i class="priority-dot" :style="{backgroundColor: item.p_color}"></i>
<span>{{item.p_name}}</span>
</template>
<template v-else></template>
</span>
<span class="f-status"><em class="status-pill" :class="item.flow_item_name ? item.flow_item_status : ''">{{item.flow_item_name || $L('未完成')}}</em></span>
<span class="f-end" :class="deadlineClass(item.end_at)">{{deadlineText(item.end_at) || '—'}}</span>
</div>
<div v-if="currentList.loading" class="focus-loading"><Loading/></div>
<div v-else-if="currentList.list.length === 0" class="focus-empty">{{$L('暂无任务')}}</div>
<div v-else-if="currentList.hasMore" class="card-more" @click="loadMore">{{$L('加载更多')}}{{$L('还有 (*) ', currentList.total - currentList.list.length)}}</div>
<div v-else-if="currentList.total > pageSize" class="card-more more-done">{{$L('全部显示完毕')}}</div>
</div>
</div>
</div>
</template>
<script>
import {fetchTeamStats, fetchTeamTasks} from "./dashboard-team-mock";
import dashboardTimeMixin from "./dashboard-time-mixin";
export default {
name: "DashboardTeam",
mixins: [dashboardTimeMixin],
props: {
initialFocus: {
type: String,
default: 'overdue'
},
},
data() {
return {
stats: {
member_count: 0,
blocks: {},
priority: [],
members: [],
},
focus: ['overdue', 'soon', 'hi', 'noowner'].includes(this.initialFocus) ? this.initialFocus : 'overdue',
extraFilter: null,
lists: {},
pageSize: 20,
memberLimit: 6,
memberExpand: false,
}
},
created() {
this.loadStats()
this.loadList()
},
activated() {
this.loadInterval(true)
},
deactivated() {
this.loadInterval(false)
},
beforeDestroy() {
this.loadInterval(false)
},
computed: {
blocks({stats}) {
return stats.blocks || {}
},
members({stats}) {
return stats.members || []
},
visibleMembers({members, memberExpand, memberLimit}) {
return memberExpand ? members : members.slice(0, memberLimit)
},
hasTestSegment({members}) {
return members.some(member => (member.segments?.test || 0) > 0)
},
memberMax({members, blocks}) {
return Math.max(...members.map(item => item.total), blocks.no_owner || 0, 1)
},
priorityList({stats, blocks}) {
const total = Math.max(blocks.uncompleted || 0, 1)
return (stats.priority || []).filter(item => item.num > 0).map(item => {
return {
...item,
name: item.name || this.$L('未设置'),
percent: Math.round(item.num / total * 100),
}
})
},
highPriorityChip({stats, blocks}) {
// 2
const tops = (stats.priority || []).filter(item => item.level > 0).slice(0, 2)
return {
label: tops.map(item => item.name).join(' / ') || this.$L('高优先级'),
num: tops.reduce((sum, item) => sum + item.num, 0),
}
},
chips({blocks, highPriorityChip}) {
return [
{type: 'overdue', label: this.$L('已超期'), num: blocks.overdue || 0},
{type: 'soon', label: this.$L('3 天内到期'), num: blocks.due_soon || 0},
{type: 'hi', label: highPriorityChip.label, num: highPriorityChip.num},
{type: 'noowner', label: this.$L('未分配负责人'), num: blocks.no_owner || 0},
]
},
weekDiffText({blocks}) {
if (!blocks.week_completed && !blocks.last_week_completed) {
return ''
}
const diff = (blocks.week_completed || 0) - (blocks.last_week_completed || 0)
if (diff > 0) {
return this.$L('较上周') + ` +${diff}`
}
if (diff < 0) {
return this.$L('较上周') + ` ${diff}`
}
return this.$L('与上周持平')
},
currentKey({focus, extraFilter}) {
if (extraFilter) {
return `${extraFilter.kind}:${extraFilter.value}`
}
return `type:${focus}`
},
currentList({lists, currentKey}) {
return lists[currentKey] || {list: [], page: 1, total: 0, hasMore: false, loading: true}
},
},
methods: {
loadStats() {
fetchTeamStats().then(stats => {
this.stats = stats
this.$emit('stats', stats)
})
},
listParams(page) {
const params = {page}
if (this.extraFilter?.kind === 'member') {
params.member = this.extraFilter.value
} else if (this.extraFilter?.kind === 'level') {
params.level = this.extraFilter.value
} else {
params.type = this.focus
}
return params
},
loadList(page = 1) {
const key = this.currentKey
if (page === 1 && this.lists[key] && !this.lists[key].loading) {
return
}
const prev = page > 1 ? this.lists[key] : null
this.$set(this.lists, key, {
list: prev ? prev.list : [],
page,
total: prev ? prev.total : 0,
hasMore: false,
loading: true,
})
fetchTeamTasks(this.listParams(page)).then(data => {
this.$set(this.lists, key, {
list: prev ? [...prev.list, ...data.list] : data.list,
page: data.page,
total: data.total,
hasMore: data.hasMore,
loading: false,
})
})
},
loadMore() {
this.loadList(this.currentList.page + 1)
},
setFocus(type) {
this.extraFilter = null
this.focus = type
$A.IDBSave("dashboardTeamFocus", type)
this.$nextTick(() => this.loadList())
},
setFilter(kind, value, label) {
this.extraFilter = {kind, value, label}
this.$nextTick(() => {
this.loadList()
this.scrollToFocus()
})
},
clearFilter() {
this.extraFilter = null
},
onBlock(type) {
if (type === 'uncompleted') {
this.scrollToFocus()
return
}
this.setFocus(type)
this.scrollToFocus()
},
onMember(member) {
this.setFilter('member', member.nickname, `${this.$L('成员')}: ${member.nickname}`)
},
onPriority(item) {
this.setFilter('level', item.level, `${this.$L('优先级')}: ${item.name}`)
},
onTask() {
$A.messageInfo("演示数据,接口对接后可打开任务详情")
},
scrollToFocus() {
this.$nextTick(() => {
$A.scrollToView(this.$refs.focusCard, {
behavior: 'smooth',
block: 'start',
})
})
},
memberBarWidth(member) {
return `${Math.max(Math.round(member.total / this.memberMax * 100), 4)}%`
},
}
}
</script>

View File

@ -0,0 +1,242 @@
/**
* 团队概览 Mock 数据仅接口对接前使用确认交互后整体删除本文件
*
* 结构与规划中的接口返回保持一致便于无缝替换
* - fetchTeamStats() api/dashboard/team/stats
* - fetchTeamTasks() api/dashboard/team/tasks?type=xxx&page=N20/
*
* 口径说明与后端实现约定一致
* - members[].segments 按任务表 flow_item_name 的状态前缀start/progress/test聚合
* 已超期的任务不论状态归入 overdue
* - priority 按系统优先级设置动态分档未设置优先级的任务归入未设置
*/
const PAGE_SIZE = 20;
// 成员池nickname/color 用于 Mock 字母头像,真实接口返回 userid 后换 UserAvatar
const MEMBERS = [
{userid: 0, nickname: "林晓雨", color: "#3a7bd5", total: 14, overdue: 2, segments: {progress: 7, start: 4, test: 1}},
{userid: 0, nickname: "王志强", color: "#8a63d2", total: 13, overdue: 3, segments: {progress: 5, start: 4, test: 1}},
{userid: 0, nickname: "陈嘉明", color: "#1e9e55", total: 11, overdue: 3, segments: {progress: 4, start: 3, test: 1}},
{userid: 0, nickname: "赵倩", color: "#d9822b", total: 9, overdue: 0, segments: {progress: 5, start: 4, test: 0}},
{userid: 0, nickname: "孙浩然", color: "#5f9ea0", total: 7, overdue: 2, segments: {progress: 3, start: 2, test: 0}},
{userid: 0, nickname: "李雪", color: "#c2554f", total: 6, overdue: 1, segments: {progress: 2, start: 3, test: 0}},
{userid: 0, nickname: "周明", color: "#4f7a5b", total: 5, overdue: 0, segments: {progress: 2, start: 3, test: 0}},
{userid: 0, nickname: "吴婷", color: "#9a6ec2", total: 4, overdue: 1, segments: {progress: 1, start: 2, test: 0}},
{userid: 0, nickname: "郑凯", color: "#3f8fa8", total: 3, overdue: 0, segments: {progress: 1, start: 2, test: 0}},
{userid: 0, nickname: "冯丽", color: "#b8863d", total: 3, overdue: 0, segments: {progress: 2, start: 1, test: 0}},
{userid: 0, nickname: "韩磊", color: "#6b7ac2", total: 2, overdue: 0, segments: {progress: 1, start: 1, test: 0}},
{userid: 0, nickname: "杨帆", color: "#52a06e", total: 1, overdue: 0, segments: {progress: 0, start: 1, test: 0}},
];
const PROJECTS = ["DooTask v2", "点餐系统", "TTPOS-MENU", "财务管理", "项目收支"];
const PRIORITIES = [
{level: 1, name: "紧急", color: "#d94f46"},
{level: 2, name: "高", color: "#d9822b"},
{level: 3, name: "中", color: "#3a7bd5"},
{level: 4, name: "低", color: "#9aa096"},
];
const FLOWS = [
{status: "start", name: "待处理"},
{status: "progress", name: "进行中"},
{status: "test", name: "审核中"},
];
const TASK_NAMES = [
"建议放开部门群组的自定义头像权限", "【收款】佳博 WiFi 打印机联调", "点餐系统会员余额对账异常排查",
"电子菜单需开通 Stripe 账户的申请", "公开分享链接的选项没有做初始化记录", "建议增加任务外链访问功能",
"客户端登录页视觉走查", "周报数据汇总脚本修复", "移动端消息推送延迟优化方案评估",
"建议将任务模板和任务描述的内容编辑框保持一致", "仪表盘统计口径梳理与确认", "客户反馈工单归类规则更新",
"灰度发布检查清单补充", "新版工作流状态配置文档评审", "财务管理报表导出字段确认",
"点餐系统打印模板多语言适配", "API 文档站点信息架构调整", "财务管理季度结算流程自动化",
"项目收支月度盘点模板整理", "移动端首页白屏问题排查", "消息中心已读状态同步异常",
"新员工入职引导流程梳理", "数据备份策略评审与演练", "第三方登录接口升级适配",
"文件预览服务内存占用优化", "会议室预定模块需求评审", "客户端自动更新失败重试机制",
"通知推送模板文案统一", "任务导出 Excel 字段缺失修复", "项目归档流程权限确认",
"服务端日志切割脚本调整", "看板拖拽卡顿性能分析", "多语言翻译缺失项补齐",
"接口限流阈值评估与调整", "运营后台数据大盘需求确认", "旧版附件迁移方案评审",
"扫码登录二维码过期时间调整", "子任务批量操作交互优化", "网盘分享链接权限梳理",
"审批流转节点通知缺失排查", "版本发布公告文案准备", "工时统计报表口径确认",
];
// —— 以下为确定性生成逻辑(不用随机数,保证每次渲染一致)——
// 时间一律基于 $A.daytz()(服务器时区),与真实接口返回口径一致
function fmt(day) {
return day.format("YYYY-MM-DD HH:mm:00");
}
function dayOffset(days, hour) {
return $A.daytz().add(days, "day").hour(hour).minute(0).second(0);
}
/**
* 生成未完成任务池84 = 成员 78 + 未分配 6
* 超期 123 日内到期 18紧急/ 29未分配 6 stats 数字咬合
*/
function buildPool() {
const pool = [];
const owners = [];
MEMBERS.forEach(member => {
for (let i = 0; i < member.total; i++) {
owners.push({member, isOverdue: i < member.overdue});
}
});
for (let i = 0; i < 6; i++) {
owners.push({member: null, isOverdue: false});
}
// 优先级配额:紧急 8、高 21、中 31、低 16、未设置 8合计 84紧急+高=29
// 用互质数打散分配顺序,避免同一成员的任务优先级扎堆
const priorityRank = owners.map((_, i) => i).sort((a, b) => ((a * 53) % owners.length) - ((b * 53) % owners.length));
const priorityMap = {};
priorityRank.forEach((ownerIndex, rank) => {
if (rank < 8) {
priorityMap[ownerIndex] = PRIORITIES[0];
} else if (rank < 29) {
priorityMap[ownerIndex] = PRIORITIES[1];
} else if (rank < 60) {
priorityMap[ownerIndex] = PRIORITIES[2];
} else if (rank < 76) {
priorityMap[ownerIndex] = PRIORITIES[3];
} else {
priorityMap[ownerIndex] = null;
}
});
// 非超期任务里18 条 3 日内到期时间保证在未来、30 条较远截止、其余无截止
const nonOverdue = owners.map((item, i) => item.isOverdue ? -1 : i).filter(i => i >= 0);
const soonRank = [...nonOverdue].sort((a, b) => ((a * 37) % owners.length) - ((b * 37) % owners.length));
const soonSet = new Set(soonRank.slice(0, 18));
const farSet = new Set(soonRank.slice(18, 48));
owners.forEach((item, index) => {
const priority = priorityMap[index];
//
let endAt = null;
if (item.isOverdue) {
endAt = dayOffset(-((index % 6) + 1), 10 + index % 8);
} else if (soonSet.has(index)) {
// 现在起 2 小时 ~ 2.5 天内,保证不会因当天时间已过而变成逾期
endAt = $A.daytz().add(2 + (index % 3) * 20, "hour").minute(0).second(0);
} else if (farSet.has(index)) {
endAt = dayOffset(4 + index % 20, 9 + index % 9);
}
const flow = FLOWS[index % FLOWS.length];
const name = TASK_NAMES[index % TASK_NAMES.length] + (index >= TASK_NAMES.length * 2 ? "(二期)" : index >= TASK_NAMES.length ? "(跟进)" : "");
pool.push({
id: 900000 + index,
name,
project_name: PROJECTS[index % PROJECTS.length],
owner: item.member ? {userid: item.member.userid, nickname: item.member.nickname, color: item.member.color} : null,
p_level: priority ? priority.level : 0,
p_name: priority ? priority.name : "",
p_color: priority ? priority.color : "",
flow_item_status: flow.status,
flow_item_name: flow.name,
end_at: endAt ? fmt(endAt) : "",
});
});
return pool;
}
let POOL = null;
function getPool() {
if (POOL === null) {
POOL = buildPool();
}
return POOL;
}
function isOverdue(task) {
return task.end_at && $A.dayjs(task.end_at) <= $A.daytz();
}
function isSoon(task) {
if (!task.end_at || isOverdue(task)) {
return false;
}
return $A.dayjs(task.end_at) <= $A.daytz().add(3, "day").endOf("day");
}
function filterPool({type, member, level}) {
let list = getPool();
if (member) {
list = list.filter(task => task.owner && task.owner.nickname === member);
} else if (level > 0) {
list = list.filter(task => task.p_level === level);
} else if (level === -1) {
list = list.filter(task => task.p_level === 0);
} else {
switch (type) {
case "overdue":
list = list.filter(task => isOverdue(task));
break;
case "soon":
list = list.filter(task => isSoon(task));
break;
case "hi":
list = list.filter(task => task.p_level > 0 && task.p_level <= 2);
break;
case "noowner":
list = list.filter(task => !task.owner);
break;
}
}
// 排序:超期最久优先,其次截止时间近的优先,无截止最后
return list.slice().sort((a, b) => {
if (!a.end_at && !b.end_at) return 0;
if (!a.end_at) return 1;
if (!b.end_at) return -1;
return a.end_at.localeCompare(b.end_at);
});
}
/**
* 团队统计对应 api/dashboard/team/stats
*/
export function fetchTeamStats() {
return new Promise(resolve => {
setTimeout(() => {
resolve({
member_count: MEMBERS.length,
blocks: {
uncompleted: 84,
overdue: 12,
overdue_owner_count: 5,
due_soon: 18,
week_completed: 27,
last_week_completed: 21,
no_owner: 6,
},
priority: [
{level: 1, name: "紧急", color: "#d94f46", num: 8},
{level: 2, name: "高", color: "#d9822b", num: 21},
{level: 3, name: "中", color: "#3a7bd5", num: 31},
{level: 4, name: "低", color: "#9aa096", num: 16},
{level: -1, name: "", color: "#c5c8ce", num: 8},
],
members: MEMBERS,
});
}, 300);
});
}
/**
* 团队任务列表对应 api/dashboard/team/tasks
* @param {Object} params {type, page, member, level}
*/
export function fetchTeamTasks(params) {
return new Promise(resolve => {
setTimeout(() => {
const page = Math.max(1, params.page || 1);
const list = filterPool(params);
resolve({
page,
total: list.length,
list: list.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
hasMore: page * PAGE_SIZE < list.length,
});
}, 300);
});
}

View File

@ -0,0 +1,80 @@
/**
* 仪表盘截止时间人性化显示个人视角 / 团队概览共用
*/
export default {
data() {
return {
nowTime: $A.dayjs().unix(),
nowInter: null,
}
},
methods: {
/**
* 截止时间状态 class
* @param endAt
* @returns {string} end-overdue|end-today|''
*/
deadlineClass(endAt) {
if (!endAt) {
return ''
}
const end = $A.dayjs(endAt)
const now = $A.daytz(this.nowTime)
if (end <= now) {
return 'end-overdue'
}
if (end.format('YYYY-MM-DD') === now.format('YYYY-MM-DD')) {
return 'end-today'
}
return ''
},
/**
* 截止时间文案逾期 N / 今天 HH:mm / 明天 HH:mm / MM-DD
* @param endAt
* @returns {string}
*/
deadlineText(endAt) {
if (!endAt) {
return ''
}
const end = $A.dayjs(endAt)
const now = $A.daytz(this.nowTime)
if (end <= now) {
const days = now.startOf('day').diff(end.startOf('day'), 'day')
if (days > 0) {
return this.$L('逾期 (*) 天', days)
}
return this.$L('逾期 (*) 小时', Math.max(now.diff(end, 'hour'), 1))
}
if (end.format('YYYY-MM-DD') === now.format('YYYY-MM-DD')) {
return `${this.$L('今天')} ${end.format('HH:mm')}`
}
if (end.format('YYYY-MM-DD') === now.clone().add(1, 'day').format('YYYY-MM-DD')) {
return `${this.$L('明天')} ${end.format('HH:mm')}`
}
if (end.year() === now.year()) {
return end.format('MM-DD')
}
return end.format('YYYY-MM-DD')
},
/**
* 时间刷新定时器驱动 deadline 文案更新
* @param load
*/
loadInterval(load) {
if (this.nowInter) {
clearInterval(this.nowInter)
this.nowInter = null
}
if (load === false) {
return
}
this.nowInter = setInterval(_ => {
this.nowTime = $A.dayjs().unix()
}, 10 * 1000)
},
}
}

View File

@ -4,141 +4,305 @@
<Alert v-if="warningMsg" class="dashboard-warning" type="warning" show-icon>
<span @click="goForward({name: 'manage-setting-license'})">{{warningMsg}}</span>
</Alert>
<div class="dashboard-wrapper" :style="wrapperStyle">
<div class="dashboard-hello">
<h2>{{dashboardHello}}</h2>
<div class="dashboard-search" :class="{'min-search': windowPortrait}" @click="openSearch">
<Icon type="ios-search"/>
<span>{{$L('搜索')}} ({{mateName}}+F)</span>
<Scrollbar class="dashboard-scroller">
<div class="dashboard-body">
<!--头部-->
<div class="dashboard-header">
<div class="header-left">
<template v-if="currentView === 'my'">
<h2 class="header-hello">{{dashboardHello}}</h2>
<div class="header-sub">
<span>{{todayText}} · {{mySummary}}</span>
<transition name="dashboard-load">
<div v-if="loadDashboardTasks" class="dashboard-load"><Loading/></div>
</transition>
</div>
</template>
<template v-else>
<h2 class="header-hello">{{$L('部门任务总览')}}</h2>
<div class="header-sub">
<span>{{todayText}}<template v-if="teamSummary"> · {{teamSummary}}</template></span>
</div>
</template>
<div v-if="systemConfig.timezoneDifference" class="header-servertime">
<span>{{$L('服务器时间')}}:</span>
<span>{{$A.daytz().format('YYYY-MM-DD HH:mm:ss')}}</span>
</div>
</div>
<div class="header-right">
<div class="header-search" :class="{'min-search': windowPortrait}" @click="openSearch">
<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>
</div>
</div>
</div>
<div v-if="systemConfig.timezoneDifference" class="dashboard-time">
<span>{{$L('服务器时间')}}:</span>
<span>{{$A.daytz().format('YYYY-MM-DD HH:mm:ss')}}</span>
</div>
<div class="dashboard-desc">
<span>{{$L('以下是你当前的任务统计数据')}}</span>
<transition name="dashboard-load">
<div v-if="loadDashboardTasks" class="dashboard-load"><Loading/></div>
</transition>
</div>
<ul class="dashboard-block">
<li @click="scrollTo('today')">
<div class="block-title">{{getTitle('today')}}</div>
<div class="block-data">
<div class="block-num">{{dashboardTask.today_count}}</div>
<i class="taskfont">&#xe6f4;</i>
</div>
</li>
<li @click="scrollTo('overdue')">
<div class="block-title">{{getTitle('overdue')}}</div>
<div class="block-data">
<div class="block-num">{{dashboardTask.overdue_count}}</div>
<i class="taskfont">&#xe603;</i>
</div>
</li>
<li @click="scrollTo('todo')">
<div class="block-title">{{getTitle('todo')}}</div>
<div class="block-data">
<div class="block-num">{{dashboardTask.todo_count}}</div>
<i class="taskfont">&#xe6f9;</i>
</div>
</li>
</ul>
<Scrollbar class="dashboard-list">
<template
v-for="column in columns"
v-if="column.list.length > 0">
<div :ref="`type_${column.type}`" class="dashboard-ref"></div>
<div class="dashboard-title" :class="{'title-close': column.hidden}" @click="onDashboardHidden(column.type)">
<span>
{{column.title}}
<template v-if="column.hidden">
({{column.count}})
</template>
</span>
<i class="taskfont">&#xe702;</i>
</div>
<ul class="dashboard-ul" :class="{'ul-hidden': column.hidden}">
<li
v-for="(item, index) in column.list"
:key="index"
:class="{complete: item.complete_at}"
:style="$A.generateColorVarStyle(item.flow_item_color, [10], 'flow-item-custom-color', item.color ? {backgroundColor: item.color} : {})"
@click="openTask(item)">
<em
v-if="item.p_name"
class="priority-color"
:style="{backgroundColor:item.p_color}"></em>
<div class="item-select" @click.stop="openMenu($event, item)">
<i class="taskfont" v-html="item.complete_at ? '&#xe627;' : '&#xe625;'"></i>
</div>
<div class="item-title">
<!--工作流状态-->
<span v-if="item.flow_item_name" :class="item.flow_item_status" @click.stop="openMenu($event, item)">{{item.flow_item_name}}</span>
<!--是否子任务-->
<span v-if="item.sub_top === true">{{$L('子任务')}}</span>
<!--有多少个子任务-->
<span v-if="item.sub_my && item.sub_my.length > 0">+{{item.sub_my.length}}</span>
<!--任务描述-->
{{item.name}}
</div>
<div v-if="item.desc" class="item-icon">
<i class="taskfont">&#xe71a;</i>
</div>
<div v-if="item.sub_num > 0" class="item-icon">
<i class="taskfont">&#xe71f;</i>
<em>{{item.sub_complete}}/{{item.sub_num}}</em>
</div>
<ETooltip v-if="item.end_at" :disabled="$isEEUIApp || windowTouch" :content="item.end_at" placement="right">
<div :class="['item-icon', item.today ? 'today' : '', item.overdue ? 'overdue' : '']">
<i class="taskfont">&#xe71d;</i>
<em>{{expiresFormat(item.end_at)}}</em>
<!--个人视角-->
<template v-if="currentView === 'my'">
<!--列表布局-->
<template v-if="layout === 'list'">
<ul class="dashboard-cards">
<li :class="{'card-off': dashboardTask.overdue_count === 0}" @click="scrollTo('overdue')">
<div class="card-label">{{getTitle('overdue')}}</div>
<div class="card-data">
<span class="card-num num-red">{{dashboardTask.overdue_count}}</span>
<span class="card-sub">{{$L('项')}}<template v-if="overdueMaxDays > 0"> · {{$L('最久 (*) ', overdueMaxDays)}}</template></span>
</div>
</ETooltip>
</li>
</ul>
</li>
<li :class="{'card-off': dashboardTask.today_count === 0}" @click="scrollTo('today')">
<div class="card-label">{{getTitle('today')}}</div>
<div class="card-data">
<span class="card-num num-orange">{{dashboardTask.today_count}}</span>
<span class="card-sub">{{$L('项')}}<template v-if="todayNearest"> · {{$L('最近')}} {{todayNearest}}</template></span>
</div>
</li>
<li :class="{'card-off': dashboardTask.todo_count === 0}" @click="scrollTo('todo')">
<div class="card-label">{{getTitle('todo')}}</div>
<div class="card-data">
<span class="card-num">{{dashboardTask.todo_count}}</span>
<span class="card-sub">{{$L('项')}}</span>
</div>
<div v-if="upcomingTask.count > 0" class="duo-upcoming" @click.stop="scrollTo('upcoming')">
<div class="side-num">{{upcomingTask.count}}</div>
<div class="side-label">{{$L('待开始')}}</div>
</div>
</li>
<li :class="{'card-off': assistTask.length === 0}" @click="scrollTo('assist')">
<div class="card-label">{{getTitle('assist')}}</div>
<div class="card-data">
<span class="card-num">{{assistTask.length}}</span>
<span class="card-sub">{{$L('项')}}<template v-if="assistTodayCount > 0"> · {{$L('今天 (*) 项到期', assistTodayCount)}}</template></span>
</div>
</li>
</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>
<div v-else class="table-row table-thead">
<span></span>
<span>{{$L('任务')}}</span>
<span class="t-project">{{$L('项目')}}</span>
<span class="t-status">{{$L('状态')}}</span>
<span class="t-priority">{{$L('优先级')}}</span>
<span class="t-sub">{{$L('子任务')}}</span>
<span class="t-end">{{$L('截止时间')}}</span>
</div>
<template v-for="group in listColumns">
<div
:key="`head-${group.type}`"
:ref="`group_${group.type}`"
class="table-group"
:class="[`group-${group.type}`, {'group-flash': flashType === group.type}]"
@click="toggleGroup(group.type)">
<i class="group-dot"></i>
<span class="group-title">{{group.title}} · {{group.count}}</span>
<i class="group-chevron taskfont" :class="{'chevron-close': group.hidden}">&#xe702;</i>
</div>
<template v-if="!group.hidden">
<div v-if="group.list.length === 0" :key="`empty-${group.type}`" class="table-empty-group">
<span class="empty-icon"><Icon type="md-checkmark"/></span>
<span>{{listEmptyText(group.type)}}</span>
</div>
<div
v-for="item in groupList(group, groupLimit)"
:key="`${group.type}-${item.id}`"
class="table-row"
:class="{complete: item.complete_at}"
:style="$A.generateColorVarStyle(item.flow_item_color, [10], 'flow-item-custom-color', item.color ? {backgroundColor: item.color} : {})"
@click="openTask(item)">
<span class="cell-check" @click.stop>
<UserAvatar v-if="group.type === 'assist' && ownerUserid(item)" :userid="ownerUserid(item)" :size="22"/>
<TaskMenu v-else :task="item"/>
</span>
<span class="cell-name">
<em
class="status-pill name-status"
:class="item.flow_item_name ? item.flow_item_status : {end: !!item.complete_at}"
@click.stop="openMenu($event, item)">{{item.flow_item_name || (item.complete_at ? $L('已完成') : $L('未完成'))}}</em>
<em v-if="item.sub_top === true" class="name-tag">{{$L('子任务')}}</em>
<em v-if="item.sub_my && item.sub_my.length > 0" class="name-tag">+{{item.sub_my.length}}</em>
{{item.name}}
</span>
<span class="t-project">{{projectName(item)}}</span>
<span class="t-status">
<em
class="status-pill"
:class="item.flow_item_name ? item.flow_item_status : {end: !!item.complete_at}"
@click.stop="openMenu($event, item)">{{item.flow_item_name || (item.complete_at ? $L('已完成') : $L('未完成'))}}</em>
</span>
<span class="t-priority">
<template v-if="item.p_name">
<i class="priority-dot" :style="{backgroundColor: item.p_color}"></i>
<span>{{item.p_name}}</span>
</template>
<template v-else></template>
</span>
<span class="t-sub">
<template v-if="item.sub_num > 0">
<i class="sub-bar"><b :style="{width: `${Math.min(item.sub_complete / item.sub_num * 100, 100)}%`}"></b></i>
<em>{{item.sub_complete}}/{{item.sub_num}}</em>
</template>
<template v-else></template>
</span>
<ETooltip :disabled="$isEEUIApp || windowTouch || !item.end_at" :content="item.end_at" placement="right">
<span class="t-end" :class="deadlineClass(item.end_at)">{{deadlineText(item.end_at) || '—'}}</span>
</ETooltip>
</div>
<div
v-if="group.list.length > groupLimit && !expandedGroups.includes(group.type)"
:key="`more-${group.type}`"
class="card-more"
@click="expandedGroups.push(group.type)">{{$L('还有 (*) 项', group.list.length - groupLimit)}} </div>
</template>
</template>
</div>
</template>
<!--四象限布局-->
<div v-else class="dashboard-quad">
<div
v-for="group in columns"
:key="group.type"
class="dashboard-card quad-card"
:class="[`group-${group.type}`]">
<div class="quad-head">
<i class="group-dot"></i>
<span class="group-title">{{group.title}}</span>
<span class="quad-num" :class="{'num-zero': group.count === 0}">{{group.count}}</span>
</div>
<div
v-for="item in groupList(group, quadLimit)"
:key="item.id"
class="quad-row"
:class="{complete: item.complete_at}"
:style="$A.generateColorVarStyle(item.flow_item_color, [10], 'flow-item-custom-color', item.color ? {backgroundColor: item.color} : {})"
@click="openTask(item)">
<span class="cell-check" @click.stop>
<UserAvatar v-if="group.type === 'assist' && ownerUserid(item)" :userid="ownerUserid(item)" :size="22"/>
<TaskMenu v-else :task="item"/>
</span>
<span class="cell-name">{{item.name}}</span>
<em
class="status-pill"
:class="item.flow_item_name ? item.flow_item_status : {end: !!item.complete_at}"
@click.stop="openMenu($event, item)">{{item.flow_item_name || (item.complete_at ? $L('已完成') : $L('未完成'))}}</em>
<span v-if="item.p_name" class="quad-priority"><i class="priority-dot" :style="{backgroundColor: item.p_color}"></i></span>
<span v-if="item.sub_num > 0" class="quad-sub">{{item.sub_complete}}/{{item.sub_num}}</span>
<span v-if="item.end_at" class="t-end" :class="deadlineClass(item.end_at)">{{deadlineText(item.end_at)}}</span>
</div>
<div v-if="group.list.length === 0" class="quad-empty">
<span class="empty-icon"><Icon type="md-checkmark"/></span>
<span>{{quadEmptyText(group.type)}}</span>
</div>
<div v-else-if="group.list.length <= quadLimit" class="quad-filler">
{{group.type === 'today' && group.list.length === 1 ? $L('今天只有这一项,处理完就轻松了') : $L('全部显示完毕')}}
</div>
<div
v-else
class="card-more"
@click="toggleExpand(group.type)">
{{expandedGroups.includes(group.type) ? $L('收起') : $L('数量较多,仅显示最近 (*) 项,展开其余 (*) 项', quadLimit, group.list.length - quadLimit) + ' →'}}
</div>
</div>
</div>
</template>
</Scrollbar>
</div>
<!--部门负责人视角-->
<keep-alive>
<DashboardTeam
v-if="currentView === 'team'"
:initial-focus="teamFocusPref"
@stats="teamStats = $event"/>
</keep-alive>
</div>
</Scrollbar>
<DepartmentOwnerView v-model="deptViewShow"/>
</div>
</template>
<script>
import {mapGetters, mapState} from "vuex";
import TaskMenu from "./components/TaskMenu";
import DashboardTeam from "./components/DashboardTeam";
import DepartmentOwnerView from "./components/DepartmentOwnerView";
import dashboardTimeMixin from "./components/dashboard-time-mixin";
import emitter from "../../store/events";
const hiddenCaches = []
const prefsCache = {
view: 'my',
layout: 'list',
overrides: {},
focus: 'overdue',
}
export default {
components: {TaskMenu},
components: {TaskMenu, DashboardTeam, DepartmentOwnerView},
mixins: [dashboardTimeMixin],
data() {
return {
nowTime: $A.dayjs().unix(),
nowInter: null,
licenseTimer: null,
loadIng: 0,
dashboard: 'today',
view: prefsCache.view,
layout: prefsCache.layout,
// /{type: 'open'|'close'}
collapsedOverrides: prefsCache.overrides,
strategySnapshot: null,
//
visibleSnapshot: null,
snapshotPending: false,
teamFocusPref: prefsCache.focus,
expandedGroups: [],
groupLimit: 10,
quadLimit: 5,
flashType: '',
flashTimer: null,
deptViewShow: false,
teamStats: {member_count: 0, blocks: {}},
mateName: /macintosh|mac os x/i.test(navigator.userAgent) ? '⌘' : 'Ctrl',
warningMsg: '',
hiddenColumns: hiddenCaches,
}
},
async beforeRouteEnter(to, from, next) {
hiddenCaches.push(...(await $A.IDBArray("dashboardHiddenColumns")))
next()
prefsCache.view = await $A.IDBString("dashboardView", "my")
prefsCache.layout = await $A.IDBString("dashboardLayout", "list")
prefsCache.focus = await $A.IDBString("dashboardTeamFocus", "overdue")
// /
const collapsed = $A.jsonParse(await $A.IDBString("dashboardCollapsed", ""), {})
prefsCache.overrides = collapsed.date === $A.daytz().format("YYYY-MM-DD") && $A.isJson(collapsed.overrides)
? collapsed.overrides
: {}
next(vm => {
vm.view = ['my', 'team'].includes(prefsCache.view) ? prefsCache.view : 'my'
vm.layout = ['list', 'quad'].includes(prefsCache.layout) ? prefsCache.layout : 'list'
vm.collapsedOverrides = prefsCache.overrides
vm.teamFocusPref = prefsCache.focus
})
},
activated() {
this.$store.dispatch("getTaskForDashboard", 600);
this.loadInterval(true);
this.loadLicense(true);
//
this.takeSnapshot();
this.snapshotPending = true;
},
deactivated() {
@ -148,17 +312,51 @@ export default {
},
computed: {
...mapState(['systemConfig', 'userInfo', 'userIsAdmin', 'cacheTasks', 'taskCompleteTemps', 'loadDashboardTasks']),
...mapState(['systemConfig', 'userInfo', 'userIsAdmin', 'cacheProjects', 'cacheTasks', 'cacheDepartmentOwnerIds', 'taskCompleteTemps', 'loadDashboardTasks']),
...mapGetters(['dashboardTask', 'assistTask', 'transforTasks']),
columns({hiddenColumns, dashboardTask, assistTask}) {
teamEnabled({systemConfig, userInfo}) {
return systemConfig.department_owner_project_view === 'open' && (userInfo.managed_departments || []).length > 0;
},
currentView({view, teamEnabled}) {
return teamEnabled && view === 'team' ? 'team' : 'my';
},
// cacheTasksdashboardTask getter
upcomingTask({cacheTasks, taskCompleteTemps}) {
const now = $A.daytz();
const filterTask = (task, checkCompleted = true) => {
if (task.archived_at) {
return false;
}
if (task.complete_at && checkCompleted === true) {
return false;
}
if (!task.start_at || $A.dayjs(task.start_at) <= now) {
return false;
}
return task.owner == 1;
};
let array = cacheTasks.filter(task => filterTask(task));
const count = array.length;
if (taskCompleteTemps.length > 0) {
const tmps = cacheTasks.filter(task => taskCompleteTemps.includes(task.id) && filterTask(task, false));
if (tmps.length > 0) {
array = array.concat(tmps);
}
}
return {count, list: array};
},
columns({dashboardTask, assistTask}) {
const list = [];
['today', 'overdue', 'todo'].some(type => {
['overdue', 'today', 'todo'].some(type => {
let data = this.transforTasks(dashboardTask[type]);
list.push({
type,
title: this.getTitle(type),
hidden: hiddenColumns.includes(type),
hidden: this.isCollapsed(type),
count: dashboardTask[`${type}_count`],
list: data.sort((a, b) => {
return $A.sortDay(a.end_at || "2099-12-31 23:59:59", b.end_at || "2099-12-31 23:59:59");
@ -168,7 +366,7 @@ export default {
list.push({
type: 'assist',
title: this.getTitle('assist'),
hidden: hiddenColumns.includes('assist'),
hidden: this.isCollapsed('assist'),
count: assistTask.length,
list: assistTask.sort((a, b) => {
return $A.sortDay(a.end_at || "2099-12-31 23:59:59", b.end_at || "2099-12-31 23:59:59");
@ -177,23 +375,115 @@ export default {
return list;
},
total() {
const {dashboardTask} = this;
return dashboardTask.today_count + dashboardTask.overdue_count + dashboardTask.todo_count;
//
listAllColumns({columns, upcomingTask}) {
const list = [...columns];
list.splice(3, 0, {
type: 'upcoming',
title: this.getTitle('upcoming'),
hidden: this.isCollapsed('upcoming'),
count: upcomingTask.count,
list: this.transforTasks(upcomingTask.list).sort((a, b) => {
return $A.sortDay(a.start_at, b.start_at);
})
});
return list;
},
wrapperStyle({warningMsg}) {
return warningMsg ? {
'max-height': 'calc(100% - 50px)'
} : null
},
dashboardHello({systemConfig, userInfo}) {
let hello = '欢迎您,{username}';
if (systemConfig.system_welcome) {
hello = systemConfig.system_welcome
//
listColumns({listAllColumns, visibleSnapshot}) {
if (visibleSnapshot === null) {
return listAllColumns.filter(group => group.count > 0);
}
return this.$L(hello.replace(/\{username}/g, userInfo.nickname))
return listAllColumns.filter(group => visibleSnapshot.includes(group.type));
},
overdueMaxDays({dashboardTask, nowTime}) {
const now = $A.daytz(nowTime).startOf('day');
return (dashboardTask.overdue || []).reduce((max, task) => {
if (!task.end_at) {
return max;
}
return Math.max(max, now.diff($A.dayjs(task.end_at).startOf('day'), 'day'));
}, 0);
},
todayNearest({dashboardTask}) {
const list = (dashboardTask.today || []).filter(task => task.end_at);
if (list.length === 0) {
return '';
}
const nearest = list.reduce((min, task) => min === null || task.end_at < min ? task.end_at : min, null);
return $A.dayjs(nearest).format('HH:mm');
},
assistTodayCount({assistTask, nowTime}) {
const today = $A.daytz(nowTime).format('YYYY-MM-DD');
return assistTask.filter(task => task.end_at && $A.dayjs(task.end_at).format('YYYY-MM-DD') === today).length;
},
todayText({nowTime}) {
const now = $A.daytz(nowTime);
const weeks = [this.$L('周日'), this.$L('周一'), this.$L('周二'), this.$L('周三'), this.$L('周四'), this.$L('周五'), this.$L('周六')];
return `${this.$L('(*)月(*)日', now.month() + 1, now.date())} ${weeks[now.day()]}`;
},
mySummary({dashboardTask}) {
if (dashboardTask.overdue_count > 0) {
return this.$L('有 (*) 项已超期,建议先处理', dashboardTask.overdue_count);
}
if (dashboardTask.today_count > 0) {
return this.$L('有 (*) 项今日到期', dashboardTask.today_count);
}
return this.$L('今日无到期任务');
},
teamSummary({teamStats}) {
const blocks = teamStats.blocks || {};
if (blocks.overdue > 0) {
if (blocks.overdue_owner_count > 0) {
return this.$L('团队有 (*) 项已超期,涉及 (*) 人', blocks.overdue, blocks.overdue_owner_count);
}
return this.$L('团队有 (*) 项已超期', blocks.overdue);
}
if (blocks.week_completed > 0) {
return this.$L('团队本周已完成 (*) 项', blocks.week_completed);
}
return '';
},
deptLabel({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('我的部门');
}
if (selected.length === 1) {
return selected[0].name;
}
return `${selected[0].name} +${selected.length - 1}`;
},
dashboardHello({systemConfig, userInfo, nowTime}) {
let hello;
if (systemConfig.system_welcome) {
hello = systemConfig.system_welcome;
} else {
const hour = $A.daytz(nowTime).hour();
if (hour < 5) {
hello = '夜深了,{username}';
} else if (hour < 11) {
hello = '早上好,{username}';
} else if (hour < 14) {
hello = '中午好,{username}';
} else if (hour < 18) {
hello = '下午好,{username}';
} else {
hello = '晚上好,{username}';
}
}
return this.$L(hello).replace(/\{username}/g, userInfo.nickname);
}
},
@ -207,6 +497,13 @@ export default {
if (active) {
this.$store.dispatch("getTaskForDashboard", 600)
}
},
loadDashboardTasks(loading) {
if (!loading && this.snapshotPending) {
this.snapshotPending = false
this.takeSnapshot()
}
}
},
@ -216,40 +513,155 @@ export default {
case 'today':
return this.$L('今日到期');
case 'overdue':
return this.$L('超期任务');
return this.$L('超期');
case 'todo':
return this.$L('待完成任务');
return this.$L('待完成');
case 'upcoming':
return this.$L('待开始');
case 'assist':
return this.$L('协助的任务');
return this.$L('协助的');
default:
return '';
}
},
scrollTo(type) {
const refs = this.$refs[`type_${type}`]
if (refs) {
const index = this.hiddenColumns.indexOf(type);
if (index !== -1) {
this.hiddenColumns.splice(index, 1)
}
this.$nextTick(_ => {
$A.scrollToView(refs[0], {
behavior: 'smooth',
inline: 'end',
});
})
setView(view) {
this.view = view;
prefsCache.view = view;
$A.IDBSave("dashboardView", view);
},
setLayout(layout) {
this.layout = layout;
prefsCache.layout = layout;
$A.IDBSave("dashboardLayout", layout);
},
/**
* 分组是否收起手动记忆当天优先其余走策略快照
*/
isCollapsed(type) {
const override = this.collapsedOverrides[type];
if (override) {
return override === 'close';
}
return (this.strategySnapshot || []).includes(type);
},
/**
* 策略快照行动优先
* - 空组不显示全部为空时列表整卡显示缺省态
* - 显示的组默认展开待完成量大且行动优先级低超期或今日到期有数据时默认收起
*/
takeSnapshot() {
const counts = {
overdue: this.dashboardTask.overdue_count,
today: this.dashboardTask.today_count,
todo: this.dashboardTask.todo_count,
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');
}
if (counts.upcoming > 0 && (counts.overdue > 0 || counts.today > 0)) {
collapsed.push('upcoming');
}
this.strategySnapshot = collapsed;
},
toggleGroup(type) {
this.$set(this.collapsedOverrides, type, this.isCollapsed(type) ? 'open' : 'close');
prefsCache.overrides = this.collapsedOverrides;
$A.IDBSave("dashboardCollapsed", JSON.stringify({
date: $A.daytz().format("YYYY-MM-DD"),
overrides: this.collapsedOverrides,
}));
},
toggleExpand(type) {
if (this.expandedGroups.includes(type)) {
this.expandedGroups = this.expandedGroups.filter(item => item !== type);
} else {
this.expandedGroups.push(type);
}
},
onDashboardHidden(type) {
const index = this.hiddenColumns.indexOf(type);
if (index === -1) {
this.hiddenColumns.push(type)
} else {
this.hiddenColumns = this.hiddenColumns.filter(item => item !== type)
groupList(group, limit) {
if (this.expandedGroups.includes(group.type)) {
return group.list;
}
$A.IDBSave("dashboardHiddenColumns", this.hiddenColumns)
return group.list.slice(0, limit);
},
listEmptyText(type) {
switch (type) {
case 'overdue':
return this.$L('暂无已超期任务');
case 'today':
return this.$L('暂无今日到期任务');
case 'todo':
return this.$L('暂无待完成任务');
case 'upcoming':
return this.$L('暂无待开始任务');
case 'assist':
return this.$L('暂无协助的任务');
default:
return this.$L('暂无任务');
}
},
quadEmptyText(type) {
switch (type) {
case 'overdue':
return this.$L('没有超期任务,保持住');
case 'today':
return this.$L('今日无到期任务');
case 'todo':
return this.$L('暂无待完成任务');
case 'assist':
return this.$L('暂无协助的任务');
default:
return this.$L('暂无任务');
}
},
scrollTo(type) {
const column = this.listAllColumns.find(group => group.type === type);
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);
}
this.$nextTick(_ => {
const refs = this.$refs[`group_${type}`];
if (refs && refs[0]) {
$A.scrollToView(refs[0], {
behavior: 'smooth',
block: 'start',
});
this.flashType = type;
this.flashTimer && clearTimeout(this.flashTimer);
this.flashTimer = setTimeout(_ => this.flashType = '', 1600);
}
})
},
projectName(task) {
const project = this.cacheProjects.find(({id}) => id == task.project_id);
return project ? project.name : '';
},
ownerUserid(task) {
const owner = (task.task_user || []).find(user => user.owner === 1);
return owner ? owner.userid : 0;
},
openSearch() {
@ -264,23 +676,6 @@ export default {
this.$store.state.taskOperation = {event, task}
},
expiresFormat(date) {
return $A.countDownFormat(this.nowTime, date)
},
loadInterval(load) {
if (this.nowInter) {
clearInterval(this.nowInter)
this.nowInter = null;
}
if (load === false) {
return
}
this.nowInter = setInterval(_ => {
this.nowTime = $A.dayjs().unix()
}, 1000)
},
loadLicense(load) {
if (this.licenseTimer) {
clearTimeout(this.licenseTimer)

File diff suppressed because it is too large Load Diff