Industrial-image-management.../src/views/task/task-progress/TaskProgress.vue

711 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<GiPageLayout>
<div class="task-tracking-page">
<!-- 固定标题和表头容器 -->
<div class="sticky-headers">
<!-- 页面标题 -->
<div class="page-header">
<h2>任务跟踪</h2>
<p class="page-description">跟踪监控和评估任务的完成情况</p>
</div>
<!-- 公共表头仅显示一次 -->
<div class="shared-header">
<div class="header-row">
<div class="col" style="width: 100px">任务描述</div>
<div class="col" style="width: 180px">任务情况总结</div>
<div class="col" style="width: 80px">任务执行人</div>
<div class="col" style="width: 60px">进展</div>
<div class="col" style="width: 120px">开始日期</div>
<div class="col" style="width: 120px">预计完成日期</div>
<div class="col" style="width: 80px">是否延期</div>
<div class="col" style="width: 120px">实际完成日期</div>
<div class="col" style="width: 180px">最新进展记录</div>
<div class="col" style="width: 100px">重要紧急程度</div>
</div>
</div>
</div>
<!-- 分组容器按重要紧急程度分组 -->
<div
v-for="(group, groupKey) in groupedTasks"
:key="groupKey"
class="task-group"
>
<!-- 分组标题(带专门的折叠/展开按钮) -->
<div class="group-header">
<span class="group-title-text" :class="groupKey">{{ groupKey }}</span>
<button
class="toggle-btn"
@click="toggleGroup(groupKey)"
:aria-expanded="!group.collapsed"
>
<i class="icon" :class="group.collapsed ? 'el-icon-plus' : 'el-icon-minus'" />
<span class="toggle-text">{{ group.collapsed ? '展开' : '收起' }}</span>
</button>
</div>
<!-- 任务列表折叠时隐藏展开时显示 -->
<div class="task-list" v-show="!group.collapsed">
<div
v-for="(task, index) in group.tasks"
:key="index"
class="task-row"
>
<!-- 任务描述 -->
<div class="col" style="width: 100px">{{ task.taskDesc }}</div>
<!-- 任务情况总结(带弹窗) -->
<div class="col info-cell" style="width: 180px">
<span @click="openPopup($event, task.summaryDetail, '任务情况总结')"
@mouseenter="cancelClosePopup"
@mouseleave="closePopup">
{{ task.summary }}
<i class="el-icon-info" />
</span>
</div>
<!-- 任务执行人 -->
<div class="col" style="width: 80px">{{ task.executor }}</div>
<!-- 进展(标签化) -->
<div class="col progress-tag" :class="task.progress" style="width: 60px">
{{ task.progress }}
</div>
<!-- 开始日期 -->
<div class="col" style="width: 120px">{{ task.startDate }}</div>
<!-- 预计完成日期 -->
<div class="col" style="width: 120px">{{ task.expectEndDate }}</div>
<!-- 是否延期(标签化) -->
<div class="col delay-tag" :class="task.isDelay" style="width: 80px">
{{ task.isDelay }}
</div>
<!-- 实际完成日期 -->
<div class="col" style="width: 120px">{{ task.actualEndDate }}</div>
<!-- 最新进展记录(带弹窗) -->
<div class="col info-cell" style="width: 180px">
<span @click="openPopup($event, task.progressDetail, '最新进展记录')"
@mouseenter="cancelClosePopup"
@mouseleave="closePopup">
{{ task.latestProgress }}
<i class="el-icon-info" />
</span>
</div>
<!-- 重要紧急程度(标签化) -->
<div class="col priority-tag" :class="groupKey" style="width: 100px">
{{ groupKey }}
</div>
</div>
</div>
</div>
<!-- 全局弹窗:所有详情共用 -->
<transition name="popup">
<div
class="popup"
v-if="popupVisible"
:style="{
top: popupTop + 'px',
left: popupLeft + 'px'
}"
@mouseenter="cancelClosePopup"
@mouseleave="closePopup"
@click.stop
>
<div class="popup-title">{{ popupTitle }}</div>
<div class="popup-content">{{ popupContent }}</div>
<div class="popup-arrow"></div>
</div>
</transition>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
// 定义任务结构
interface Task {
taskDesc: string;
summary: string; // 任务情况总结简略
summaryDetail: string; // 任务情况总结详细
executor: string;
progress: string; // 进展进行中已完成
priority: string; // 重要紧急程度重要紧急紧急不重要等
startDate: string;
expectEndDate: string;
isDelay: string; // 是否延期已延期正常
actualEndDate: string;
latestProgress: string; // 最新进展简略
progressDetail: string; // 最新进展详细
}
// 定义分组结构
interface TaskGroup {
collapsed: boolean; // 是否折叠
tasks: Task[]; // 该分组下的任务
}
// 原始任务数据模拟实际从接口获取
const rawTasks = ref<Task[]>([
{
taskDesc: '完成年度财务报告',
summary: '1. 任务执行人于小宁正...',
summaryDetail: '任务执行人于小宁正按流程推进,已梳理数据框架,待最终核算。目前已完成资产负债表初步编制,利润表数据核对中,预计下周完成全部核算工作。',
executor: '周北北',
progress: '进行中',
priority: '重要紧急',
startDate: '2023/02/05',
expectEndDate: '2024/11/25',
isDelay: '已延期',
actualEndDate: '',
latestProgress: '已经收集了所有必要的财...',
progressDetail: '已收集资产负债表、利润表原始数据,待合并现金流量表。本周重点完成了各部门费用核算,正在处理年末调整事项。'
},
{
taskDesc: '更新公司官网内容',
summary: '1. 正在收集各部门最新...',
summaryDetail: '正在收集各部门最新资料,市场部和销售部已提交更新内容,技术部和人力资源部资料待收。预计下周一开始页面制作。',
executor: '李小华',
progress: '进行中',
priority: '重要紧急',
startDate: '2023/11/01',
expectEndDate: '2023/11/30',
isDelay: '正常',
actualEndDate: '',
latestProgress: '设计稿已确认,等待内容...',
progressDetail: '设计稿已确认,等待各部门内容素材。目前已完成首页和产品页的设计,正在准备关于我们页面的素材。'
},
{
taskDesc: '制定明年培训计划',
summary: '1. 已完成需求调研,正...',
summaryDetail: '已完成需求调研正在整理各部门培训需求。调研显示技术类和管理类培训需求最高分别占比42%和35%。',
executor: '张明明',
progress: '进行中',
priority: '重要不紧急',
startDate: '2023/10/15',
expectEndDate: '2023/12/15',
isDelay: '正常',
actualEndDate: '',
latestProgress: '正在分析培训需求数据...',
progressDetail: '正在分析培训需求数据计划11月中旬完成初稿11月底组织各部门负责人评审。'
},
{
taskDesc: '组织年度员工团建活动',
summary: '1. 任务已经完成,因特...',
summaryDetail: '活动已落地执行含团队协作游戏、主题分享环节反馈良好。参与率达到95%收集到23条有效反馈其中85%为正面评价。',
executor: '周北北',
progress: '已完成',
priority: '紧急不重要',
startDate: '2023/01/18',
expectEndDate: '2024/12/02',
isDelay: '正常',
actualEndDate: '2023/05/25',
latestProgress: '已经确定了活动日期和地...',
progressDetail: '选定XX营地日期2023/05/20含露营、烧烤、团队挑战。活动预算控制在计划内实际花费比预算节省8%。'
},
{
taskDesc: '办公室绿植更换',
summary: '1. 已联系3家供应商...',
summaryDetail: '已联系3家供应商正在比较报价和服务。现有绿植约60%需要更换,主要是走廊和公共区域的大型绿植。',
executor: '王静静',
progress: '待开始',
priority: '不紧急不重要',
startDate: '2023/11/20',
expectEndDate: '2023/11/30',
isDelay: '正常',
actualEndDate: '',
latestProgress: '正在筛选供应商,等待批...',
progressDetail: '正在筛选供应商等待审批。初步选定两家供应商报价相差约15%,正在核实服务内容差异。'
},
{
taskDesc: '更新员工通讯录',
summary: '1. 收集各部门最新联...',
summaryDetail: '正在收集各部门最新联系方式,已完成市场部和销售部的信息更新,技术部和人力资源部资料待收。',
executor: '李小明',
progress: '待开始',
priority: '不紧急不重要',
startDate: '2023/11/25',
expectEndDate: '2023/12/15',
isDelay: '正常',
actualEndDate: '',
latestProgress: '等待各部门提交最新联...',
progressDetail: '已发送通知邮件给各部门负责人要求提供最新员工联系方式目前收到60%的回复。'
},
{
taskDesc: '整理归档旧项目文档',
summary: '1. 开始整理2022年...',
summaryDetail: '开始整理2022年度已完成项目的文档按照项目类型和日期进行分类归档预计需要两周时间完成。',
executor: '张小红',
progress: '待开始',
priority: '不紧急不重要',
startDate: '2023/12/01',
expectEndDate: '2023/12/15',
isDelay: '正常',
actualEndDate: '',
latestProgress: '准备归档工具和分类标...',
progressDetail: '已准备好归档所需的文件夹和标签,正在制定分类标准,等待主管审批。'
}
])
// 分组键(固定四个重要紧急程度)
const groupKeys = ref(['重要紧急', '紧急不重要', '重要不紧急', '不紧急不重要'])
// 分组状态管理
const groupCollapseState = ref<Record<string, boolean>>({})
// 构建分组数据:按重要紧急程度分组
const groupedTasks = computed(() => {
const groups: Record<string, TaskGroup> = {}
// 初始化分组
groupKeys.value.forEach(key => {
// 初始化折叠状态,如果已有状态则使用,否则默认展开(false)
groupCollapseState.value[key] = groupCollapseState.value[key] ?? false
groups[key] = {
collapsed: groupCollapseState.value[key],
tasks: []
}
})
// 将任务分配到对应分组
rawTasks.value.forEach(task => {
const groupKey = task.priority
if (groups[groupKey]) {
groups[groupKey].tasks.push(task)
}
})
return groups
})
// 折叠/展开分组
function toggleGroup(groupKey: string) {
groupCollapseState.value[groupKey] = !groupCollapseState.value[groupKey]
// 保存状态到本地存储
localStorage.setItem('taskGroupCollapse', JSON.stringify(groupCollapseState.value))
}
// 组件挂载时恢复折叠状态
onMounted(() => {
const savedState = localStorage.getItem('taskGroupCollapse')
if (savedState) {
groupCollapseState.value = JSON.parse(savedState)
}
})
// 弹窗状态
const popupVisible = ref(false)
const popupTitle = ref('')
const popupContent = ref('')
const popupTop = ref(0)
const popupLeft = ref(0)
let popupTimer: number | null = null
// 打开详情弹窗
function openPopup(event: MouseEvent, content: string, title: string) {
// 清除之前的关闭计时器
if (popupTimer) {
clearTimeout(popupTimer)
popupTimer = null
}
const target = event.currentTarget as HTMLElement
const rect = target.getBoundingClientRect()
popupVisible.value = true
popupTitle.value = title
popupContent.value = content
// 固定居中显示
popupTop.value = window.scrollY + window.innerHeight / 2 - 100
popupLeft.value = window.innerWidth / 2 - 150
}
// 延迟关闭弹窗(防止鼠标移动时意外关闭)
function closePopup() {
popupTimer = setTimeout(() => {
popupVisible.value = false
popupTimer = null
}, 200)
}
// 取消延迟关闭
function cancelClosePopup() {
if (popupTimer) {
clearTimeout(popupTimer)
popupTimer = null
}
}
// 点击页面其他区域关闭弹窗
function handleDocumentClick(e: MouseEvent) {
const popup = document.querySelector('.popup')
const infoCells = document.querySelectorAll('.info-cell')
// 如果点击的不是弹窗也不是信息单元格,则关闭弹窗
if (popup && !popup.contains(e.target as Node) &&
!Array.from(infoCells).some(cell => cell.contains(e.target as Node))) {
closePopup()
}
}
onMounted(() => {
document.addEventListener('click', handleDocumentClick)
})
onUnmounted(() => {
document.removeEventListener('click', handleDocumentClick)
})
</script>
<style scoped>
/* 页面基础样式 */
.task-tracking-page {
padding: 20px;
background: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.page-header {
margin-bottom: 20px;
}
.page-header h2 {
font-size: 20px;
font-weight: bold;
margin-bottom: 5px;
color: #333;
}
.page-description {
color: #666;
font-size: 14px;
}
/* 公共表头 */
.shared-header {
background: #f8f9fa;
border: 1px solid #eee;
margin-bottom: 10px;
border-radius: 4px;
}
.header-row {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.header-row .col {
display: flex;
align-items: center;
justify-content: flex-start;
border-right: 1px solid #eee;
padding: 0 8px;
font-weight: 500;
color: #333;
font-size: 14px;
}
.header-row .col:last-child {
border-right: none;
}
/* 分组样式 */
.task-group {
margin-bottom: 10px;
border: 1px solid #eee;
border-radius: 4px;
background: #fff;
}
/* 分组标题(带折叠/展开按钮) */
.group-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 15px;
border-bottom: 1px solid #eee;
cursor: pointer;
background: transparent;
}
.group-title-text {
font-weight: 500;
font-size: 15px;
padding: 4px 8px;
border-radius: 8px;
color: #fff;
}
.group-title-text.重要紧急 {
background: #dc3545;
}
.group-title-text.紧急不重要 {
background: #fd7e14;
}
.group-title-text.重要不紧急 {
background: #ffc107;
color: #333;
}
.group-title-text.不紧急不重要 {
background: #28a745;
}
/* 折叠/展开按钮 */
.toggle-btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 10px;
background-color: #e9ecef;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
color: #495057;
transition: all 0.2s ease;
}
.toggle-btn:hover {
background-color: #dee2e6;
color: #212529;
}
.toggle-btn .icon {
font-size: 14px;
}
.toggle-text {
user-select: none;
}
/* 任务列表(展开时显示) */
.task-tracking-page {
padding: 20px;
background: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
height: calc(100vh - 100px);
overflow-y: auto;
position: relative;
}
.sticky-headers {
position: sticky;
top: -20px; /* 向上移动消除空隙 */
background: #fff;
z-index: 10;
padding: 20px 0 10px;
margin-top: -20px; /* 消除外部空隙 */
}
.page-header {
padding: 20px 0 10px;
margin-bottom: 0;
}
.shared-header {
background: #fff;
border: 1px solid #eee;
border-radius: 4px;
margin-bottom: 10px;
}
.task-list {
padding: 10px;
}
/* 自定义滚动条样式 */
.task-tracking-page::-webkit-scrollbar {
width: 8px;
}
.task-tracking-page::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.task-tracking-page::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
.task-tracking-page::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 任务行样式 */
.task-row {
display: flex;
align-items: center;
border-bottom: 1px solid #eee;
padding: 10px 0;
transition: background-color 0.2s;
}
.task-row:hover {
background-color: #f8f9fa;
}
.task-row:last-child {
border-bottom: none;
}
.task-row .col {
display: flex;
align-items: center;
justify-content: flex-start;
border-right: 1px solid #eee;
padding: 0 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
color: #333;
height: 40px;
}
.task-row .col:last-child {
border-right: none;
}
/* 信息单元格(带弹窗) */
.info-cell {
cursor: pointer;
color: #1890ff;
position: relative;
display: flex;
align-items: center;
gap: 4px;
}
.info-cell:hover {
text-decoration: underline;
}
.info-cell .el-icon-info {
font-size: 14px;
color: #666;
}
/* 进展标签样式 */
.progress-tag {
padding: 4px 8px;
border-radius: 8px;
color: #fff;
text-align: center;
font-weight: 500;
}
.progress-tag.进行中 {
background: #ffc107;
}
.progress-tag.已完成 {
background: #4caf50;
}
.progress-tag.待开始 {
background: #ff9800;
}
/* 是否延期标签 */
.delay-tag {
text-align: center;
font-weight: 500;
}
.delay-tag.正常 {
color: #28a745;
}
.delay-tag.已延期 {
color: #f44336;
}
/* 重要紧急程度标签 */
.priority-tag {
padding: 4px 8px;
border-radius: 8px;
color: #fff;
text-align: center;
font-weight: 500;
}
.priority-tag.重要紧急 {
background: #dc3545;
}
.priority-tag.紧急不重要 {
background: #fd7e14;
}
.priority-tag.重要不紧急 {
background: #ffc107;
color: #333;
}
.priority-tag.不紧急不重要 {
background: #28a745;
}
/* 弹窗样式 */
.popup {
/* 定位到页面中间 */
top: 50%;
left: 50%;
position: fixed;
width: 300px;
max-width: 80vw;
background: #fff;
border: 1px solid #f70b0b;
border-radius: 4px;
padding: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
z-index: 999;
animation: fadeIn 0.15s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
.popup-title {
font-weight: bold;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
color: #333;
font-size: 15px;
}
.popup-content {
color: #555;
font-size: 14px;
line-height: 1.6;
max-height: 200px;
overflow-y: auto;
white-space: pre-wrap;
}
/* 弹窗过渡动画 */
.popup-enter-active {
transition: all 0.15s ease-out;
}
.popup-enter-from {
opacity: 0;
transform: translateY(5px);
}
.popup-enter-to {
opacity: 1;
transform: translateY(0);
}
</style>