整合前端

This commit is contained in:
砂糖
2026-04-13 17:04:38 +08:00
parent 69609a2cb1
commit 5d4794c9bd
915 changed files with 144259 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
<template>
<el-dialog :visible.sync="visible" custom-class="add-dialog" title="新增项目进度" width="800px" @close="reset">
<el-form :model="form" label-width="80px" :rules="rules" ref="progressForm">
<!-- ================= 主体区域 ================= -->
<el-row :gutter="20">
<!-- ---------- 左侧项目选择 + 信息 ---------- -->
<el-col :span="24">
<el-form-item label="项目" prop="projectId">
<ProjectSelect v-model="form.projectId" style="width: 100%" placeholder="选择项目" filterable
@change="handleProjectChange" />
</el-form-item>
<el-form-item label="进度模板" prop="templateType">
<el-select v-model="form.templateType" style="width: 100%" placeholder="选择进度模板">
<el-option v-for="t in templateTypeOptions" :key="t.value" :label="t.label" :value="t.value" />
</el-select>
</el-form-item>
<!-- 选中项目后展示详情 -->
<ProjectInfo :info="projectDetail" />
</el-col>
</el-row>
<!-- ---------- 底部按钮 ---------- -->
<el-form-item style="margin-top: 20px; text-align: right">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="submit">保存</el-button>
</el-form-item>
</el-form>
</el-dialog>
</template>
<script>
import { getProject } from '@/api/oa/project';
import ProjectInfo from "@/components/fad-service/ProjectInfo/index.vue";
import ProjectSelect from "@/components/fad-service/ProjectSelect/index.vue";
export default {
name: 'FormDialog',
components: { ProjectInfo, ProjectSelect },
props: {
value: { type: Boolean, required: true },
projects: { type: Array, default: () => [] },
},
data () {
return {
visible: false,
projectDetail: null,
availableSteps: [],
form: { projectId: '', mode: 'custom', templateType: 'automation', steps: [] },
rules: { projectId: [{ required: true, message: '请选择项目', trigger: 'change' }] },
stepParams: { pageNum: 1, pageSize: 9999, stepName: '' },
templateTypeOptions: [
{ label: '信息化', value: 'software' },
{ label: '自动化', value: 'automation' }
]
}
},
watch: {
value: {
immediate: true, handler (v) {
this.visible = v
}
},
visible (v) {
this.$emit('input', v)
}
},
methods: {
handleProjectChange (projectId) {
getProject(projectId).then(r => {
this.projectDetail = r.data
})
},
/* -------- 提交 -------- */
submit () {
this.$refs.progressForm.validate(valid => {
if (!valid) return
const payload = { ...this.form }
this.$emit('save', payload)
this.visible = false
this.$message.success('保存成功')
})
},
reset () {
this.visible = false
this.projectDetail = null
this.form = { projectId: '', mode: 'preset', templateType: 'automation' }
},
/* 工具函数 */
parseTime (time, fmt) {
const date = new Date(time)
const o = { '{y}': date.getFullYear(), '{m}': date.getMonth() + 1, '{d}': date.getDate() }
return fmt.replace(/\{[ymd]\}/g, k => String(o[k]).padStart(2, '0'))
}
}
}
</script>
<style scoped>
.add-dialog ::v-deep .el-dialog__body {
padding-top: 12px
}
.project-detail {
font-size: 13px;
line-height: 1.6
}
.detail-row+.detail-row {
margin-top: 4px
}
.box-title {
font-weight: 600;
margin-bottom: 4px
}
.step-box {
min-height: 120px
}
.clickable {
margin: 4px;
cursor: pointer
}
.empty-hint {
color: #c0c4cc;
font-size: 12px;
text-align: center;
padding: 20px 0
}
</style>

View File

