895 lines
25 KiB
Vue
895 lines
25 KiB
Vue
|
|
<!--
|
|||
|
|
前端路径:d:\RuoYI_workspace\fad_oa\ruoyi-ui\src\views\oa\project\dashboard2\index.vue
|
|||
|
|
后端聚合接口:GET /oa/project/dashboard/{projectId}
|
|||
|
|
-->
|
|||
|
|
<template>
|
|||
|
|
<div class="app-container dashboard2" v-loading="pageLoading">
|
|||
|
|
<div class="layout">
|
|||
|
|
<!-- 左侧 20%:项目列表 -->
|
|||
|
|
<div class="left">
|
|||
|
|
<div class="left-search">
|
|||
|
|
<el-input
|
|||
|
|
v-model="projectQuery.keyword"
|
|||
|
|
size="small"
|
|||
|
|
clearable
|
|||
|
|
placeholder="名称 / 编号 / 代号"
|
|||
|
|
class="left-keyword-input"
|
|||
|
|
prefix-icon="el-icon-search"
|
|||
|
|
@keyup.enter.native="getProjectList"
|
|||
|
|
/>
|
|||
|
|
<el-button type="primary" size="small" class="left-search-btn" @click="getProjectList">搜索</el-button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="project-list" v-loading="projectLoading">
|
|||
|
|
<div
|
|||
|
|
v-for="p in projectList"
|
|||
|
|
:key="p.projectId"
|
|||
|
|
class="project-item"
|
|||
|
|
:class="{ active: String(p.projectId) === String(currentProjectId) }"
|
|||
|
|
@click="handleSelectProject(p)"
|
|||
|
|
:title="p.projectName"
|
|||
|
|
>
|
|||
|
|
<div class="project-name text-ellipsis">{{ p.projectName }}</div>
|
|||
|
|
<div class="project-meta text-ellipsis">
|
|||
|
|
<span v-if="p.projectCode" class="code">{{ p.projectCode }}</span>
|
|||
|
|
<span class="time">{{ formatDate(p.beginTime) }} ~ {{ formatDate(p.finishTime) }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-if="!projectList || projectList.length === 0" class="left-empty">
|
|||
|
|
<el-empty :image-size="72" description="暂无项目"></el-empty>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="left-pager">
|
|||
|
|
<el-pagination
|
|||
|
|
small
|
|||
|
|
background
|
|||
|
|
layout="total, prev, pager, next"
|
|||
|
|
:total="projectTotal"
|
|||
|
|
:current-page.sync="projectQuery.pageNum"
|
|||
|
|
:page-size.sync="projectQuery.pageSize"
|
|||
|
|
@current-change="getProjectList"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 右侧 80% -->
|
|||
|
|
<div class="right">
|
|||
|
|
<div v-if="!currentProjectId" class="right-empty">
|
|||
|
|
<el-empty description="请选择项目"></el-empty>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<template v-else>
|
|||
|
|
<div class="panel task-panel">
|
|||
|
|
<div class="panel-header task-panel__toolbar">
|
|||
|
|
<div class="toolbar">
|
|||
|
|
<el-input
|
|||
|
|
v-model="taskQuery.projectCode"
|
|||
|
|
size="small"
|
|||
|
|
clearable
|
|||
|
|
placeholder="项目代号"
|
|||
|
|
style="width: 140px"
|
|||
|
|
/>
|
|||
|
|
<el-input
|
|||
|
|
v-model="taskQuery.taskKeyword"
|
|||
|
|
size="small"
|
|||
|
|
clearable
|
|||
|
|
placeholder="任务主题关键词"
|
|||
|
|
style="width: 160px"
|
|||
|
|
@keyup.enter.native="resetTaskPage"
|
|||
|
|
/>
|
|||
|
|
<el-button type="primary" size="mini" @click="resetTaskPage">搜索</el-button>
|
|||
|
|
<el-button size="mini" @click="resetTaskFilters">重置</el-button>
|
|||
|
|
<el-button size="mini" @click="refreshCurrent">刷新</el-button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="panel-body panel-body-flex">
|
|||
|
|
<div class="task-table-wrap">
|
|||
|
|
<vxe-table
|
|||
|
|
ref="taskTable"
|
|||
|
|
size="mini"
|
|||
|
|
class="task-vxe"
|
|||
|
|
border
|
|||
|
|
stripe
|
|||
|
|
show-overflow="tooltip"
|
|||
|
|
height="auto"
|
|||
|
|
:data="pagedTaskList"
|
|||
|
|
:row-config="{ isHover: true }"
|
|||
|
|
>
|
|||
|
|
<vxe-column field="projectCode" title="代号" width="72"></vxe-column>
|
|||
|
|
<vxe-column field="taskTitle" title="任务主题" min-width="96"></vxe-column>
|
|||
|
|
<vxe-column field="scheduleProgress" title="对应进度" min-width="112">
|
|||
|
|
<template #default="{ row }">
|
|||
|
|
<div v-if="scheduleProgressUnlinked(row)" class="schedule-progress-wrap">
|
|||
|
|
<el-tag type="info" effect="plain" size="mini">未关联进度</el-tag>
|
|||
|
|
</div>
|
|||
|
|
<div
|
|||
|
|
v-else
|
|||
|
|
class="schedule-progress-wrap schedule-progress-wrap--linked"
|
|||
|
|
:title="scheduleProgressTitle(row)"
|
|||
|
|
>
|
|||
|
|
<el-tag :type="scheduleStepTagType(row)" size="mini" class="schedule-progress-status-tag">
|
|||
|
|
{{ scheduleStepStatusLabel(row) }}
|
|||
|
|
</el-tag>
|
|||
|
|
<span class="schedule-progress-path">{{ scheduleProgressPath(row) }}</span>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
</vxe-column>
|
|||
|
|
<vxe-column field="taskType" title="工作类型" width="86">
|
|||
|
|
<template #default="{ row }">
|
|||
|
|
<dict-tag :options="dict.type.sys_work_type" :value="row.taskType" />
|
|||
|
|
</template>
|
|||
|
|
</vxe-column>
|
|||
|
|
<vxe-column field="taskGrade" title="任务级别" width="78">
|
|||
|
|
<template #default="{ row }">
|
|||
|
|
<dict-tag :options="dict.type.sys_sort_grade" :value="row.taskGrade" />
|
|||
|
|
</template>
|
|||
|
|
</vxe-column>
|
|||
|
|
<vxe-column field="workerNickName" title="执行人" width="76">
|
|||
|
|
<template #default="{ row }">{{ row.workerNickName || '-' }}</template>
|
|||
|
|
</vxe-column>
|
|||
|
|
<vxe-column field="createUserNickName" title="创建人" width="76">
|
|||
|
|
<template #default="{ row }">{{ row.createUserNickName || '-' }}</template>
|
|||
|
|
</vxe-column>
|
|||
|
|
<vxe-column field="state" title="状态" width="88">
|
|||
|
|
<template #default="{ row }">
|
|||
|
|
<el-tag :type="getStateTagType(row.state)" size="mini">{{ stateText(row.state) }}</el-tag>
|
|||
|
|
</template>
|
|||
|
|
</vxe-column>
|
|||
|
|
<vxe-column field="finishTime" title="预期结束" width="100">
|
|||
|
|
<template #default="{ row }">{{ formatDate(row.finishTime) }}</template>
|
|||
|
|
</vxe-column>
|
|||
|
|
</vxe-table>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="task-pager">
|
|||
|
|
<el-pagination
|
|||
|
|
small
|
|||
|
|
background
|
|||
|
|
layout="total, prev, pager, next"
|
|||
|
|
:total="filteredTaskTotal"
|
|||
|
|
:current-page.sync="taskQuery.pageNum"
|
|||
|
|
:page-size.sync="taskQuery.pageSize"
|
|||
|
|
@current-change="noop"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 下:进度板块 — Tab1 进度明细表(默认);Tab2 原思维导图 + 图例 -->
|
|||
|
|
<div class="panel schedule-panel">
|
|||
|
|
<el-tabs v-model="scheduleViewTab" class="schedule-panel-tabs">
|
|||
|
|
<el-tab-pane label="进度明细" name="list">
|
|||
|
|
<!-- 进度数据:表格自然撑开高度,由 .progress-table-scroll 单独承担纵向/横向滚动 -->
|
|||
|
|
<div class="progress-table-pane" v-loading="xmindLoading">
|
|||
|
|
<el-empty
|
|||
|
|
v-if="!stepList || stepList.length === 0"
|
|||
|
|
:image-size="72"
|
|||
|
|
description="暂无进度步骤数据"
|
|||
|
|
/>
|
|||
|
|
<div v-else class="progress-table-scroll">
|
|||
|
|
<el-table
|
|||
|
|
:data="scheduleStepsForTable"
|
|||
|
|
size="mini"
|
|||
|
|
border
|
|||
|
|
stripe
|
|||
|
|
class="progress-step-el-table"
|
|||
|
|
>
|
|||
|
|
<el-table-column prop="firstLevelNode" label="一级节点" min-width="112" show-overflow-tooltip />
|
|||
|
|
<el-table-column prop="secondLevelNode" label="二级节点" min-width="112" show-overflow-tooltip />
|
|||
|
|
<el-table-column prop="stepName" label="步骤名称" min-width="128" show-overflow-tooltip />
|
|||
|
|
<el-table-column label="状态" width="92" align="center">
|
|||
|
|
<template slot-scope="{ row }">
|
|||
|
|
<el-tag :type="stepStatusTagType(row)" size="mini">{{ stepStatusLabel(row) }}</el-tag>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="计划开始" width="108" align="center">
|
|||
|
|
<template slot-scope="{ row }">{{ formatStepPlanStart(row) }}</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="计划结束" width="108" align="center">
|
|||
|
|
<template slot-scope="{ row }">{{ formatStepPlanEnd(row) }}</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="负责人" min-width="96" show-overflow-tooltip>
|
|||
|
|
<template slot-scope="{ row }">{{ formatStepResponsible(row) }}</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
</el-table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</el-tab-pane>
|
|||
|
|
<el-tab-pane label="进度导图" name="mind">
|
|||
|
|
<div class="schedule-tab-pane-inner schedule-tab-pane-inner--mind">
|
|||
|
|
<div class="schedule-mind">
|
|||
|
|
<div class="xmind-wrap" v-loading="xmindLoading">
|
|||
|
|
<div v-if="xmindEmpty" class="xmind-empty">
|
|||
|
|
<el-empty :image-size="80" description="暂无进度步骤数据"></el-empty>
|
|||
|
|
</div>
|
|||
|
|
<xmind
|
|||
|
|
v-else-if="scheduleViewTab === 'mind'"
|
|||
|
|
:list="stepList"
|
|||
|
|
height="100%"
|
|||
|
|
dashboard-mode
|
|||
|
|
@refresh="onXmindRefresh"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<aside v-if="scheduleViewTab === 'mind' && !xmindEmpty && stepList && stepList.length" class="mind-legend-aside">
|
|||
|
|
<div class="mind-legend-title">状态图例</div>
|
|||
|
|
<div class="mind-legend-row">
|
|||
|
|
<i class="lg-dot lg-dot--done" />
|
|||
|
|
<span>已完成</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="mind-legend-row">
|
|||
|
|
<i class="lg-dot lg-dot--doing" />
|
|||
|
|
<span>进行中 / 待验收</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="mind-legend-row">
|
|||
|
|
<i class="lg-dot lg-dot--todo" />
|
|||
|
|
<span>未开始 / 暂停</span>
|
|||
|
|
</div>
|
|||
|
|
<p class="mind-legend-tip">连线:已完成节点为绿色;可滚轮缩放、拖动画布。</p>
|
|||
|
|
</aside>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</el-tab-pane>
|
|||
|
|
</el-tabs>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import { mapState } from 'vuex'
|
|||
|
|
import Xmind from '@/views/oa/project/pace/components/xmind.vue'
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
name: 'OaProjectDashboard2',
|
|||
|
|
components: { Xmind },
|
|||
|
|
dicts: ['sys_work_type', 'sys_sort_grade'],
|
|||
|
|
data () {
|
|||
|
|
return {
|
|||
|
|
pageLoading: false,
|
|||
|
|
projectLoading: false,
|
|||
|
|
xmindLoading: false,
|
|||
|
|
/** 进度板块:默认进度明细表,切换为进度导图(原思维导图) */
|
|||
|
|
scheduleViewTab: 'list'
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
computed: {
|
|||
|
|
...mapState('oaProjectDashboard2', [
|
|||
|
|
'currentProjectId',
|
|||
|
|
'projectQuery',
|
|||
|
|
'projectList',
|
|||
|
|
'projectTotal',
|
|||
|
|
'taskQuery',
|
|||
|
|
'taskList',
|
|||
|
|
'stepList'
|
|||
|
|
]),
|
|||
|
|
filteredTaskList () {
|
|||
|
|
const list = this.taskList || []
|
|||
|
|
const code = (this.taskQuery.projectCode || '').trim().toLowerCase()
|
|||
|
|
const kw = (this.taskQuery.taskKeyword || '').trim().toLowerCase()
|
|||
|
|
return list.filter(r => {
|
|||
|
|
const okCode = !code || String(r.projectCode || '').toLowerCase().indexOf(code) !== -1
|
|||
|
|
const title = String(r.taskTitle || '').toLowerCase()
|
|||
|
|
const okKw = !kw || title.indexOf(kw) !== -1
|
|||
|
|
return okCode && okKw
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
filteredTaskTotal () {
|
|||
|
|
return (this.filteredTaskList || []).length
|
|||
|
|
},
|
|||
|
|
pagedTaskList () {
|
|||
|
|
const pageNum = Number(this.taskQuery.pageNum || 1)
|
|||
|
|
const pageSize = Number(this.taskQuery.pageSize || 10)
|
|||
|
|
const start = (pageNum - 1) * pageSize
|
|||
|
|
return (this.filteredTaskList || []).slice(start, start + pageSize)
|
|||
|
|
},
|
|||
|
|
xmindEmpty () {
|
|||
|
|
return !this.stepList || this.stepList.length === 0
|
|||
|
|
},
|
|||
|
|
/** 进度明细表:按步骤序号排序,便于阅读 */
|
|||
|
|
scheduleStepsForTable () {
|
|||
|
|
const list = this.stepList || []
|
|||
|
|
return [...list].sort((a, b) => {
|
|||
|
|
const oa = Number(a.stepOrder != null ? a.stepOrder : 0)
|
|||
|
|
const ob = Number(b.stepOrder != null ? b.stepOrder : 0)
|
|||
|
|
return oa - ob
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
created () {
|
|||
|
|
this.getProjectList()
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
async getProjectList () {
|
|||
|
|
this.projectLoading = true
|
|||
|
|
try {
|
|||
|
|
await this.$store.dispatch('oaProjectDashboard2/fetchProjectList', {
|
|||
|
|
pageNum: this.projectQuery.pageNum,
|
|||
|
|
pageSize: this.projectQuery.pageSize,
|
|||
|
|
keyword: this.projectQuery.keyword
|
|||
|
|
})
|
|||
|
|
} catch (e) {
|
|||
|
|
this.$message.error('项目列表加载失败,请稍后重试')
|
|||
|
|
} finally {
|
|||
|
|
this.projectLoading = false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
async handleSelectProject (p) {
|
|||
|
|
const projectId = p && p.projectId
|
|||
|
|
if (!projectId) return
|
|||
|
|
this.scheduleViewTab = 'list'
|
|||
|
|
this.pageLoading = true
|
|||
|
|
this.xmindLoading = true
|
|||
|
|
try {
|
|||
|
|
this.$store.commit('oaProjectDashboard2/SET_TASK_QUERY', { pageNum: 1 })
|
|||
|
|
await this.$store.dispatch('oaProjectDashboard2/selectProject', projectId)
|
|||
|
|
} catch (e) {
|
|||
|
|
this.$message.error((e && e.message) || '加载项目数据失败,请稍后重试')
|
|||
|
|
} finally {
|
|||
|
|
this.pageLoading = false
|
|||
|
|
this.xmindLoading = false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
resetTaskPage () {
|
|||
|
|
this.$store.commit('oaProjectDashboard2/SET_TASK_QUERY', { pageNum: 1 })
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
resetTaskFilters () {
|
|||
|
|
this.$store.commit('oaProjectDashboard2/SET_TASK_QUERY', {
|
|||
|
|
pageNum: 1,
|
|||
|
|
pageSize: 10,
|
|||
|
|
projectCode: '',
|
|||
|
|
taskKeyword: ''
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
async refreshCurrent () {
|
|||
|
|
if (!this.currentProjectId) return
|
|||
|
|
this.pageLoading = true
|
|||
|
|
this.xmindLoading = true
|
|||
|
|
try {
|
|||
|
|
await this.$store.dispatch('oaProjectDashboard2/selectProject', this.currentProjectId)
|
|||
|
|
} catch (e) {
|
|||
|
|
this.$message.error('刷新失败,请稍后重试')
|
|||
|
|
} finally {
|
|||
|
|
this.pageLoading = false
|
|||
|
|
this.xmindLoading = false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/** 计划开始:库表 plan_start 可能为空,与进度页一致回退 start_time */
|
|||
|
|
formatStepPlanStart (row) {
|
|||
|
|
const raw = row && (row.planStart != null && row.planStart !== '' ? row.planStart : row.startTime)
|
|||
|
|
return this.formatDateFlexible(raw)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
formatStepPlanEnd (row) {
|
|||
|
|
const raw = row && (row.planEnd != null && row.planEnd !== '' ? row.planEnd : row.endTime)
|
|||
|
|
return this.formatDateFlexible(raw)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
formatDateFlexible (val) {
|
|||
|
|
if (val == null || val === '') return '-'
|
|||
|
|
if (Array.isArray(val) && val.length >= 3) {
|
|||
|
|
const y = val[0]
|
|||
|
|
const mo = String(val[1]).padStart(2, '0')
|
|||
|
|
const d = String(val[2]).padStart(2, '0')
|
|||
|
|
return `${y}-${mo}-${d}`
|
|||
|
|
}
|
|||
|
|
if (typeof val === 'string') {
|
|||
|
|
const s = val.trim()
|
|||
|
|
if (!s) return '-'
|
|||
|
|
if (s.length >= 10 && s[4] === '-' && s[7] === '-') return s.substring(0, 10)
|
|||
|
|
const m = s.match(/^(\d{4}-\d{2}-\d{2})/)
|
|||
|
|
return m ? m[1] : s
|
|||
|
|
}
|
|||
|
|
if (val instanceof Date && !isNaN(val.getTime())) {
|
|||
|
|
const y = val.getFullYear()
|
|||
|
|
const mo = String(val.getMonth() + 1).padStart(2, '0')
|
|||
|
|
const d = String(val.getDate()).padStart(2, '0')
|
|||
|
|
return `${y}-${mo}-${d}`
|
|||
|
|
}
|
|||
|
|
return '-'
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/** 负责人:业务侧常用 node_header,兼容 header */
|
|||
|
|
formatStepResponsible (row) {
|
|||
|
|
if (!row) return '-'
|
|||
|
|
const h = row.header != null && String(row.header).trim() !== '' ? String(row.header).trim() : ''
|
|||
|
|
if (h) return h
|
|||
|
|
const n = row.nodeHeader != null && String(row.nodeHeader).trim() !== '' ? String(row.nodeHeader).trim() : ''
|
|||
|
|
return n || '-'
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
async onXmindRefresh () {
|
|||
|
|
await this.refreshCurrent()
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
noop () {},
|
|||
|
|
|
|||
|
|
/** 无 track_id 或无步骤名称 → 未关联(后端联表 oa_project_schedule_step,use_flag=1) */
|
|||
|
|
scheduleProgressUnlinked (row) {
|
|||
|
|
if (!row || row.trackId == null || row.trackId === '') return true
|
|||
|
|
const n = row.scheduleStepName
|
|||
|
|
return n == null || String(n).trim() === ''
|
|||
|
|
},
|
|||
|
|
scheduleStepTagType (row) {
|
|||
|
|
const s = row && row.scheduleStatus
|
|||
|
|
if (s === null || s === undefined) return 'info'
|
|||
|
|
const v = Number(s)
|
|||
|
|
if (v === 2) return 'success'
|
|||
|
|
if (v === 3) return 'warning'
|
|||
|
|
if (v === 1) return 'primary'
|
|||
|
|
if (v === 0) return 'info'
|
|||
|
|
return 'info'
|
|||
|
|
},
|
|||
|
|
scheduleStepStatusLabel (row) {
|
|||
|
|
const s = row && row.scheduleStatus
|
|||
|
|
if (s === null || s === undefined) return '—'
|
|||
|
|
const v = Number(s)
|
|||
|
|
if (v === 2) return '已完成'
|
|||
|
|
if (v === 1) return '进行中'
|
|||
|
|
if (v === 3) return '暂停'
|
|||
|
|
if (v === 0) return '未开始'
|
|||
|
|
return '—'
|
|||
|
|
},
|
|||
|
|
scheduleProgressPath (row) {
|
|||
|
|
if (!row) return ''
|
|||
|
|
const n = row.scheduleStepName
|
|||
|
|
return (n != null && String(n).trim() !== '') ? String(n).trim() : ''
|
|||
|
|
},
|
|||
|
|
scheduleProgressTitle (row) {
|
|||
|
|
return `${this.scheduleStepStatusLabel(row)} ${this.scheduleProgressPath(row)}`.trim()
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
/** oa_project_schedule_step.status:与任务表「对应进度」标签一致 */
|
|||
|
|
stepStatusLabel (row) {
|
|||
|
|
return this.scheduleStepStatusLabel({ scheduleStatus: row && row.status })
|
|||
|
|
},
|
|||
|
|
stepStatusTagType (row) {
|
|||
|
|
return this.scheduleStepTagType({ scheduleStatus: row && row.status })
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
formatDate (val) {
|
|||
|
|
if (!val) return '-'
|
|||
|
|
const s = String(val)
|
|||
|
|
return s.length >= 10 ? s.substring(0, 10) : s
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
stateText (val) {
|
|||
|
|
if (val === null || val === undefined || val === '') return '-'
|
|||
|
|
const v = Number(val)
|
|||
|
|
if (v === 2) return '执行完成'
|
|||
|
|
if (v === 1) return '待验收'
|
|||
|
|
if (v === 15) return '延期申请中'
|
|||
|
|
if (v === 0) return '进行中'
|
|||
|
|
return '其他'
|
|||
|
|
},
|
|||
|
|
getStateTagType (val) {
|
|||
|
|
if (val === null || val === undefined || val === '') return 'info'
|
|||
|
|
const v = Number(val)
|
|||
|
|
if (v === 2) return 'success'
|
|||
|
|
if (v === 1) return 'warning'
|
|||
|
|
if (v === 15) return 'warning'
|
|||
|
|
if (v === 0) return 'info'
|
|||
|
|
return 'info'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.dashboard2 {
|
|||
|
|
font-size: 13px;
|
|||
|
|
color: #606266;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.layout {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 8px;
|
|||
|
|
align-items: stretch;
|
|||
|
|
height: calc(100vh - 120px);
|
|||
|
|
min-height: 0;
|
|||
|
|
max-height: calc(100vh - 120px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.left {
|
|||
|
|
width: 20%;
|
|||
|
|
flex: 0 0 20%;
|
|||
|
|
min-width: 220px;
|
|||
|
|
max-width: 300px;
|
|||
|
|
border: 1px solid #e4e7ed;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
padding: 10px;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
background: #fafbfc;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.left-search {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: row;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
margin-bottom: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.left-keyword-input {
|
|||
|
|
flex: 1;
|
|||
|
|
min-width: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.left-search-btn {
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
padding-left: 14px;
|
|||
|
|
padding-right: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.project-list {
|
|||
|
|
flex: 1;
|
|||
|
|
overflow: auto;
|
|||
|
|
min-height: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.project-item {
|
|||
|
|
padding: 6px 8px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
border-bottom: 1px solid #eef0f3;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
margin-bottom: 2px;
|
|||
|
|
min-height: 42px;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
justify-content: center;
|
|||
|
|
transition: background 0.15s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.project-item:hover {
|
|||
|
|
background: #f0f2f5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.project-item.active {
|
|||
|
|
background: #ecf5ff;
|
|||
|
|
border-left: 3px solid #409eff;
|
|||
|
|
padding-left: 5px;
|
|||
|
|
box-shadow: inset 0 0 0 1px rgba(64, 158, 255, 0.15);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.project-name {
|
|||
|
|
font-weight: 500;
|
|||
|
|
font-size: 12px;
|
|||
|
|
line-height: 1.35;
|
|||
|
|
color: #303133;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.project-meta {
|
|||
|
|
font-size: 11px;
|
|||
|
|
color: #909399;
|
|||
|
|
line-height: 1.35;
|
|||
|
|
margin-top: 3px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.project-meta .code {
|
|||
|
|
margin-right: 8px;
|
|||
|
|
color: #409eff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.left-pager {
|
|||
|
|
padding-top: 8px;
|
|||
|
|
margin-top: 4px;
|
|||
|
|
border-top: 1px solid #ebeef5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.left-pager :deep(.el-pagination) {
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.left-empty {
|
|||
|
|
padding: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.right {
|
|||
|
|
flex: 1;
|
|||
|
|
min-width: 0;
|
|||
|
|
min-height: 0;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 8px;
|
|||
|
|
height: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.right-empty {
|
|||
|
|
border: 1px dashed #dcdfe6;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
flex: 1;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.panel {
|
|||
|
|
border: 1px solid #ebeef5;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
background: #fff;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
min-height: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.panel-header {
|
|||
|
|
padding: 8px 12px;
|
|||
|
|
border-bottom: 1px solid #ebeef5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 原「任务列表」标题条去掉后,工具栏区沿用顶栏灰底,与下方表格白底区分不变 */
|
|||
|
|
.task-panel__toolbar {
|
|||
|
|
background: #fafafa;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.panel-body {
|
|||
|
|
padding: 8px 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.panel-body-flex {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
flex: 1;
|
|||
|
|
min-height: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.toolbar {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 4px;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.task-panel {
|
|||
|
|
flex: 4 1 0%;
|
|||
|
|
min-height: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.schedule-panel {
|
|||
|
|
flex: 6 1 0%;
|
|||
|
|
min-height: 0;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
padding: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.schedule-panel-tabs {
|
|||
|
|
flex: 1 1 0%;
|
|||
|
|
min-height: 0;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Element Tabs 根节点参与纵向 flex,才能把剩余高度交给内容区 */
|
|||
|
|
.schedule-panel-tabs :deep(.el-tabs) {
|
|||
|
|
flex: 1 1 0%;
|
|||
|
|
min-height: 0;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
height: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.schedule-panel-tabs :deep(.el-tabs__header) {
|
|||
|
|
margin-bottom: 0;
|
|||
|
|
padding: 0 12px;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.schedule-panel-tabs :deep(.el-tabs__content) {
|
|||
|
|
flex: 1 1 0%;
|
|||
|
|
min-height: 0;
|
|||
|
|
overflow: hidden;
|
|||
|
|
padding: 0;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 当前激活的 pane 占满内容区高度,子元素才能算出可滚动区域 */
|
|||
|
|
.schedule-panel-tabs :deep(.el-tab-pane) {
|
|||
|
|
flex: 1 1 0%;
|
|||
|
|
min-height: 0;
|
|||
|
|
overflow: hidden;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.schedule-tab-pane-inner {
|
|||
|
|
flex: 1 1 0%;
|
|||
|
|
min-height: 0;
|
|||
|
|
height: 100%;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.schedule-tab-pane-inner--mind {
|
|||
|
|
min-height: 280px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 进度明细 Tab:占满下方板块;内部仅滚动区参与滚动 */
|
|||
|
|
.progress-table-pane {
|
|||
|
|
flex: 1 1 0%;
|
|||
|
|
min-height: 0;
|
|||
|
|
width: 100%;
|
|||
|
|
height:300px;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
padding: 8px 12px 10px;
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.progress-table-scroll {
|
|||
|
|
flex: 1 1 0%;
|
|||
|
|
min-height: 0;
|
|||
|
|
min-width: 0;
|
|||
|
|
overflow: auto;
|
|||
|
|
-webkit-overflow-scrolling: touch;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.progress-step-el-table {
|
|||
|
|
width: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.schedule-mind {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: row;
|
|||
|
|
align-items: stretch;
|
|||
|
|
flex: 1;
|
|||
|
|
min-height: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.task-table-wrap {
|
|||
|
|
flex: 1;
|
|||
|
|
min-height: 0;
|
|||
|
|
min-width: 0;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.task-table-wrap :deep(.vxe-table) {
|
|||
|
|
font-size: 12px;
|
|||
|
|
width: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.task-table-wrap :deep(.vxe-table--body-wrapper),
|
|||
|
|
.task-table-wrap :deep(.vxe-table--header-wrapper) {
|
|||
|
|
overflow-x: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.schedule-progress-wrap {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 6px;
|
|||
|
|
max-width: 100%;
|
|||
|
|
line-height: 1.35;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.schedule-progress-wrap--linked {
|
|||
|
|
min-width: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.schedule-progress-status-tag {
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.schedule-progress-path {
|
|||
|
|
flex: 1;
|
|||
|
|
min-width: 0;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: #606266;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.task-pager {
|
|||
|
|
margin-top: 8px;
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: flex-end;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.xmind-wrap {
|
|||
|
|
flex: 1;
|
|||
|
|
min-width: 0;
|
|||
|
|
position: relative;
|
|||
|
|
padding: 0;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
min-height: 320px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.xmind-wrap :deep(.xmind-box--dashboard) {
|
|||
|
|
flex: 1;
|
|||
|
|
min-height: 0;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.xmind-wrap :deep(.xmind-container) {
|
|||
|
|
flex: 1;
|
|||
|
|
min-height: 300px !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mind-legend-aside {
|
|||
|
|
flex: 0 0 132px;
|
|||
|
|
padding: 10px 12px;
|
|||
|
|
border-left: 1px solid #ebeef5;
|
|||
|
|
background: #fafafa;
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: #606266;
|
|||
|
|
line-height: 1.5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mind-legend-title {
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #303133;
|
|||
|
|
margin-bottom: 10px;
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mind-legend-row {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.lg-dot {
|
|||
|
|
display: inline-block;
|
|||
|
|
width: 8px;
|
|||
|
|
height: 8px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.lg-dot--done {
|
|||
|
|
background: #67c23a;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.lg-dot--doing {
|
|||
|
|
background: #e6a23c;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.lg-dot--todo {
|
|||
|
|
background: #909399;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mind-legend-tip {
|
|||
|
|
margin: 12px 0 0;
|
|||
|
|
font-size: 11px;
|
|||
|
|
color: #909399;
|
|||
|
|
line-height: 1.45;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.xmind-empty {
|
|||
|
|
height: 100%;
|
|||
|
|
min-height: 240px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
border: 1px dashed #ebeef5;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.text-ellipsis {
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
</style>
|