推送项目重构代码
This commit is contained in:
@@ -100,6 +100,21 @@
|
||||
</template>
|
||||
{{ detailData.remark == null ? '空' : detailData.remark }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3">
|
||||
<template slot="label">
|
||||
<i class="el-icon-paperclip"></i>
|
||||
收货单
|
||||
</template>
|
||||
<template v-if="detailData.receiptFiles">
|
||||
<a v-for="f in parseReceiptFiles(detailData.receiptFiles)" :key="f.ossId"
|
||||
:href="f.url" target="_blank" download
|
||||
style="color:#409eff; margin-right:10px; font-size:12px;">
|
||||
<i class="el-icon-paperclip"></i>{{ f.name }}
|
||||
</a>
|
||||
</template>
|
||||
<span v-else style="color:#c0c4cc;">无</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-table v-loading="loading" :data="oaOutWarehouseList" @selection-change="handleSelectionChange">
|
||||
@@ -148,6 +163,9 @@
|
||||
<el-input placeholder="请输入编号" v-model="form.masterNum">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="收货单" prop="receiptDoc">
|
||||
<file-upload v-model="form.receiptDoc" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="drawer" label="项目名">
|
||||
<el-input :value="form.projectName" disabled></el-input>
|
||||
</el-form-item>
|
||||
@@ -246,12 +264,14 @@ import {
|
||||
updateOaWarehouseMaster
|
||||
} from "@/api/oa/warehouse/warehouseMaster";
|
||||
import { listRequirements } from "@/api/oa/requirement";
|
||||
import FileUpload from "@/components/FileUpload";
|
||||
import ProjectSelect from "@/components/fad-service/ProjectSelect";
|
||||
|
||||
export default {
|
||||
name: "OaOutWarehouse",
|
||||
components: {
|
||||
ProjectSelect
|
||||
ProjectSelect,
|
||||
FileUpload
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -338,8 +358,31 @@ export default {
|
||||
},
|
||||
created () {
|
||||
this.getList();
|
||||
// 路由带 requirementId 进来时自动打开新增入库表单并预填
|
||||
const q = this.$route && this.$route.query;
|
||||
if (q && q.requirementId) {
|
||||
this.$nextTick(() => {
|
||||
this.handleAdd();
|
||||
this.form.requirementId = Number(q.requirementId) || q.requirementId;
|
||||
// 顺手把预填项推到选项列表,让 select 能显示
|
||||
if (q.requirementTitle) {
|
||||
this.requirementOptions = [{
|
||||
requirementId: this.form.requirementId,
|
||||
title: q.requirementTitle
|
||||
}];
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 后端联查的收货单文件解析("ossId|name|url,,...")
|
||||
parseReceiptFiles (raw) {
|
||||
if (!raw) return []
|
||||
return String(raw).split(',,').map(s => {
|
||||
const [ossId, name, url] = s.split('|')
|
||||
return { ossId, name: name || '收货单', url: url || '' }
|
||||
}).filter(f => f.ossId)
|
||||
},
|
||||
// 远程搜索采购需求
|
||||
loadRequirementOptions (keyword) {
|
||||
this.requirementLoading = true
|
||||
@@ -409,6 +452,7 @@ export default {
|
||||
this.form = {
|
||||
projectId: undefined,
|
||||
requirementId: undefined,
|
||||
receiptDoc: undefined,
|
||||
warehouseList: [],
|
||||
};
|
||||
this.resetForm("form");
|
||||
|
||||
@@ -54,9 +54,15 @@
|
||||
<el-select v-model="batchStatus" size="mini" placeholder="批量设置状态" style="width:140px">
|
||||
<el-option v-for="s in statusOptions" :key="s.value" :value="s.value" :label="s.label" />
|
||||
</el-select>
|
||||
<el-button size="mini" type="success" @click="submitComplete">执行入库</el-button>
|
||||
<el-button size="mini" type="success" @click="submitComplete(props.row)">执行入库</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 收货单上传:仅入库批量场景需要 -->
|
||||
<div v-if="mode === 'batch' && props.row.status === 0" class="receipt-row">
|
||||
<span class="r-label"><i class="el-icon-paperclip"></i> 收货单:</span>
|
||||
<file-upload v-model="props.row.receiptDoc" />
|
||||
<span class="r-hint">提交时会一并保存到本单据</span>
|
||||
</div>
|
||||
<el-table v-loading="itemsLoading[props.row.masterId]"
|
||||
:data="itemsMap[props.row.masterId] || []" size="mini" stripe ref="warehouseTable">
|
||||
<el-table-column v-if="mode === 'batch' && props.row.status === 0"
|
||||
@@ -161,6 +167,7 @@
|
||||
|
||||
<script>
|
||||
import { listRequirements } from "@/api/oa/requirement";
|
||||
import FileUpload from "@/components/FileUpload";
|
||||
import AddPurchaseDialog from "./components/AddPurchaseDialog.vue";
|
||||
import {
|
||||
addOaWarehouseMaster,
|
||||
@@ -179,7 +186,7 @@ import {
|
||||
|
||||
export default {
|
||||
name: "OaOutWarehouse",
|
||||
components: { AddPurchaseDialog },
|
||||
components: { AddPurchaseDialog, FileUpload },
|
||||
data () {
|
||||
return {
|
||||
// 顶部状态筛选
|
||||
@@ -365,7 +372,7 @@ export default {
|
||||
this.warehouseTaskList.splice(index, 1)
|
||||
this.$message.success('删除成功')
|
||||
},
|
||||
submitComplete () {
|
||||
submitComplete (masterRow) {
|
||||
const rows = this.$refs.warehouseTable.selection || []
|
||||
if (!rows.length) {
|
||||
return this.$message.warning('请先勾选物料')
|
||||
@@ -374,17 +381,22 @@ export default {
|
||||
return this.$message.warning('请选择批量状态')
|
||||
}
|
||||
|
||||
// 前端直接设值(实际项目可调用接口)
|
||||
rows.forEach(r => { r.taskStatus = this.batchStatus })
|
||||
|
||||
// 批量入库采购单
|
||||
updateOaWarehouseTaskBatch(rows).then(res => {
|
||||
this.getList();
|
||||
this.drawer = false;
|
||||
// 1. 如果有收货单 → 先保存到 master
|
||||
const saveMaster = (masterRow && masterRow.receiptDoc)
|
||||
? updateOaWarehouseMaster({
|
||||
masterId: masterRow.masterId,
|
||||
receiptDoc: masterRow.receiptDoc,
|
||||
type: masterRow.type
|
||||
})
|
||||
: Promise.resolve()
|
||||
|
||||
saveMaster.then(() => updateOaWarehouseTaskBatch(rows)).then(() => {
|
||||
this.getList()
|
||||
this.drawer = false
|
||||
this.$message.success(`已批量入库 ${rows.length} 条`)
|
||||
|
||||
})
|
||||
|
||||
},
|
||||
/** 执行入库操作 */
|
||||
handleIn (row) {
|
||||
@@ -671,4 +683,20 @@ export default {
|
||||
border-radius: 4px;
|
||||
i { font-size: 11px; margin-right: 2px; }
|
||||
}
|
||||
.receipt-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 0;
|
||||
margin-bottom: 6px;
|
||||
border-bottom: 1px dashed #ebeef5;
|
||||
.r-label {
|
||||
color: #606266;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
i { margin-right: 2px; color: #909399; }
|
||||
}
|
||||
.r-hint { color: #909399; font-size: 11px; }
|
||||
::v-deep .el-upload-list { font-size: 12px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
193
ruoyi-ui/src/views/oa/project/pace/components/GanttView.vue
Normal file
193
ruoyi-ui/src/views/oa/project/pace/components/GanttView.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="gantt-view">
|
||||
<div class="gantt-header">
|
||||
<div class="left-label">项目</div>
|
||||
<div class="time-axis">
|
||||
<div v-for="(m, i) in months" :key="i" class="month-cell">{{ m.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gantt-body">
|
||||
<div v-for="item in items" :key="item.scheduleId" class="gantt-row" @click="$emit('open-detail', item)">
|
||||
<div class="left-label">
|
||||
<el-tag v-if="item.projectCode" size="mini" type="info">{{ item.projectCode }}</el-tag>
|
||||
<span class="row-name" :title="item.projectName">{{ item.projectName }}</span>
|
||||
</div>
|
||||
<div class="bar-track">
|
||||
<div class="today-line" :style="todayStyle"></div>
|
||||
<div class="bar" :class="barClass(item)" :style="barStyle(item)" :title="barTitle(item)">
|
||||
<span class="bar-label">{{ Math.round(item.schedulePercentage || 0) }}%</span>
|
||||
<div class="bar-progress" :style="{ width: (item.schedulePercentage || 0) + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!items.length" class="empty">无可显示的项目</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'GanttView',
|
||||
props: {
|
||||
list: { type: Array, default: () => [] }
|
||||
},
|
||||
computed: {
|
||||
items () {
|
||||
// 只展示有起止时间的
|
||||
return (this.list || []).filter(r => r.startTime && r.endTime)
|
||||
},
|
||||
range () {
|
||||
if (!this.items.length) {
|
||||
const now = new Date()
|
||||
return { min: new Date(now.getFullYear(), now.getMonth() - 1, 1),
|
||||
max: new Date(now.getFullYear(), now.getMonth() + 3, 1) }
|
||||
}
|
||||
const times = this.items.flatMap(r => [new Date(r.startTime), new Date(r.endTime)])
|
||||
const min = new Date(Math.min(...times)); min.setDate(1)
|
||||
const max = new Date(Math.max(...times)); max.setMonth(max.getMonth() + 1, 1)
|
||||
return { min, max }
|
||||
},
|
||||
months () {
|
||||
const arr = []
|
||||
const cur = new Date(this.range.min)
|
||||
while (cur < this.range.max) {
|
||||
arr.push({ label: `${cur.getFullYear()}/${cur.getMonth() + 1}`, date: new Date(cur) })
|
||||
cur.setMonth(cur.getMonth() + 1)
|
||||
}
|
||||
return arr
|
||||
},
|
||||
spanMs () {
|
||||
return this.range.max - this.range.min
|
||||
},
|
||||
todayStyle () {
|
||||
const now = new Date()
|
||||
if (now < this.range.min || now > this.range.max) return { display: 'none' }
|
||||
const pct = (now - this.range.min) / this.spanMs * 100
|
||||
return { left: pct + '%' }
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
barStyle (r) {
|
||||
const s = new Date(r.startTime)
|
||||
const e = new Date(r.endTime)
|
||||
const left = (s - this.range.min) / this.spanMs * 100
|
||||
const width = (e - s) / this.spanMs * 100
|
||||
return { left: left + '%', width: Math.max(width, 1) + '%' }
|
||||
},
|
||||
barClass (r) {
|
||||
if (r.status === 2) return 'done'
|
||||
if ((r.delayCount || 0) > 0) return 'delayed'
|
||||
return 'running'
|
||||
},
|
||||
barTitle (r) {
|
||||
const s = new Date(r.startTime).toISOString().slice(0, 10)
|
||||
const e = new Date(r.endTime).toISOString().slice(0, 10)
|
||||
return `${r.projectName}\n${s} ~ ${e}\n进度 ${Math.round(r.schedulePercentage || 0)}%`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.gantt-view {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
.gantt-header {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
background: #fafafa;
|
||||
}
|
||||
.gantt-body {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.gantt-row {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #f4f4f4;
|
||||
cursor: pointer;
|
||||
&:hover { background: #f9fbff; }
|
||||
}
|
||||
.left-label {
|
||||
flex: 0 0 220px;
|
||||
border-right: 1px solid #ebeef5;
|
||||
padding: 6px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
background: inherit;
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.row-name {
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
color: #303133;
|
||||
}
|
||||
.time-axis {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
.month-cell {
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: #606266;
|
||||
padding: 6px 0;
|
||||
border-right: 1px solid #ebeef5;
|
||||
&:last-child { border-right: none; }
|
||||
}
|
||||
.bar-track {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
height: 28px;
|
||||
background-image: linear-gradient(to right, #f4f4f4 1px, transparent 1px);
|
||||
background-size: 80px 100%;
|
||||
}
|
||||
.bar {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
background: rgba(64, 158, 255, 0.15);
|
||||
border: 1px solid #409eff;
|
||||
&.running { background: rgba(64, 158, 255, 0.15); border-color: #409eff; }
|
||||
&.delayed { background: rgba(245, 108, 108, 0.18); border-color: #f56c6c; }
|
||||
&.done { background: rgba(103, 194, 58, 0.18); border-color: #67c23a; }
|
||||
}
|
||||
.bar-progress {
|
||||
position: absolute; left: 0; top: 0; bottom: 0;
|
||||
background: currentColor;
|
||||
opacity: 0.4;
|
||||
}
|
||||
.bar.running .bar-progress { color: #409eff; }
|
||||
.bar.delayed .bar-progress { color: #f56c6c; }
|
||||
.bar.done .bar-progress { color: #67c23a; }
|
||||
.bar-label {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
color: #303133;
|
||||
}
|
||||
.today-line {
|
||||
position: absolute;
|
||||
top: 0; bottom: 0;
|
||||
width: 0;
|
||||
border-left: 1px dashed #e6a23c;
|
||||
z-index: 0;
|
||||
}
|
||||
.empty {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
163
ruoyi-ui/src/views/oa/project/pace/components/KanbanView.vue
Normal file
163
ruoyi-ui/src/views/oa/project/pace/components/KanbanView.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div class="kanban-view">
|
||||
<div v-for="col in columns" :key="col.key" class="kanban-col">
|
||||
<div class="col-header" :style="{ borderTopColor: col.color }">
|
||||
<span class="col-title">{{ col.title }}</span>
|
||||
<span class="col-count">{{ filterCol(col.key).length }}</span>
|
||||
</div>
|
||||
<div class="col-body">
|
||||
<div v-for="item in filterCol(col.key)" :key="item.scheduleId" class="kb-card"
|
||||
@click="$emit('open-detail', item)">
|
||||
<div class="card-title">
|
||||
<el-tag v-if="item.projectCode" size="mini" type="info">{{ item.projectCode }}</el-tag>
|
||||
<span>{{ item.projectName }}</span>
|
||||
</div>
|
||||
<div class="card-meta">
|
||||
<span class="meta-item"><i class="el-icon-user"></i> {{ item.steward || item.functionary || '—' }}</span>
|
||||
<span class="meta-item" v-if="item.endTime">
|
||||
<i class="el-icon-time"></i> {{ formatDate(item.endTime) }}
|
||||
</span>
|
||||
</div>
|
||||
<el-progress :percentage="Math.round(item.schedulePercentage || 0)"
|
||||
:status="progressStatus(item)" :stroke-width="6" :show-text="false" />
|
||||
<div class="card-bottom">
|
||||
<span>{{ Math.round(item.schedulePercentage || 0) }}%</span>
|
||||
<el-tag v-if="item.delayCount > 0" type="danger" size="mini" effect="plain">
|
||||
⚠ {{ item.delayCount }} 延期
|
||||
</el-tag>
|
||||
<el-tag v-if="item.unFinishCount > 0 && col.key !== 'done'" size="mini" effect="plain">
|
||||
{{ item.unFinishCount }} 未完成
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!filterCol(col.key).length" class="col-empty">无</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'KanbanView',
|
||||
props: {
|
||||
list: { type: Array, default: () => [] }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
columns: [
|
||||
{ key: 'pending', title: '未开始', color: '#909399' },
|
||||
{ key: 'running', title: '进行中', color: '#409eff' },
|
||||
{ key: 'delayed', title: '存在延期', color: '#f56c6c' },
|
||||
{ key: 'done', title: '已完成', color: '#67c23a' }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
filterCol (key) {
|
||||
const list = this.list || []
|
||||
switch (key) {
|
||||
case 'done': return list.filter(r => r.status === 2)
|
||||
case 'delayed': return list.filter(r => r.status !== 2 && (r.delayCount || 0) > 0)
|
||||
case 'pending': return list.filter(r => r.status !== 2 && (r.schedulePercentage || 0) === 0 && (r.delayCount || 0) === 0)
|
||||
case 'running': return list.filter(r => r.status !== 2 && (r.schedulePercentage || 0) > 0 && (r.delayCount || 0) === 0)
|
||||
}
|
||||
return []
|
||||
},
|
||||
progressStatus (r) {
|
||||
if (r.status === 2) return 'success'
|
||||
if ((r.delayCount || 0) > 0) return 'exception'
|
||||
return ''
|
||||
},
|
||||
formatDate (t) {
|
||||
if (!t) return ''
|
||||
const d = new Date(t)
|
||||
return `${d.getMonth() + 1}/${d.getDate()}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.kanban-view {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
min-height: 500px;
|
||||
}
|
||||
.kanban-col {
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.col-header {
|
||||
padding: 6px 12px;
|
||||
border-top: 3px solid;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
background: #fff;
|
||||
}
|
||||
.col-title { color: #303133; }
|
||||
.col-count {
|
||||
background: #e4e7ed;
|
||||
color: #606266;
|
||||
padding: 0 6px;
|
||||
border-radius: 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.col-body {
|
||||
padding: 6px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
max-height: 600px;
|
||||
}
|
||||
.col-empty {
|
||||
text-align: center;
|
||||
color: #c0c4cc;
|
||||
padding: 24px 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
.kb-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
margin-bottom: 6px;
|
||||
cursor: pointer;
|
||||
transition: all .15s;
|
||||
&:hover {
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
.card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
}
|
||||
.card-meta {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
color: #909399;
|
||||
font-size: 11px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.card-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="two-level-filter">
|
||||
<!-- 第一级:进度类别 -->
|
||||
<div class="filter-panel first-level">
|
||||
<!-- 第一级:进度类别 — 只有一个时隐藏面板(自动选中) -->
|
||||
<div v-if="tabOption.length > 1" class="filter-panel first-level">
|
||||
<h3 class="panel-title">进度类别</h3>
|
||||
<ul class="option-list">
|
||||
<li v-for="item in tabOption" :key="item.value" :class="{ 'active': defaultTabNode === item.value }"
|
||||
@@ -13,7 +13,10 @@
|
||||
|
||||
<!-- 第二级:一级分类 -->
|
||||
<div class="filter-panel second-level">
|
||||
<h3 class="panel-title">一级分类</h3>
|
||||
<h3 class="panel-title">
|
||||
一级分类
|
||||
<span v-if="tabOption.length === 1 && defaultTabNode" class="sub-cat">({{ tabOption[0].label }})</span>
|
||||
</h3>
|
||||
<div class="second-level-content">
|
||||
<template v-if="defaultTabNode">
|
||||
<ul class="option-list" v-if="renderFirstLevelOption.length">
|
||||
@@ -51,6 +54,23 @@ export default {
|
||||
defaultFirstLevelNode: ""
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
tabOption: {
|
||||
immediate: true,
|
||||
handler (v) {
|
||||
// 只有一个进度类别 → 自动选中
|
||||
if (v && v.length === 1 && !this.defaultTabNode) {
|
||||
this.$nextTick(() => this.handleTabChange(v[0].value))
|
||||
}
|
||||
}
|
||||
},
|
||||
renderFirstLevelOption (v) {
|
||||
// 只有一个一级分类 → 自动选中
|
||||
if (v && v.length === 1 && !this.defaultFirstLevelNode) {
|
||||
this.$nextTick(() => this.handleFirstLevelChange(v[0].value))
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 根据选中的进度类别过滤一级分类
|
||||
renderFirstLevelOption () {
|
||||
@@ -123,12 +143,16 @@ export default {
|
||||
|
||||
.panel-title {
|
||||
margin: 0;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
background-color: #f9fafb;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.panel-title .sub-cat {
|
||||
color: #909399; font-size: 11px; font-weight: normal; margin-left: 4px;
|
||||
}
|
||||
|
||||
.option-list {
|
||||
margin: 0;
|
||||
@@ -137,8 +161,9 @@ export default {
|
||||
}
|
||||
|
||||
.option-item {
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
@@ -171,9 +196,9 @@ export default {
|
||||
|
||||
.empty-tip {
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
font-size: 11px;
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
padding: 8px 10px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,22 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row style="margin-bottom: 10px;">
|
||||
<el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="addInnerData">新增</el-button>
|
||||
<el-button type="warning" plain icon="el-icon-time" size="mini" @click="handleBatchDelay" :disabled="selectedRows.length === 0">批量延期</el-button>
|
||||
<slot name="extra-buttons"></slot>
|
||||
</el-col>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<vxe-table size="mini" height="500" ref="tableRef" border show-overflow :edit-config="editConfig" :data="innerData"
|
||||
<div class="step-table-toolbar">
|
||||
<el-button type="text" icon="el-icon-plus" @click="addInnerData">新增</el-button>
|
||||
<el-button type="text" icon="el-icon-time" style="color:#e6a23c"
|
||||
@click="handleBatchDelay" :disabled="selectedRows.length === 0">批量延期</el-button>
|
||||
<slot name="extra-buttons"></slot>
|
||||
</div>
|
||||
<vxe-table size="mini" :height="tableHeight" ref="tableRef" border show-overflow :edit-config="editConfig" :data="innerData"
|
||||
:row-config="{ 'isCurrent': true, 'isHover': true }" :column-config="{ 'isCurrent': true }" :sort-config="sortConfig"
|
||||
@checkbox-change="handleCheckboxChange" @checkbox-all="handleCheckboxAll">
|
||||
<vxe-column type="checkbox" width="50"></vxe-column>
|
||||
<vxe-column field="sortNum" title="顺序" :edit-render="{ name: 'input' }" width="70" sortable></vxe-column>
|
||||
<vxe-column field="secondLevelNode" title="步骤名称" :edit-render="{ name: 'input' }"></vxe-column>
|
||||
<vxe-column field="tabNode" title="进度类别" :edit-render="{ name: 'input' }"></vxe-column>
|
||||
<vxe-column field="firstLevelNode" title="一级分类" :edit-render="{ name: 'input' }"></vxe-column>
|
||||
<vxe-column field="firstLevelNode" title="一级分类" :edit-render="{ name: 'input' }"
|
||||
min-width="140" show-overflow="false"></vxe-column>
|
||||
<vxe-column field="nodeHeader" title="负责人" :edit-render="{}">
|
||||
<template slot-scope="{ row }" slot="default">
|
||||
{{ row.nodeHeader }}
|
||||
@@ -121,24 +119,21 @@
|
||||
<!-- <vxe-button>更多</vxe-button> -->
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="操作" width="200" v-if="editable && isCEO">
|
||||
<vxe-column title="操作" width="100" align="center" v-if="editable && isCEO">
|
||||
<template v-slot:default="{ row }">
|
||||
<template v-if="showEdit(row)">
|
||||
<template v-if="row.trackId">
|
||||
<template v-if="hasEditStatus(row)">
|
||||
<vxe-button @click="saveRowEvent(row)">保存</vxe-button>
|
||||
<vxe-button @click="cancelRowEvent()">取消</vxe-button>
|
||||
<el-button type="text" size="mini" @click="saveRowEvent(row)" style="color:#67c23a">保存</el-button>
|
||||
<el-button type="text" size="mini" @click="cancelRowEvent()">取消</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- <vxe-button v-if="row.useFlag == 0" @click="agreeDelay(row)">
|
||||
同意延期
|
||||
</vxe-button> -->
|
||||
<vxe-button @click="editRowEvent(row)">编辑</vxe-button>
|
||||
<vxe-button @click="handleDelete(row)">删除</vxe-button>
|
||||
<el-button type="text" size="mini" @click="editRowEvent(row)">编辑</el-button>
|
||||
<el-button type="text" size="mini" style="color:#f56c6c" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<vxe-button @click="handleAdd(row)">新增</vxe-button>
|
||||
<el-button type="text" size="mini" @click="handleAdd(row)">新增</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
@@ -282,6 +277,7 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
innerData: [],
|
||||
tableHeight: 640,
|
||||
buttonLoading: false,
|
||||
editConfig: { trigger: 'manual', mode: 'row' },
|
||||
sortConfig: {
|
||||
@@ -808,4 +804,27 @@ export default {
|
||||
color: #409eff !important;
|
||||
/* 蓝色:申请中 */
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
<style scoped>
|
||||
/* 顶部工具条:text 风格按钮,紧凑排列 */
|
||||
.step-table-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 4px 0;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.step-table-toolbar > .el-button {
|
||||
padding: 2px 6px !important;
|
||||
}
|
||||
/* vxe-table 行高/字号放大,避免覆盖 */
|
||||
.step-table-toolbar + .vxe-table /deep/ .vxe-body--column,
|
||||
.step-table-toolbar + .vxe-table /deep/ .vxe-header--column {
|
||||
height: 38px !important;
|
||||
line-height: 1.4 !important;
|
||||
}
|
||||
.step-table-toolbar + .vxe-table /deep/ .vxe-cell {
|
||||
white-space: normal !important;
|
||||
word-break: break-word !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
169
ruoyi-ui/src/views/oa/project/pace/components/StepTimeline.vue
Normal file
169
ruoyi-ui/src/views/oa/project/pace/components/StepTimeline.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div class="step-timeline">
|
||||
<div class="tl-header">
|
||||
<span class="title">步骤时间线({{ stepList.length }})</span>
|
||||
<slot name="extra-buttons" />
|
||||
</div>
|
||||
<div v-if="!stepList.length" class="empty">无步骤</div>
|
||||
<div v-else class="tl-list">
|
||||
<div v-for="step in stepList" :key="step.trackId"
|
||||
class="tl-item" :class="cardClass(step)" @click="$emit('edit', step)">
|
||||
<div class="tl-dot" :style="{ background: dotColor(step) }">
|
||||
<span>{{ step.sortNum != null ? step.sortNum : '·' }}</span>
|
||||
</div>
|
||||
<div class="tl-card">
|
||||
<div class="tl-line1">
|
||||
<span class="step-name">{{ step.stepName || '(未命名)' }}</span>
|
||||
<el-tag v-if="step.status === 2" type="success" size="mini">已完成</el-tag>
|
||||
<el-tag v-else-if="step.status === 1" type="warning" size="mini">待验收</el-tag>
|
||||
<el-tag v-else type="info" size="mini">进行中</el-tag>
|
||||
</div>
|
||||
<div class="tl-line2">
|
||||
<span class="cat">{{ step.tabNode || '' }} · {{ step.firstLevelNode || '' }}</span>
|
||||
</div>
|
||||
<div class="tl-line3">
|
||||
<span class="meta">
|
||||
<i class="el-icon-user"></i>
|
||||
{{ step.nodeHeader || '—' }}
|
||||
</span>
|
||||
<span class="meta">
|
||||
<i class="el-icon-time"></i>
|
||||
{{ formatDate(step.planEnd) }}
|
||||
</span>
|
||||
<span class="meta remain" :style="{ color: remainColor(step) }">
|
||||
{{ remainText(step) }}
|
||||
</span>
|
||||
<span v-if="(step.fileCount || 0) > 0" class="meta">
|
||||
<i class="el-icon-paperclip"></i>{{ step.fileCount }}
|
||||
</span>
|
||||
<span v-if="(step.picCount || 0) > 0" class="meta">
|
||||
<i class="el-icon-picture-outline"></i>{{ step.picCount }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'StepTimeline',
|
||||
props: {
|
||||
stepList: { type: Array, default: () => [] }
|
||||
},
|
||||
methods: {
|
||||
formatDate (t) {
|
||||
if (!t) return '未设置'
|
||||
const d = new Date(t)
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
||||
},
|
||||
daysUntil (t) {
|
||||
if (!t) return null
|
||||
const d = new Date(t); d.setHours(0, 0, 0, 0)
|
||||
const today = new Date(); today.setHours(0, 0, 0, 0)
|
||||
return Math.floor((d - today) / 86400000)
|
||||
},
|
||||
remainText (s) {
|
||||
if (s.status === 2) return ''
|
||||
if (!s.planEnd) return ''
|
||||
const n = this.daysUntil(s.planEnd)
|
||||
if (n < 0) return `逾期 ${-n} 天`
|
||||
if (n === 0) return '今日到期'
|
||||
if (n <= 3) return `临期 ${n} 天`
|
||||
return `剩余 ${n} 天`
|
||||
},
|
||||
remainColor (s) {
|
||||
if (s.status === 2) return '#67c23a'
|
||||
const n = this.daysUntil(s.planEnd)
|
||||
if (n == null) return '#909399'
|
||||
if (n < 0) return '#f56c6c'
|
||||
if (n <= 3) return '#e6a23c'
|
||||
return '#909399'
|
||||
},
|
||||
dotColor (s) {
|
||||
if (s.status === 2) return '#67c23a'
|
||||
if (s.status === 1) return '#e6a23c'
|
||||
const n = this.daysUntil(s.planEnd)
|
||||
if (n != null && n < 0) return '#f56c6c'
|
||||
return '#409eff'
|
||||
},
|
||||
cardClass (s) {
|
||||
if (s.status === 2) return 'done'
|
||||
const n = this.daysUntil(s.planEnd)
|
||||
if (n != null && n < 0) return 'delayed'
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.step-timeline { padding: 8px 4px; }
|
||||
.tl-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
.title { font-weight: 600; color: #303133; font-size: 13px; }
|
||||
}
|
||||
.empty { text-align: center; color: #909399; padding: 32px 0; font-size: 12px; }
|
||||
.tl-list {
|
||||
position: relative;
|
||||
padding-left: 26px;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 14px; top: 6px; bottom: 6px;
|
||||
width: 2px;
|
||||
background: #ebeef5;
|
||||
}
|
||||
}
|
||||
.tl-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
&:hover .tl-card { box-shadow: 0 2px 8px rgba(0,0,0,0.08); border-color: #409eff; }
|
||||
}
|
||||
.tl-dot {
|
||||
position: absolute;
|
||||
left: -22px; top: 4px;
|
||||
width: 22px; height: 22px;
|
||||
border-radius: 50%;
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 0 1px #ebeef5;
|
||||
}
|
||||
.tl-card {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
transition: all .15s;
|
||||
}
|
||||
.tl-item.done .tl-card { background: #f0f9eb; }
|
||||
.tl-item.delayed .tl-card { background: #fff1f0; border-color: #fbc4c4; }
|
||||
|
||||
.tl-line1 {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
.step-name { font-weight: 600; color: #303133; font-size: 13px; }
|
||||
}
|
||||
.tl-line2 {
|
||||
margin-top: 2px;
|
||||
.cat { font-size: 11px; color: #909399; }
|
||||
}
|
||||
.tl-line3 {
|
||||
margin-top: 5px;
|
||||
display: flex; flex-wrap: wrap; gap: 12px;
|
||||
.meta {
|
||||
font-size: 11px; color: #606266;
|
||||
i { margin-right: 2px; }
|
||||
}
|
||||
.remain { font-weight: 600; }
|
||||
}
|
||||
</style>
|
||||
@@ -43,31 +43,29 @@
|
||||
|
||||
<el-divider></el-divider>
|
||||
<div style="position: relative;">
|
||||
<el-radio-group v-model="viewMode" style="position: absolute; top: -40px; left: 0; z-index: 9999;">
|
||||
<el-radio-button label="xmind">思维导图</el-radio-button>
|
||||
<el-radio-group v-model="viewMode" size="mini"
|
||||
style="position: absolute; top: -40px; left: 0; z-index: 9999;">
|
||||
<el-radio-button label="table">表格</el-radio-button>
|
||||
<el-radio-button label="timeline">时间线</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-row v-show="viewMode === 'xmind'">
|
||||
<xmind :list="projectScheduleStepList" @refresh="getList"></xmind>
|
||||
</el-row>
|
||||
<el-row :gutter="20" v-show="viewMode === 'table'">
|
||||
<el-col :span="4">
|
||||
<div class="step-layout">
|
||||
<div class="step-side">
|
||||
<menu-select ref="menuSelectRef" :tabOption="tabOption" :firstLevelOption="firstLevelOption"
|
||||
@change="handleChange"></menu-select>
|
||||
</el-col>
|
||||
<el-col :span="20">
|
||||
<step-table ref="stepTableRef" :defaultTabNode="defaultTabNode" :defaultFirstLevelNode="defaultFirstLevelNode"
|
||||
:stepList="filterList" @refresh="getList" @add="submitForm" @delete="handleDelete" :editable="true"
|
||||
:master="master">
|
||||
<template slot="extra-buttons">
|
||||
<el-button type="primary" plain icon="el-icon-camera" size="mini" @click="handleOverview">总览</el-button>
|
||||
<el-button type="primary" plain icon="el-icon-refresh" size="mini" @click="getList">刷新</el-button>
|
||||
<el-checkbox style="margin-left: 10px;" v-model="filterParams.onlyMy">只看我的</el-checkbox>
|
||||
<el-checkbox v-model="filterParams.onlyUnfinished">只看未完成</el-checkbox>
|
||||
</template>
|
||||
</step-table>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="step-main">
|
||||
<div class="step-toolbar">
|
||||
<el-button type="text" icon="el-icon-refresh" @click="getList">刷新</el-button>
|
||||
<el-checkbox v-model="filterParams.onlyMy" style="margin-left:8px;">只看我的</el-checkbox>
|
||||
<el-checkbox v-model="filterParams.onlyUnfinished">只看未完成</el-checkbox>
|
||||
</div>
|
||||
<step-timeline v-if="viewMode === 'timeline'" :stepList="filterList" @edit="handleEditStep" />
|
||||
<step-table v-show="viewMode === 'table'" ref="stepTableRef"
|
||||
:defaultTabNode="defaultTabNode" :defaultFirstLevelNode="defaultFirstLevelNode"
|
||||
:stepList="filterList" @refresh="getList" @add="submitForm" @delete="handleDelete"
|
||||
:editable="true" :master="master" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -78,7 +76,7 @@ import { addProjectScheduleStep, delProjectScheduleStep, getProjectScheduleStep,
|
||||
import ProjectInfo from "@/components/fad-service/ProjectInfo/index.vue";
|
||||
import MenuSelect from "@/views/oa/project/pace/components/MenuSelect.vue";
|
||||
import StepTable from "@/views/oa/project/pace/components/StepTable.vue";
|
||||
import Xmind from "./xmind.vue";
|
||||
import StepTimeline from "@/views/oa/project/pace/components/StepTimeline.vue";
|
||||
|
||||
export default {
|
||||
name: "ProjectScheduleStep",
|
||||
@@ -115,13 +113,13 @@ export default {
|
||||
},
|
||||
components: {
|
||||
StepTable,
|
||||
StepTimeline,
|
||||
MenuSelect,
|
||||
Xmind,
|
||||
ProjectInfo,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
viewMode: 'xmind',
|
||||
viewMode: 'table',
|
||||
defaultTabNode: "",
|
||||
defaultFirstLevelNode: "",
|
||||
// 按钮loading
|
||||
@@ -236,6 +234,20 @@ export default {
|
||||
tabNode: item.tabNode // 关联的 tabNode(此时必然正确)
|
||||
}));
|
||||
|
||||
// 按中文数字 一二三四五六七八九十百 顺序排(无前缀按字母顺序排到后面)
|
||||
const CN_NUM = ['一','二','三','四','五','六','七','八','九','十','十一','十二','十三','十四','十五','十六','十七','十八','十九','二十'];
|
||||
const cnOrder = (label) => {
|
||||
if (!label) return 9999;
|
||||
const head = String(label).trim().split(/[、.\s]/)[0];
|
||||
const idx = CN_NUM.indexOf(head);
|
||||
return idx === -1 ? 9999 : idx;
|
||||
};
|
||||
firstLevelNodes.sort((a, b) => {
|
||||
const oa = cnOrder(a.label), ob = cnOrder(b.label);
|
||||
if (oa !== ob) return oa - ob;
|
||||
return String(a.label).localeCompare(String(b.label));
|
||||
});
|
||||
|
||||
return firstLevelNodes;
|
||||
}
|
||||
},
|
||||
@@ -302,6 +314,16 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 时间线点击行 → 切换到表格视图并尝试聚焦该 trackId
|
||||
handleEditStep (step) {
|
||||
if (!step || !step.trackId) return
|
||||
this.viewMode = 'table'
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.stepTableRef && this.$refs.stepTableRef.scrollToTrack) {
|
||||
this.$refs.stepTableRef.scrollToTrack(step.trackId)
|
||||
}
|
||||
})
|
||||
},
|
||||
applyInitialStepFocus () {
|
||||
const hint = this.initialStepFocus;
|
||||
if (!hint || !this.projectScheduleStepList.length) {
|
||||
@@ -461,3 +483,44 @@ export default {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 左 + 右 自适应布局:左侧分类自然宽,不裁剪文字 */
|
||||
.step-layout {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.step-side {
|
||||
flex: 0 0 auto;
|
||||
min-width: 140px;
|
||||
max-width: 200px;
|
||||
/* 让一级分类的文字(如"一、技术审查")完整显示,左侧菜单整体缩小 */
|
||||
}
|
||||
.step-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.step-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* 表格行高放大,避免文字被遮挡 */
|
||||
::v-deep .vxe-table .vxe-body--row .vxe-body--column,
|
||||
::v-deep .vxe-table .vxe-header--row .vxe-header--column {
|
||||
height: 40px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
::v-deep .vxe-table .vxe-body--row .vxe-body--column {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
::v-deep .vxe-table .vxe-cell {
|
||||
white-space: normal !important;
|
||||
word-break: break-all;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<template>
|
||||
<div class="app-container" v-loading="loading">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<el-form-item label="项目名称" prop="projectId">
|
||||
<el-form v-show="!detailMode && showSearch" :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="68px">
|
||||
<el-form-item label="项目名称" prop="projectId" class="form-project-select">
|
||||
<project-select v-model="queryParams.projectId" placeholder="请选择项目" clearable />
|
||||
<!-- <el-select v-model="queryParams.projectId" filterable placeholder="请选择">
|
||||
<el-option v-for="item in projects" :key="item.projectId" :label="item.projectName" :value="item.projectId">
|
||||
</el-option>
|
||||
</el-select> -->
|
||||
</el-form-item>
|
||||
<el-form-item label="项目编号" prop="projectNum">
|
||||
<el-input v-model="queryParams.projectNum" placeholder="请输入项目编号" clearable />
|
||||
@@ -19,7 +15,7 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="项目代号" prop="projectCode">
|
||||
<el-select v-model="queryParams.projectCode" placeholder="请选择代号类型" style="width: 100%" filterable
|
||||
<el-select v-model="queryParams.projectCode" placeholder="代号" style="width: 120px" filterable
|
||||
@change="handleQuery">
|
||||
<el-option v-for="dict in dict.type.sys_project_code" :key="dict.value" :label="dict.label"
|
||||
:value="dict.value">
|
||||
@@ -55,7 +51,7 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-row v-show="!detailMode" :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">绑定进度
|
||||
</el-button>
|
||||
@@ -69,9 +65,16 @@
|
||||
</el-row>
|
||||
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-table v-loading="loading" :data="scheduleList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-tabs v-show="!detailMode" v-model="statusTab" @tab-click="onStatusTab" class="compact-tabs">
|
||||
<el-tab-pane label="未完成" name="undone" />
|
||||
<el-tab-pane label="全部" name="all" />
|
||||
<el-tab-pane label="已完成" name="done" />
|
||||
</el-tabs>
|
||||
|
||||
<el-row v-show="!detailMode" :gutter="20">
|
||||
<el-table v-loading="loading" :data="scheduleList" @selection-change="handleSelectionChange"
|
||||
:row-class-name="paceRowClass" stripe>
|
||||
<el-table-column type="selection" width="44" align="center" />
|
||||
<el-table-column label="代号" prop="projectCode" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.projectCode == null" type="danger">无</el-tag>
|
||||
@@ -99,8 +102,43 @@
|
||||
<span v-else style="">一般项目</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="延期进度数" prop="delayCount" />
|
||||
<el-table-column label="未完成进度" prop="unFinishCount" />
|
||||
<el-table-column label="进度" min-width="240">
|
||||
<template slot-scope="scope">
|
||||
<div class="progress-cell">
|
||||
<svg class="mini-donut" viewBox="0 0 36 36">
|
||||
<circle cx="18" cy="18" r="15" fill="none" stroke="#ebeef5" stroke-width="6" />
|
||||
<circle cx="18" cy="18" r="15" fill="none"
|
||||
:stroke="progressColor(scope.row)" stroke-width="6"
|
||||
stroke-linecap="round"
|
||||
:stroke-dasharray="`${(scope.row.schedulePercentage || 0) * 0.942} 100`"
|
||||
transform="rotate(-90 18 18)" />
|
||||
<text x="18" y="20" text-anchor="middle" font-size="10" fill="#303133" font-weight="600">
|
||||
{{ Math.round(scope.row.schedulePercentage || 0) }}
|
||||
</text>
|
||||
</svg>
|
||||
<div class="progress-meta">
|
||||
<span class="step-count">{{ doneSteps(scope.row) }} / {{ scope.row.totalCount || 0 }} 步</span>
|
||||
<div class="progress-tags">
|
||||
<!-- 进度 100% 但项目状态尚未标为完成 → 高亮提示,点击切换 -->
|
||||
<a v-if="Math.round(scope.row.schedulePercentage || 0) === 100 && scope.row.status !== 2"
|
||||
class="all-done-hint" @click.stop="handleComplete(scope.row)">
|
||||
<i class="el-icon-success"></i>
|
||||
进度已完成 · 点击切换为已完成
|
||||
</a>
|
||||
<el-tag v-if="(scope.row.unFinishCount || 0) > 0" type="warning" size="mini" effect="dark">
|
||||
未完 {{ scope.row.unFinishCount }}
|
||||
</el-tag>
|
||||
<el-tag v-if="(scope.row.delayCount || 0) > 0" type="danger" size="mini" effect="dark">
|
||||
⚠ 延期 {{ scope.row.delayCount }}
|
||||
</el-tag>
|
||||
<el-tag v-if="scope.row.currentStepName" type="info" size="mini">
|
||||
当前: {{ scope.row.currentStepName }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="完成状态" align="center" prop="sortNum">
|
||||
<template slot-scope="scope">
|
||||
<el-select size="mini" v-model="scope.row.status" placeholder="请选择完成状态"
|
||||
@@ -120,6 +158,9 @@
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleDetail(scope.row)">进度详情
|
||||
</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-bell" style="color:#e6a23c"
|
||||
v-if="scope.row.status !== 2" @click="handleUrge(scope.row)">催促
|
||||
</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-time" @click="handlePostpone(scope.row)">延期记录
|
||||
</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-s-order" @click="handleOpLog(scope.row)">操作历史
|
||||
@@ -138,14 +179,21 @@
|
||||
@pagination="getList" />
|
||||
</el-row>
|
||||
|
||||
<el-drawer title="进度详情" :visible.sync="detailDrawer" direction="btt" size="90%" :before-close="closeDetailShow">
|
||||
<div style="padding:0 20px">
|
||||
<project-schedule-step :scheduleId="scheduleDetail.scheduleId" :master="scheduleDetail.functionary"
|
||||
:projectName="scheduleDetail.projectName" :projectStatus="scheduleDetail.projectStatus"
|
||||
:isTop="scheduleDetail.isTop" :projectId="scheduleDetail.projectId"
|
||||
:initial-step-focus="scheduleStepFocusHint" />
|
||||
<!-- 旧抽屉保留为 v-if false,避免误改其它逻辑 -->
|
||||
<el-drawer v-if="false" title="进度详情" :visible.sync="detailDrawer" direction="btt" size="90%" />
|
||||
|
||||
<!-- 进度详情 inline 面板(master/detail,同页面切换,无抽屉) -->
|
||||
<div v-if="detailMode" class="detail-pane">
|
||||
<div class="detail-bar">
|
||||
<el-button type="text" icon="el-icon-arrow-left" @click="closeDetailShow">返回项目列表</el-button>
|
||||
<span class="bar-divider"></span>
|
||||
<span class="bar-title">进度详情</span>
|
||||
</div>
|
||||
</el-drawer>
|
||||
<project-schedule-step :scheduleId="scheduleDetail.scheduleId" :master="scheduleDetail.functionary"
|
||||
:projectName="scheduleDetail.projectName" :projectStatus="scheduleDetail.projectStatus"
|
||||
:isTop="scheduleDetail.isTop" :projectId="scheduleDetail.projectId"
|
||||
:initial-step-focus="scheduleStepFocusHint" />
|
||||
</div>
|
||||
|
||||
<FormDialog v-model="addDialog" :projects="projects" @save="handleSave" />
|
||||
|
||||
@@ -164,7 +212,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import { listProject } from "@/api/oa/project";
|
||||
import { addByProjectId, delProjectSchedule, getProjectSchedule, listProjectSchedule, updateProjectSchedule } from "@/api/oa/projectSchedule";
|
||||
import { addByProjectId, delProjectSchedule, getProjectSchedule, listProjectSchedule, updateProjectSchedule, urgeProgress } from "@/api/oa/projectSchedule";
|
||||
import { listUser } from "@/api/system/user";
|
||||
import ProjectSelect from "@/components/fad-service/ProjectSelect/index.vue";
|
||||
import UserSelect from "@/components/UserSelect/index.vue";
|
||||
@@ -186,7 +234,9 @@ export default {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
statusTab: 'undone',
|
||||
loading: false,
|
||||
detailMode: false,
|
||||
detailDrawer: false,
|
||||
fileShow: false,
|
||||
addDialog: false,
|
||||
@@ -239,6 +289,8 @@ export default {
|
||||
mounted () {
|
||||
this.currentUser = this.$store.state.user
|
||||
this.applyPaceRouteQueryBeforeFetch();
|
||||
// 默认进入"未完成" tab
|
||||
if (this.queryParams.status === undefined) this.queryParams.status = 1;
|
||||
this.getList();
|
||||
this.getProjectList();
|
||||
this.getAllUser();
|
||||
@@ -248,6 +300,45 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleUrge (row) {
|
||||
if (!row.header && !row.functionary) {
|
||||
this.$modal.msgWarning('该项目没有设置负责人')
|
||||
return
|
||||
}
|
||||
const who = row.header || row.functionary
|
||||
this.$confirm(`确认通过 IM 催促 ${who}(项目:${row.projectName})?`, '催促进度', {
|
||||
confirmButtonText: '发送',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
urgeProgress(row.scheduleId).then(res => {
|
||||
if (res && res.code === 200) this.$modal.msgSuccess(`已通过 IM 催促 ${who}`)
|
||||
else this.$modal.msgError(res && res.msg || '催促失败')
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
onStatusTab () {
|
||||
if (this.statusTab === 'all') this.queryParams.status = undefined
|
||||
else if (this.statusTab === 'done') this.queryParams.status = 2
|
||||
else this.queryParams.status = 1
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
progressColor (row) {
|
||||
if (row.status === 2) return '#67c23a'
|
||||
if ((row.delayCount || 0) > 0) return '#f56c6c'
|
||||
return '#409eff'
|
||||
},
|
||||
doneSteps (row) {
|
||||
const total = row.totalCount || 0
|
||||
const unfin = row.unFinishCount || 0
|
||||
return Math.max(0, total - unfin)
|
||||
},
|
||||
paceRowClass ({ row }) {
|
||||
if (row.status === 2) return ''
|
||||
if ((row.delayCount || 0) > 0) return 'row-delayed'
|
||||
return ''
|
||||
},
|
||||
applyPaceRouteQueryBeforeFetch () {
|
||||
const q = this.$route.query || {};
|
||||
if (q.projectId != null && q.projectId !== '') {
|
||||
@@ -300,8 +391,10 @@ export default {
|
||||
},
|
||||
closeDetailShow (done) {
|
||||
this.scheduleStepFocusHint = null;
|
||||
this.detailMode = false;
|
||||
this.detailDrawer = false;
|
||||
this.getList();
|
||||
done()
|
||||
if (typeof done === 'function') done()
|
||||
},
|
||||
getAllUser () {
|
||||
listUser({ pageNum: 1, pageSize: 999 }).then(res => {
|
||||
@@ -404,7 +497,7 @@ export default {
|
||||
},
|
||||
getScheduleDetail (row) {
|
||||
this.scheduleDetail = row
|
||||
this.detailDrawer = true
|
||||
this.detailMode = true
|
||||
},
|
||||
|
||||
/* ========= 左侧主列表删除(支持单删或批量 ids) ========= */
|
||||
@@ -475,4 +568,88 @@ export default {
|
||||
.file-actions el-button+el-button {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* 进度列 */
|
||||
.progress-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
.mini-donut {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.progress-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.progress-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.step-count {
|
||||
font-size: 11px;
|
||||
color: #606266;
|
||||
font-weight: 600;
|
||||
}
|
||||
.all-done-hint {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 1px 6px;
|
||||
background: #f0f9eb;
|
||||
border: 1px solid #c2e7b0;
|
||||
color: #67c23a;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: all .15s;
|
||||
i { font-size: 13px; }
|
||||
&:hover {
|
||||
background: #67c23a;
|
||||
color: #fff;
|
||||
border-color: #67c23a;
|
||||
}
|
||||
}
|
||||
/* 含延期的行整行染浅红 */
|
||||
::v-deep .el-table .row-delayed > td.el-table__cell {
|
||||
background: #fff1f0 !important;
|
||||
}
|
||||
::v-deep .el-table .row-delayed:hover > td.el-table__cell {
|
||||
background: #ffd8d6 !important;
|
||||
}
|
||||
|
||||
/* 项目名称(搜索代号 + 项目选择)放在一行 */
|
||||
::v-deep .form-project-select .el-form-item__content {
|
||||
width: 360px;
|
||||
}
|
||||
::v-deep .form-project-select .project-select-wrap {
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
/* 进度详情 inline 面板 */
|
||||
.detail-pane {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
min-height: 600px;
|
||||
}
|
||||
.detail-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 4px;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
.bar-divider {
|
||||
width: 1px; height: 14px; background: #dcdfe6;
|
||||
}
|
||||
.bar-title { font-weight: 600; color: #303133; font-size: 13px; }
|
||||
</style>
|
||||
|
||||
@@ -1,306 +1,201 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px">
|
||||
<!-- 状态 -->
|
||||
<!-- <el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态">
|
||||
<el-option label="进行中" value="0" />
|
||||
<el-option label="待验收" value="1" />
|
||||
<el-option label="已完成" value="2" />
|
||||
</el-select>
|
||||
</el-form-item> -->
|
||||
<!-- 计划结束时间 -->
|
||||
<el-form-item label="计划结束时间" prop="planEnd">
|
||||
<!-- 状态筛选 -->
|
||||
<el-tabs v-model="statusTab" @tab-click="onStatusTab" class="compact-tabs">
|
||||
<el-tab-pane name="undone">
|
||||
<span slot="label" :style="stat.delayed > 0 ? 'color:#f56c6c;' : ''">
|
||||
<i v-if="stat.delayed > 0" class="el-icon-warning-outline"></i>
|
||||
未完成 ({{ stat.undone }})
|
||||
</span>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="pending">
|
||||
<span slot="label">待验收 ({{ stat.pending }})</span>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="done">
|
||||
<span slot="label">已完成 ({{ stat.done }})</span>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="全部" name="all" />
|
||||
</el-tabs>
|
||||
|
||||
<el-form :model="queryParams" ref="queryForm" size="mini" :inline="true" v-show="showSearch"
|
||||
label-width="60px" class="compact-search">
|
||||
<el-form-item label="项目" prop="projectName">
|
||||
<el-input v-model="queryParams.projectName" placeholder="项目名" clearable style="width: 180px"
|
||||
@keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="步骤" prop="stepName">
|
||||
<el-input v-model="queryParams.stepName" placeholder="步骤名" clearable style="width: 160px"
|
||||
@keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="计划完成" prop="planEnd">
|
||||
<el-date-picker v-model="queryParams.planEndRange" type="daterange" value-format="yyyy-MM-dd"
|
||||
placeholder="选择计划结束时间" />
|
||||
start-placeholder="开始" end-placeholder="结束" style="width: 220px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="projectScheduleStepList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="所属项目" align="center" prop="projectName" />
|
||||
<el-table-column label="步骤名称" align="center" prop="tabNode">
|
||||
<el-table v-loading="loading" :data="projectScheduleStepList" stripe size="small"
|
||||
:row-class-name="rowClassName" @row-click="goToProject">
|
||||
<el-table-column label="所属项目" prop="projectName" min-width="180" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.tabNode }}/ {{ scope.row.firstLevelNode }} / {{ scope.row.secondLevelNode }}</span>
|
||||
<span style="color:#409eff; cursor:pointer">{{ scope.row.projectName || '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计划完成" align="center" prop="planEnd" width="180">
|
||||
<el-table-column label="步骤" prop="secondLevelNode" min-width="200" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.planEnd, '{y}-{m}-{d}') }}</span>
|
||||
<span class="path">{{ scope.row.tabNode }} · {{ scope.row.firstLevelNode }} ·</span>
|
||||
<span class="step-name">{{ scope.row.secondLevelNode }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="剩余时间" align="center" prop="endTime" width="180">
|
||||
<el-table-column label="计划完成" prop="planEnd" width="110" align="center">
|
||||
<template slot-scope="scope">{{ parseTime(scope.row.planEnd, '{y}-{m}-{d}') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="剩余" width="120" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.status == 2" style="color: #36d399">{{ parseTime(scope.row.endTime, '{y}-{m}-{d}') }}
|
||||
已完成</span>
|
||||
<span v-else-if="scope.row.status == 1" style="color: #4096ff">待验收</span>
|
||||
<span v-else>
|
||||
<!-- 调用计算方法获取剩余天数 -->
|
||||
<template v-if="scope.row.planEnd">
|
||||
<span v-if="calcRemainingDays(scope.row.planEnd) < 0" style="color: #f56c6c">已逾期 {{
|
||||
-calcRemainingDays(scope.row.planEnd) }} 天</span>
|
||||
<span v-else-if="calcRemainingDays(scope.row.planEnd) <= 3" style="color: #e6a23c">临期 | 还剩 {{
|
||||
calcRemainingDays(scope.row.planEnd) }} 天</span>
|
||||
<span v-else style="color: #67c23a">还剩 {{ calcRemainingDays(scope.row.planEnd) }} 天</span>
|
||||
</template>
|
||||
<span v-else>未设置计划日期</span>
|
||||
<span v-if="scope.row.status === 2" style="color:#67c23a;">
|
||||
✓ {{ parseTime(scope.row.endTime, '{m}-{d}') }}
|
||||
</span>
|
||||
<span v-else-if="scope.row.status === 1" style="color:#409eff;">待验收</span>
|
||||
<template v-else-if="scope.row.planEnd">
|
||||
<el-tag v-if="dayDiff(scope.row.planEnd) < 0" type="danger" size="mini" effect="dark">
|
||||
逾期 {{ -dayDiff(scope.row.planEnd) }} 天
|
||||
</el-tag>
|
||||
<el-tag v-else-if="dayDiff(scope.row.planEnd) === 0" type="danger" size="mini" effect="plain">
|
||||
今日到期
|
||||
</el-tag>
|
||||
<el-tag v-else-if="dayDiff(scope.row.planEnd) <= 3" type="warning" size="mini" effect="plain">
|
||||
剩 {{ dayDiff(scope.row.planEnd) }} 天
|
||||
</el-tag>
|
||||
<span v-else style="color:#67c23a;">剩 {{ dayDiff(scope.row.planEnd) }} 天</span>
|
||||
</template>
|
||||
<span v-else style="color:#c0c4cc;">未设</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status">
|
||||
<!-- 0进行中,1待验收,2已完成 -->
|
||||
<el-table-column label="状态" prop="status" width="90" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.status === 0 ? '进行中' : (scope.row.status === 1 ? '待验收' : '已完成') }}</span>
|
||||
<el-tag size="mini" :type="statusTagType(scope.row.status)">
|
||||
{{ statusLabel(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||||
@pagination="getList" />
|
||||
|
||||
<!-- 添加或修改项目进度步骤跟踪对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addProjectScheduleStep, delProjectScheduleStep, getProjectScheduleStep, listMyPage as listProjectScheduleStep, updateProjectScheduleStep } from "@/api/oa/projectScheduleStep";
|
||||
import { listUser } from "@/api/system/user";
|
||||
import { listMyPage as listProjectScheduleStep } from '@/api/oa/projectScheduleStep'
|
||||
|
||||
export default {
|
||||
name: "ProjectScheduleStep",
|
||||
name: 'MyProjectScheduleStep',
|
||||
data () {
|
||||
return {
|
||||
// 按钮loading
|
||||
buttonLoading: false,
|
||||
// 用户列表loading
|
||||
userLoading: false,
|
||||
// 遮罩层
|
||||
loading: true,
|
||||
// 选中数组
|
||||
ids: [],
|
||||
// 非单个禁用
|
||||
single: true,
|
||||
// 非多个禁用
|
||||
multiple: true,
|
||||
// 显示搜索条件
|
||||
showSearch: true,
|
||||
// 总条数
|
||||
loading: false,
|
||||
showSearch: false,
|
||||
total: 0,
|
||||
// 项目进度步骤跟踪表格数据
|
||||
statusTab: 'all',
|
||||
stat: { undone: 0, pending: 0, done: 0, delayed: 0 },
|
||||
projectScheduleStepList: [],
|
||||
// 弹出层标题
|
||||
title: "",
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
scheduleId: undefined,
|
||||
pageSize: 50,
|
||||
projectName: undefined,
|
||||
stepName: undefined,
|
||||
planEnd: undefined,
|
||||
status: undefined,
|
||||
tabNode: undefined,
|
||||
firstLevelNode: undefined,
|
||||
secondLevelNode: undefined,
|
||||
endTime: undefined,
|
||||
nodeHeader: undefined,
|
||||
relatedDocs: undefined,
|
||||
relatedImages: undefined,
|
||||
supplierId: undefined,
|
||||
requirementFile: undefined,
|
||||
specification: undefined,
|
||||
planEndRange: undefined,
|
||||
},
|
||||
// 表单参数
|
||||
form: {},
|
||||
// 表单校验
|
||||
rules: {
|
||||
},
|
||||
userList: [],
|
||||
};
|
||||
},
|
||||
created () {
|
||||
this.getList();
|
||||
this.getListUser();
|
||||
planEndRange: undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
created () { this.getList() },
|
||||
methods: {
|
||||
/** 查询项目进度步骤跟踪列表 */
|
||||
/** 计算剩余天数(当前日期 - 计划完成日期) */
|
||||
calcRemainingDays (planEnd) {
|
||||
if (!planEnd) return '无计划日期';
|
||||
// 转换计划日期为时间戳(忽略时分秒,按日期当天结束计算)
|
||||
const planEndTime = new Date(planEnd).setHours(23, 59, 59, 999);
|
||||
const currentTime = new Date().getTime();
|
||||
// 计算天数差值(向上取整)
|
||||
const diffDays = Math.ceil((planEndTime - currentTime) / (1000 * 60 * 60 * 24));
|
||||
return diffDays;
|
||||
onStatusTab () {
|
||||
if (this.statusTab === 'all') this.queryParams.status = undefined
|
||||
else if (this.statusTab === 'done') this.queryParams.status = 2
|
||||
else if (this.statusTab === 'pending') this.queryParams.status = 1
|
||||
else this.queryParams.status = 0
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
/** 查询用户列表 */
|
||||
getListUser () {
|
||||
this.userLoading = true;
|
||||
listUser({ pageSize: 999 }).then(response => {
|
||||
this.userList = response.rows;
|
||||
this.userLoading = false;
|
||||
});
|
||||
handleQuery () { this.queryParams.pageNum = 1; this.getList() },
|
||||
resetQuery () {
|
||||
this.queryParams = {
|
||||
pageNum: 1, pageSize: 50,
|
||||
projectName: undefined, stepName: undefined,
|
||||
status: this.queryParams.status, planEndRange: undefined
|
||||
}
|
||||
this.getList()
|
||||
},
|
||||
buildQuery () {
|
||||
const { planEndRange, ...rest } = this.queryParams
|
||||
const startTime = planEndRange ? planEndRange[0] + ' 00:00:00' : undefined
|
||||
const endTime = planEndRange ? planEndRange[1] + ' 23:59:59' : undefined
|
||||
return { ...rest, startTime, endTime }
|
||||
},
|
||||
getList () {
|
||||
this.loading = true;
|
||||
const endTime = this.queryParams.planEndRange ? this.queryParams.planEndRange[1] + ' 23:59:59' : undefined;
|
||||
const startTime = this.queryParams.planEndRange ? this.queryParams.planEndRange[0] + ' 00:00:00' : undefined;
|
||||
const { planEndRange, ...querys } = {
|
||||
...this.queryParams,
|
||||
startTime,
|
||||
endTime,
|
||||
}
|
||||
listProjectScheduleStep(querys).then(response => {
|
||||
this.projectScheduleStepList = response.rows;
|
||||
this.total = response.total;
|
||||
this.loading = false;
|
||||
});
|
||||
this.loading = true
|
||||
listProjectScheduleStep(this.buildQuery()).then(res => {
|
||||
this.projectScheduleStepList = res.rows || []
|
||||
this.total = res.total || 0
|
||||
// 顺便算出当前页里有多少逾期
|
||||
this.stat.delayed = (res.rows || []).filter(r =>
|
||||
r.status !== 2 && r.planEnd && this.dayDiff(r.planEnd) < 0).length
|
||||
}).finally(() => { this.loading = false })
|
||||
this.refreshStat()
|
||||
},
|
||||
// 取消按钮
|
||||
cancel () {
|
||||
this.open = false;
|
||||
this.reset();
|
||||
refreshStat () {
|
||||
const base = { ...this.buildQuery(), pageNum: 1, pageSize: 1, status: undefined }
|
||||
Promise.all([
|
||||
listProjectScheduleStep({ ...base, status: 0 }).catch(() => ({ total: 0 })),
|
||||
listProjectScheduleStep({ ...base, status: 1 }).catch(() => ({ total: 0 })),
|
||||
listProjectScheduleStep({ ...base, status: 2 }).catch(() => ({ total: 0 }))
|
||||
]).then(([u, p, d]) => {
|
||||
this.stat.undone = u.total || 0
|
||||
this.stat.pending = p.total || 0
|
||||
this.stat.done = d.total || 0
|
||||
})
|
||||
},
|
||||
// 表单重置
|
||||
reset () {
|
||||
this.form = {
|
||||
trackId: undefined,
|
||||
accessory: undefined,
|
||||
scheduleId: undefined,
|
||||
stepOrder: undefined,
|
||||
stepName: undefined,
|
||||
planStart: undefined,
|
||||
planEnd: undefined,
|
||||
actualStart: undefined,
|
||||
actualEnd: undefined,
|
||||
status: undefined,
|
||||
createBy: undefined,
|
||||
createTime: undefined,
|
||||
updateBy: undefined,
|
||||
updateTime: undefined,
|
||||
delFlag: undefined,
|
||||
header: undefined,
|
||||
useFlag: undefined,
|
||||
batchId: undefined,
|
||||
tabNode: undefined,
|
||||
firstLevelNode: undefined,
|
||||
secondLevelNode: undefined,
|
||||
startTime: undefined,
|
||||
originalEndTime: undefined,
|
||||
endTime: undefined,
|
||||
nodeHeader: undefined,
|
||||
relatedDocs: undefined,
|
||||
relatedImages: undefined,
|
||||
supplierId: undefined,
|
||||
requirementFile: undefined,
|
||||
other: undefined,
|
||||
specification: undefined,
|
||||
sortNum: undefined
|
||||
};
|
||||
this.resetForm("form");
|
||||
dayDiff (date) {
|
||||
if (!date) return null
|
||||
const d = new Date(date); d.setHours(0, 0, 0, 0)
|
||||
const t = new Date(); t.setHours(0, 0, 0, 0)
|
||||
return Math.floor((d - t) / 86400000)
|
||||
},
|
||||
/** 搜索按钮操作 */
|
||||
handleQuery () {
|
||||
this.queryParams.pageNum = 1;
|
||||
this.getList();
|
||||
statusLabel (s) {
|
||||
return s === 2 ? '已完成' : (s === 1 ? '待验收' : '进行中')
|
||||
},
|
||||
/** 重置按钮操作 */
|
||||
resetQuery () {
|
||||
this.resetForm("queryForm");
|
||||
this.handleQuery();
|
||||
statusTagType (s) {
|
||||
return s === 2 ? 'success' : (s === 1 ? 'warning' : 'info')
|
||||
},
|
||||
// 多选框选中数据
|
||||
handleSelectionChange (selection) {
|
||||
this.ids = selection.map(item => item.trackId)
|
||||
this.single = selection.length !== 1
|
||||
this.multiple = !selection.length
|
||||
rowClassName ({ row }) {
|
||||
if (row.status === 2) return ''
|
||||
if (row.planEnd && this.dayDiff(row.planEnd) < 0) return 'row-delayed'
|
||||
return ''
|
||||
},
|
||||
/** 新增按钮操作 */
|
||||
handleAdd () {
|
||||
this.reset();
|
||||
this.open = true;
|
||||
this.title = "添加项目进度步骤跟踪";
|
||||
goToProject (row) {
|
||||
if (!row.scheduleId) return
|
||||
this.$router.push({
|
||||
path: '/step/files',
|
||||
query: { scheduleId: String(row.scheduleId), trackId: String(row.trackId || ''),
|
||||
tabNode: row.tabNode || '', firstLevelNode: row.firstLevelNode || '' }
|
||||
})
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
handleUpdate (row) {
|
||||
this.loading = true;
|
||||
this.reset();
|
||||
const trackId = row.trackId || this.ids
|
||||
getProjectScheduleStep(trackId).then(response => {
|
||||
this.loading = false;
|
||||
this.form = response.data;
|
||||
this.open = true;
|
||||
this.title = "修改项目进度步骤跟踪";
|
||||
});
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm () {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
this.buttonLoading = true;
|
||||
if (this.form.trackId != null) {
|
||||
updateProjectScheduleStep(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
} else {
|
||||
addProjectScheduleStep(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete (row) {
|
||||
const trackIds = row.trackId || this.ids;
|
||||
this.$modal.confirm('是否确认删除项目进度步骤跟踪编号为"' + trackIds + '"的数据项?').then(() => {
|
||||
this.loading = true;
|
||||
return delProjectScheduleStep(trackIds);
|
||||
}).then(() => {
|
||||
this.loading = false;
|
||||
this.getList();
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
/** 导出按钮操作 */
|
||||
handleExport () {
|
||||
this.download('oa/projectScheduleStep/export', {
|
||||
...this.queryParams
|
||||
}, `projectScheduleStep_${new Date().getTime()}.xlsx`)
|
||||
this.download('oa/projectScheduleStep/export', this.buildQuery(),
|
||||
`my_progress_${Date.now()}.xlsx`)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.path { color: #909399; font-size: 11px; }
|
||||
.step-name { color: #303133; font-weight: 600; margin-left: 4px; }
|
||||
::v-deep .el-table .row-delayed > td.el-table__cell { background: #fff1f0 !important; }
|
||||
::v-deep .el-table .row-delayed:hover > td.el-table__cell { background: #ffd8d6 !important; }
|
||||
</style>
|
||||
|
||||
@@ -1,342 +1,254 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px">
|
||||
<el-form-item label="节点负责人" prop="nodeHeader">
|
||||
<el-select v-loading="userLoading" v-model="queryParams.nodeHeader" placeholder="请选择节点负责人" clearable filterable>
|
||||
<!-- 状态筛选 -->
|
||||
<el-tabs v-model="statusTab" @tab-click="onStatusTab" class="compact-tabs">
|
||||
<el-tab-pane name="undone">
|
||||
<span slot="label">未完成 ({{ stat.undone }})</span>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="pending">
|
||||
<span slot="label">待验收 ({{ stat.pending }})</span>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="done">
|
||||
<span slot="label">已完成 ({{ stat.done }})</span>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="全部" name="all" />
|
||||
</el-tabs>
|
||||
|
||||
<el-form :model="queryParams" ref="queryForm" size="mini" :inline="true" v-show="showSearch"
|
||||
label-width="68px" class="compact-search">
|
||||
<el-form-item label="项目" prop="projectName">
|
||||
<el-input v-model="queryParams.projectName" placeholder="项目名" clearable style="width: 160px"
|
||||
@keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="步骤" prop="stepName">
|
||||
<el-input v-model="queryParams.stepName" placeholder="步骤名" clearable style="width: 140px"
|
||||
@keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人" prop="nodeHeader">
|
||||
<el-select v-model="queryParams.nodeHeader" placeholder="选择负责人" clearable filterable style="width: 160px">
|
||||
<el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.nickName" />
|
||||
</el-select>
|
||||
<!-- <el-input v-model="queryParams.nodeHeader" placeholder="请输入节点负责人" clearable @keyup.enter.native="handleQuery" /> -->
|
||||
</el-form-item>
|
||||
<!-- 状态 -->
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态">
|
||||
<el-option label="进行中" value="0" />
|
||||
<el-option label="待验收" value="1" />
|
||||
<el-option label="已完成" value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 计划结束时间 -->
|
||||
<el-form-item label="计划结束时间" prop="planEnd">
|
||||
<el-form-item label="计划完成" prop="planEnd">
|
||||
<el-date-picker v-model="queryParams.planEndRange" type="daterange" value-format="yyyy-MM-dd"
|
||||
placeholder="选择计划结束时间" />
|
||||
start-placeholder="开始" end-placeholder="结束" style="width: 220px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<!-- <el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single"
|
||||
@click="handleUpdate">修改</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple"
|
||||
@click="handleDelete">删除</el-button>
|
||||
</el-col> -->
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="projectScheduleStepList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<!-- <el-table-column label="跟踪记录主键" align="center" prop="trackId" v-if="false" /> -->
|
||||
<el-table-column label="所属项目" align="center" prop="projectName" />
|
||||
<el-table-column label="步骤名称" align="center" prop="tabNode">
|
||||
<el-table v-loading="loading" :data="sortedList" stripe size="small"
|
||||
:row-class-name="rowClassName" :span-method="spanMethod" :row-key="rowKey"
|
||||
@row-click="goToProject">
|
||||
<el-table-column label="所属项目" prop="projectName" min-width="180" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.tabNode }}/ {{ scope.row.firstLevelNode }} / {{ scope.row.secondLevelNode }}</span>
|
||||
<span style="color:#409eff; cursor:pointer">{{ scope.row.projectName || '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column label="一级节点" align="center" prop="firstLevelNode" />
|
||||
<el-table-column label="二级节点" align="center" prop="secondLevelNode" /> -->
|
||||
<el-table-column label="计划完成" align="center" prop="planEnd" width="180">
|
||||
<el-table-column label="步骤" prop="secondLevelNode" min-width="200" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.planEnd, '{y}-{m}-{d}') }}</span>
|
||||
<span class="path">{{ scope.row.tabNode }} · {{ scope.row.firstLevelNode }} ·</span>
|
||||
<span class="step-name">{{ scope.row.secondLevelNode }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="剩余时间" align="center" prop="endTime" width="180">
|
||||
<el-table-column label="负责人" prop="nodeHeader" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.status == 2" style="color: #36d399">{{ parseTime(scope.row.endTime, '{y}-{m}-{d}') }}
|
||||
已完成</span>
|
||||
<span v-else-if="scope.row.status == 1" style="color: #4096ff">待验收</span>
|
||||
<span v-else>
|
||||
<!-- 调用计算方法获取剩余天数 -->
|
||||
<template v-if="scope.row.planEnd">
|
||||
<span v-if="calcRemainingDays(scope.row.planEnd) < 0" style="color: #f56c6c">已逾期 {{
|
||||
-calcRemainingDays(scope.row.planEnd) }} 天</span>
|
||||
<span v-else-if="calcRemainingDays(scope.row.planEnd) <= 3" style="color: #e6a23c">临期 | 还剩 {{
|
||||
calcRemainingDays(scope.row.planEnd) }} 天</span>
|
||||
<span v-else style="color: #67c23a">还剩 {{ calcRemainingDays(scope.row.planEnd) }} 天</span>
|
||||
</template>
|
||||
<span v-else>未设置计划日期</span>
|
||||
<span v-if="scope.row.nodeHeader">{{ scope.row.nodeHeader }}</span>
|
||||
<span v-else style="color:#c0c4cc;">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计划完成" prop="planEnd" width="110" align="center">
|
||||
<template slot-scope="scope">{{ parseTime(scope.row.planEnd, '{y}-{m}-{d}') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="剩余" width="110" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.status === 2" style="color:#67c23a;">
|
||||
✓ {{ parseTime(scope.row.endTime, '{m}-{d}') }}
|
||||
</span>
|
||||
<span v-else-if="scope.row.status === 1" style="color:#409eff;">待验收</span>
|
||||
<template v-else-if="scope.row.planEnd">
|
||||
<span v-if="dayDiff(scope.row.planEnd) < 0" style="color:#f56c6c;">
|
||||
逾 {{ -dayDiff(scope.row.planEnd) }}d
|
||||
</span>
|
||||
<span v-else-if="dayDiff(scope.row.planEnd) <= 3" style="color:#e6a23c;">
|
||||
剩 {{ dayDiff(scope.row.planEnd) }}d
|
||||
</span>
|
||||
<span v-else style="color:#67c23a;">剩 {{ dayDiff(scope.row.planEnd) }}d</span>
|
||||
</template>
|
||||
<span v-else style="color:#c0c4cc;">未设</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status">
|
||||
<!-- 0进行中,1待验收,2已完成 -->
|
||||
<el-table-column label="状态" prop="status" width="90" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.status === 0 ? '进行中' : (scope.row.status === 1 ? '待验收' : '已完成') }}</span>
|
||||
<el-tag size="mini" :type="statusTagType(scope.row.status)">
|
||||
{{ statusLabel(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" align="center" prop="nodeHeader" />
|
||||
<!-- <el-table-column label="相关资料" align="center" prop="relatedDocs" /> -->
|
||||
<!-- <el-table-column label="相关图片" align="center" prop="relatedImages" width="100">
|
||||
<template slot-scope="scope">
|
||||
<image-preview :src="scope.row.relatedImages" :width="50" :height="50" />
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<!-- <el-table-column label="供应商ID" align="center" prop="supplierName" /> -->
|
||||
<!-- <el-table-column label="需求文件" align="center" prop="requirementFile" /> -->
|
||||
<!-- <el-table-column label="规范说明" align="center" prop="specification" /> -->
|
||||
<!-- <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||||
@pagination="getList" />
|
||||
|
||||
<!-- 添加或修改项目进度步骤跟踪对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addProjectScheduleStep, delProjectScheduleStep, getProjectScheduleStep, listPage as listProjectScheduleStep, updateProjectScheduleStep } from "@/api/oa/projectScheduleStep";
|
||||
import { listUser } from "@/api/system/user";
|
||||
import { listPage as listProjectScheduleStep } from '@/api/oa/projectScheduleStep'
|
||||
import { listUser } from '@/api/system/user'
|
||||
|
||||
export default {
|
||||
name: "ProjectScheduleStep",
|
||||
name: 'ProjectScheduleStepOverview',
|
||||
data () {
|
||||
return {
|
||||
// 按钮loading
|
||||
buttonLoading: false,
|
||||
// 用户列表loading
|
||||
userLoading: false,
|
||||
// 遮罩层
|
||||
loading: true,
|
||||
// 选中数组
|
||||
ids: [],
|
||||
// 非单个禁用
|
||||
single: true,
|
||||
// 非多个禁用
|
||||
multiple: true,
|
||||
// 显示搜索条件
|
||||
showSearch: true,
|
||||
// 总条数
|
||||
loading: false,
|
||||
showSearch: false,
|
||||
total: 0,
|
||||
// 项目进度步骤跟踪表格数据
|
||||
statusTab: 'all',
|
||||
stat: { undone: 0, pending: 0, done: 0 },
|
||||
projectScheduleStepList: [],
|
||||
// 弹出层标题
|
||||
title: "",
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
// 查询参数
|
||||
userList: [],
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
scheduleId: undefined,
|
||||
pageSize: 50,
|
||||
projectName: undefined,
|
||||
stepName: undefined,
|
||||
planEnd: undefined,
|
||||
status: undefined,
|
||||
tabNode: undefined,
|
||||
firstLevelNode: undefined,
|
||||
secondLevelNode: undefined,
|
||||
endTime: undefined,
|
||||
nodeHeader: undefined,
|
||||
relatedDocs: undefined,
|
||||
relatedImages: undefined,
|
||||
supplierId: undefined,
|
||||
requirementFile: undefined,
|
||||
specification: undefined,
|
||||
planEndRange: undefined,
|
||||
},
|
||||
// 表单参数
|
||||
form: {},
|
||||
// 表单校验
|
||||
rules: {
|
||||
},
|
||||
userList: [],
|
||||
};
|
||||
status: undefined,
|
||||
planEndRange: undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 按项目分组排序:同一个项目的行紧挨在一起
|
||||
sortedList () {
|
||||
try {
|
||||
const list = [...(this.projectScheduleStepList || [])]
|
||||
list.sort((a, b) => {
|
||||
const ka = String(a.scheduleId || a.projectId || '')
|
||||
const kb = String(b.scheduleId || b.projectId || '')
|
||||
if (ka !== kb) return ka.localeCompare(kb)
|
||||
return (Number(a.sortNum) || 0) - (Number(b.sortNum) || 0)
|
||||
})
|
||||
return list
|
||||
} catch (e) {
|
||||
console.warn('sortedList failed', e)
|
||||
return this.projectScheduleStepList || []
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.getList();
|
||||
this.getListUser();
|
||||
this.getList()
|
||||
this.fetchUsers()
|
||||
},
|
||||
methods: {
|
||||
/** 查询项目进度步骤跟踪列表 */
|
||||
/** 计算剩余天数(当前日期 - 计划完成日期) */
|
||||
calcRemainingDays (planEnd) {
|
||||
if (!planEnd) return '无计划日期';
|
||||
// 转换计划日期为时间戳(忽略时分秒,按日期当天结束计算)
|
||||
const planEndTime = new Date(planEnd).setHours(23, 59, 59, 999);
|
||||
const currentTime = new Date().getTime();
|
||||
// 计算天数差值(向上取整)
|
||||
const diffDays = Math.ceil((planEndTime - currentTime) / (1000 * 60 * 60 * 24));
|
||||
return diffDays;
|
||||
fetchUsers () {
|
||||
listUser({ pageSize: 999 }).then(res => { this.userList = res.rows || [] })
|
||||
},
|
||||
/** 查询用户列表 */
|
||||
getListUser () {
|
||||
this.userLoading = true;
|
||||
listUser({ pageSize: 999 }).then(response => {
|
||||
this.userList = response.rows;
|
||||
this.userLoading = false;
|
||||
});
|
||||
onStatusTab () {
|
||||
if (this.statusTab === 'all') this.queryParams.status = undefined
|
||||
else if (this.statusTab === 'done') this.queryParams.status = 2
|
||||
else if (this.statusTab === 'pending') this.queryParams.status = 1
|
||||
else this.queryParams.status = 0
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
handleQuery () { this.queryParams.pageNum = 1; this.getList() },
|
||||
resetQuery () {
|
||||
this.queryParams = {
|
||||
pageNum: 1, pageSize: 50,
|
||||
projectName: undefined, stepName: undefined, nodeHeader: undefined,
|
||||
status: this.queryParams.status, planEndRange: undefined
|
||||
}
|
||||
this.getList()
|
||||
},
|
||||
buildQuery () {
|
||||
const { planEndRange, ...rest } = this.queryParams
|
||||
const startTime = planEndRange ? planEndRange[0] + ' 00:00:00' : undefined
|
||||
const endTime = planEndRange ? planEndRange[1] + ' 23:59:59' : undefined
|
||||
return { ...rest, startTime, endTime }
|
||||
},
|
||||
getList () {
|
||||
this.loading = true;
|
||||
const endTime = this.queryParams.planEndRange ? this.queryParams.planEndRange[1] + ' 23:59:59' : undefined;
|
||||
const startTime = this.queryParams.planEndRange ? this.queryParams.planEndRange[0] + ' 00:00:00' : undefined;
|
||||
const { planEndRange, ...querys } = {
|
||||
...this.queryParams,
|
||||
startTime,
|
||||
endTime,
|
||||
}
|
||||
listProjectScheduleStep(querys).then(response => {
|
||||
this.projectScheduleStepList = response.rows;
|
||||
this.total = response.total;
|
||||
this.loading = false;
|
||||
});
|
||||
this.loading = true
|
||||
listProjectScheduleStep(this.buildQuery()).then(res => {
|
||||
this.projectScheduleStepList = res.rows || []
|
||||
this.total = res.total || 0
|
||||
}).finally(() => { this.loading = false })
|
||||
this.refreshStat()
|
||||
},
|
||||
// 取消按钮
|
||||
cancel () {
|
||||
this.open = false;
|
||||
this.reset();
|
||||
refreshStat () {
|
||||
// 异步刷新计数;失败/不支持都不阻塞主列表
|
||||
const base = { ...this.buildQuery(), pageNum: 1, pageSize: 1, status: undefined }
|
||||
Promise.all([
|
||||
listProjectScheduleStep({ ...base, status: 0 }).catch(() => ({ total: 0 })),
|
||||
listProjectScheduleStep({ ...base, status: 1 }).catch(() => ({ total: 0 })),
|
||||
listProjectScheduleStep({ ...base, status: 2 }).catch(() => ({ total: 0 }))
|
||||
]).then(([u, p, d]) => {
|
||||
this.stat.undone = u.total || 0
|
||||
this.stat.pending = p.total || 0
|
||||
this.stat.done = d.total || 0
|
||||
})
|
||||
},
|
||||
// 表单重置
|
||||
reset () {
|
||||
this.form = {
|
||||
trackId: undefined,
|
||||
accessory: undefined,
|
||||
scheduleId: undefined,
|
||||
stepOrder: undefined,
|
||||
stepName: undefined,
|
||||
planStart: undefined,
|
||||
planEnd: undefined,
|
||||
actualStart: undefined,
|
||||
actualEnd: undefined,
|
||||
status: undefined,
|
||||
createBy: undefined,
|
||||
createTime: undefined,
|
||||
updateBy: undefined,
|
||||
updateTime: undefined,
|
||||
delFlag: undefined,
|
||||
header: undefined,
|
||||
useFlag: undefined,
|
||||
batchId: undefined,
|
||||
tabNode: undefined,
|
||||
firstLevelNode: undefined,
|
||||
secondLevelNode: undefined,
|
||||
startTime: undefined,
|
||||
originalEndTime: undefined,
|
||||
endTime: undefined,
|
||||
nodeHeader: undefined,
|
||||
relatedDocs: undefined,
|
||||
relatedImages: undefined,
|
||||
supplierId: undefined,
|
||||
requirementFile: undefined,
|
||||
other: undefined,
|
||||
specification: undefined,
|
||||
sortNum: undefined
|
||||
};
|
||||
this.resetForm("form");
|
||||
dayDiff (date) {
|
||||
if (!date) return null
|
||||
const d = new Date(date); d.setHours(0, 0, 0, 0)
|
||||
const t = new Date(); t.setHours(0, 0, 0, 0)
|
||||
return Math.floor((d - t) / 86400000)
|
||||
},
|
||||
/** 搜索按钮操作 */
|
||||
handleQuery () {
|
||||
this.queryParams.pageNum = 1;
|
||||
this.getList();
|
||||
statusLabel (s) {
|
||||
return s === 2 ? '已完成' : (s === 1 ? '待验收' : '进行中')
|
||||
},
|
||||
/** 重置按钮操作 */
|
||||
resetQuery () {
|
||||
this.resetForm("queryForm");
|
||||
this.handleQuery();
|
||||
statusTagType (s) {
|
||||
return s === 2 ? 'success' : (s === 1 ? 'warning' : 'info')
|
||||
},
|
||||
// 多选框选中数据
|
||||
handleSelectionChange (selection) {
|
||||
this.ids = selection.map(item => item.trackId)
|
||||
this.single = selection.length !== 1
|
||||
this.multiple = !selection.length
|
||||
rowClassName ({ row }) {
|
||||
if (row.status === 2) return ''
|
||||
if (row.planEnd && this.dayDiff(row.planEnd) < 0) return 'row-delayed'
|
||||
return ''
|
||||
},
|
||||
/** 新增按钮操作 */
|
||||
handleAdd () {
|
||||
this.reset();
|
||||
this.open = true;
|
||||
this.title = "添加项目进度步骤跟踪";
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
handleUpdate (row) {
|
||||
this.loading = true;
|
||||
this.reset();
|
||||
const trackId = row.trackId || this.ids
|
||||
getProjectScheduleStep(trackId).then(response => {
|
||||
this.loading = false;
|
||||
this.form = response.data;
|
||||
this.open = true;
|
||||
this.title = "修改项目进度步骤跟踪";
|
||||
});
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm () {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
this.buttonLoading = true;
|
||||
if (this.form.trackId != null) {
|
||||
updateProjectScheduleStep(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
} else {
|
||||
addProjectScheduleStep(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
}
|
||||
rowKey (row) { return row.trackId || (row.scheduleId + '_' + (row.sortNum || 0)) },
|
||||
// 项目列合并相邻同项目的行
|
||||
spanMethod ({ row, column, rowIndex }) {
|
||||
try {
|
||||
if (!column || column.property !== 'projectName') return { rowspan: 1, colspan: 1 }
|
||||
const list = this.sortedList
|
||||
if (!list || !list.length) return { rowspan: 1, colspan: 1 }
|
||||
const key = r => String(r.scheduleId || r.projectId || '')
|
||||
if (rowIndex > 0 && key(list[rowIndex - 1]) === key(row)) {
|
||||
return { rowspan: 0, colspan: 0 }
|
||||
}
|
||||
});
|
||||
let span = 1
|
||||
for (let i = rowIndex + 1; i < list.length; i++) {
|
||||
if (key(list[i]) === key(row)) span++
|
||||
else break
|
||||
}
|
||||
return { rowspan: span, colspan: 1 }
|
||||
} catch (e) {
|
||||
console.warn('spanMethod failed', e)
|
||||
return { rowspan: 1, colspan: 1 }
|
||||
}
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete (row) {
|
||||
const trackIds = row.trackId || this.ids;
|
||||
this.$modal.confirm('是否确认删除项目进度步骤跟踪编号为"' + trackIds + '"的数据项?').then(() => {
|
||||
this.loading = true;
|
||||
return delProjectScheduleStep(trackIds);
|
||||
}).then(() => {
|
||||
this.loading = false;
|
||||
this.getList();
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
goToProject (row) {
|
||||
if (!row.scheduleId) return
|
||||
this.$router.push({
|
||||
path: '/step/files',
|
||||
query: { scheduleId: String(row.scheduleId), trackId: String(row.trackId || ''),
|
||||
tabNode: row.tabNode || '', firstLevelNode: row.firstLevelNode || '' }
|
||||
})
|
||||
},
|
||||
/** 导出按钮操作 */
|
||||
handleExport () {
|
||||
this.download('oa/projectScheduleStep/export', {
|
||||
...this.queryParams
|
||||
}, `projectScheduleStep_${new Date().getTime()}.xlsx`)
|
||||
this.download('oa/projectScheduleStep/export', this.buildQuery(),
|
||||
`progress_overview_${Date.now()}.xlsx`)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.path { color: #909399; font-size: 11px; }
|
||||
.step-name { color: #303133; font-weight: 600; margin-left: 4px; }
|
||||
::v-deep .el-table .row-delayed > td.el-table__cell { background: #fff1f0 !important; }
|
||||
::v-deep .el-table .row-delayed:hover > td.el-table__cell { background: #ffd8d6 !important; }
|
||||
</style>
|
||||
|
||||
@@ -141,6 +141,9 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-truck" style="color:#409eff"
|
||||
v-if="scope.row.status !== 2 && scope.row.status !== 3"
|
||||
@click="handleGoToInbound(scope.row)">执行入库</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-check" @click="handleComplete(scope.row)"
|
||||
v-if="scope.row.status === 1">完成</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
|
||||
@@ -302,6 +305,13 @@ export default {
|
||||
this.getUsers();
|
||||
},
|
||||
methods: {
|
||||
// 跳到入库明细页面,并预填该采购需求
|
||||
handleGoToInbound (row) {
|
||||
this.$router.push({
|
||||
path: '/step/in',
|
||||
query: { requirementId: String(row.requirementId), requirementTitle: row.title }
|
||||
})
|
||||
},
|
||||
// 后端已联查 sys_oss 拼好字符串 "ossId|name|url,,ossId|name|url"
|
||||
parseAccessoryFiles (raw) {
|
||||
if (!raw) return []
|
||||
|
||||
177
ruoyi-ui/src/views/system/feedback/index.vue
Normal file
177
ruoyi-ui/src/views/system/feedback/index.vue
Normal file
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-tabs v-model="statusTab" @tab-click="onStatusTab" class="compact-tabs">
|
||||
<el-tab-pane name="pending">
|
||||
<span slot="label" :style="stat.pending > 0 ? 'color:#e6a23c;' : ''">
|
||||
待处理 ({{ stat.pending }})
|
||||
</span>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="`已受理 (${stat.accepted})`" name="accepted" />
|
||||
<el-tab-pane :label="`已完成 (${stat.finished})`" name="finished" />
|
||||
<el-tab-pane label="已关闭" name="closed" />
|
||||
<el-tab-pane label="全部" name="all" />
|
||||
</el-tabs>
|
||||
|
||||
<el-form :model="queryParams" size="mini" :inline="true" v-show="showSearch"
|
||||
label-width="60px" class="compact-search">
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="queryParams.title" placeholder="模糊匹配" clearable style="width: 180px"
|
||||
@keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="category">
|
||||
<el-select v-model="queryParams.category" clearable style="width: 110px">
|
||||
<el-option label="Bug" value="bug" />
|
||||
<el-option label="新功能" value="feature" />
|
||||
<el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
|
||||
|
||||
<el-table v-loading="loading" :data="list" stripe size="small">
|
||||
<el-table-column type="expand" width="36">
|
||||
<template slot-scope="{ row }">
|
||||
<div style="padding:8px 24px; background:#fafafa;">
|
||||
<p style="white-space: pre-wrap; margin:0 0 8px; color:#303133;">{{ row.content }}</p>
|
||||
<div style="font-size:11px; color:#909399;">
|
||||
<span v-if="row.pagePath">页面:<code>{{ row.pagePath }}</code> </span>
|
||||
<span v-if="row.acceptRemark">处理备注:{{ row.acceptRemark }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提交人" prop="submitterName" width="90" />
|
||||
<el-table-column label="类型" width="80" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag size="mini" :type="catTag(row.category)">{{ catLabel(row.category) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="优先级" width="80" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag size="mini" :type="priTag(row.priority)">{{ priLabel(row.priority) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标题" prop="title" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag size="mini" :type="statusTag(row.status)">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提交时间" prop="createTime" width="160">
|
||||
<template slot-scope="{ row }">{{ parseTime(row.createTime, '{y}-{m}-{d} {h}:{i}') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="处理人" prop="handlerName" width="90" />
|
||||
<el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width" v-if="isIt">
|
||||
<template slot-scope="{ row }">
|
||||
<el-button v-if="row.status === 0" type="text" size="mini" @click="onAccept(row)">受理</el-button>
|
||||
<el-button v-if="row.status === 1" type="text" size="mini" style="color:#67c23a"
|
||||
@click="onFinish(row)">完成</el-button>
|
||||
<el-button v-if="row.status !== 3 && row.status !== 2" type="text" size="mini"
|
||||
style="color:#f56c6c" @click="onClose(row)">关闭</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" :total="total"
|
||||
:page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||||
@pagination="getList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { acceptSuggestion as acceptFeedback, closeSuggestion as closeFeedback,
|
||||
finishSuggestion as finishFeedback, isItMember, listSuggestion as listFeedback } from '@/api/oa/suggestion'
|
||||
|
||||
const CAT = { bug: ['Bug', 'danger'], feature: ['新功能', 'success'], other: ['其他', 'info'] }
|
||||
const PRI = { 1: ['高', 'danger'], 2: ['中', 'warning'], 3: ['低', 'info'] }
|
||||
const STATUS = { 0: ['待处理', 'warning'], 1: ['已受理', 'primary'], 2: ['已完成', 'success'], 3: ['已关闭', 'info'] }
|
||||
|
||||
export default {
|
||||
name: 'FeedbackManagement',
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
showSearch: true,
|
||||
isIt: false,
|
||||
total: 0,
|
||||
statusTab: 'pending',
|
||||
stat: { pending: 0, accepted: 0, finished: 0 },
|
||||
list: [],
|
||||
queryParams: { pageNum: 1, pageSize: 30, status: 0, title: undefined, category: undefined }
|
||||
}
|
||||
},
|
||||
created () {
|
||||
isItMember().then(res => { this.isIt = !!(res && res.data) })
|
||||
this.getList()
|
||||
},
|
||||
methods: {
|
||||
catLabel (c) { return (CAT[c] || [c])[0] },
|
||||
catTag (c) { return (CAT[c] || [])[1] || 'info' },
|
||||
priLabel (p) { return (PRI[p] || [p])[0] },
|
||||
priTag (p) { return (PRI[p] || [])[1] || 'info' },
|
||||
statusLabel (s) { return (STATUS[s] || [s])[0] },
|
||||
statusTag (s) { return (STATUS[s] || [])[1] || 'info' },
|
||||
onStatusTab () {
|
||||
const map = { pending: 0, accepted: 1, finished: 2, closed: 3, all: undefined }
|
||||
this.queryParams.status = map[this.statusTab]
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
handleQuery () { this.queryParams.pageNum = 1; this.getList() },
|
||||
resetQuery () {
|
||||
this.queryParams = { pageNum: 1, pageSize: 30, status: this.queryParams.status }
|
||||
this.getList()
|
||||
},
|
||||
getList () {
|
||||
this.loading = true
|
||||
listFeedback(this.queryParams).then(res => {
|
||||
this.list = res.rows || []
|
||||
this.total = res.total || 0
|
||||
}).finally(() => { this.loading = false })
|
||||
this.refreshStat()
|
||||
},
|
||||
refreshStat () {
|
||||
const base = { pageNum: 1, pageSize: 1 }
|
||||
Promise.all([
|
||||
listFeedback({ ...base, status: 0 }).catch(() => ({ total: 0 })),
|
||||
listFeedback({ ...base, status: 1 }).catch(() => ({ total: 0 })),
|
||||
listFeedback({ ...base, status: 2 }).catch(() => ({ total: 0 }))
|
||||
]).then(([p, a, f]) => {
|
||||
this.stat.pending = p.total || 0
|
||||
this.stat.accepted = a.total || 0
|
||||
this.stat.finished = f.total || 0
|
||||
})
|
||||
},
|
||||
onAccept (row) {
|
||||
this.$prompt('受理备注(可空)', '受理', { confirmButtonText: '确定受理',
|
||||
cancelButtonText: '取消', inputType: 'textarea' }).then(({ value }) => {
|
||||
acceptFeedback(row.feedbackId, value || '').then(() => {
|
||||
this.$modal.msgSuccess('已受理,已通知提出者')
|
||||
this.getList()
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
onFinish (row) {
|
||||
this.$prompt('完成说明(可空)', '完成', { confirmButtonText: '标记完成',
|
||||
cancelButtonText: '取消', inputType: 'textarea' }).then(({ value }) => {
|
||||
finishFeedback(row.feedbackId, value || '').then(() => {
|
||||
this.$modal.msgSuccess('已完成,已通知提出者')
|
||||
this.getList()
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
onClose (row) {
|
||||
this.$confirm('关闭后将不再处理,确认?', '关闭确认', { type: 'warning' }).then(() => {
|
||||
closeFeedback(row.feedbackId, '').then(() => {
|
||||
this.$modal.msgSuccess('已关闭')
|
||||
this.getList()
|
||||
})
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user