@@ -0,0 +1,170 @@
<template>
<div class="two-level-filter">
<!-- 第一级进度类别 -->
<div 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 }"
@click="handleTabChange(item.value)" class="option-item">
{{ item.label }}
</li>
</ul>
</div>
<!-- 第二级一级分类 -->
<div class="filter-panel second-level">
<h3 class="panel-title">一级分类</h3>
<div class="second-level-content">
<template v-if="defaultTabNode">
<ul class="option-list" v-if="renderFirstLevelOption.length">
<li v-for="item in renderFirstLevelOption" :key="item.value"
:class="{ 'active': defaultFirstLevelNode === item.value }" @click="handleFirstLevelChange(item.value)"
class="option-item">
{{ item.label }}
</li>
</ul>
<p class="empty-tip" v-else>当前进度类别下无一级分类</p>
</template>
<p class="empty-tip" v-else>请先选择进度类别</p>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
tabOption: {
// 格式: { label: 'xxx', value: 'xxx' }
type: Array,
default: () => []
},
firstLevelOption: {
// 格式: { label: 'xxx', value: 'xxx', tabNode: 'xxx' }
type: Array,
default: () => []
}
},
data () {
return {
defaultTabNode: "",
defaultFirstLevelNode: ""
};
},
computed: {
// 根据选中的进度类别过滤一级分类
renderFirstLevelOption () {
if (this.defaultTabNode) {
return this.firstLevelOption.filter(
item => item.tabNode === this.defaultTabNode
);
}
return [];
}
},
methods: {
// 处理进度类别变更
handleTabChange (tabNode) {
this.defaultTabNode = tabNode;
this.defaultFirstLevelNode = ""; // 重置二级选中值
this.$emit("change", {
tabNode: this.defaultTabNode,
firstLevelNode: this.defaultFirstLevelNode
});
},
// 处理一级分类变更
handleFirstLevelChange (firstLevelNode) {
this.defaultFirstLevelNode = firstLevelNode;
this.$emit("change", {
tabNode: this.defaultTabNode,
firstLevelNode: this.defaultFirstLevelNode
});
},
clear () {
this.defaultTabNode = "";
this.defaultFirstLevelNode = "";
this.$emit("change", {
tabNode: this.defaultTabNode,
firstLevelNode: this.defaultFirstLevelNode
});
}
}
};
</script>
<style scoped>
.two-level-filter {
display: flex;
width: 100%;
height: 540px;
border: 1px solid #e5e7eb;
border-radius: 4px;
overflow: hidden;
}
.filter-panel {
flex: 1;
min-height: 200px;
box-sizing: border-box;
}
.first-level {
border-right: 1px solid #e5e7eb;
}
.panel-title {
margin: 0;
padding: 12px 16px;
font-size: 14px;
font-weight: 500;
background-color: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.option-list {
margin: 0;
padding: 0;
list-style: none;
}
.option-item {
padding: 12px 16px;
font-size: 14px;
color: #374151;
cursor: pointer;
transition: background-color 0.2s;
}
.option-item:hover {
background-color: #f3f4f6;
}
.option-item.active {
background-color: #eff6ff;
color: #2563eb;
font-weight: 500;
position: relative;
}
.option-item.active::before {
content: "";
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 3px;
background-color: #2563eb;
}
.second-level-content {
height: calc(100% - 45px);
}
.empty-tip {
color: #9ca3af;
font-size: 14px;
margin: 0;
padding: 0 16px;
text-align: center;
}
</style>

View File

@@ -0,0 +1,744 @@
<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>
<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"
:row-config="{ 'isCurrent': true }" :column-config="{ 'isCurrent': true }" :sort-config="sortConfig">
<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="nodeHeader" title="负责人" :edit-render="{}">
<template slot-scope="{ row }" slot="default">
{{ row.nodeHeader }}
</template>
<template slot-scope="{ row }" slot="edit">
<el-select v-model="row.nodeHeader" placeholder="请选择负责人" filterable clearable>
<el-option v-for="item in users" :key="item.key" :label="item.label" :value="item.value" />
</el-select>
</template>
</vxe-column>
<vxe-column field="relatedDocs" title="成果资料" width="60">
<template slot-scope="{ row }" slot="default">
<vxe-button type="primary" @click="previewFiles(row)" style="cursor: pointer;">
{{ row.relatedDocs ? row.relatedDocs.split(',').length : 0 }}
</vxe-button>
</template>
</vxe-column>
<!-- 资料上传和图片上传也需要定制 -->
<vxe-column field="relatedImages" title="相关图片" width="60">
<template slot-scope="{ row }" slot="default">
<vxe-button type="primary" @click="previewImages(row)" style="cursor: pointer;">
{{ row.relatedImages ? row.relatedImages.split(',').length : 0 }}
</vxe-button>
</template>
</vxe-column>
<!-- 结束时间的渲染需要增强 -->
<!-- 首先状态改为待验收的时候就自动填写真实结束时间 -->
<!-- 与原定结束时间和当前时间做对比, 如果当前时间接近真实时间则提示临期或逾期 -->
<!-- 可以申请逾期 -->
<!-- <vxe-column field="planEnd" title="计划结束" v-if="isCEO"
:edit-render="{ name: 'ElDatePicker', props: { type: 'datetime', valueFormat: 'yyyy-MM-dd HH:mm:ss' } }">
</vxe-column> -->
<vxe-column title="剩余时间" width="120">
<template slot-scope="{ row }" slot="default">
<div class="remain-time-container">
<vxe-button v-if="row.planEnd && row.useFlag == 1 && row.status == 0 && isSelf(row)" size="mini" type="text"
@click="handleApplyDelay(row)" class="delay-btn">
申请延期
</vxe-button>
<!-- 剩余时间显示 -->
<span :class="{
'text-red': remainStatus(row) === 'overdue',
'text-orange': remainStatus(row) === 'warning',
'text-green': remainStatus(row) === 'enough',
'text-gray': !row.planEnd,
'text-blue': row.useFlag == 0
}">
{{ getRemainText(row) }}
</span>
</div>
</template>
</vxe-column>
<vxe-column title="结束时间" :edit-render="{}" width="140">
<template slot="header">
<span title="当前状态为进行中时,表示计划结束时间,此时若字体颜色为蓝色则表示有延期申请,其他状态下表示实际结束时间">
结束时间
</span>
</template>
<template slot-scope="{ row }" slot="default">
<span v-if="row.status == 0">{{ row.planEnd }}</span>
<span v-else>{{ row.endTime }}</span>
</template>
<template slot-scope="{ row }" slot="edit">
<el-date-picker v-if="row.status == 0" v-model="row.planEnd" type="datetime"
value-format="yyyy-MM-dd HH:mm:ss" placeholder="选择计划结束时间" />
<el-date-picker v-else v-model="row.endTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
placeholder="选择结束时间" />
</template>
</vxe-column>
<vxe-column field="status" title="状态">
<template slot-scope="{ row }">
<!-- 对于总经办的人,可以任意变更状态0,null进行中1待验收2已完成 -->
<el-select v-if="isCEO" :value="row.status" placeholder="请选择状态" @change="changeStatus(row, $event)">
<el-option label="进行中" :value="0"></el-option>
<el-option label="待验收" :value="1"></el-option>
<el-option label="已完成" :value="2"></el-option>
</el-select>
<!-- 对于nodeHeader是自己的, 可以变更为待验收状态 -->
<template v-else-if="isSelf(row)">
<div v-if="row.status === 1">待验收</div>
<el-button v-else-if="row.status === 0" type="primary" @click="changeStatus(row, 1)">提交</el-button>
<div v-else>已完成</div>
</template>
<!-- 对于其他人只能查看状态 -->
<template v-else>
<div>{{ row.status === 0 ? '进行中' : (row.status === 1 ? '待验收' : '已完成') }}</div>
</template>
</template>
</vxe-column>
<vxe-column title="更多信息" show-overflow="tooltip">
<template slot-scope="{ row }" slot="default">
<el-tooltip v-if="row.specification" class="item" :content="row.specification" placement="top">
<div title="点击查看更多信息" @click="handleMore(row)"
style="cursor: pointer; width: 100%; min-width: 80px; height: 100%">
{{ row.specification || '暂无更多' }}
</div>
</el-tooltip>
<div v-else title="点击查看更多信息" @click="handleMore(row)"
style="cursor: pointer; width: 100%; min-width: 80px; height: 100%; min-height: 30px;">
{{ row.specification || '暂无更多' }}
</div>
<!-- <vxe-button>更多</vxe-button> -->
</template>
</vxe-column>
<vxe-column title="操作" width="200" 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>
</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>
</template>
</template>
<template v-else>
<vxe-button @click="handleAdd(row)">新增</vxe-button>
</template>
</template>
</template>
</vxe-column>
</vxe-table>
<el-dialog :visible.sync="dialogDocsVisible" title="成果资料上传" append-to-body>
<file-upload v-model="dialogRelatedDocs" @success="handleFileSuccess" @delete="handleFileDelete"></file-upload>
<el-button type="primary" @click="uploadRelatedDocs">保存</el-button>
</el-dialog>
<el-dialog :visible.sync="dialogImagesVisible" title="相关图片上传" append-to-body>
<image-upload v-model="dialogRelatedImages"></image-upload>
<el-button type="primary" @click="uploadRelatedImages">保存</el-button>
</el-dialog>
<!-- 延期申请对话框 -->
<el-dialog :visible.sync="dialogApplyDelayVisible" title="申请延期" append-to-body>
<el-form :model="dialogApplyDelayForm" ref="formRef" label-width="120px">
<el-form-item label="延期到" prop="delayTo">
<el-date-picker v-model="dialogApplyDelayForm.delayTo" type="date" value-format="yyyy-MM-dd"
placeholder="选择延期到的日期" />
</el-form-item>
<el-form-item label="申请理由" prop="applyReason">
<el-input type="textarea" v-model="dialogApplyDelayForm.applyReason" placeholder="请输入申请理由"></el-input>
</el-form-item>
</el-form>
<el-button type="primary" @click="submitApplyDelay">确认申请</el-button>
</el-dialog>
<el-dialog :visible.sync="addDialogVisible" title="新增进度" append-to-body>
<el-form :model="dialogAddForm" ref="formRef" label-width="120px">
<el-form-item label="进度类别" prop="tabNode">
<el-input v-model="dialogAddForm.tabNode" placeholder="请输入进度类别"></el-input>
</el-form-item>
<el-form-item label="一级分类" prop="firstLevelNode">
<el-input v-model="dialogAddForm.firstLevelNode" placeholder="请输入一级分类"></el-input>
</el-form-item>
<el-form-item label="步骤名称" prop="secondLevelNode">
<el-input v-model="dialogAddForm.secondLevelNode" placeholder="请输入进度描述"></el-input>
</el-form-item>
<el-form-item label="计划结束时间" prop="planEnd">
<el-date-picker v-model="dialogAddForm.planEnd" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
placeholder="选择计划结束时间" />
</el-form-item>
<el-form-item label="进度规范" prop="specification">
<el-input type="textarea" v-model="dialogAddForm.specification" placeholder="进度规范"></el-input>
</el-form-item>
</el-form>
<el-button type="primary" @click="handleAdd">确认新增</el-button>
</el-dialog>
<el-dialog :visible.sync="dialogMoreVisible" title="更多信息" append-to-body v-loading="buttonLoading">
<el-row align="middle" justify="space-between" type="flex">
<el-col :span="16">
<el-alert title="可以任意添加所需的信息,例如:需求资料,需求描述" type="info"></el-alert>
</el-col>
<el-col :span="6">
<el-button icon="el-icon-plus" @click="handleAddMore" plain size="mini">新增</el-button>
<el-button type="primary" icon="el-icon-check" @click="handleMoreSave" plain size="mini">保存</el-button>
</el-col>
</el-row>
<el-descriptions :column="1" border style="margin-top: 20px;">
<el-descriptions-item label="规格需求">
<el-input v-model="dialogMoreForm.specification" placeholder="规格需求" type="textarea"></el-input>
</el-descriptions-item>
<el-descriptions-item label="需求资料">
<file-upload v-model="dialogMoreForm.requirementFile"></file-upload>
</el-descriptions-item>
<el-descriptions-item label="供应商">
<el-select v-model="dialogMoreForm.supplierId" placeholder="请选择供应商">
<el-option v-for="item in supplierList" :key="item.value" :label="item.label"
:value="item.value"></el-option>
</el-select>
</el-descriptions-item>
</el-descriptions>
<el-descriptions :column="1" border style="margin-top: 20px;" :labelStyle="{ width: '150px' }">
<el-descriptions-item v-for="item in dialogMoreForm.other" :key="item.label">
<template slot="label">
<el-input v-model="item.label" placeholder="请输入信息名称"></el-input>
</template>
<el-input v-model="item.value" placeholder="请输入" type="textarea"></el-input>
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script>
import { addFileOperationRecord } from '@/api/oa/fileOperationRecord';
import { applyProjectScheduleDelay } from "@/api/oa/projectScheduleDelay";
import { updateProjectScheduleStep } from "@/api/oa/projectScheduleStep";
import { listSupplier } from "@/api/oa/supplier";
import { listUser } from "@/api/system/user";
export default {
name: "StepTable",
props: {
stepList: {
type: Array,
default: () => []
},
master: {
type: String,
default: ""
},
editable: {
type: Boolean,
default: false
},
defaultTabNode: {
type: String,
default: ""
},
defaultFirstLevelNode: {
type: String,
default: ""
},
},
data () {
return {
innerData: [],
buttonLoading: false,
editConfig: { trigger: 'manual', mode: 'row' },
sortConfig: {
defaultSort: {
field: 'sortNum',
order: 'asc'
}
},
dialogRelatedDocs: "",
dialogRelatedImages: "",
dialogDocsVisible: false,
dialogImagesVisible: false,
currentRow: {},
// 延期对话框控制
dialogApplyDelayVisible: false,
dialogApplyDelayForm: {
trackId: '',
delayTo: '',
applyReason: '',
},
dialogMoreVisible: false,
// 新增对话框控制
addDialogVisible: false,
dialogMoreForm: {
specification: '',
requirementFile: '',
supplierId: '',
other: [{
label: '',
value: '',
}]
},
dialogAddForm: {
stepOrder: '',
secondLevelNode: '',
tabNode: '',
firstLevelNode: '',
specification: '',
nodeHeader: '',
relatedDocs: '',
relatedImages: '',
startTime: '',
endTime: '',
status: '',
},
users: [],
supplierList: [],
}
},
watch: {
stepList: {
handler (newVal) {
this.innerData = newVal;
},
immediate: true
}
},
computed: {
nickName () {
return this.$store.getters.nickName;
},
isCEO () {
// console.log(this.$store.getters.roles);
// 拥有ceo权限或者admin权限或者是项目负责人
return this.$store.getters.roles.includes('ceo') ||
this.$store.getters.roles.includes('doctor') ||
this.$store.getters.roles.includes('admin') ||
this.$store.getters.roles.includes('13') ||
this.master === this.nickName;
}
},
mounted () {
this.getUsers();
this.getSupplierList();
},
methods: {
/**
* 获取所有用户
*/
getUsers () {
listUser({ pageNum: 1, pageSize: 1000 }).then(res => {
console.log(res.rows);
this.users = res.rows.map(item => ({
key: item.userId,
label: item.nickName,
value: item.nickName,
}));
});
},
getSupplierList () {
listSupplier({ pageNum: 1, pageSize: 1000 }).then(res => {
this.supplierList = res.rows.map(item => ({
label: item.supplierName,
value: item.supplierId,
}));
});
},
handleFileSuccess (resList, res) {
addFileOperationRecord({
fileId: res.ossId,
fileName: res.name,
type: 1,
projectId: this.currentRow.projectId,
trackId: this.currentRow.trackId,
})
console.log(this.currentRow, this.dialogRelatedDocs);
updateProjectScheduleStep({
...this.currentRow,
relatedDocs: this.dialogRelatedDocs,
})
},
handleFileDelete (res) {
addFileOperationRecord({
fileId: res.ossId,
fileName: res.name,
type: 2,
projectId: this.currentRow.projectId,
trackId: this.currentRow.trackId,
})
console.log(this.currentRow, this.dialogRelatedDocs);
updateProjectScheduleStep({
...this.currentRow,
relatedDocs: this.dialogRelatedDocs,
})
},
/**
* 计算剩余时间状态
* @param {Object} row 行数据
* @returns {String} 状态标识overdue/warning/enough
*/
remainStatus (row) {
if (!row.planEnd) return '';
// 处理时间格式将计划结束时间设为当天23:59:59
const endTime = new Date(row.planEnd);
endTime.setHours(23, 59, 59, 999);
const currentTime = new Date();
// 计算剩余天数(向上取整)
const diffMs = endTime - currentTime;
const diffDays = Math.ceil(diffMs / (24 * 60 * 60 * 1000));
if (diffDays < 0) return 'overdue'; // 逾期
if (diffDays <= 3) return 'warning'; // 警告0-3天
return 'enough'; // 充足(>3天
},
/**
* 获取剩余时间显示文本
* @param {Object} row 行数据
* @returns {String} 显示文本
*/
getRemainText (row) {
if (!row.planEnd) return '未设置';
// 不修改planEnd的原始时间直接用row.planEnd
const endTime = new Date(row.planEnd);
const currentTime = new Date();
const diffMs = endTime - currentTime;
const diffDays = Math.floor(diffMs / (24 * 60 * 60 * 1000)); // 改为向下取整
if (row.status == 1) return '等待验收';
if (row.status == 2) return '已完成';
if (diffMs < 0) {
// 逾期:计算逾期天数(绝对值向下取整)
const overdueDays = Math.floor(Math.abs(diffMs) / (24 * 60 * 60 * 1000));
return overdueDays > 0 ? `逾期${overdueDays}` : '已逾期不足1天';
} else if (diffDays === 0) {
// 剩余不足1天
return '剩余不足1天';
} else {
// 剩余≥1天
return `剩余${diffDays}`;
}
},
handleMore (row) {
// 初始化表单数据
// 中文键json转数组
// this.dialogMoreForm.other = row.other || [];
const other = []
const otherJson = JSON.parse(row.other || '{}');
if (row.other) {
for (const key in otherJson) {
other.push({
label: key,
value: otherJson[key],
})
}
}
this.dialogMoreForm.trackId = row.trackId;
this.dialogMoreForm.other = other
this.dialogMoreForm.specification = row.specification;
this.dialogMoreForm.requirementFile = row.requirementFile;
this.dialogMoreForm.supplierId = row.supplierId;
// 显示对话框
this.dialogMoreVisible = true;
},
handleAddMore () {
// 初始化表单数据
this.dialogMoreForm.other.push({
label: '',
value: '',
})
},
handleMoreSave () {
// 数组转中文键json
const o = {}
this.dialogMoreForm.other.forEach(item => {
if (item.label) {
o[item.label] = item.value
}
})
// this.buttonLoading = true;
const payload = { ...this.dialogMoreForm, other: JSON.stringify(o) }
console.log(payload)
// this.loading = true;
this.buttonLoading = true;
updateProjectScheduleStep(payload).then(res => {
this.buttonLoading = false;
// this.loading = false;
this.$emit("refresh", this.innerData);
this.$modal.msgSuccess("更新成功");
this.dialogMoreVisible = false;
})
},
/**
* 处理申请延期逻辑
* @param {Object} row 行数据
*/
handleApplyDelay (row) {
// 这里添加申请延期的逻辑,例如打开弹窗
console.log('申请延期:', row);
// 初始化表单数据
this.dialogApplyDelayForm.trackId = row.trackId;
this.dialogApplyDelayForm.delayTo = row.planEnd;
this.dialogApplyDelayForm.originalEndTime = row.planEnd;
this.dialogApplyDelayForm.applyReason = '';
// 显示对话框
this.dialogApplyDelayVisible = true;
},
/**
* 提交延期申请
*/
submitApplyDelay () {
// 修改计划结束时间为申请的时间,并更新计划延期申请状态
const payload = {
trackId: this.dialogApplyDelayForm.trackId,
planEnd: this.dialogApplyDelayForm.delayTo,
useFlag: 0, // 计划延期申请状态0申请中1已处理
}
// 调用接口更新数据
this.loading = true;
this.buttonLoading = true;
updateProjectScheduleStep(payload).then(response => {
this.$emit("refresh", this.innerData);
applyProjectScheduleDelay({
trackId: this.dialogApplyDelayForm.trackId,
expectEndTime: this.dialogApplyDelayForm.delayTo + ' 23:59:59',
applyReason: this.dialogApplyDelayForm.applyReason,
originalEndTime: this.dialogApplyDelayForm.originalEndTime,
})
this.$modal.msgSuccess("延期申请已提交");
this.dialogApplyDelayVisible = false;
}).catch(() => {
this.$modal.msgError("提交失败");
}).finally(() => {
this.buttonLoading = false;
this.loading = false;
});
},
isSelf (row) {
return row.nodeHeader === this.nickName;
},
changeStatus (row, status) {
const payload = { ...row, status };
// 如果从0直接改为2或者从0改为1需要设置endTime
if ((status === 2 || status === 1) && (row.status == 0 || row.status == null)) {
payload.endTime = new Date().toISOString().substring(0, 19).replace('T', ' ');
}
this.loading = true;
this.buttonLoading = true;
updateProjectScheduleStep(payload).then(response => {
this.$emit("refresh", this.innerData);
this.$modal.msgSuccess("提交成功");
this.dialogImagesVisible = false;
}).catch(() => {
this.$modal.msgError("提交失败");
}).finally(() => {
this.buttonLoading = false;
this.loading = false;
});
},
previewFiles (row) {
this.dialogRelatedDocs = row.relatedDocs ? row.relatedDocs : '';
this.currentRow = row;
this.dialogDocsVisible = true;
},
uploadRelatedDocs () {
this.loading = true;
this.buttonLoading = true;
updateProjectScheduleStep({ ...this.currentRow, relatedDocs: this.dialogRelatedDocs }).then(response => {
this.$emit("refresh", this.innerData);
this.$modal.msgSuccess("保存成功");
this.dialogDocsVisible = false;
}).catch(() => {
this.$modal.msgError("保存失败");
}).finally(() => {
this.buttonLoading = false;
this.loading = false;
});
},
previewImages (row) {
this.dialogRelatedImages = row.relatedImages ? row.relatedImages : '';
this.currentRow = row;
this.dialogImagesVisible = true;
},
uploadRelatedImages () {
this.loading = true;
this.buttonLoading = true;
updateProjectScheduleStep({ ...this.currentRow, relatedImages: this.dialogRelatedImages }).then(response => {
this.$emit("refresh", this.innerData);
this.$modal.msgSuccess("保存成功");
this.dialogImagesVisible = false;
}).catch(() => {
this.$modal.msgError("保存失败");
}).finally(() => {
this.buttonLoading = false;
this.loading = false;
});
},
hasEditStatus (row) {
const $table = this.$refs.tableRef;
return $table ? $table.isEditByRow(row) : false;
},
showEdit (row) {
return !row.isAggregate;
},
editRowEvent (row) {
const $table = this.$refs.tableRef;
if ($table) {
$table.setEditRow(row);
}
},
agreeDelay (row) {
// 同意延期,设置计划延期申请状态为已处理
this.loading = true;
this.buttonLoading = true;
updateProjectScheduleStep({
...row,
useFlag: 1, // 计划延期申请状态0申请中1已处理
}).then(response => {
this.$emit("refresh", this.innerData);
this.loading = false;
this.$modal.msgSuccess("同意延期成功");
}).catch(() => {
this.loading = false;
this.$modal.msgError("同意延期失败");
}).finally(() => {
this.buttonLoading = false;
});
},
saveRowEvent (row) {
const $table = this.$refs.tableRef;
this.currentRow = row;
if ($table) {
$table.clearEdit().then(() => {
this.loading = true;
this.buttonLoading = true;
updateProjectScheduleStep({
...row,
// useFlag: 1, // 计划延期申请状态0申请中1已处理
}).then(response => {
this.$emit("refresh", this.innerData);
this.loading = false;
this.$modal.msgSuccess("保存成功");
}).catch(() => {
this.loading = false;
}).finally(() => {
this.buttonLoading = false;
});
});
}
},
cancelRowEvent () {
const $table = this.$refs.tableRef;
if ($table) {
$table.clearEdit();
}
},
addInnerData () {
this.addDialogVisible = true;
this.dialogAddForm = {
stepOrder: this.innerData.length + 1,
secondLevelNode: "",
tabNode: this.defaultTabNode,
firstLevelNode: this.defaultFirstLevelNode,
specification: "",
nodeHeader: "",
relatedDocs: "",
relatedImages: "",
startTime: "",
endTime: "",
status: "",
};
// this.innerData.push({
// stepOrder: this.innerData.length + 1,
// secondLevelNode: "",
// tabNode: this.defaultTabNode,
// firstLevelNode: this.defaultFirstLevelNode,
// specification: "",
// nodeHeader: "",
// relatedDocs: "",
// relatedImages: "",
// startTime: "",
// endTime: "",
// status: "",
// });
// this.editRowEvent(this.innerData[this.innerData.length - 1]);
},
handleAdd () {
this.addDialogVisible = false;
this.$emit("add", this.dialogAddForm);
},
handleDelete (row) {
this.$emit('delete', row.trackId);
},
},
}
</script>
<style>
/* 样式部分 */
.remain-time-container {
display: flex;
align-items: center;
gap: 8px;
}
.text-red {
color: #f56c6c;
/* 红色:逾期 */
}
.text-orange {
color: #e6a23c;
/* 橙色警告0-3天 */
}
.text-green {
color: #67c23a;
/* 绿色:充足(>3天 */
}
.text-gray {
color: #909399;
/* 灰色:未设置 */
}
.delay-btn {
color: #409eff;
/* 按钮蓝色 */
padding: 0 4px;
}
.text-blue {
color: #409eff !important;
/* 蓝色:申请中 */
}
</style>

