mirror of
https://github.com/kuaifan/dootask.git
synced 2026-09-06 12:09:20 +00:00
feat(dashboard): 完善负责人视角界面与范围切换
This commit is contained in:
parent
7ad27e3326
commit
71ca6ba249
@ -357,6 +357,21 @@ SMTP服务器
|
|||||||
任务提醒
|
任务提醒
|
||||||
职位/职称
|
职位/职称
|
||||||
验收/测试
|
验收/测试
|
||||||
|
(*) 超期
|
||||||
|
未来 3 天内暂无到期任务
|
||||||
|
暂无高优先级任务
|
||||||
|
所有任务均已分配负责人
|
||||||
|
当前筛选条件下暂无任务
|
||||||
|
可以切换上方条件查看其他任务
|
||||||
|
数据更新说明
|
||||||
|
页面统计数据在 60 秒内复用,重点关注任务列表除外。
|
||||||
|
上次更新:(*)
|
||||||
|
立即刷新
|
||||||
|
刷新成功
|
||||||
|
查看全部 (*) 个优先级
|
||||||
|
任务概况
|
||||||
|
流程阶段
|
||||||
|
未完成总数
|
||||||
上传图片
|
上传图片
|
||||||
上传失败
|
上传失败
|
||||||
上传成功
|
上传成功
|
||||||
@ -2599,3 +2614,9 @@ AI任务分析
|
|||||||
本周 (*) 项
|
本周 (*) 项
|
||||||
当前没有待处理任务
|
当前没有待处理任务
|
||||||
可以新建一项任务,或等待新的工作安排
|
可以新建一项任务,或等待新的工作安排
|
||||||
|
选择需要统计的部门,包含所选部门及其所有下级部门。
|
||||||
|
部门范围与负责人仪表盘保持一致;开启后,可只读查看范围内成员参与的项目和任务。
|
||||||
|
项目负责人视角
|
||||||
|
开启后在项目列表中显示额外的只读项目
|
||||||
|
选择团队范围
|
||||||
|
在项目列表中启用
|
||||||
|
|||||||
@ -649,6 +649,7 @@ export default {
|
|||||||
'mobileTabbar',
|
'mobileTabbar',
|
||||||
'longpressData',
|
'longpressData',
|
||||||
'departmentOwnerProjectsRefreshing',
|
'departmentOwnerProjectsRefreshing',
|
||||||
|
'departmentOwnerProjectViewEnabled',
|
||||||
|
|
||||||
'mcpServerStatus',
|
'mcpServerStatus',
|
||||||
'microAppsIds'
|
'microAppsIds'
|
||||||
@ -823,8 +824,8 @@ export default {
|
|||||||
name: '负责人视角',
|
name: '负责人视角',
|
||||||
divided: !userIsAdmin,
|
divided: !userIsAdmin,
|
||||||
visible: true,
|
visible: true,
|
||||||
selected: this.cacheDepartmentOwnerIds.length > 0,
|
selected: this.departmentOwnerProjectViewEnabled,
|
||||||
selectedCount: this.cacheDepartmentOwnerIds.length,
|
selectedCount: this.departmentOwnerProjectViewEnabled ? this.cacheDepartmentOwnerIds.length : 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
array.push(...[
|
array.push(...[
|
||||||
@ -872,7 +873,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
ownerProjectTabsVisible() {
|
ownerProjectTabsVisible() {
|
||||||
return this.departmentOwnerViewAvailable && this.cacheDepartmentOwnerIds.length > 0;
|
return this.departmentOwnerViewAvailable && this.departmentOwnerProjectViewEnabled;
|
||||||
},
|
},
|
||||||
|
|
||||||
ownerProjectTabs() {
|
ownerProjectTabs() {
|
||||||
|
|||||||
@ -40,48 +40,73 @@
|
|||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<span class="head-title">{{$L('成员任务分配')}}</span>
|
<span class="head-title">{{$L('成员任务分配')}}</span>
|
||||||
<span class="head-legend">
|
<span class="head-legend">
|
||||||
<span><i style="background:#1e9e55"></i>{{$L('进行中')}}</span>
|
|
||||||
<span><i style="background:#bfe3cd"></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:#1e9e55"></i>{{$L('进行中')}}</span>
|
||||||
<span><i style="background:#d94f46"></i>{{$L('已超期')}}</span>
|
<span><i style="background:#6ba7d8"></i>{{$L('验收/测试')}}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ul class="member-list">
|
<div v-if="showStatsLoading" class="team-stats-loading"><Loading/></div>
|
||||||
<li v-for="member in visibleMembers" :key="member.nickname" class="member-row" @click="onMember(member)">
|
<template v-else>
|
||||||
<span class="mock-avatar" :style="{backgroundColor: member.color}">{{member.nickname.substring(0, 1)}}</span>
|
<ul class="member-list">
|
||||||
<span class="member-name">{{member.nickname}}</span>
|
<li v-for="member in visibleMembers" :key="member.userid" class="member-row" @click="onMember(member)">
|
||||||
<span class="member-track">
|
<UserAvatar class="team-avatar" :userid="member.userid" :size="26"/>
|
||||||
<span class="member-bar" :style="{width: memberBarWidth(member)}">
|
<span class="member-name">{{member.nickname}}</span>
|
||||||
<i v-if="member.segments.progress > 0" :style="{flex: member.segments.progress, background: '#1e9e55'}"></i>
|
<ETooltip
|
||||||
<i v-if="member.segments.start > 0" :style="{flex: member.segments.start, background: '#bfe3cd'}"></i>
|
:disabled="$isEEUIApp || $store.state.windowTouch || member.total === 0"
|
||||||
<i v-if="member.segments.test > 0" :style="{flex: member.segments.test, background: '#6ba7d8'}"></i>
|
:open-delay="300"
|
||||||
<i v-if="member.overdue > 0" :style="{flex: member.overdue, background: '#d94f46'}"></i>
|
effect="light"
|
||||||
|
placement="top"
|
||||||
|
popper-class="dashboard-member-progress-popper">
|
||||||
|
<span class="member-track">
|
||||||
|
<span class="member-bar" :style="{width: memberBarWidth(member)}">
|
||||||
|
<i v-if="member.segments.start > 0" :style="{flex: member.segments.start, background: '#bfe3cd'}"></i>
|
||||||
|
<i v-if="member.segments.progress > 0" :style="{flex: member.segments.progress, background: '#1e9e55'}"></i>
|
||||||
|
<i v-if="member.segments.test > 0" :style="{flex: member.segments.test, background: '#6ba7d8'}"></i>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<div slot="content" class="member-progress-tooltip">
|
||||||
|
<span class="progress-tooltip-title">{{$L('任务概况')}}</span>
|
||||||
|
<div class="progress-tooltip-row">
|
||||||
|
<span class="progress-tooltip-label">{{$L('未完成总数')}}</span>
|
||||||
|
<strong>{{$L('(*) 项', member.total)}}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="progress-tooltip-row row-overdue">
|
||||||
|
<span class="progress-tooltip-label">{{$L('已超期')}}</span>
|
||||||
|
<strong>{{$L('(*) 项', member.overdue)}} · {{memberOverduePercent(member)}}%</strong>
|
||||||
|
</div>
|
||||||
|
<span class="progress-tooltip-title title-stage">{{$L('流程阶段')}}</span>
|
||||||
|
<div v-for="segment in memberProgressDetails(member)" :key="segment.type" class="progress-tooltip-row">
|
||||||
|
<span class="progress-tooltip-label"><i :style="{background: segment.color}"></i>{{segment.label}}</span>
|
||||||
|
<strong>{{$L('(*) 项', segment.count)}} · {{segment.percent}}%</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ETooltip>
|
||||||
|
<span class="member-num" :title="memberSummary(member)">
|
||||||
|
<em v-if="member.overdue > 0" class="num-overdue">{{$L('(*) 超期', member.overdue)}} · </em>{{$L('(*) 项', member.total)}}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</li>
|
||||||
<span class="member-num">
|
<li v-if="blocks.no_owner > 0" class="member-row member-noowner" @click="onBlock('noowner')">
|
||||||
<em v-if="member.overdue > 0" class="num-overdue">{{member.overdue}} {{$L('超期')}} · </em>{{$L('(*) 项', member.total)}}
|
<span class="mock-avatar avatar-none">?</span>
|
||||||
</span>
|
<span class="member-name">{{$L('未分配')}}</span>
|
||||||
</li>
|
<span class="member-track">
|
||||||
<li v-if="blocks.no_owner > 0" class="member-row member-noowner" @click="onBlock('noowner')">
|
<span class="member-bar bar-hatched" :style="{width: memberBarWidth({total: blocks.no_owner})}"></span>
|
||||||
<span class="mock-avatar avatar-none">?</span>
|
</span>
|
||||||
<span class="member-name">{{$L('未分配')}}</span>
|
<span class="member-num num-noowner">{{$L('(*) 项', blocks.no_owner)}}</span>
|
||||||
<span class="member-track">
|
</li>
|
||||||
<span class="member-bar bar-hatched" :style="{width: memberBarWidth({total: blocks.no_owner})}"></span>
|
</ul>
|
||||||
</span>
|
<div v-if="members.length > memberLimit" class="card-more" @click="memberExpand = !memberExpand">
|
||||||
<span class="member-num num-noowner">{{$L('(*) 项', blocks.no_owner)}}</span>
|
{{memberExpand ? $L('收起') : $L('查看全部 (*) 人', members.length) + ' →'}}
|
||||||
</li>
|
</div>
|
||||||
</ul>
|
</template>
|
||||||
<div v-if="members.length > memberLimit" class="card-more" @click="memberExpand = !memberExpand">
|
|
||||||
{{memberExpand ? $L('收起') : $L('查看全部 (*) 人', members.length) + ' →'}}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-card team-priority">
|
<div class="dashboard-card team-priority">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<span class="head-title">{{$L('优先级分布')}}</span>
|
<span class="head-title">{{$L('优先级分布')}}</span>
|
||||||
<span class="head-note">{{$L('未完成 (*) 项', blocks.uncompleted || 0)}}</span>
|
<span v-if="!showStatsLoading" class="head-note">{{$L('未完成 (*) 项', blocks.uncompleted || 0)}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="priority-list">
|
<div v-if="showStatsLoading" class="team-stats-loading"><Loading/></div>
|
||||||
<div v-for="item in priorityList" :key="item.level" class="priority-row" @click="onPriority(item)">
|
<div v-else class="priority-list">
|
||||||
|
<div v-for="item in visiblePriorityList" :key="item.level" class="priority-row" @click="onPriority(item)">
|
||||||
<div class="priority-head">
|
<div class="priority-head">
|
||||||
<span class="priority-name">{{item.name}}</span>
|
<span class="priority-name">{{item.name}}</span>
|
||||||
<span class="priority-num">{{$L('(*) 项', item.num)}} · {{item.percent}}%</span>
|
<span class="priority-num">{{$L('(*) 项', item.num)}} · {{item.percent}}%</span>
|
||||||
@ -90,66 +115,79 @@
|
|||||||
<i :style="{width: item.percent + '%', background: item.color}"></i>
|
<i :style="{width: item.percent + '%', background: item.color}"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="priority-tip">{{$L('点击任一档位可下钻到任务列表')}}</div>
|
<div
|
||||||
|
v-if="priorityList.length > priorityLimit"
|
||||||
|
class="priority-tip priority-more"
|
||||||
|
@click="priorityExpand = !priorityExpand">
|
||||||
|
{{priorityExpand ? $L('收起') : $L('查看全部 (*) 个优先级', priorityList.length) + ' →'}}
|
||||||
|
</div>
|
||||||
|
<div v-else class="priority-tip">{{$L('点击任一档位可下钻到任务列表')}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--重点关注任务-->
|
<!--重点关注任务-->
|
||||||
<div ref="focusCard" class="dashboard-card team-focus">
|
<div class="team-focus-section" :class="{'focus-scroll-reserve': focusScrollReserved}">
|
||||||
<div class="card-head focus-head">
|
<div ref="focusCard" class="dashboard-card team-focus">
|
||||||
<span class="head-title">{{$L('重点关注任务')}}</span>
|
<div class="card-head focus-head">
|
||||||
<span
|
<span class="head-title">{{$L('重点关注任务')}}</span>
|
||||||
v-for="chip in chips"
|
<span
|
||||||
:key="chip.type"
|
v-for="chip in chips"
|
||||||
class="focus-chip"
|
:key="chip.type"
|
||||||
:class="[`chip-${chip.type}`, {'chip-active': !extraFilter && focus === chip.type}]"
|
class="focus-chip"
|
||||||
@click="setFocus(chip.type)">{{chip.label}} {{chip.num}}</span>
|
:class="[`chip-${chip.type}`, {'chip-active': !extraFilter && focus === chip.type}]"
|
||||||
<span v-if="extraFilter" class="focus-chip chip-extra chip-active" @click="clearFilter">
|
@click="setFocus(chip.type)">{{chip.label}} {{chip.num}}</span>
|
||||||
{{extraFilter.label}}<i>×</i>
|
<span v-if="extraFilter" class="focus-chip chip-extra chip-active" @click="clearFilter">
|
||||||
</span>
|
{{extraFilter.label}}<i>×</i>
|
||||||
</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>
|
||||||
<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>
|
||||||
<div v-if="currentList.loading" class="focus-loading"><Loading/></div>
|
<div class="focus-table">
|
||||||
<div v-else-if="currentList.list.length === 0" class="focus-empty">{{$L('暂无任务')}}</div>
|
<div class="focus-row focus-thead">
|
||||||
<div v-else-if="currentList.hasMore" class="card-more" @click="loadMore">{{$L('加载更多')}}({{$L('还有 (*) 项', currentList.total - currentList.list.length)}})</div>
|
<span>{{$L('任务')}}</span>
|
||||||
<div v-else-if="currentList.total > pageSize" class="card-more more-done">{{$L('全部显示完毕')}}</div>
|
<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">
|
||||||
|
<UserAvatar class="team-avatar" :userid="item.owner.userid" :size="20"/>
|
||||||
|
<span class="owner-name">{{item.owner.nickname}}</span>
|
||||||
|
<em v-if="item.owners.length > 1" class="owner-more">+{{item.owners.length - 1}}</em>
|
||||||
|
</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 && !currentList.silent" class="focus-loading"><Loading/></div>
|
||||||
|
<div v-else-if="currentList.list.length === 0" class="focus-empty">
|
||||||
|
<span class="focus-empty-icon"><Icon type="md-checkmark"/></span>
|
||||||
|
<strong>{{focusEmptyTitle}}</strong>
|
||||||
|
<span>{{$L('可以切换上方条件查看其他任务')}}</span>
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {fetchTeamStats, fetchTeamTasks} from "./dashboard-team-mock";
|
import {mapState} from "vuex";
|
||||||
import dashboardTimeMixin from "./dashboard-time-mixin";
|
import dashboardTimeMixin from "./dashboard-time-mixin";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -169,29 +207,40 @@ export default {
|
|||||||
priority: [],
|
priority: [],
|
||||||
members: [],
|
members: [],
|
||||||
},
|
},
|
||||||
|
statsLoading: false,
|
||||||
|
statsLoadedAt: 0,
|
||||||
|
statsToken: 0,
|
||||||
|
|
||||||
focus: ['overdue', 'soon', 'hi', 'noowner'].includes(this.initialFocus) ? this.initialFocus : 'overdue',
|
focus: ['overdue', 'soon', 'hi', 'noowner'].includes(this.initialFocus) ? this.initialFocus : 'overdue',
|
||||||
extraFilter: null,
|
extraFilter: null,
|
||||||
|
|
||||||
lists: {},
|
lists: {},
|
||||||
|
listTokens: {},
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
|
refreshSeconds: 60,
|
||||||
|
|
||||||
memberLimit: 6,
|
memberLimit: 6,
|
||||||
memberExpand: false,
|
memberExpand: false,
|
||||||
|
priorityLimit: 6,
|
||||||
|
priorityExpand: false,
|
||||||
|
focusScrollReserved: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.loadStats()
|
this.loadAll()
|
||||||
this.loadList()
|
|
||||||
},
|
},
|
||||||
|
|
||||||
activated() {
|
activated() {
|
||||||
this.loadInterval(true)
|
this.loadInterval(true)
|
||||||
|
if (this.statsLoadedAt > 0 && $A.dayjs().unix() - this.statsLoadedAt >= this.refreshSeconds) {
|
||||||
|
this.loadAll(true)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
deactivated() {
|
deactivated() {
|
||||||
this.loadInterval(false)
|
this.loadInterval(false)
|
||||||
|
this.focusScrollReserved = false
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
@ -199,6 +248,13 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState(['cacheDepartmentOwnerIds']),
|
||||||
|
|
||||||
|
scopeKey({cacheDepartmentOwnerIds}) {
|
||||||
|
const ids = (cacheDepartmentOwnerIds || []).map(id => parseInt(id)).filter(id => id > 0).sort((a, b) => a - b)
|
||||||
|
return ids.length > 0 ? ids.join(',') : 'all'
|
||||||
|
},
|
||||||
|
|
||||||
blocks({stats}) {
|
blocks({stats}) {
|
||||||
return stats.blocks || {}
|
return stats.blocks || {}
|
||||||
},
|
},
|
||||||
@ -211,8 +267,8 @@ export default {
|
|||||||
return memberExpand ? members : members.slice(0, memberLimit)
|
return memberExpand ? members : members.slice(0, memberLimit)
|
||||||
},
|
},
|
||||||
|
|
||||||
hasTestSegment({members}) {
|
showStatsLoading({statsLoading, statsLoadedAt}) {
|
||||||
return members.some(member => (member.segments?.test || 0) > 0)
|
return statsLoading && statsLoadedAt === 0
|
||||||
},
|
},
|
||||||
|
|
||||||
memberMax({members, blocks}) {
|
memberMax({members, blocks}) {
|
||||||
@ -230,6 +286,10 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
visiblePriorityList({priorityList, priorityExpand, priorityLimit}) {
|
||||||
|
return priorityExpand ? priorityList : priorityList.slice(0, priorityLimit)
|
||||||
|
},
|
||||||
|
|
||||||
highPriorityChip({stats, blocks}) {
|
highPriorityChip({stats, blocks}) {
|
||||||
// 与后端口径一致:系统优先级前 2 档视为高优先级
|
// 与后端口径一致:系统优先级前 2 档视为高优先级
|
||||||
const tops = (stats.priority || []).filter(item => item.level > 0).slice(0, 2)
|
const tops = (stats.priority || []).filter(item => item.level > 0).slice(0, 2)
|
||||||
@ -262,64 +322,179 @@ export default {
|
|||||||
return this.$L('与上周持平')
|
return this.$L('与上周持平')
|
||||||
},
|
},
|
||||||
|
|
||||||
currentKey({focus, extraFilter}) {
|
currentKey({scopeKey, focus, extraFilter}) {
|
||||||
if (extraFilter) {
|
if (extraFilter) {
|
||||||
return `${extraFilter.kind}:${extraFilter.value}`
|
return `${scopeKey}|${extraFilter.kind}:${extraFilter.value}`
|
||||||
}
|
}
|
||||||
return `type:${focus}`
|
return `${scopeKey}|type:${focus}`
|
||||||
},
|
},
|
||||||
|
|
||||||
currentList({lists, currentKey}) {
|
currentList({lists, currentKey}) {
|
||||||
return lists[currentKey] || {list: [], page: 1, total: 0, hasMore: false, loading: true}
|
return lists[currentKey] || {list: [], page: 1, total: 0, hasMore: false, loading: true, silent: false}
|
||||||
|
},
|
||||||
|
|
||||||
|
focusEmptyTitle({extraFilter, focus}) {
|
||||||
|
if (extraFilter) {
|
||||||
|
return this.$L('当前筛选条件下暂无任务')
|
||||||
|
}
|
||||||
|
switch (focus) {
|
||||||
|
case 'overdue':
|
||||||
|
return this.$L('暂无已超期任务')
|
||||||
|
case 'soon':
|
||||||
|
return this.$L('未来 3 天内暂无到期任务')
|
||||||
|
case 'hi':
|
||||||
|
return this.$L('暂无高优先级任务')
|
||||||
|
case 'noowner':
|
||||||
|
return this.$L('所有任务均已分配负责人')
|
||||||
|
default:
|
||||||
|
return this.$L('暂无任务')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
scopeKey() {
|
||||||
|
this.statsToken++
|
||||||
|
this.statsLoading = false
|
||||||
|
this.$emit('stats-loading', false)
|
||||||
|
this.stats = {member_count: 0, blocks: {}, priority: [], members: []}
|
||||||
|
this.statsLoadedAt = 0
|
||||||
|
this.lists = {}
|
||||||
|
this.listTokens = {}
|
||||||
|
this.extraFilter = null
|
||||||
|
this.memberExpand = false
|
||||||
|
this.priorityExpand = false
|
||||||
|
this.$emit('stats', this.stats)
|
||||||
|
this.loadAll(true)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
loadStats() {
|
deptParams() {
|
||||||
fetchTeamStats().then(stats => {
|
const ids = (this.cacheDepartmentOwnerIds || []).map(id => parseInt(id)).filter(id => id > 0)
|
||||||
this.stats = stats
|
return ids.length > 0 ? {department_owner_ids: ids.join(',')} : {}
|
||||||
this.$emit('stats', stats)
|
},
|
||||||
|
|
||||||
|
loadAll(force = false) {
|
||||||
|
const statsRequest = this.loadStats(force)
|
||||||
|
this.loadList(1, force)
|
||||||
|
return statsRequest
|
||||||
|
},
|
||||||
|
|
||||||
|
loadStats(force = false) {
|
||||||
|
if (this.statsLoading) {
|
||||||
|
return Promise.resolve(false)
|
||||||
|
}
|
||||||
|
if (!force && this.statsLoadedAt > 0 && $A.dayjs().unix() - this.statsLoadedAt < this.refreshSeconds) {
|
||||||
|
return Promise.resolve(true)
|
||||||
|
}
|
||||||
|
const scopeKey = this.scopeKey
|
||||||
|
const token = ++this.statsToken
|
||||||
|
this.statsLoading = true
|
||||||
|
this.$emit('stats-loading', true)
|
||||||
|
return this.$store.dispatch("call", {
|
||||||
|
url: 'dashboard/team/stats',
|
||||||
|
data: Object.assign(this.deptParams(), force ? {refresh: 1} : {}),
|
||||||
|
}).then(({data}) => {
|
||||||
|
if (scopeKey !== this.scopeKey || token !== this.statsToken) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
this.stats = Object.assign({member_count: 0, blocks: {}, priority: [], members: []}, data)
|
||||||
|
this.statsLoadedAt = $A.dayjs().unix()
|
||||||
|
this.$emit('stats', this.stats)
|
||||||
|
return true
|
||||||
|
}).catch(({msg}) => {
|
||||||
|
if (scopeKey === this.scopeKey && token === this.statsToken) {
|
||||||
|
this.statsLoadedAt = 0
|
||||||
|
msg && $A.messageWarning(msg)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}).finally(() => {
|
||||||
|
if (token === this.statsToken) {
|
||||||
|
this.statsLoading = false
|
||||||
|
this.$emit('stats-loading', false)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
refreshAll() {
|
||||||
|
return this.loadAll(true)
|
||||||
|
},
|
||||||
|
|
||||||
listParams(page) {
|
listParams(page) {
|
||||||
const params = {page}
|
const params = Object.assign(this.deptParams(), {page, pagesize: this.pageSize})
|
||||||
if (this.extraFilter?.kind === 'member') {
|
if (this.extraFilter?.kind === 'member') {
|
||||||
params.member = this.extraFilter.value
|
params.member_id = this.extraFilter.value
|
||||||
} else if (this.extraFilter?.kind === 'level') {
|
} else if (this.extraFilter?.kind === 'level') {
|
||||||
params.level = this.extraFilter.value
|
params.level = this.extraFilter.value
|
||||||
|
} else if (this.extraFilter?.kind === 'type') {
|
||||||
|
params.type = this.extraFilter.value
|
||||||
} else {
|
} else {
|
||||||
params.type = this.focus
|
params.type = this.focus
|
||||||
}
|
}
|
||||||
return params
|
return params
|
||||||
},
|
},
|
||||||
|
|
||||||
loadList(page = 1) {
|
loadList(page = 1, force = false) {
|
||||||
const key = this.currentKey
|
const key = this.currentKey
|
||||||
if (page === 1 && this.lists[key] && !this.lists[key].loading) {
|
const current = this.lists[key]
|
||||||
|
if (current?.loading) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const prev = page > 1 ? this.lists[key] : null
|
if (page === 1 && !force && current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const prev = current || null
|
||||||
|
const previousList = page > 1 || (force && prev) ? (prev?.list || []) : []
|
||||||
|
const scopeKey = this.scopeKey
|
||||||
|
const token = (this.listTokens[key] || 0) + 1
|
||||||
|
this.$set(this.listTokens, key, token)
|
||||||
this.$set(this.lists, key, {
|
this.$set(this.lists, key, {
|
||||||
list: prev ? prev.list : [],
|
list: previousList,
|
||||||
page,
|
page,
|
||||||
total: prev ? prev.total : 0,
|
total: prev ? prev.total : 0,
|
||||||
hasMore: false,
|
hasMore: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
silent: force && previousList.length > 0,
|
||||||
|
loadedAt: prev ? prev.loadedAt : 0,
|
||||||
})
|
})
|
||||||
fetchTeamTasks(this.listParams(page)).then(data => {
|
this.$store.dispatch("call", {
|
||||||
|
url: 'dashboard/team/tasks',
|
||||||
|
data: this.listParams(page),
|
||||||
|
}).then(({data}) => {
|
||||||
|
if (scopeKey !== this.scopeKey || token !== this.listTokens[key]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const rows = data.data || []
|
||||||
this.$set(this.lists, key, {
|
this.$set(this.lists, key, {
|
||||||
list: prev ? [...prev.list, ...data.list] : data.list,
|
list: page > 1 ? [...previousList, ...rows] : rows,
|
||||||
page: data.page,
|
page: data.current_page || page,
|
||||||
total: data.total,
|
total: data.total || 0,
|
||||||
hasMore: data.hasMore,
|
hasMore: (data.current_page || page) < (data.last_page || 1),
|
||||||
loading: false,
|
loading: false,
|
||||||
|
silent: false,
|
||||||
|
loadedAt: $A.dayjs().unix(),
|
||||||
})
|
})
|
||||||
|
}).catch(({msg}) => {
|
||||||
|
if (scopeKey === this.scopeKey && token === this.listTokens[key]) {
|
||||||
|
this.$set(this.lists, key, {
|
||||||
|
list: previousList,
|
||||||
|
page: prev ? prev.page : 1,
|
||||||
|
total: prev ? prev.total : 0,
|
||||||
|
hasMore: prev ? prev.hasMore : false,
|
||||||
|
loading: false,
|
||||||
|
silent: false,
|
||||||
|
loadedAt: prev ? prev.loadedAt : 0,
|
||||||
|
})
|
||||||
|
msg && previousList.length === 0 && $A.messageWarning(msg)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
loadMore() {
|
loadMore() {
|
||||||
this.loadList(this.currentList.page + 1)
|
if (!this.currentList.loading && this.currentList.hasMore) {
|
||||||
|
this.loadList(this.currentList.page + 1)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setFocus(type) {
|
setFocus(type) {
|
||||||
@ -339,11 +514,12 @@ export default {
|
|||||||
|
|
||||||
clearFilter() {
|
clearFilter() {
|
||||||
this.extraFilter = null
|
this.extraFilter = null
|
||||||
|
this.$nextTick(() => this.loadList())
|
||||||
},
|
},
|
||||||
|
|
||||||
onBlock(type) {
|
onBlock(type) {
|
||||||
if (type === 'uncompleted') {
|
if (type === 'uncompleted') {
|
||||||
this.scrollToFocus()
|
this.setFilter('type', 'uncompleted', this.$L('未完成'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.setFocus(type)
|
this.setFocus(type)
|
||||||
@ -351,18 +527,19 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onMember(member) {
|
onMember(member) {
|
||||||
this.setFilter('member', member.nickname, `${this.$L('成员')}: ${member.nickname}`)
|
this.setFilter('member', member.userid, `${this.$L('成员')}: ${member.nickname}`)
|
||||||
},
|
},
|
||||||
|
|
||||||
onPriority(item) {
|
onPriority(item) {
|
||||||
this.setFilter('level', item.level, `${this.$L('优先级')}: ${item.name}`)
|
this.setFilter('level', item.level, `${this.$L('优先级')}: ${item.name}`)
|
||||||
},
|
},
|
||||||
|
|
||||||
onTask() {
|
onTask(task) {
|
||||||
$A.messageInfo("演示数据,接口对接后可打开任务详情")
|
this.$store.dispatch("openTask", task)
|
||||||
},
|
},
|
||||||
|
|
||||||
scrollToFocus() {
|
scrollToFocus() {
|
||||||
|
this.focusScrollReserved = true
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
$A.scrollToView(this.$refs.focusCard, {
|
$A.scrollToView(this.$refs.focusCard, {
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
@ -374,6 +551,29 @@ export default {
|
|||||||
memberBarWidth(member) {
|
memberBarWidth(member) {
|
||||||
return `${Math.max(Math.round(member.total / this.memberMax * 100), 4)}%`
|
return `${Math.max(Math.round(member.total / this.memberMax * 100), 4)}%`
|
||||||
},
|
},
|
||||||
|
|
||||||
|
memberProgressDetails(member) {
|
||||||
|
const total = Math.max(member.total || 0, 1)
|
||||||
|
return [
|
||||||
|
{type: 'start', label: this.$L('待处理'), color: '#bfe3cd', count: member.segments.start || 0},
|
||||||
|
{type: 'progress', label: this.$L('进行中'), color: '#1e9e55', count: member.segments.progress || 0},
|
||||||
|
{type: 'test', label: this.$L('验收/测试'), color: '#6ba7d8', count: member.segments.test || 0},
|
||||||
|
].filter(item => item.count > 0).map(item => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
percent: Math.round(item.count / total * 1000) / 10,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
memberOverduePercent(member) {
|
||||||
|
return member.total > 0 ? Math.round(member.overdue / member.total * 1000) / 10 : 0
|
||||||
|
},
|
||||||
|
|
||||||
|
memberSummary(member) {
|
||||||
|
const total = this.$L('(*) 项', member.total)
|
||||||
|
return member.overdue > 0 ? `${this.$L('(*) 超期', member.overdue)} · ${total}` : total
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,36 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
:value="value"
|
:value="value"
|
||||||
:title="$L('负责人视角')"
|
:title="modalTitle"
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
width="520"
|
width="520"
|
||||||
@input="$emit('input', $event)">
|
@input="$emit('input', $event)">
|
||||||
<div class="department-owner-view-modal">
|
<div class="department-owner-view-modal">
|
||||||
<Alert type="info" show-icon>
|
<Alert type="info" show-icon>
|
||||||
{{$L('可查看所选部门及所有下级部门成员参与的项目和任务,仅支持只读查看。')}}
|
<template v-if="scopeOnly">{{$L('选择需要统计的部门,包含所选部门及其所有下级部门。')}}</template>
|
||||||
|
<template v-else>{{$L('部门范围与负责人仪表盘保持一致;开启后,可只读查看范围内成员参与的项目和任务。')}}</template>
|
||||||
</Alert>
|
</Alert>
|
||||||
<div v-if="managedDepartments.length > 1" class="department-owner-view-actions">
|
<div v-if="!scopeOnly" class="department-owner-view-switch">
|
||||||
<a href="javascript:void(0)" @click="draftIds=[]">{{$L('清空')}}</a>
|
<div>
|
||||||
<a href="javascript:void(0)" @click="draftIds=managedDepartments.map(item => item.id)">{{$L('全选')}}</a>
|
<strong>{{$L('在项目列表中启用')}}</strong>
|
||||||
<a href="javascript:void(0)" @click="reverseDraft">{{$L('反选')}}</a>
|
<span class="department-owner-view-switch-desc">{{$L('开启后在项目列表中显示额外的只读项目')}}</span>
|
||||||
</div>
|
|
||||||
<CheckboxGroup v-model="draftIds" class="department-owner-view-list">
|
|
||||||
<div
|
|
||||||
v-for="dept in managedDepartments"
|
|
||||||
:key="dept.id"
|
|
||||||
:class="['department-owner-view-item', draftIds.includes(dept.id) ? 'active' : '']"
|
|
||||||
@click="toggleDraft(dept.id)">
|
|
||||||
<div class="department-owner-view-icon">
|
|
||||||
<i class="taskfont"></i>
|
|
||||||
</div>
|
|
||||||
<div class="department-owner-view-name">{{dept.name}}</div>
|
|
||||||
<Checkbox class="department-owner-view-checkbox" :label="dept.id" @click.native.stop><span></span></Checkbox>
|
|
||||||
</div>
|
</div>
|
||||||
</CheckboxGroup>
|
<iSwitch v-model="draftProjectViewEnabled"/>
|
||||||
|
</div>
|
||||||
|
<template v-if="scopeOnly || draftProjectViewEnabled">
|
||||||
|
<div v-if="managedDepartments.length > 1" class="department-owner-view-actions">
|
||||||
|
<a href="javascript:void(0)" @click="draftIds=[]">{{$L('清空')}}</a>
|
||||||
|
<a
|
||||||
|
href="javascript:void(0)"
|
||||||
|
@click="draftIds=managedDepartments.map(item => item.id)">{{$L('全选')}}</a>
|
||||||
|
<a href="javascript:void(0)" @click="reverseDraft">{{$L('反选')}}</a>
|
||||||
|
</div>
|
||||||
|
<CheckboxGroup v-model="draftIds" class="department-owner-view-list">
|
||||||
|
<div
|
||||||
|
v-for="dept in managedDepartments"
|
||||||
|
:key="dept.id"
|
||||||
|
:class="['department-owner-view-item', draftIds.includes(dept.id) ? 'active' : '']"
|
||||||
|
@click="toggleDraft(dept.id)">
|
||||||
|
<div class="department-owner-view-icon">
|
||||||
|
<i class="taskfont"></i>
|
||||||
|
</div>
|
||||||
|
<div class="department-owner-view-name">{{dept.name}}</div>
|
||||||
|
<Checkbox
|
||||||
|
class="department-owner-view-checkbox"
|
||||||
|
:label="dept.id"
|
||||||
|
@click.native.stop><span></span></Checkbox>
|
||||||
|
</div>
|
||||||
|
</CheckboxGroup>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div slot="footer" class="adaption">
|
<div slot="footer" class="adaption">
|
||||||
<Button type="default" :disabled="applyLoading" @click="$emit('input', false)">{{$L('取消')}}</Button>
|
<Button type="default" :disabled="applyLoading" @click="$emit('input', false)">{{$L('取消')}}</Button>
|
||||||
<Button type="primary" :loading="applyLoading" @click="apply">{{$L('确定')}}</Button>
|
<Button type="primary" :disabled="draftIds.length === 0" :loading="applyLoading" @click="apply">{{$L('确定')}}</Button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
@ -42,15 +57,20 @@ export default {
|
|||||||
name: "DepartmentOwnerView",
|
name: "DepartmentOwnerView",
|
||||||
props: {
|
props: {
|
||||||
value: Boolean,
|
value: Boolean,
|
||||||
|
scopeOnly: Boolean,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
draftIds: [],
|
draftIds: [],
|
||||||
|
draftProjectViewEnabled: false,
|
||||||
applyLoading: false,
|
applyLoading: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['userInfo', 'cacheDepartmentOwnerIds']),
|
...mapState(['userInfo', 'cacheDepartmentOwnerIds', 'departmentOwnerProjectViewEnabled']),
|
||||||
|
modalTitle() {
|
||||||
|
return this.scopeOnly ? this.$L('选择团队范围') : this.$L('项目负责人视角');
|
||||||
|
},
|
||||||
managedDepartments() {
|
managedDepartments() {
|
||||||
return (this.userInfo.managed_departments || []).map(item => ({
|
return (this.userInfo.managed_departments || []).map(item => ({
|
||||||
...item,
|
...item,
|
||||||
@ -63,7 +83,11 @@ export default {
|
|||||||
immediate: true,
|
immediate: true,
|
||||||
handler(show) {
|
handler(show) {
|
||||||
if (show) {
|
if (show) {
|
||||||
this.draftIds = (this.cacheDepartmentOwnerIds || []).slice();
|
const cachedIds = (this.cacheDepartmentOwnerIds || []).slice();
|
||||||
|
this.draftIds = cachedIds.length > 0
|
||||||
|
? cachedIds
|
||||||
|
: this.managedDepartments.map(item => item.id);
|
||||||
|
this.draftProjectViewEnabled = this.departmentOwnerProjectViewEnabled;
|
||||||
} else {
|
} else {
|
||||||
this.applyLoading = false;
|
this.applyLoading = false;
|
||||||
}
|
}
|
||||||
@ -92,7 +116,10 @@ export default {
|
|||||||
}
|
}
|
||||||
this.applyLoading = true;
|
this.applyLoading = true;
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch("setDepartmentOwnerIds", this.draftIds);
|
await this.$store.dispatch("setDepartmentOwnerIds", this.scopeOnly ? this.draftIds : {
|
||||||
|
ids: this.draftIds,
|
||||||
|
projectViewEnabled: this.draftProjectViewEnabled,
|
||||||
|
});
|
||||||
this.$emit('input', false);
|
this.$emit('input', false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
$A.modalError(e?.msg || this.$L('切换失败'));
|
$A.modalError(e?.msg || this.$L('切换失败'));
|
||||||
@ -112,6 +139,27 @@ export default {
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
margin: 12px 8px 0;
|
margin: 12px 8px 0;
|
||||||
}
|
}
|
||||||
|
.department-owner-view-switch {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin: 14px 8px 4px;
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
strong {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.department-owner-view-switch-desc {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
.department-owner-view-list {
|
.department-owner-view-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
@on-click="onClick"
|
@on-click="onClick"
|
||||||
:readonly="readonly">
|
:readonly="readonly">
|
||||||
<template #titleTool>
|
<template #titleTool>
|
||||||
<Dropdown class="project-gstc-dropdown-filtr" trigger="click" @on-click="onSwitchColumn">
|
<Dropdown class="project-gstc-dropdown-filtr" trigger="click" @on-click="onSwitchColumn" transfer>
|
||||||
<Icon class="project-gstc-dropdown-icon" :class="{filtr:filtrProjectId > 0}" type="md-funnel" />
|
<Icon class="project-gstc-dropdown-icon" :class="{filtr:filtrProjectId > 0}" type="md-funnel" />
|
||||||
<DropdownMenu slot="list">
|
<DropdownMenu slot="list">
|
||||||
<DropdownItem :name="0" :class="{'dropdown-active':filtrProjectId == 0}">{{ $L('全部') }}</DropdownItem>
|
<DropdownItem :name="0" :class="{'dropdown-active':filtrProjectId == 0}">{{ $L('全部') }}</DropdownItem>
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
class="owner-view-button"
|
class="owner-view-button"
|
||||||
@click="departmentOwnerViewShow=true">
|
@click="departmentOwnerViewShow=true">
|
||||||
<i class="taskfont"></i>
|
<i class="taskfont"></i>
|
||||||
<em v-if="ownerDepartmentIds.length > 0">{{ownerDepartmentIds.length}}</em>
|
<em v-if="departmentOwnerProjectViewEnabled">{{ownerDepartmentIds.length}}</em>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="owner-project-wrapper">
|
<div class="owner-project-wrapper">
|
||||||
@ -155,7 +155,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['cacheProjects', 'loadProjects', 'longpressData', 'userInfo', 'systemConfig', 'cacheDepartmentOwnerIds', 'departmentOwnerProjectsRefreshing']),
|
...mapState(['cacheProjects', 'loadProjects', 'longpressData', 'userInfo', 'systemConfig', 'cacheDepartmentOwnerIds', 'departmentOwnerProjectsRefreshing', 'departmentOwnerProjectViewEnabled']),
|
||||||
|
|
||||||
managedDepartments() {
|
managedDepartments() {
|
||||||
return (this.userInfo.managed_departments || []).map(item => ({
|
return (this.userInfo.managed_departments || []).map(item => ({
|
||||||
@ -193,7 +193,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
ownerProjectTabsVisible() {
|
ownerProjectTabsVisible() {
|
||||||
return this.ownerViewAvailable && this.ownerDepartmentIds.length > 0;
|
return this.ownerViewAvailable && this.departmentOwnerProjectViewEnabled;
|
||||||
},
|
},
|
||||||
|
|
||||||
ownerProjectTabs() {
|
ownerProjectTabs() {
|
||||||
|
|||||||
@ -2141,7 +2141,9 @@ export default {
|
|||||||
okText: this.$L('立即下载'),
|
okText: this.$L('立即下载'),
|
||||||
content: `${file.name} (${$A.bytesToSize(file.size)})`,
|
content: `${file.name} (${$A.bytesToSize(file.size)})`,
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
const departmentOwnerIds = (this.$store.state.cacheDepartmentOwnerIds || []).join(',')
|
const departmentOwnerIds = this.$store.state.departmentOwnerProjectViewEnabled
|
||||||
|
? (this.$store.state.cacheDepartmentOwnerIds || []).join(',')
|
||||||
|
: ''
|
||||||
const url = $A.urlAddParams(`project/task/filedown?file_id=${file.id}`, departmentOwnerIds ? {department_owner_ids: departmentOwnerIds} : {})
|
const url = $A.urlAddParams(`project/task/filedown?file_id=${file.id}`, departmentOwnerIds ? {department_owner_ids: departmentOwnerIds} : {})
|
||||||
this.$store.dispatch('downUrl', $A.apiUrl(url))
|
this.$store.dispatch('downUrl', $A.apiUrl(url))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,242 +0,0 @@
|
|||||||
/**
|
|
||||||
* 团队概览 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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -21,7 +21,26 @@
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<h2 class="header-hello">{{$L('部门任务总览')}}</h2>
|
<h2 class="header-hello">{{$L('部门任务总览')}}</h2>
|
||||||
<div class="header-sub">
|
<div class="header-sub">
|
||||||
<span>{{todayText}}<template v-if="teamSummary"> · {{teamSummary}}</template></span>
|
<span>
|
||||||
|
{{todayText}}<template v-if="teamSummary"> · {{teamSummary}}</template>
|
||||||
|
<EPopover
|
||||||
|
v-model="teamCachePopoverShow"
|
||||||
|
placement="bottom-start"
|
||||||
|
popper-class="dashboard-cache-popper"
|
||||||
|
trigger="click">
|
||||||
|
<div class="dashboard-cache-popover">
|
||||||
|
<strong>{{$L('数据更新说明')}}</strong>
|
||||||
|
<p>{{$L('页面统计数据在 60 秒内复用,重点关注任务列表除外。')}}</p>
|
||||||
|
<span>{{$L('上次更新:(*)', teamStatsUpdatedTime)}}</span>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:loading="teamStatsLoading"
|
||||||
|
@click="refreshTeamDashboard">{{$L('立即刷新')}}</Button>
|
||||||
|
</div>
|
||||||
|
<Icon slot="reference" class="team-cache-help" type="ios-information-circle-outline"/>
|
||||||
|
</EPopover>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="systemConfig.timezoneDifference" class="header-servertime">
|
<div v-if="systemConfig.timezoneDifference" class="header-servertime">
|
||||||
@ -146,56 +165,59 @@
|
|||||||
<span class="empty-icon"><Icon type="md-checkmark"/></span>
|
<span class="empty-icon"><Icon type="md-checkmark"/></span>
|
||||||
<span>{{listEmptyText(group.type)}}</span>
|
<span>{{listEmptyText(group.type)}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="table-body" :key="`list-${group.type}`">
|
||||||
v-for="item in groupList(group, listLimit(group))"
|
<div
|
||||||
:key="`${group.type}-${item.id}`"
|
v-for="item in groupList(group, listLimit(group))"
|
||||||
class="table-row"
|
:key="`${group.type}-${item.id}`"
|
||||||
:class="{complete: item.complete_at}"
|
class="table-row"
|
||||||
:style="$A.generateColorVarStyle(item.flow_item_color, [10], 'flow-item-custom-color', item.color ? {backgroundColor: item.color} : {})"
|
:class="{complete: item.complete_at}"
|
||||||
@click="openTask(item)">
|
:style="$A.generateColorVarStyle(item.flow_item_color, [10], 'flow-item-custom-color', item.color ? {backgroundColor: item.color} : {})"
|
||||||
<span class="cell-check" @click.stop>
|
@click="openTask(item)">
|
||||||
<UserAvatar v-if="group.type === 'assist' && ownerUserid(item)" :userid="ownerUserid(item)" :size="22"/>
|
<span class="cell-check" @click.stop>
|
||||||
<TaskMenu v-else :task="item"/>
|
<UserAvatar v-if="group.type === 'assist' && ownerUserid(item)" :userid="ownerUserid(item)" :size="22"/>
|
||||||
</span>
|
<TaskMenu v-else :task="item"/>
|
||||||
<span class="cell-name">
|
</span>
|
||||||
<em
|
<span class="cell-name">
|
||||||
class="status-pill name-status"
|
<em
|
||||||
:class="item.flow_item_name ? item.flow_item_status : {end: !!item.complete_at}"
|
class="status-pill name-status"
|
||||||
@click.stop="openMenu($event, item)">{{item.flow_item_name || (item.complete_at ? $L('已完成') : $L('未完成'))}}</em>
|
:class="item.flow_item_name ? item.flow_item_status : {end: !!item.complete_at}"
|
||||||
<em v-if="item.sub_top === true" class="name-tag">{{$L('子任务')}}</em>
|
@click.stop="openMenu($event, item)">{{item.flow_item_name || (item.complete_at ? $L('已完成') : $L('未完成'))}}</em>
|
||||||
<em v-if="item.sub_my && item.sub_my.length > 0" class="name-tag">+{{item.sub_my.length}}</em>
|
<em v-if="item.sub_top === true" class="name-tag">{{$L('子任务')}}</em>
|
||||||
{{item.name}}
|
<em v-if="item.sub_my && item.sub_my.length > 0" class="name-tag">+{{item.sub_my.length}}</em>
|
||||||
</span>
|
{{item.name}}
|
||||||
<span class="t-project">{{projectName(item)}}</span>
|
</span>
|
||||||
<span class="t-status">
|
<span class="t-project">{{projectName(item)}}</span>
|
||||||
<em
|
<span class="t-status">
|
||||||
class="status-pill"
|
<em
|
||||||
:class="item.flow_item_name ? item.flow_item_status : {end: !!item.complete_at}"
|
class="status-pill"
|
||||||
@click.stop="openMenu($event, item)">{{item.flow_item_name || (item.complete_at ? $L('已完成') : $L('未完成'))}}</em>
|
:class="item.flow_item_name ? item.flow_item_status : {end: !!item.complete_at}"
|
||||||
</span>
|
@click.stop="openMenu($event, item)">{{item.flow_item_name || (item.complete_at ? $L('已完成') : $L('未完成'))}}</em>
|
||||||
<span class="t-priority">
|
</span>
|
||||||
<template v-if="item.p_name">
|
<span class="t-priority">
|
||||||
<i class="priority-dot" :style="{backgroundColor: item.p_color}"></i>
|
<template v-if="item.p_name">
|
||||||
<span>{{item.p_name}}</span>
|
<i class="priority-dot" :style="{backgroundColor: item.p_color}"></i>
|
||||||
</template>
|
<span>{{item.p_name}}</span>
|
||||||
<template v-else>—</template>
|
</template>
|
||||||
</span>
|
<template v-else>—</template>
|
||||||
<span class="t-sub">
|
</span>
|
||||||
<template v-if="item.sub_num > 0">
|
<span class="t-sub">
|
||||||
<i class="sub-bar"><b :style="{width: `${Math.min(item.sub_complete / item.sub_num * 100, 100)}%`}"></b></i>
|
<template v-if="item.sub_num > 0">
|
||||||
<em>{{item.sub_complete}}/{{item.sub_num}}</em>
|
<i class="sub-bar"><b :style="{width: `${Math.min(item.sub_complete / item.sub_num * 100, 100)}%`}"></b></i>
|
||||||
</template>
|
<em>{{item.sub_complete}}/{{item.sub_num}}</em>
|
||||||
<template v-else>—</template>
|
</template>
|
||||||
</span>
|
<template v-else>—</template>
|
||||||
<ETooltip :disabled="$isEEUIApp || windowTouch || !item.end_at" :content="item.end_at" placement="right">
|
</span>
|
||||||
<span class="t-end" :class="deadlineClass(item.end_at)">{{deadlineText(item.end_at) || '—'}}</span>
|
<span
|
||||||
</ETooltip>
|
class="t-end"
|
||||||
|
:class="deadlineClass(item.end_at)"
|
||||||
|
:title="item.end_at">{{deadlineText(item.end_at) || '—'}}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="group.list.length > listLimit(group) && !expandedGroups.includes(group.type)"
|
||||||
|
:key="`more-${group.type}`"
|
||||||
|
class="card-more"
|
||||||
|
@click="expandedGroups.push(group.type)">{{$L('还有 (*) 项', group.list.length - listLimit(group))}} →</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
v-if="group.list.length > listLimit(group) && !expandedGroups.includes(group.type)"
|
|
||||||
:key="`more-${group.type}`"
|
|
||||||
class="card-more"
|
|
||||||
@click="expandedGroups.push(group.type)">{{$L('还有 (*) 项', group.list.length - listLimit(group))}} →</div>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -252,12 +274,14 @@
|
|||||||
<keep-alive>
|
<keep-alive>
|
||||||
<DashboardTeam
|
<DashboardTeam
|
||||||
v-if="currentView === 'team'"
|
v-if="currentView === 'team'"
|
||||||
|
ref="dashboardTeam"
|
||||||
:initial-focus="teamFocusPref"
|
:initial-focus="teamFocusPref"
|
||||||
@stats="teamStats = $event"/>
|
@stats="teamStats = $event"
|
||||||
|
@stats-loading="teamStatsLoading = $event"/>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</div>
|
</div>
|
||||||
</Scrollbar>
|
</Scrollbar>
|
||||||
<DepartmentOwnerView v-model="deptViewShow"/>
|
<DepartmentOwnerView v-model="deptViewShow" scope-only/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -299,6 +323,8 @@ export default {
|
|||||||
|
|
||||||
deptViewShow: false,
|
deptViewShow: false,
|
||||||
teamStats: {member_count: 0, blocks: {}},
|
teamStats: {member_count: 0, blocks: {}},
|
||||||
|
teamStatsLoading: false,
|
||||||
|
teamCachePopoverShow: false,
|
||||||
|
|
||||||
mateName: /macintosh|mac os x/i.test(navigator.userAgent) ? '⌘' : 'Ctrl',
|
mateName: /macintosh|mac os x/i.test(navigator.userAgent) ? '⌘' : 'Ctrl',
|
||||||
|
|
||||||
@ -541,6 +567,10 @@ export default {
|
|||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
teamStatsUpdatedTime({teamStats}) {
|
||||||
|
return teamStats.generated_at ? $A.dayjs(teamStats.generated_at).format('HH:mm:ss') : '--'
|
||||||
|
},
|
||||||
|
|
||||||
deptLabel({userInfo, cacheDepartmentOwnerIds}) {
|
deptLabel({userInfo, cacheDepartmentOwnerIds}) {
|
||||||
const managed = (userInfo.managed_departments || []).map(item => ({...item, id: parseInt(item.id)}));
|
const managed = (userInfo.managed_departments || []).map(item => ({...item, id: parseInt(item.id)}));
|
||||||
const ids = (cacheDepartmentOwnerIds || []).map(id => parseInt(id));
|
const ids = (cacheDepartmentOwnerIds || []).map(id => parseInt(id));
|
||||||
@ -608,6 +638,9 @@ export default {
|
|||||||
|
|
||||||
setView(view) {
|
setView(view) {
|
||||||
this.view = view;
|
this.view = view;
|
||||||
|
if (view !== 'team') {
|
||||||
|
this.teamCachePopoverShow = false;
|
||||||
|
}
|
||||||
prefsCache.view = view;
|
prefsCache.view = view;
|
||||||
$A.IDBSave("dashboardView", view);
|
$A.IDBSave("dashboardView", view);
|
||||||
},
|
},
|
||||||
@ -733,6 +766,15 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
refreshTeamDashboard() {
|
||||||
|
this.$refs.dashboardTeam?.refreshAll().then(success => {
|
||||||
|
if (success) {
|
||||||
|
this.teamCachePopoverShow = false
|
||||||
|
$A.messageSuccess("刷新成功")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
projectName(task) {
|
projectName(task) {
|
||||||
const project = this.cacheProjects.find(({id}) => id == task.project_id);
|
const project = this.cacheProjects.find(({id}) => id == task.project_id);
|
||||||
return project ? project.name : '';
|
return project ? project.name : '';
|
||||||
|
|||||||
@ -44,7 +44,10 @@ export default {
|
|||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
getProjectData() {
|
getProjectData() {
|
||||||
if (this.projectId <= 0) return;
|
if (this.projectId <= 0) {
|
||||||
|
this.$store.state.projectId = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
const projectId = this.projectId;
|
const projectId = this.projectId;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$store.state.projectId = projectId;
|
this.$store.state.projectId = projectId;
|
||||||
|
|||||||
162
resources/assets/js/store/actions.js
vendored
162
resources/assets/js/store/actions.js
vendored
@ -249,6 +249,7 @@ export default {
|
|||||||
}
|
}
|
||||||
if (params.departmentOwner !== false
|
if (params.departmentOwner !== false
|
||||||
&& state.systemConfig.department_owner_project_view === 'open'
|
&& state.systemConfig.department_owner_project_view === 'open'
|
||||||
|
&& state.departmentOwnerProjectViewEnabled
|
||||||
&& state.departmentOwnerReadonlyUrls.includes(params.url)
|
&& state.departmentOwnerReadonlyUrls.includes(params.url)
|
||||||
&& (state.cacheDepartmentOwnerIds || []).length > 0) {
|
&& (state.cacheDepartmentOwnerIds || []).length > 0) {
|
||||||
if (!$A.isJson(params.data)) params.data = {}
|
if (!$A.isJson(params.data)) params.data = {}
|
||||||
@ -1203,11 +1204,13 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 批量获取数据
|
// 批量获取数据
|
||||||
|
const departmentOwnerProjectViewEnabledPromise = $A.IDBValue('departmentOwnerProjectViewEnabled');
|
||||||
const data = await Promise.all([
|
const data = await Promise.all([
|
||||||
...dataMap.string.map(key => $A.IDBString(key)),
|
...dataMap.string.map(key => $A.IDBString(key)),
|
||||||
...dataMap.array.map(key => $A.IDBArray(key)),
|
...dataMap.array.map(key => $A.IDBArray(key)),
|
||||||
...dataMap.json.map(key => $A.IDBJson(key))
|
...dataMap.json.map(key => $A.IDBJson(key))
|
||||||
]);
|
]);
|
||||||
|
const cachedDepartmentOwnerProjectViewEnabled = await departmentOwnerProjectViewEnabledPromise;
|
||||||
|
|
||||||
// 更新 state
|
// 更新 state
|
||||||
[...dataMap.string, ...dataMap.array, ...dataMap.json].forEach((key, index) => {
|
[...dataMap.string, ...dataMap.array, ...dataMap.json].forEach((key, index) => {
|
||||||
@ -1217,6 +1220,10 @@ export default {
|
|||||||
state[key] = data[index];
|
state[key] = data[index];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const departmentOwnerProjectViewEnabled = typeof cachedDepartmentOwnerProjectViewEnabled === 'boolean'
|
||||||
|
? cachedDepartmentOwnerProjectViewEnabled
|
||||||
|
: (state.cacheDepartmentOwnerIds || []).length > 0;
|
||||||
|
commit('department/owner/enabled/save', departmentOwnerProjectViewEnabled);
|
||||||
|
|
||||||
// 特殊处理 cacheDialogs
|
// 特殊处理 cacheDialogs
|
||||||
state.cacheDialogs = state.cacheDialogs.map(item => ({
|
state.cacheDialogs = state.cacheDialogs.map(item => ({
|
||||||
@ -1251,6 +1258,21 @@ export default {
|
|||||||
state.userIsAdmin = $A.inArray("admin", state.userInfo.identity);
|
state.userIsAdmin = $A.inArray("admin", state.userInfo.identity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 负责人部门范围始终保留明确选择;旧缓存为空时默认使用全部可管理部门。
|
||||||
|
const managedDepartmentIds = (state.userInfo.managed_departments || [])
|
||||||
|
.map(item => parseInt(item.id))
|
||||||
|
.filter(id => id > 0);
|
||||||
|
let departmentOwnerIds = (state.cacheDepartmentOwnerIds || [])
|
||||||
|
.map(id => parseInt(id))
|
||||||
|
.filter(id => managedDepartmentIds.includes(id));
|
||||||
|
if (departmentOwnerIds.length === 0) {
|
||||||
|
departmentOwnerIds = managedDepartmentIds;
|
||||||
|
}
|
||||||
|
commit('department/owner/ids/save', [...new Set(departmentOwnerIds)]);
|
||||||
|
if (departmentOwnerIds.length === 0) {
|
||||||
|
commit('department/owner/enabled/save', false);
|
||||||
|
}
|
||||||
|
|
||||||
// 处理 ServerUrl
|
// 处理 ServerUrl
|
||||||
if (state.cacheServerUrl) {
|
if (state.cacheServerUrl) {
|
||||||
window.systemInfo.apiUrl = state.cacheServerUrl
|
window.systemInfo.apiUrl = state.cacheServerUrl
|
||||||
@ -1593,9 +1615,9 @@ export default {
|
|||||||
normalizeDepartmentOwnerIds({state}, ids) {
|
normalizeDepartmentOwnerIds({state}, ids) {
|
||||||
const validIds = (state.userInfo.managed_departments || []).map(item => parseInt(item.id));
|
const validIds = (state.userInfo.managed_departments || []).map(item => parseInt(item.id));
|
||||||
if (!$A.isArray(ids)) ids = [];
|
if (!$A.isArray(ids)) ids = [];
|
||||||
return ids
|
return [...new Set(ids
|
||||||
.map(id => parseInt(id))
|
.map(id => parseInt(id))
|
||||||
.filter(id => id > 0 && (validIds.length === 0 || validIds.includes(id)));
|
.filter(id => id > 0 && validIds.includes(id)))];
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1608,16 +1630,16 @@ export default {
|
|||||||
await dispatch("systemSetting").catch(() => {});
|
await dispatch("systemSetting").catch(() => {});
|
||||||
if (state.systemConfig.department_owner_project_view !== 'open') {
|
if (state.systemConfig.department_owner_project_view !== 'open') {
|
||||||
commit('department/owner/ids/save', []);
|
commit('department/owner/ids/save', []);
|
||||||
|
commit('department/owner/enabled/save', false);
|
||||||
dispatch("getProjectByQueue");
|
dispatch("getProjectByQueue");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const restoredDepartmentOwnerIds = await dispatch("restoreDepartmentOwnerView");
|
const restored = await dispatch("restoreDepartmentOwnerView");
|
||||||
if ((restoredDepartmentOwnerIds || []).length > 0) {
|
if (restored.enabled && restored.ids.length > 0) {
|
||||||
await dispatch("getProjects", {
|
await dispatch("getProjects", {
|
||||||
__replace: true,
|
__replace: true,
|
||||||
department_owner_ids: restoredDepartmentOwnerIds.join(',')
|
department_owner_ids: restored.ids.join(',')
|
||||||
});
|
});
|
||||||
commit('department/owner/ids/save', restoredDepartmentOwnerIds);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch("getProjectByQueue");
|
dispatch("getProjectByQueue");
|
||||||
@ -1631,45 +1653,78 @@ export default {
|
|||||||
*/
|
*/
|
||||||
async restoreDepartmentOwnerView({state, dispatch, commit}) {
|
async restoreDepartmentOwnerView({state, dispatch, commit}) {
|
||||||
if (state.departmentOwnerViewRestored) {
|
if (state.departmentOwnerViewRestored) {
|
||||||
return [];
|
return {
|
||||||
|
enabled: state.departmentOwnerProjectViewEnabled,
|
||||||
|
ids: state.cacheDepartmentOwnerIds || [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (state.systemConfig.department_owner_project_view !== 'open') {
|
if (state.systemConfig.department_owner_project_view !== 'open') {
|
||||||
commit('department/owner/ids/save', []);
|
commit('department/owner/ids/save', []);
|
||||||
return [];
|
commit('department/owner/enabled/save', false);
|
||||||
|
return {enabled: false, ids: []};
|
||||||
}
|
}
|
||||||
state.departmentOwnerViewRestored = true;
|
state.departmentOwnerViewRestored = true;
|
||||||
const ids = await $A.IDBArray("cacheDepartmentOwnerIds", []);
|
const managedIds = (state.userInfo.managed_departments || [])
|
||||||
if (!ids.length) {
|
.map(item => parseInt(item.id))
|
||||||
return [];
|
.filter(id => id > 0);
|
||||||
|
let restored = await dispatch("normalizeDepartmentOwnerIds", state.cacheDepartmentOwnerIds);
|
||||||
|
if (restored.length === 0) {
|
||||||
|
restored = [...new Set(managedIds)];
|
||||||
}
|
}
|
||||||
const restored = await dispatch("normalizeDepartmentOwnerIds", ids);
|
const enabled = state.departmentOwnerProjectViewEnabled && restored.length > 0;
|
||||||
if (restored.length > 0) {
|
commit('department/owner/ids/save', restored);
|
||||||
|
commit('department/owner/enabled/save', enabled);
|
||||||
|
if (enabled) {
|
||||||
state.departmentOwnerProjectsRefreshing = true;
|
state.departmentOwnerProjectsRefreshing = true;
|
||||||
}
|
}
|
||||||
commit('department/owner/ids/save', restored);
|
return {enabled, ids: restored};
|
||||||
return restored;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置部门负责人视角
|
* 设置部门负责人视角
|
||||||
* @param state
|
* @param state
|
||||||
* @param dispatch
|
* @param dispatch
|
||||||
* @param ids
|
* @param payload
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async setDepartmentOwnerIds({state, dispatch, commit}, ids) {
|
async setDepartmentOwnerIds({state, dispatch, commit}, payload) {
|
||||||
|
const hasOptions = $A.isJson(payload);
|
||||||
|
let ids = hasOptions ? payload.ids : payload;
|
||||||
|
let enabled = hasOptions && typeof payload.projectViewEnabled === 'boolean'
|
||||||
|
? payload.projectViewEnabled
|
||||||
|
: state.departmentOwnerProjectViewEnabled;
|
||||||
if (state.systemConfig.department_owner_project_view !== 'open') {
|
if (state.systemConfig.department_owner_project_view !== 'open') {
|
||||||
ids = [];
|
ids = [];
|
||||||
|
enabled = false;
|
||||||
}
|
}
|
||||||
const normalized = await dispatch("normalizeDepartmentOwnerIds", ids);
|
let normalized = await dispatch("normalizeDepartmentOwnerIds", ids);
|
||||||
|
if (state.systemConfig.department_owner_project_view === 'open' && normalized.length === 0) {
|
||||||
|
normalized = (state.userInfo.managed_departments || [])
|
||||||
|
.map(item => parseInt(item.id))
|
||||||
|
.filter(id => id > 0);
|
||||||
|
}
|
||||||
|
normalized = [...new Set(normalized)];
|
||||||
|
enabled = enabled && normalized.length > 0;
|
||||||
const oldValue = (state.cacheDepartmentOwnerIds || []).slice().sort().join(',');
|
const oldValue = (state.cacheDepartmentOwnerIds || []).slice().sort().join(',');
|
||||||
const newValue = normalized.slice().sort().join(',');
|
const newValue = normalized.slice().sort().join(',');
|
||||||
if (oldValue === newValue) {
|
const scopeChanged = oldValue !== newValue;
|
||||||
|
const enabledChanged = state.departmentOwnerProjectViewEnabled !== enabled;
|
||||||
|
if (!scopeChanged && !enabledChanged) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.departmentOwnerProjectsRefreshing = true;
|
let refreshResult = null;
|
||||||
await dispatch("refreshDepartmentOwnerProjects", normalized);
|
if (enabledChanged || (enabled && scopeChanged)) {
|
||||||
|
state.departmentOwnerProjectsRefreshing = true;
|
||||||
|
refreshResult = await dispatch("refreshDepartmentOwnerProjects", {
|
||||||
|
ownerIds: normalized,
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
commit('department/owner/ids/save', normalized);
|
commit('department/owner/ids/save', normalized);
|
||||||
|
commit('department/owner/enabled/save', enabled);
|
||||||
|
if (refreshResult?.route) {
|
||||||
|
$A.goForward(refreshResult.route);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1678,9 +1733,14 @@ export default {
|
|||||||
* @param dispatch
|
* @param dispatch
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async refreshDepartmentOwnerProjects({state, dispatch}, ownerIds = state.cacheDepartmentOwnerIds) {
|
async refreshDepartmentOwnerProjects({state, dispatch}, options = {}) {
|
||||||
const currentProjectId = state.projectId;
|
const currentProjectId = state.routeName === 'manage-project' ? state.projectId : 0;
|
||||||
|
const enabled = typeof options.enabled === 'boolean'
|
||||||
|
? options.enabled
|
||||||
|
: state.departmentOwnerProjectViewEnabled;
|
||||||
|
let ownerIds = options.ownerIds || state.cacheDepartmentOwnerIds;
|
||||||
ownerIds = (ownerIds || []).map(id => parseInt(id)).filter(id => id > 0);
|
ownerIds = (ownerIds || []).map(id => parseInt(id)).filter(id => id > 0);
|
||||||
|
const departmentOwnerIds = enabled ? ownerIds.join(',') : '';
|
||||||
state.departmentOwnerProjectsRefreshing = true;
|
state.departmentOwnerProjectsRefreshing = true;
|
||||||
state.callAt = state.callAt.filter(item => {
|
state.callAt = state.callAt.filter(item => {
|
||||||
const key = String(item.key);
|
const key = String(item.key);
|
||||||
@ -1689,7 +1749,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
await dispatch("getProjects", {
|
await dispatch("getProjects", {
|
||||||
__replace: true,
|
__replace: true,
|
||||||
department_owner_ids: ownerIds.join(',')
|
department_owner_ids: departmentOwnerIds
|
||||||
});
|
});
|
||||||
if (currentProjectId > 0) {
|
if (currentProjectId > 0) {
|
||||||
const exists = state.cacheProjects.find(({id}) => id == currentProjectId);
|
const exists = state.cacheProjects.find(({id}) => id == currentProjectId);
|
||||||
@ -1700,19 +1760,20 @@ export default {
|
|||||||
}
|
}
|
||||||
return b.id - a.id;
|
return b.id - a.id;
|
||||||
}).find(({id}) => id);
|
}).find(({id}) => id);
|
||||||
if (project) {
|
return {
|
||||||
$A.goForward({name: 'manage-project', params: {projectId: project.id}});
|
route: project
|
||||||
} else {
|
? {name: 'manage-project', params: {projectId: project.id}}
|
||||||
$A.goForward({name: 'manage-dashboard'});
|
: {name: 'manage-dashboard'},
|
||||||
}
|
};
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
await dispatch("getProjectOne", currentProjectId).catch(() => {});
|
await dispatch("getProjectOne", {
|
||||||
await dispatch("getTaskForProject", currentProjectId).catch(() => {});
|
project_id: currentProjectId,
|
||||||
}
|
department_owner_ids: departmentOwnerIds,
|
||||||
} catch (e) {
|
}).catch(() => {});
|
||||||
if ((state.cacheDepartmentOwnerIds || []).length === 0 && currentProjectId > 0) {
|
await dispatch("getTaskForProject", {
|
||||||
$A.goForward({name: 'manage-dashboard'});
|
project_id: currentProjectId,
|
||||||
|
department_owner_ids: departmentOwnerIds,
|
||||||
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
state.departmentOwnerProjectsRefreshing = false;
|
state.departmentOwnerProjectsRefreshing = false;
|
||||||
@ -1818,7 +1879,7 @@ export default {
|
|||||||
if (state.systemConfig.department_owner_project_view !== 'open') {
|
if (state.systemConfig.department_owner_project_view !== 'open') {
|
||||||
delete requestData.department_owner_ids
|
delete requestData.department_owner_ids
|
||||||
} else if (requestData.department_owner_ids === undefined) {
|
} else if (requestData.department_owner_ids === undefined) {
|
||||||
if ((state.cacheDepartmentOwnerIds || []).length > 0) {
|
if (state.departmentOwnerProjectViewEnabled && (state.cacheDepartmentOwnerIds || []).length > 0) {
|
||||||
requestData.department_owner_ids = state.cacheDepartmentOwnerIds.join(',')
|
requestData.department_owner_ids = state.cacheDepartmentOwnerIds.join(',')
|
||||||
} else {
|
} else {
|
||||||
delete requestData.department_owner_ids
|
delete requestData.department_owner_ids
|
||||||
@ -1881,11 +1942,13 @@ export default {
|
|||||||
* 获取单个项目
|
* 获取单个项目
|
||||||
* @param state
|
* @param state
|
||||||
* @param dispatch
|
* @param dispatch
|
||||||
* @param project_id
|
* @param project
|
||||||
* @returns {Promise<unknown>}
|
* @returns {Promise<unknown>}
|
||||||
*/
|
*/
|
||||||
getProjectOne({state, dispatch}, project_id) {
|
getProjectOne({state, dispatch}, project) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
|
const requestData = $A.isJson(project) ? {...project} : {project_id: project};
|
||||||
|
const project_id = requestData.project_id;
|
||||||
if ($A.runNum(project_id) === 0) {
|
if ($A.runNum(project_id) === 0) {
|
||||||
reject({msg: 'Parameter error'});
|
reject({msg: 'Parameter error'});
|
||||||
return;
|
return;
|
||||||
@ -1893,9 +1956,7 @@ export default {
|
|||||||
state.projectLoad++;
|
state.projectLoad++;
|
||||||
dispatch("call", {
|
dispatch("call", {
|
||||||
url: 'project/one',
|
url: 'project/one',
|
||||||
data: {
|
data: requestData,
|
||||||
project_id
|
|
||||||
},
|
|
||||||
}).then(result => {
|
}).then(result => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
state.projectLoad--;
|
state.projectLoad--;
|
||||||
@ -2328,10 +2389,12 @@ export default {
|
|||||||
if (!$A.isJson(requestData)) {
|
if (!$A.isJson(requestData)) {
|
||||||
requestData = {}
|
requestData = {}
|
||||||
}
|
}
|
||||||
if ((state.cacheDepartmentOwnerIds || []).length > 0) {
|
if (requestData.department_owner_ids === undefined) {
|
||||||
requestData.department_owner_ids = state.cacheDepartmentOwnerIds.join(',')
|
if (state.departmentOwnerProjectViewEnabled && (state.cacheDepartmentOwnerIds || []).length > 0) {
|
||||||
} else {
|
requestData.department_owner_ids = state.cacheDepartmentOwnerIds.join(',')
|
||||||
delete requestData.department_owner_ids
|
} else {
|
||||||
|
delete requestData.department_owner_ids
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const callData = $callData('tasks', requestData, state)
|
const callData = $callData('tasks', requestData, state)
|
||||||
//
|
//
|
||||||
@ -2482,12 +2545,13 @@ export default {
|
|||||||
* 获取项目任务
|
* 获取项目任务
|
||||||
* @param state
|
* @param state
|
||||||
* @param dispatch
|
* @param dispatch
|
||||||
* @param project_id
|
* @param project
|
||||||
* @returns {Promise<unknown>}
|
* @returns {Promise<unknown>}
|
||||||
*/
|
*/
|
||||||
getTaskForProject({state, dispatch}, project_id) {
|
getTaskForProject({state, dispatch}, project) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
dispatch("getTasks", {project_id}).then(resolve).catch(reject)
|
const requestData = $A.isJson(project) ? {...project} : {project_id: project};
|
||||||
|
dispatch("getTasks", requestData).then(resolve).catch(reject)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
4
resources/assets/js/store/mutations.js
vendored
4
resources/assets/js/store/mutations.js
vendored
@ -253,6 +253,10 @@ export default {
|
|||||||
state.cacheDepartmentOwnerIds = $A.normalizeIntArray(data)
|
state.cacheDepartmentOwnerIds = $A.normalizeIntArray(data)
|
||||||
$A.IDBSet("cacheDepartmentOwnerIds", state.cacheDepartmentOwnerIds).catch(() => {});
|
$A.IDBSet("cacheDepartmentOwnerIds", state.cacheDepartmentOwnerIds).catch(() => {});
|
||||||
},
|
},
|
||||||
|
'department/owner/enabled/save': function(state, enabled) {
|
||||||
|
state.departmentOwnerProjectViewEnabled = enabled === true
|
||||||
|
$A.IDBSet("departmentOwnerProjectViewEnabled", state.departmentOwnerProjectViewEnabled).catch(() => {});
|
||||||
|
},
|
||||||
|
|
||||||
// 文件管理
|
// 文件管理
|
||||||
'file/push': function(state, data) {
|
'file/push': function(state, data) {
|
||||||
|
|||||||
1
resources/assets/js/store/state.js
vendored
1
resources/assets/js/store/state.js
vendored
@ -105,6 +105,7 @@ export default {
|
|||||||
cacheTasks: [],
|
cacheTasks: [],
|
||||||
cacheProjectParameter: [],
|
cacheProjectParameter: [],
|
||||||
cacheDepartmentOwnerIds: [],
|
cacheDepartmentOwnerIds: [],
|
||||||
|
departmentOwnerProjectViewEnabled: false,
|
||||||
departmentOwnerViewRestored: false,
|
departmentOwnerViewRestored: false,
|
||||||
departmentOwnerProjectsRefreshing: false,
|
departmentOwnerProjectsRefreshing: false,
|
||||||
departmentOwnerReadonlyUrls: [
|
departmentOwnerReadonlyUrls: [
|
||||||
|
|||||||
221
resources/assets/sass/pages/page-dashboard.scss
vendored
221
resources/assets/sass/pages/page-dashboard.scss
vendored
@ -62,6 +62,19 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #7a8078;
|
color: #7a8078;
|
||||||
|
|
||||||
|
.team-cache-help {
|
||||||
|
display: inline-flex;
|
||||||
|
padding-left: 6px;
|
||||||
|
color: #a2a89f;
|
||||||
|
font-size: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #1e9e55;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard-load {
|
.dashboard-load {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -417,6 +430,9 @@
|
|||||||
|
|
||||||
// ==================== 个人视角:列表 ====================
|
// ==================== 个人视角:列表 ====================
|
||||||
.my-table {
|
.my-table {
|
||||||
|
.table-body {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
.table-row {
|
.table-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 28px minmax(0, 1fr) 140px 90px 90px 76px 96px;
|
grid-template-columns: 28px minmax(0, 1fr) 140px 90px 90px 76px 96px;
|
||||||
@ -1014,7 +1030,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 20px;
|
padding: 36px 20px 44px;
|
||||||
color: #a2a89f;
|
color: #a2a89f;
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
}
|
}
|
||||||
@ -1031,6 +1047,10 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
||||||
|
.team-avatar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.card-head {
|
.card-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -1090,18 +1110,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.team-stats-loading {
|
||||||
|
min-height: 220px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.member-list {
|
.member-list {
|
||||||
padding: 2px 0 6px;
|
--member-grid-columns: 26px 60px minmax(80px, 1fr) fit-content(220px);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: var(--member-grid-columns);
|
||||||
|
column-gap: 12px;
|
||||||
|
padding: 2px 20px 6px;
|
||||||
|
|
||||||
> li {
|
> li {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-row {
|
.member-row {
|
||||||
display: flex;
|
grid-column: 1 / -1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: subgrid;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
column-gap: 12px;
|
||||||
padding: 9px 20px;
|
padding: 9px 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -1109,8 +1142,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.member-name {
|
.member-name {
|
||||||
width: 60px;
|
min-width: 0;
|
||||||
flex-shrink: 0;
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #22261f;
|
color: #22261f;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -1119,7 +1151,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.member-track {
|
.member-track {
|
||||||
flex: 1;
|
min-width: 0;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: #f0efea;
|
background: #f0efea;
|
||||||
@ -1141,13 +1173,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.member-num {
|
.member-num {
|
||||||
width: 110px;
|
min-width: 0;
|
||||||
flex-shrink: 0;
|
max-width: 220px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #7a8078;
|
color: #7a8078;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
> em {
|
> em {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@ -1223,6 +1257,23 @@
|
|||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
font-size: 11.5px;
|
font-size: 11.5px;
|
||||||
color: #a2a89f;
|
color: #a2a89f;
|
||||||
|
|
||||||
|
&.priority-more {
|
||||||
|
color: #7a8078;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #177f44;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-focus-section {
|
||||||
|
&.focus-scroll-reserve {
|
||||||
|
// 点击下钻后预留一个可视区高度,离开负责人视角时恢复正常布局
|
||||||
|
min-height: calc(var(--window-height) - var(--status-bar-height) - var(--navigation-bar-height) - 52px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1335,6 +1386,13 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.owner-more {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #a2a89f;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.f-priority {
|
.f-priority {
|
||||||
@ -1389,14 +1447,139 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 24px;
|
min-height: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.focus-empty {
|
.focus-empty {
|
||||||
padding: 30px 20px;
|
min-height: 150px;
|
||||||
text-align: center;
|
padding: 24px 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 7px;
|
||||||
color: #a2a89f;
|
color: #a2a89f;
|
||||||
font-size: 13px;
|
font-size: 12.5px;
|
||||||
|
|
||||||
|
.focus-empty-icon {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #e7f5ec;
|
||||||
|
color: #1e9e55;
|
||||||
|
font-size: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> strong {
|
||||||
|
color: #555c53;
|
||||||
|
font-size: 13.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-cache-popper {
|
||||||
|
.dashboard-cache-popover {
|
||||||
|
width: 260px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
> strong {
|
||||||
|
color: #22261f;
|
||||||
|
font-size: 13.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> p {
|
||||||
|
color: #7a8078;
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
color: #a2a89f;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> button {
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-top: 2px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports not (grid-template-columns: subgrid) {
|
||||||
|
.page-dashboard {
|
||||||
|
.dashboard-team {
|
||||||
|
.member-list {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.member-row {
|
||||||
|
grid-template-columns: var(--member-grid-columns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-member-progress-popper {
|
||||||
|
.member-progress-tooltip {
|
||||||
|
min-width: 180px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 7px;
|
||||||
|
|
||||||
|
.progress-tooltip-title {
|
||||||
|
color: #a2a89f;
|
||||||
|
font-size: 11px;
|
||||||
|
|
||||||
|
&.title-stage {
|
||||||
|
margin-top: 2px;
|
||||||
|
padding-top: 7px;
|
||||||
|
border-top: 1px solid rgba(128, 128, 128, 0.18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-tooltip-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.progress-tooltip-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
> i {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> strong {
|
||||||
|
font-weight: 500;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.row-overdue {
|
||||||
|
.progress-tooltip-label,
|
||||||
|
> strong {
|
||||||
|
color: #d94f46;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1620,16 +1803,18 @@ body.window-portrait {
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
|
|
||||||
.member-list {
|
.member-list {
|
||||||
|
--member-grid-columns: 26px 48px minmax(48px, 1fr) fit-content(180px);
|
||||||
|
padding: 2px 14px 6px;
|
||||||
|
|
||||||
.member-row {
|
.member-row {
|
||||||
padding: 9px 14px;
|
padding: 9px 0;
|
||||||
|
|
||||||
.member-name {
|
.member-name {
|
||||||
width: 48px;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-num {
|
.member-num {
|
||||||
width: auto;
|
max-width: 180px;
|
||||||
min-width: 64px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user