mirror of
https://github.com/kuaifan/dootask.git
synced 2026-07-22 05:59:58 +00:00
feat: 完善管理后台团队仪表盘
This commit is contained in:
parent
844db09c12
commit
6666605f7d
@ -12,6 +12,7 @@
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"jsx": "preserve",
|
||||
"ignoreDeprecations": "6.0",
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
|
||||
@ -2534,3 +2534,58 @@ AI任务分析
|
||||
服务器返回错误(HTTP (*))
|
||||
你撤回了一条消息
|
||||
重新编辑
|
||||
四象限
|
||||
个人视角
|
||||
部门任务总览
|
||||
(*) 人
|
||||
项
|
||||
最久 (*) 天
|
||||
今天 (*) 项到期
|
||||
还有 (*) 项
|
||||
待完成
|
||||
我协助的
|
||||
(*)月(*)日
|
||||
有 (*) 项已超期,建议先处理
|
||||
有 (*) 项今日到期
|
||||
今日无到期任务
|
||||
团队有 (*) 项已超期,涉及 (*) 人
|
||||
团队有 (*) 项已超期
|
||||
团队本周已完成 (*) 项
|
||||
查看任务
|
||||
涉及 (*) 人
|
||||
3 天内到期
|
||||
本周完成
|
||||
较上周
|
||||
与上周持平
|
||||
成员任务分配
|
||||
(*) 项
|
||||
未分配
|
||||
查看全部 (*) 人
|
||||
优先级分布
|
||||
未完成 (*) 项
|
||||
点击任一档位可下钻到任务列表
|
||||
高优先级
|
||||
重点关注任务
|
||||
未分配负责人
|
||||
待分配
|
||||
全部显示完毕
|
||||
逾期 (*) 天
|
||||
逾期 (*) 小时
|
||||
夜深了,{username}
|
||||
早上好,{username}
|
||||
中午好,{username}
|
||||
下午好,{username}
|
||||
晚上好,{username}
|
||||
演示数据,接口对接后可打开任务详情
|
||||
服务器时间
|
||||
暂无已超期任务
|
||||
暂无今日到期任务
|
||||
暂无待完成任务
|
||||
暂无协助的任务
|
||||
没有超期任务,保持住
|
||||
今天只有这一项,处理完就轻松了
|
||||
数量较多,仅显示最近 (*) 项,展开其余 (*) 项
|
||||
太棒了,当前没有需要处理的任务
|
||||
我的部门
|
||||
待开始
|
||||
暂无待开始任务
|
||||
|
||||
379
resources/assets/js/pages/manage/components/DashboardTeam.vue
Normal file
379
resources/assets/js/pages/manage/components/DashboardTeam.vue
Normal 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>
|
||||
242
resources/assets/js/pages/manage/components/dashboard-team-mock.js
vendored
Normal file
242
resources/assets/js/pages/manage/components/dashboard-team-mock.js
vendored
Normal file
@ -0,0 +1,242 @@
|
||||
/**
|
||||
* 团队概览 Mock 数据(仅接口对接前使用,确认交互后整体删除本文件)
|
||||
*
|
||||
* 结构与规划中的接口返回保持一致,便于无缝替换:
|
||||
* - fetchTeamStats() → api/dashboard/team/stats
|
||||
* - fetchTeamTasks() → api/dashboard/team/tasks?type=xxx&page=N(20条/页)
|
||||
*
|
||||
* 口径说明(与后端实现约定一致):
|
||||
* - 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 条
|
||||
* 超期 12、3 日内到期 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);
|
||||
});
|
||||
}
|
||||
80
resources/assets/js/pages/manage/components/dashboard-time-mixin.js
vendored
Normal file
80
resources/assets/js/pages/manage/components/dashboard-time-mixin.js
vendored
Normal 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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -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"></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"></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"></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"></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 ? '' : ''"></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"></i>
|
||||
</div>
|
||||
<div v-if="item.sub_num > 0" class="item-icon">
|
||||
<i class="taskfont"></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"></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}"></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';
|
||||
},
|
||||
|
||||
// 待开始:还没到开始时间的任务(数据已在 cacheTasks,dashboardTask 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)
|
||||
|
||||
1677
resources/assets/sass/pages/page-dashboard.scss
vendored
1677
resources/assets/sass/pages/page-dashboard.scss
vendored
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user