View File

@@ -0,0 +1,111 @@
<template>
<div v-loading="loading" class="app-container">
<el-table :data="list">
<el-table-column label="相关项目" align="center" prop="projectName" show-overflow-tooltip />
<el-table-column label="进度步骤" align="center" prop="trackId" show-overflow-tooltip>
<template slot-scope="scope">
<span>{{ scope.row.tabNode }} / {{ scope.row.firstLevelNode }} / {{ scope.row.secondLevelNode }}</span>
</template>
</el-table-column>
<el-table-column label="申请人姓名" align="center" prop="applyUserName" />
<el-table-column label="原时间" align="center" prop="originalEndTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.originalEndTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="申请至" align="center" prop="expectEndTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.expectEndTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="延期原因" align="center" prop="applyReason" />
<el-table-column label="审批人" align="center" prop="approveUserName" />
<el-table-column label="审批时间" align="center" prop="approveTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.approveTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="审批结果" align="center" prop="approveResult">
<template slot-scope="scope">
<span v-if="scope.row.approveResult == 1">已同意</span>
<span v-else-if="scope.row.approveResult == 2">已拒绝</span>
<span v-else>待审批</span>
</template>
</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-check" @click="handleAgree(scope.row)"
v-if="scope.row.approveResult == 0">同意延期</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 { agreeProjectScheduleDelay, listProjectScheduleDelay } from "@/api/oa/projectScheduleDelay";
export default {
props: {
scheduleId: {
type: String,
required: true
}
},
data () {
return {
queryParams: {
scheduleId: this.scheduleId,
pageNum: 1,
pageSize: 10
},
list: [],
total: 0,
loading: true
}
},
watch: {
scheduleId: {
handler (newVal, oldVal) {
if (newVal !== oldVal) {
this.queryParams.scheduleId = newVal;
this.getList();
}
},
immediate: true
}
},
methods: {
getList () {
this.loading = true;
listProjectScheduleDelay(this.queryParams).then(res => {
this.list = res.rows;
this.total = res.total;
this.loading = false;
});
},
handleAgree (row) {
this.$modal.confirm("确认同意延期吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
agreeProjectScheduleDelay({
delayId: row.delayId,
approveResult: 1
}).then(response => {
this.$modal.success("同意成功");
this.$message({
message: "已同意延期",
type: "success"
});
this.getList();
});
});
},
}
}
</script>

View File

@@ -0,0 +1,421 @@
<template>
<div v-loading="loading">
<el-row>
<div>
<el-row>
<el-col :span="12">
<div style="font-size: small;">
<span style="color:#d0d0d0 ">项目名</span>
<el-popover placement="bottom" trigger="hover" width="800">
<template slot="reference">
<span style="color: #409eff;">{{ projectName }}</span>
</template>
<ProjectInfo :info="projectDetail" />
</el-popover>
</div>
</el-col>
<el-col :span="12">
<div style="font-size: small;">
<span style="color:#d0d0d0 ">项目负责人</span>
<span style="">{{ master }}</span>
</div>
</el-col>
<el-col :span="12">
<div style="font-size: small;">
<span style="color:#d0d0d0 ">当前进度</span>
<span style="">{{ scheduleSummary }}</span>
</div>
</el-col>
<el-col :span="12">
<div style="font-size: small;">
<span style="color:#d0d0d0 ">
项目状态
</span>
<span v-if="isTop" style="color: #ff4d4f;">重点关注</span>
<span v-else style="">一般项目</span>
</div>
</el-col>
</el-row>
</div>
</el-row>
<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-button label="table">表格</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">
<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>
</template>
<script>
import { getProject } from "@/api/oa/project";
import { addProjectScheduleStep, delProjectScheduleStep, getProjectScheduleStep, listPage, updateProjectScheduleStep } from "@/api/oa/projectScheduleStep";
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";
export default {
name: "ProjectScheduleStep",
props: {
scheduleId: {
type: String,
required: true
},
projectId: {
type: String,
default: ''
},
master: {
type: String,
default: ''
},
projectName: {
type: String,
default: ''
},
projectStatus: {
type: String,
default: ''
},
isTop: {
type: Boolean | Number,
default: false
},
},
components: {
StepTable,
MenuSelect,
Xmind,
ProjectInfo,
},
data () {
return {
viewMode: 'xmind',
defaultTabNode: "",
defaultFirstLevelNode: "",
// 按钮loading
buttonLoading: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 项目进度步骤跟踪表格数据
projectScheduleStepList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
projectDetail: {},
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 999,
accessory: undefined,
scheduleId: undefined,
stepOrder: undefined,
stepName: undefined,
planStart: undefined,
planEnd: undefined,
actualStart: undefined,
actualEnd: undefined,
status: undefined,
},
// 筛选参数
filterParams: {
onlyMy: false,
onlyUnfinished: false,
},
// 表单参数
form: {},
gridOptions: {
// columns: [
// { title: '步骤序号', field: 'stepOrder', editRender: { name: 'el-input-number' } },
// { title: '步骤名称', field: 'stepName', editRender: { name: 'el-input' } },
// { title: '计划开始', field: 'planStart', editRender: { name: 'el-date-picker' } },
// { title: '计划完成', field: 'planEnd', editRender: { name: 'el-date-picker' } },
// { title: '实际开始', field: 'actualStart', editRender: { name: 'el-date-picker' } },
// { title: '实际完成', field: 'actualEnd', editRender: { name: 'el-date-picker' } },
// ],
// data: this.projectScheduleStepList
columns: [
{ type: 'seq', width: 70 },
{ field: 'name', title: 'Name' },
{ field: 'sex', title: 'Sex' },
{ field: 'age', title: 'Age' }
],
data: [
{ id: 10001, name: 'Test1', role: 'Develop', sex: 'Man', age: 28, address: 'test abc' },
{ id: 10002, name: 'Test2', role: 'Test', sex: 'Women', age: 22, address: 'Guangzhou' },
{ id: 10003, name: 'Test3', role: 'PM', sex: 'Man', age: 32, address: 'Shanghai' },
{ id: 10004, name: 'Test4', role: 'Designer', sex: 'Women', age: 24, address: 'Shanghai' }
]
}
};
},
computed: {
tabOption: {
get () {
const seen = new Set(); // 用于记录已出现的tabNode
const tabNodes = [];
// 遍历原数组只保留首次出现的tabNode保持原有顺序
for (const item of this.projectScheduleStepList) {
const currentTabNode = item.tabNode;
if (!seen.has(currentTabNode)) {
seen.add(currentTabNode);
tabNodes.push(currentTabNode);
}
}
return tabNodes.map(item => ({ label: item, value: item }));
}
},
scheduleSummary: {
get () {
// 统计总数和状态为2以及1的数量为2表示已完成为1表示待验收
const totalCount = this.projectScheduleStepList.length;
const completedCount = this.projectScheduleStepList.filter(item => item.status === 2).length;
const pendingCount = this.projectScheduleStepList.filter(item => item.status === 1).length;
return `已完成(${completedCount}+ 待验收(${pendingCount} / 总节点数(${totalCount}`;
// return this.projectScheduleStepList.find(item => item.scheduleId === this.scheduleId);
}
},
firstLevelOption: {
get () {
// 用 "firstLevelNode-tabNode" 作为组合键,确保唯一
const uniqueMap = {};
this.projectScheduleStepList.forEach(item => {
// 生成组合键(确保 firstLevelNode 和 tabNode 都相同时才重复)
const uniqueKey = `${item.firstLevelNode}-${item.tabNode}`;
// 存储唯一键对应的完整对象(重复键会覆盖,保留最后一个;若需保留第一个可加判断)
uniqueMap[uniqueKey] = item;
});
// 转换为数组,再映射成目标格式
const firstLevelNodes = Object.values(uniqueMap).map(item => ({
label: item.firstLevelNode, // 显示文本
value: item.firstLevelNode, // 选中值
tabNode: item.tabNode // 关联的 tabNode此时必然正确
}));
return firstLevelNodes;
}
},
filterList: {
get () {
// 筛选参数
const { onlyMy, onlyUnfinished } = this.filterParams;
if (!this.defaultTabNode || !this.defaultFirstLevelNode) {
return this.projectScheduleStepList.filter(item => {
if (onlyMy) {
console.log(item.nodeHeader, this.$store.getters);
return item.nodeHeader === this.$store.getters.nickName;
}
return true;
})
.filter(item => {
if (onlyUnfinished) {
return item.status !== 2;
}
return true;
});;
}
console.log(onlyMy, onlyUnfinished);
return this.projectScheduleStepList.filter(item => item.tabNode === this.defaultTabNode && item.firstLevelNode === this.defaultFirstLevelNode)
.filter(item => {
if (onlyMy) {
console.log(item.nodeHeader, this.$store.getters);
return item.nodeHeader === this.$store.getters.nickName;
}
return true;
})
.filter(item => {
if (onlyUnfinished) {
return item.status !== 2;
}
return true;
});
}
}
},
watch: {
scheduleId: {
handler (newVal) {
this.queryParams.scheduleId = newVal;
this.getList();
},
immediate: true
},
projectId: {
handler (newVal) {
getProject(newVal).then(r => {
console.log(r);
this.projectDetail = r.data
})
},
immediate: true
},
},
methods: {
/** 查询项目进度步骤跟踪列表 */
getList () {
this.loading = true;
listPage(this.queryParams).then(response => {
this.projectScheduleStepList = response.rows;
this.total = response.total;
this.loading = false;
});
},
handleOverview () {
this.$refs.menuSelectRef.clear();
},
handleChange (val) {
this.defaultTabNode = val.tabNode;
this.defaultFirstLevelNode = val.firstLevelNode;
},
// 取消按钮
cancel () {
this.open = false;
this.reset();
},
// 表单重置
reset () {
this.form = {
trackId: undefined,
accessory: undefined,
scheduleId: this.scheduleId,
stepOrder: undefined,
stepName: undefined,
planStart: undefined,
planEnd: undefined,
actualStart: undefined,
actualEnd: undefined,
status: undefined,
createBy: undefined,
createTime: undefined,
updateBy: undefined,
updateTime: undefined,
delFlag: undefined
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery () {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery () {
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange (selection) {
this.ids = selection.map(item => item.trackId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
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 (row) {
this.buttonLoading = true;
this.loading = true;
if (this.form.trackId != null) {
updateProjectScheduleStep({
...row,
scheduleId: this.scheduleId,
}).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
} else {
addProjectScheduleStep({
...row,
originalEndTime: row.planEnd,
scheduleId: this.scheduleId,
}).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
},
/** 删除按钮操作 */
handleDelete (trackIds) {
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`)
}
}
};
</script>

View File

@@ -0,0 +1,365 @@
<template>
<div class="xmind-box">
<div class='action-panel'>
<!-- <el-button type="primary" icon="el-icon-plus" @click="handleAdd">新增</el-button> -->
<el-button type="primary" icon="el-icon-refresh" @click="handleRefresh">刷新</el-button>
<!-- <el-button type="primary" icon="el-icon-view" @click="handleRefresh">详情</el-button>
<el-button type="primary" icon="el-icon-edit" @click="handleRefresh">编辑</el-button>
<el-button type="primary" icon="el-icon-folder" @click="previewFiles(currentNode)">文件</el-button>
<el-button type="primary" icon="el-icon-picture" @click="previewImages(currentNode)">图片</el-button> -->
</div>
<div class="xmind-container" ref="chart" style="width: 100%; height: 800px;"></div>
<!-- 新增三级节点点击弹窗-查看完整信息 -->
<el-dialog title="节点详情信息" :visible.sync="dialogVisible" width="1200px" center append-to-body>
<el-form>
<el-row :gutter="10">
<el-col :span="6">
<el-form-item label="进度类别" prop="name">
<el-input v-model="currentNode.tabNode" placeholder="请输入进度类型"></el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="一级分类" prop="firstLevelNode">
<el-input v-model="currentNode.firstLevelNode" placeholder="请输入一级分类"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="步骤名称" prop="secondLevelNode">
<el-input v-model="currentNode.secondLevelNode" placeholder="请输入步骤名称"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="负责人" prop="nodeHeader">
<el-input v-model="currentNode.nodeHeader" placeholder="请输入负责人"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="规格需求" prop="specification">
<el-input v-model="currentNode.specification" placeholder="规格需求"></el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="成果资料" prop="relatedDocs">
<file-upload @success="handleFileSuccess" @delete="handleFileDelete" v-model="currentNode.relatedDocs"
placeholder="成果资料"></file-upload>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="相关图片" prop="relatedImages">
<image-upload v-model="currentNode.relatedImages" placeholder="相关图片"></image-upload>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="需求资料" prop="requirementFile">
<file-upload @success="handleFileSuccess" @delete="handleFileDelete" v-model="currentNode.requirementFile"
placeholder="需求资料"></file-upload>
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- <div class="dialog-content">
<div class="node-title">{{ currentNode.name }}</div>
<div class="node-info-item" v-for="(val, key) in currentNode.value" :key="key">
<span class="label">{{ key }}</span>
<span class="value">{{ val || '无' }}</span>
</div>
</div> -->
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="handleSubmit">提交修改</el-button>
<el-button type="primary" @click="dialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import { addFileOperationRecord } from '@/api/oa/fileOperationRecord';
import { updateProjectScheduleStep } from "@/api/oa/projectScheduleStep";
import * as echarts from 'echarts';
export default {
name: "Xmind",
props: {
list: {
type: Array,
default: () => []
}
},
data () {
return {
chartInstance: null, // 保存图表实例,用于后续重绘/销毁
dialogVisible: false, // 新增:弹窗显示隐藏开关
currentNode: {}, // 新增:存储当前点击的三级节点完整数据
clickEvent: null, // 新增:存储点击事件句柄,用于销毁解绑
users: [],
supplierList: [],
};
},
watch: {
// 监听列表数据变化,自动更新图表
list: {
deep: true,
handler () {
if (this.chartInstance) {
this.initChart();
}
}
}
},
mounted () {
this.initChart();
// 监听窗口大小变化,自适应重绘
window.addEventListener('resize', this.resizeChart);
},
beforeDestroy () {
window.removeEventListener('resize', this.resizeChart);
// 新增解绑Echarts点击事件防止内存泄漏
if (this.chartInstance && this.clickEvent) {
this.chartInstance.off('click', this.clickEvent);
}
// 销毁图表实例,防止内存泄漏
this.chartInstance?.dispose();
},
methods: {
// 优化:增加防抖处理-窗口自适应,避免频繁触发
resizeChart () {
this.chartInstance?.resize()
},
handleSubmit () {
updateProjectScheduleStep(this.currentNode).then(response => {
this.$modal.msgSuccess("修改成功");
this.dialogVisible = false;
this.handleRefresh();
});
},
handleRefresh () {
this.$emit('refresh')
},
handleFileSuccess (resList, res) {
addFileOperationRecord({
fileId: res.ossId,
fileName: res.name,
type: 1,
projectId: this.currentNode.projectId,
trackId: this.currentNode.trackId,
})
console.log(this.currentNode, this.currentNode.relatedDocs);
updateProjectScheduleStep({
...this.currentNode,
relatedDocs: this.currentNode.relatedDocs,
})
},
handleFileDelete (res) {
addFileOperationRecord({
fileId: res.ossId,
fileName: res.name,
type: 2,
projectId: this.currentNode.projectId,
trackId: this.currentNode.trackId,
})
console.log(this.currentNode, this.currentNode.relatedDocs);
updateProjectScheduleStep({
...this.currentNode,
relatedDocs: this.currentNode.relatedDocs,
})
},
// 核心方法:把扁平数组 转为 ECharts树图需要的嵌套树形结构
transformToTreeData (list) {
if (!list.length) return { name: '暂无项目数据', children: [] };
// 1. 获取项目名称(所有数据是同一个项目,取第一条即可)
const projectName = list[0].projectName || '项目进度树图';
// 2. 构建层级Map去重+归集子节点
const levelMap = new Map();
list.forEach(item => {
const firstLevel = item.firstLevelNode || '未分类一级节点';
const secondLevel = item.secondLevelNode || '未分类二级节点';
// 状态映射0=未开始(蓝色) 2=已完成(绿色) 其他=进行中(橙色),可根据业务调整
const statusText = item.status === 0 ? '待开始' : item.status === 2 ? '✅已完成' : '🔵进行中';
const statusColor = item.status === 0 ? '#409EFF' : item.status === 2 ? '#67C23A' : '#E6A23C';
// 组装节点数据:显示名称+业务信息+样式
const nodeData = {
name: secondLevel,
itemStyle: { color: statusColor },
label: { color: statusColor },
// 自定义业务数据,鼠标悬浮时显示
value: {
...item,
负责人: item.nodeHeader || '无',
状态: statusText,
计划完成: item.planEnd || '无',
说明: item.specification || '无'
}
};
// 归集一级节点和二级节点
if (!levelMap.has(firstLevel)) {
levelMap.set(firstLevel, []);
}
levelMap.get(firstLevel).push(nodeData);
});
// 3. 组装最终的树形结构
const treeChildren = Array.from(levelMap).map(([firstName, children]) => ({
name: firstName,
itemStyle: { color: '#303133' }, // 一级节点统一深灰色
children: children
}));
return {
name: projectName,
itemStyle: { color: '#1890FF' }, // 根节点(项目名)蓝色高亮
children: treeChildren
};
},
// 初始化图表
initChart () {
// 初始化图表实例
if (!this.chartInstance) {
this.chartInstance = echarts.init(this.$refs.chart);
}
// 重要:先解绑已有点击事件,防止多次绑定导致弹窗多次触发
if (this.clickEvent) {
this.chartInstance.off('click', this.clickEvent);
}
// 转换数据格式
const treeData = this.transformToTreeData(this.list);
// 设置图表配置项
const option = {
tooltip: {
trigger: 'item',
formatter: ({ data }) => {
// 鼠标悬浮展示完整业务信息
let tip = `<div style="font-size:14px"><b>${data.name}</b></div>`;
if (data.value) {
// Object.keys(data.value).forEach(key => {
// tip += `<div>${key}${data.value[key]}</div>`;
// });
tip += `<div>负责人:${data.value.nodeHeader || '无'}</div>`;
tip += `<div>规格需求:${data.value.specification || '无'}</div>`;
tip += `<div>任务状态:${data.value.statusText || '无'}</div>`;
tip += `<div>计划完成:${data.value.planEnd || '无'}</div>`;
}
return tip;
}
},
series: [
{
type: 'tree', // 树图核心类型
data: [treeData],
symbol: 'circle', // 节点形状:圆点
symbolSize: 6, // 节点大小
orient: 'LR', // 树图展开方向LR=从左到右(脑图样式),可选 TB(从上到下)
initialTreeDepth: 2, // 默认展开层级2级
roam: true, // 开启鼠标拖拽+滚轮缩放
label: {
show: true,
fontSize: 12,
fontWeight: 500,
position: 'left', // 文字在节点左侧
verticalAlign: 'middle'
},
lineStyle: {
width: 1.2,
curveness: 0.3, // 连接线曲率0=直线0.3=轻微曲线
color: '#ccc'
},
emphasis: {
focus: 'descendant' // 鼠标悬浮时高亮当前节点及子节点
},
expandAndCollapse: true, // 开启节点折叠/展开功能
animationDuration: 300 // 展开折叠动画时长
}
]
};
// 渲染图表
this.chartInstance?.setOption(option, true);
// ========== 核心新增绑定ECharts点击事件只对三级节点生效 ==========
this.clickEvent = (params) => {
console.log(params);
const { data, treeAncestors } = params;
// ✅ 核心判断treeAncestors是当前节点的「所有上级节点数组」
// 根节点(项目名) → 一级节点 → 三级节点 treeAncestors.length = 2 → 精准匹配第三级节点
// 层级对应关系:根节点(0级) → 一级分类(1级) → 业务节点(3级/你要的三级)
if (treeAncestors.length === 4) {
console.log(data);
this.currentNode = { ...data.value }; // 深拷贝当前节点完整数据
this.dialogVisible = true; // 打开弹窗
}
};
// 绑定点击事件
this.chartInstance.on('click', this.clickEvent);
}
}
};
</script>
<style scoped>
.xmind-box {
position: relative;
}
.action-panel {
position: absolute;
top: 10px;
right: 10px;
z-index: 1000;
}
.xmind-container {
background: #fafafa;
border-radius: 8px;
}
/* 新增:弹窗内部样式美化 */
:deep(.dialog-content) {
padding: 10px 0;
}
:deep(.node-title) {
font-size: 16px;
font-weight: bold;
color: #1890FF;
text-align: center;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 15px;
}
:deep(.node-info-item) {
display: flex;
padding: 6px 0;
font-size: 14px;
line-height: 1.6;
}
:deep(.label) {
width: 80px;
color: #666;
font-weight: 500;
}
:deep(.value) {
flex: 1;
color: #333;
}
:deep(.dialog-footer) {
text-align: center;
}
</style>

View File

@@ -0,0 +1,237 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="文件标识" prop="fileId">
<el-input v-model="queryParams.fileId" placeholder="请输入文件标识" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="文件名" prop="fileName">
<el-input v-model="queryParams.fileName" placeholder="请输入文件名" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="操作人" prop="operatorName">
<el-input v-model="queryParams.operatorName" placeholder="请输入操作人姓名" clearable
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="相关项目" prop="projectName">
<project-select v-model="queryParams.projectId" placeholder="请选择相关项目" clearable
@keyup.enter.native="handleQuery" />
</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>
<el-table v-loading="loading" :data="fileOperationRecordList" @selection-change="handleSelectionChange">
<el-table-column label="文件标识" align="center" prop="fileId">
<template slot-scope="scope">
<el-tag type="primary" @click="previewFile(scope.row)">{{ scope.row.fileId }}</el-tag>
</template>
</el-table-column>
<el-table-column label="文件名" align="center" prop="fileName" />
<el-table-column label="操作人姓名" align="center" prop="operatorName" />
<el-table-column label="操作时间" align="center" prop="createTime" />
<el-table-column label="相关项目" align="center" prop="projectName" />
<!-- 相关进度 -->
<el-table-column label="相关进度" align="center" show-overflow-tooltip>
<template slot-scope="scope">
{{ scope.row.tabNode }} / {{ scope.row.firstLevelNode }} / {{ scope.row.secondLevelNode }}
</template>
</el-table-column>
<el-table-column label="操作类型" align="center" prop="type">
<template slot-scope="scope">
{{ scope.row.type === 1 ? '上传' : '删除' }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" show-overflow-tooltip>
<template slot-scope="scope">
<el-button type="text" size="mini" icon="el-icon-view" @click="previewFile(scope.row)">预览</el-button>
<el-button type="text" size="mini" icon="el-icon-download" @click="handleDownload(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" />
</div>
</template>
<script>
import { addFileOperationRecord, delFileOperationRecord, getFileOperationRecord, listFileOperationRecord, updateFileOperationRecord } from "@/api/oa/fileOperationRecord";
import { listByIds } from "@/api/system/oss";
import ProjectSelect from "@/components/fad-service/ProjectSelect/index.vue";
export default {
name: "FileOperationRecord",
components: {
ProjectSelect,
},
data () {
return {
// 按钮loading
buttonLoading: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// OA文件操作记录表格数据
fileOperationRecordList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
fileId: undefined,
fileName: undefined,
operatorName: undefined,
type: undefined,
projectId: undefined,
},
// 表单参数
form: {},
// 表单校验
rules: {
}
};
},
created () {
this.getList();
},
inject: ['$folder'],
methods: {
/** 查询OA文件操作记录列表 */
getList () {
this.loading = true;
listFileOperationRecord(this.queryParams).then(response => {
this.fileOperationRecordList = response.rows;
this.total = response.total;
this.loading = false;
});
},
previewFile (row) {
this.loading = true;
const folder = this.$folder();
listByIds(row.fileId).then(res => {
folder.previewSimple(res.data[0]);
}).finally(() => {
this.loading = false;
});
},
handleDownload (row) {
this.$download.oss(row.fileId)
},
// 取消按钮
cancel () {
this.open = false;
this.reset();
},
// 表单重置
reset () {
this.form = {
recordId: undefined,
fileId: undefined,
fileName: undefined,
operatorName: undefined,
type: undefined,
remark: undefined,
createBy: undefined,
createTime: undefined,
updateBy: undefined,
updateTime: undefined,
delFlag: undefined
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery () {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery () {
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange (selection) {
this.ids = selection.map(item => item.recordId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd () {
this.reset();
this.open = true;
this.title = "添加OA文件操作记录";
},
/** 修改按钮操作 */
handleUpdate (row) {
this.loading = true;
this.reset();
const recordId = row.recordId || this.ids
getFileOperationRecord(recordId).then(response => {
this.loading = false;
this.form = response.data;
this.open = true;
this.title = "修改OA文件操作记录";
});
},
/** 提交按钮 */
submitForm () {
this.$refs["form"].validate(valid => {
if (valid) {
this.buttonLoading = true;
if (this.form.recordId != null) {
updateFileOperationRecord(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
} else {
addFileOperationRecord(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
}
});
},
/** 删除按钮操作 */
handleDelete (row) {
const recordIds = row.recordId || this.ids;
this.$modal.confirm('是否确认删除OA文件操作记录编号为"' + recordIds + '"的数据项?').then(() => {
this.loading = true;
return delFileOperationRecord(recordIds);
}).then(() => {
this.loading = false;
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
this.loading = false;
});
},
/** 导出按钮操作 */
handleExport () {
this.download('oa/fileOperationRecord/export', {
...this.queryParams
}, `fileOperationRecord_${new Date().getTime()}.xlsx`)
}
}
};
</script>

View File

@@ -0,0 +1,383 @@
<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">
<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 />
</el-form-item>
<el-form-item label="贸易类型" prop="tradeType">
<el-select v-model="queryParams.tradeType" placeholder="请选择项目类型" clearable>
<el-option v-for="dict in dict.type.sys_trade_type" :key="dict.value" :label="dict.label"
:value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="项目代号" prop="projectCode">
<el-select v-model="queryParams.projectCode" placeholder="请选择代号类型" style="width: 100%" filterable
@change="handleQuery">
<el-option v-for="dict in dict.type.sys_project_code" :key="dict.value" :label="dict.label"
:value="dict.value">
<span style="float: left">{{ dict.label }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ dict.value }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="项目周期">
<el-date-picker v-model="searchTime" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期"
:default-time="['00:00:00', '23:59:59']">
</el-date-picker>
</el-form-item>
<el-form-item label="负责人">
<el-select filterable allow-add v-model="queryParams.steward" @change="handleStatusChange(scope.row)">
<el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.nickName">
</el-option>
</el-select>
</el-form-item>
<!-- <el-form-item label="优质筛选">
<el-switch v-model="queryParams.prePay" active-text="开" active-value="0.1" inactive-value="0"
@change="selectPrePay" inactive-text="">
</el-switch>
</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>
<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="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除
</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</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-table-column label="代号" prop="projectCode" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.projectCode == null" type="danger"></el-tag>
<el-tag v-else>{{ scope.row.projectCode }}</el-tag>
</template>
</el-table-column>
<el-table-column label="项目名称" prop="projectName" show-overflow-tooltip>
<template slot-scope="scope">
<span v-if="scope.row.prePay > 0"></span>
<span>{{ scope.row.projectName }}</span>
</template>
</el-table-column>
<el-table-column label="项目编号" prop="projectNum"></el-table-column>
<el-table-column label="负责人" align="center">
<template slot-scope="scope">
<el-select filterable allow-add v-model="scope.row.steward" @change="handleStatusChange(scope.row)">
<el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.nickName">
</el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="状态" align="center">
<template slot-scope="scope">
<span v-if="scope.row.isTop" style="color: #ff4d4f;">重点关注</span>
<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="完成状态" align="center" prop="sortNum">
<template slot-scope="scope">
<el-select size="mini" v-model="scope.row.status" placeholder="请选择完成状态"
@change="handleStatusChange(scope.row)">
<el-option label="进行中" :value="1"></el-option>
<el-option label="已完成" :value="2"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="开始时间" align="center" prop="startTime">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d}') }}</span>
</template>
</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-edit" @click="handleDetail(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-check"
v-if="scope.row.schedulePercentage === 100 && scope.row.status !== 2"
@click="handleComplete(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-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" />
</div>
</el-drawer>
<FormDialog v-model="addDialog" :projects="projects" @save="handleSave" />
<el-drawer title="延期记录" :visible.sync="postponeDrawer" direction="btt" size="90%">
<postpone :scheduleId="scheduleDetail.scheduleId" />
</el-drawer>
</div>
</template>
<script>
import { listProject } from "@/api/oa/project";
import { addByProjectId, delProjectSchedule, listProjectSchedule, updateProjectSchedule } 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";
import FormDialog from "./components/FormDialog.vue";
import Postpone from "./components/postpone.vue";
import ProjectScheduleStep from "./components/step.vue";
export default {
name: "Schedule",
dicts: ['sys_project_status', 'sys_trade_type', 'sys_project_type', 'sys_project_code'],
components: {
UserSelect,
ProjectScheduleStep,
FormDialog,
ProjectSelect,
Postpone
},
data () {
return {
loading: false,
detailDrawer: false,
fileShow: false,
addDialog: false,
multiple: true,
form: {},
projects: [],
scheduleList: [],
queryParams: {
pageNum: 1,
pageSize: 10,
startTime: '',
endTime: '',
steward: ''
},
total: 0,
searchTime: [],
showSearch: true,
recentProjects: [],
scheduleDetail: {},
userList: [],
postponeDrawer: false
};
},
mounted () {
this.currentUser = this.$store.state.user
this.getList();
this.getProjectList();
this.getAllUser();
const cache = localStorage.getItem('oa_recent_projects');
if (cache) {
this.recentProjects = JSON.parse(cache);
}
},
methods: {
// 关闭细节窗口
closeDetailShow (done) {
this.getList();
done()
},
getAllUser () {
listUser({ pageNum: 1, pageSize: 999 }).then(res => {
this.userList = res.rows
})
},
handleStatusChange (row) {
updateProjectSchedule(row).then(res => {
this.getList();
this.$modal.msgSuccess("操作成功")
})
},
// 绑定进度
handleAdd () {
this.addDialog = true;
},
selectPrePay () {
this.handleQuery()
},
/** 搜索按钮操作 */
handleQuery () {
this.queryParams.pageNum = 1;
this.getList();
},
getDateStr (date) {
if (!date) {
return ''
}
return this.parseTime(date, '{y}-{m}-{d} {h}:{i}:{s}')
},
getList () {
this.loading = true
console.log(this.queryParams, this.searchTime)
/* 日期搜索条件 */
if (this.searchTime && this.searchTime.length) {
this.queryParams.startTime = this.getDateStr(this.searchTime[0])
this.queryParams.endTime = this.getDateStr(this.searchTime[1])
}
listProjectSchedule(this.queryParams).then(res => {
this.scheduleList = res.rows
this.total = res.total
let cache = JSON.parse(localStorage.getItem('oa_recent_projects') || '[]')
const id2idx = new Map()
cache.forEach((item, idx) => id2idx.set(item.scheduleId, idx))
for (const row of res.rows) {
const hit = id2idx.get(row.scheduleId)
if (hit !== undefined) {
cache[hit] = row
} else {
cache.unshift(row)
}
}
cache = cache.slice(0, 2)
/* 25 回写缓存 + 更新响应式数据 */
localStorage.setItem('oa_recent_projects', JSON.stringify(cache))
this.recentProjects = cache
/* 3. 结束 loading */
this.loading = false
})
},
getProjectList () {
let params = {
pageNum: 1,
pageSize: 999,
}
listProject(params).then(res => {
this.projects = res.rows
})
},
/** 保存处理*/
handleSave (payload) {
addByProjectId(payload).then(response => {
this.getList();
this.$modal.msgSuccess("绑定成功")
})
},
handleDetail (row) {
// 把当前项目放到数组最前面,去重
const list = [row, ...this.recentProjects.filter(p => p.projectId !== row.projectId)];
// 只保留前 2 条
this.recentProjects = list.slice(0, 2);
// 持久化
localStorage.setItem('oa_recent_projects', JSON.stringify(this.recentProjects));
this.getScheduleDetail(row)
},
handlePostpone (row) {
// 打开延期记录弹窗
this.postponeDrawer = true
this.scheduleDetail = row
},
getScheduleDetail (row) {
this.scheduleDetail = row
this.detailDrawer = true
},
/* ========= 左侧主列表删除(支持单删或批量 ids ========= */
handleDelete (row) {
/* 支持row.scheduleId 或 this.ids = '1,2,3' */
const scheduleIds = row.scheduleId || this.ids
this.$modal.confirm(`将会删除进度编号为 "${scheduleIds}" 的数据项, 同时会删除所有的子进度且无法找回 !!! 是否继续? `)
.then(() => {
this.loading = true
return delProjectSchedule(scheduleIds)
})
.then(() => {
/* 刷新左侧列表并提示 */
this.getList()
this.$modal.msgSuccess('删除成功')
})
},
// 多选框选中数据
handleSelectionChange (selection) {
this.ids = selection.map(item => item.scheduleId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 重置按钮操作 */
resetQuery () {
this.searchTime = [];
this.resetForm("queryForm");
this.handleQuery();
},
},
}
</script>
<style scoped>
.uploader {
margin-top: 12px;
}
.file-item {
margin-top: 12px;
}
.file-row {
display: flex;
font-size: small;
color: #414141;
align-items: center;
}
.file-icon {
width: 40px;
text-align: center;
margin-right: 12px;
}
.file-info {
flex: 1;
}
.file-meta {
color: #909399;
font-size: 12px;
margin-top: 2px;
}
.file-actions el-button+el-button {
margin-left: 4px;
}
</style>

View File

@@ -0,0 +1,306 @@
<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-date-picker v-model="queryParams.planEndRange" type="daterange" value-format="yyyy-MM-dd"
placeholder="选择计划结束时间" />
</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>
<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">
<template slot-scope="scope">
<span>{{ scope.row.tabNode }}/ {{ scope.row.firstLevelNode }} / {{ scope.row.secondLevelNode }}</span>
</template>
</el-table-column>
<el-table-column label="计划完成" align="center" prop="planEnd" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.planEnd, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="剩余时间" align="center" prop="endTime" width="180">
<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>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status">
<!-- 0进行中,1待验收,2已完成 -->
<template slot-scope="scope">
<span>{{ scope.row.status === 0 ? '进行中' : (scope.row.status === 1 ? '待验收' : '已完成') }}</span>
</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, listMyPage as listProjectScheduleStep, updateProjectScheduleStep } from "@/api/oa/projectScheduleStep";
import { listUser } from "@/api/system/user";
export default {
name: "ProjectScheduleStep",
data () {
return {
// 按钮loading
buttonLoading: false,
// 用户列表loading
userLoading: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 项目进度步骤跟踪表格数据
projectScheduleStepList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
scheduleId: 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();
},
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;
},
/** 查询用户列表 */
getListUser () {
this.userLoading = true;
listUser({ pageSize: 999 }).then(response => {
this.userList = response.rows;
this.userLoading = false;
});
},
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;
});
},
// 取消按钮
cancel () {
this.open = false;
this.reset();
},
// 表单重置
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");
},
/** 搜索按钮操作 */
handleQuery () {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery () {
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange (selection) {
this.ids = selection.map(item => item.trackId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
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;
});
}
}
});
},
/** 删除按钮操作 */
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`)
}
}
};
</script>

View File

@@ -0,0 +1,301 @@
<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="申请人姓名">
<el-input v-model="queryParams.applyUserName" placeholder="请输入申请人姓名" clearable />
</el-form-item>
<el-form-item label="相关项目">
<project-select v-model="queryParams.projectId" placeholder="请选择相关项目" clearable />
</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>
<el-table v-loading="loading" :data="projectScheduleDelayList">
<el-table-column label="相关项目" align="center" prop="projectName" show-overflow-tooltip />
<el-table-column label="进度步骤" align="center" prop="trackId" show-overflow-tooltip>
<template slot-scope="scope">
<span>{{ scope.row.tabNode }} / {{ scope.row.firstLevelNode }} / {{ scope.row.secondLevelNode }}</span>
</template>
</el-table-column>
<el-table-column label="申请人姓名" align="center" prop="applyUserName" />
<el-table-column label="原时间" align="center" prop="originalEndTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.originalEndTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="申请至" align="center" prop="expectEndTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.expectEndTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="延期原因" align="center" prop="applyReason" />
<el-table-column label="审批人" align="center" prop="approveUserName" />
<el-table-column label="审批时间" align="center" prop="approveTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.approveTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="审批结果" align="center" prop="approveResult">
<template slot-scope="scope">
<span v-if="scope.row.approveResult == 1">已同意</span>
<span v-else-if="scope.row.approveResult == 2">已拒绝</span>
<span v-else>待审批</span>
</template>
</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-check" @click="handleAgree(scope.row)"
v-if="scope.row.approveResult == 0">同意延期</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-item label="关联的进度步骤跟踪记录主键" prop="trackId">
<el-input v-model="form.trackId" placeholder="请输入关联的进度步骤跟踪记录主键" />
</el-form-item>
<el-form-item label="申请人姓名" prop="applyUserName">
<el-input v-model="form.applyUserName" placeholder="请输入申请人姓名" />
</el-form-item>
<el-form-item label="申请提交时间" prop="applyTime">
<el-date-picker clearable v-model="form.applyTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择申请提交时间">
</el-date-picker>
</el-form-item>
<el-form-item label="延期申请原因" prop="applyReason">
<el-input v-model="form.applyReason" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="原计划结束时间" prop="originalEndTime">
<el-date-picker clearable v-model="form.originalEndTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择原计划结束时间">
</el-date-picker>
</el-form-item>
<el-form-item label="申请延期后的预计结束时间" prop="expectEndTime">
<el-date-picker clearable v-model="form.expectEndTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择申请延期后的预计结束时间">
</el-date-picker>
</el-form-item>
<el-form-item label="审批人姓名" prop="approveUserName">
<el-input v-model="form.approveUserName" placeholder="请输入审批人姓名" />
</el-form-item>
<el-form-item label="审批完成时间" prop="approveTime">
<el-date-picker clearable v-model="form.approveTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择审批完成时间">
</el-date-picker>
</el-form-item>
<el-form-item label="审批结果0-待审批 1-审批通过 2-审批驳回" prop="approveResult">
<el-input v-model="form.approveResult" placeholder="请输入审批结果0-待审批 1-审批通过 2-审批驳回" />
</el-form-item>
<el-form-item label="审批备注/驳回原因" prop="approveRemark">
<el-input v-model="form.approveRemark" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="备注(如需)" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</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 { addProjectScheduleDelay, agreeProjectScheduleDelay, delProjectScheduleDelay, getProjectScheduleDelay, listProjectScheduleDelay, updateProjectScheduleDelay } from "@/api/oa/projectScheduleDelay";
import ProjectSelect from "@/components/fad-service/ProjectSelect";
export default {
name: "ProjectScheduleDelay",
components: {
ProjectSelect
},
data () {
return {
// 按钮loading
buttonLoading: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 项目进度步骤延期记录表格数据
projectScheduleDelayList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
applyUserName: undefined,
projectId: undefined,
pageNum: 1,
pageSize: 10,
},
// 表单参数
form: {},
// 表单校验
rules: {
}
};
},
created () {
this.getList();
},
methods: {
/** 查询项目进度步骤延期记录列表 */
getList () {
this.loading = true;
listProjectScheduleDelay(this.queryParams).then(response => {
this.projectScheduleDelayList = response.rows;
this.total = response.total;
this.loading = false;
});
},
handleAgree (row) {
this.$modal.confirm("确认同意延期吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
agreeProjectScheduleDelay({
delayId: row.delayId,
approveResult: 1
}).then(response => {
this.$modal.success("同意成功");
this.$message({
message: "已同意延期",
type: "success"
});
this.getList();
});
});
},
// 取消按钮
cancel () {
this.open = false;
this.reset();
},
// 表单重置
reset () {
this.form = {
delayId: undefined,
trackId: undefined,
applyUserName: undefined,
applyTime: undefined,
applyReason: undefined,
originalEndTime: undefined,
expectEndTime: undefined,
approveUserName: undefined,
approveTime: undefined,
approveResult: undefined,
approveRemark: undefined,
delayStatus: undefined,
createBy: undefined,
createTime: undefined,
updateBy: undefined,
updateTime: undefined,
delFlag: undefined,
remark: undefined
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery () {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery () {
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange (selection) {
this.ids = selection.map(item => item.delayId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd () {
this.reset();
this.open = true;
this.title = "添加项目进度步骤延期记录";
},
/** 修改按钮操作 */
handleUpdate (row) {
this.loading = true;
this.reset();
const delayId = row.delayId || this.ids
getProjectScheduleDelay(delayId).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.delayId != null) {
updateProjectScheduleDelay(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
} else {
addProjectScheduleDelay(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
}
});
},
/** 删除按钮操作 */
handleDelete (row) {
const delayIds = row.delayId || this.ids;
this.$modal.confirm('是否确认删除项目进度步骤延期记录编号为"' + delayIds + '"的数据项?').then(() => {
this.loading = true;
return delProjectScheduleDelay(delayIds);
}).then(() => {
this.loading = false;
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
this.loading = false;
});
},
/** 导出按钮操作 */
handleExport () {
this.download('oa/projectScheduleDelay/export', {
...this.queryParams
}, `projectScheduleDelay_${new Date().getTime()}.xlsx`)
}
}
};
</script>

View File

@@ -0,0 +1,342 @@
<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-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-date-picker v-model="queryParams.planEndRange" type="daterange" value-format="yyyy-MM-dd"
placeholder="选择计划结束时间" />
</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>
<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">
<template slot-scope="scope">
<span>{{ scope.row.tabNode }}/ {{ scope.row.firstLevelNode }} / {{ scope.row.secondLevelNode }}</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">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.planEnd, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="剩余时间" align="center" prop="endTime" width="180">
<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>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status">
<!-- 0进行中,1待验收,2已完成 -->
<template slot-scope="scope">
<span>{{ scope.row.status === 0 ? '进行中' : (scope.row.status === 1 ? '待验收' : '已完成') }}</span>
</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";
export default {
name: "ProjectScheduleStep",
data () {
return {
// 按钮loading
buttonLoading: false,
// 用户列表loading
userLoading: false,
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 项目进度步骤跟踪表格数据
projectScheduleStepList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
scheduleId: 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();
},
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;
},
/** 查询用户列表 */
getListUser () {
this.userLoading = true;
listUser({ pageSize: 999 }).then(response => {
this.userList = response.rows;
this.userLoading = false;
});
},
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;
});
},
// 取消按钮
cancel () {
this.open = false;
this.reset();
},
// 表单重置
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");
},
/** 搜索按钮操作 */
handleQuery () {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery () {
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange (selection) {
this.ids = selection.map(item => item.trackId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
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;
});
}
}
});
},
/** 删除按钮操作 */
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`)
}
}
};
</script>