整合前端
This commit is contained in:
207
ruoyi-ui/src/views/oa/project/components/AddProject.vue
Normal file
207
ruoyi-ui/src/views/oa/project/components/AddProject.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<el-form ref="form" :model="form" label-width="110px">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="项目名称" prop="projectName">
|
||||
<el-input v-model="form.projectName" placeholder="请输入项目名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="项目编号" prop="projectNum">
|
||||
<el-input v-model="form.projectNum" placeholder="请输入项目编号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="负责人" prop="functionary">
|
||||
<el-input v-model="form.functionary" placeholder="请输入项目负责人" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="项目地址" prop="address">
|
||||
<el-input v-model="form.address" placeholder="请输入项目地址" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="项目类型" prop="projectType">
|
||||
<el-select v-model="form.projectType" placeholder="请选择项目类型" style="width: 100%">
|
||||
<el-option v-for="dict in dict.type.sys_project_type" :key="dict.value" :label="dict.label"
|
||||
:value="dict.value"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="贸易类型" prop="tradeType">
|
||||
<el-select v-model="form.tradeType" placeholder="请选择贸易类型">
|
||||
<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-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="代号类型" prop="projectCode">
|
||||
<el-select v-model="form.projectCode" placeholder="请选择代号类型" style="width: 100%" filterable
|
||||
:filter-method="filterCode">
|
||||
<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-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="优先级" prop="projectGrade">
|
||||
<el-radio-group v-model="form.projectGrade">
|
||||
<el-radio v-for="dict in dict.type.sys_sort_grade" :key="dict.value" :label="dict.value">{{ dict.label
|
||||
}}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="项目总金额" prop="funds">
|
||||
<el-input v-model="form.funds" placeholder="请输入项目总金额" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="交货期" prop="address">
|
||||
<el-input v-model="form.delivery" placeholder="请输入交货期" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="签约公司" prop="signingCompany">
|
||||
<el-select v-model="form.signingCompany" placeholder="请选择签约公司" style="width: 100%" clearable>
|
||||
<el-option
|
||||
v-for="dict in dict.type.signing_company.map(item => ({ value: parseInt(item.value), label: item.label }))"
|
||||
:key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="项目周期">
|
||||
<el-date-picker v-model="periodItem" value-format="yyyy-MM-dd HH:mm:ss" type="daterange" range-separator="至"
|
||||
start-placeholder="开始日期" end-placeholder="结束日期" @blur="getTimeBlur">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="质保期" prop="address">
|
||||
<el-input v-model="form.guarantee" placeholder="请输入质保期" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="预付款" prop="prePay">
|
||||
<el-input v-model="form.prePay" placeholder="请输入预付款" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
|
||||
<el-col :span="24">
|
||||
<el-form-item label="项目介绍" prop="introduction">
|
||||
<el-input v-model="form.introduction" type="textarea" placeholder="请输入内容" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="附件" prop="accessory">
|
||||
<file-upload v-model="form.accessory" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-button @click="submitForm">创建项目</el-button>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AddProject',
|
||||
dicts: ['sys_trade_type', 'sys_project_code', 'sys_sort_grade', 'sys_sort_grade', 'signing_company'],
|
||||
data () {
|
||||
return {
|
||||
form: {
|
||||
projectId: undefined,
|
||||
projectName: undefined,
|
||||
projectNum: undefined,
|
||||
projectType: undefined,
|
||||
address: undefined,
|
||||
functionary: undefined,
|
||||
beginTime: undefined,
|
||||
finishTime: undefined,
|
||||
delivery: undefined,
|
||||
guarantee: undefined,
|
||||
introduction: undefined,
|
||||
projectGrade: undefined,
|
||||
projectStatus: undefined,
|
||||
contractId: undefined,
|
||||
invoiceName: undefined,
|
||||
invoiceNumber: undefined,
|
||||
invoiceAddress: undefined,
|
||||
invoiceBank: undefined,
|
||||
accessory: undefined,
|
||||
remark: undefined,
|
||||
createBy: undefined,
|
||||
createTime: undefined,
|
||||
updateBy: undefined,
|
||||
updateTime: undefined,
|
||||
tradeType: undefined,
|
||||
prePay: undefined,
|
||||
projectCode: undefined,
|
||||
closureFiles: undefined,
|
||||
signingCompany: undefined,
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submitForm () {
|
||||
this.$emit('submit', this.form)
|
||||
},
|
||||
/** 输入时同时按 label / value 本地过滤 */
|
||||
filterCode (query) {
|
||||
query = (query || '').toLowerCase();
|
||||
this.dict.type.sys_project_code = this.dictBackup.filter(item => {
|
||||
return item.label.toLowerCase().indexOf(query) > -1 || item.value.toLowerCase().indexOf(query) > -1;
|
||||
});
|
||||
},
|
||||
reset() {
|
||||
this.form = {
|
||||
projectId: undefined,
|
||||
projectName: undefined,
|
||||
projectNum: undefined,
|
||||
projectType: undefined,
|
||||
address: undefined,
|
||||
functionary: undefined,
|
||||
beginTime: undefined,
|
||||
finishTime: undefined,
|
||||
delivery: undefined,
|
||||
guarantee: undefined,
|
||||
introduction: undefined,
|
||||
projectGrade: undefined,
|
||||
projectStatus: undefined,
|
||||
contractId: undefined,
|
||||
invoiceName: undefined,
|
||||
invoiceNumber: undefined,
|
||||
invoiceAddress: undefined,
|
||||
invoiceBank: undefined,
|
||||
accessory: undefined,
|
||||
remark: undefined,
|
||||
createBy: undefined,
|
||||
createTime: undefined,
|
||||
updateBy: undefined,
|
||||
updateTime: undefined,
|
||||
tradeType: undefined,
|
||||
prePay: undefined,
|
||||
projectCode: undefined,
|
||||
closureFiles: undefined,
|
||||
signingCompany: undefined,
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
90
ruoyi-ui/src/views/oa/project/components/PostPone.vue
Normal file
90
ruoyi-ui/src/views/oa/project/components/PostPone.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div>
|
||||
<ProjectInfo :info="projectInfo" />
|
||||
|
||||
<el-form inline style="margin-top: 10px;">
|
||||
<el-form-item label="项目延期日期" prop="postPoneDate">
|
||||
<el-date-picker v-model="postPoneDate" type="datetime" value-format="yyyy-MM-dd HH:mm:ss"
|
||||
placeholder="选择项目延期日期" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="责任人签名" prop="signing">
|
||||
<el-input v-model="signing" :placeholder="'请您确认签名: ' + nickname" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handlePostPone">确认延期</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getProject, postPoneProject } from "@/api/oa/project";
|
||||
|
||||
import ProjectInfo from "@/components/fad-service/ProjectInfo/index.vue";
|
||||
|
||||
export default {
|
||||
name: "PostPone",
|
||||
components: { ProjectInfo },
|
||||
props: {
|
||||
projectId: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
postPoneDate: "",
|
||||
projectInfo: {},
|
||||
nickname: this.$store.getters.nickName,
|
||||
signing: "",
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
projectId: {
|
||||
handler (newVal) {
|
||||
if (newVal) {
|
||||
getProject(newVal).then((response) => {
|
||||
this.projectInfo = response.data;
|
||||
});
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handlePostPone () {
|
||||
if (!this.postPoneDate) {
|
||||
this.$message.error("请选择项目延期日期");
|
||||
return;
|
||||
}
|
||||
if (!this.signing) {
|
||||
this.$message.error("请输入责任人签名");
|
||||
return;
|
||||
}
|
||||
if (this.signing !== this.nickname) {
|
||||
this.$message.error("签名错误");
|
||||
return;
|
||||
}
|
||||
this.$confirm("确认延期项目吗?", "确认提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
// 确认延期项目
|
||||
postPoneProject({
|
||||
projectId: this.projectId,
|
||||
tempTime: this.postPoneDate,
|
||||
}).then((response) => {
|
||||
this.$message({
|
||||
message: "项目已延期",
|
||||
type: "success"
|
||||
});
|
||||
this.$emit('finish');
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,76 @@
|
||||
<!-- src/components/CompareChart.vue -->
|
||||
<template>
|
||||
<div ref="chart" class="chart" style="width: 100%; height: 300px"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'CompareChart',
|
||||
props: {
|
||||
xAxis: {type: Array, required: true},
|
||||
data: {type: Array, required: true}
|
||||
},
|
||||
watch: {
|
||||
// 只要 props 变了就调用
|
||||
xAxis: 'updateChart',
|
||||
data: 'updateChart'
|
||||
},
|
||||
computed: {
|
||||
chartOptions() {
|
||||
return {
|
||||
tooltip: {trigger: 'axis'},
|
||||
xAxis: {type: 'category', data: this.xAxis},
|
||||
yAxis: {type: 'value'},
|
||||
series: [{
|
||||
type: 'bar',
|
||||
data: this.data,
|
||||
barMaxWidth: 30
|
||||
}]
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.chart = echarts.init(this.$refs.chart)
|
||||
this.chart.setOption(this.chartOptions)
|
||||
window.addEventListener('resize', this.resize)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
resize() {
|
||||
this.chart && this.chart.resize()
|
||||
},
|
||||
updateChart() {
|
||||
if (!this.chart) return
|
||||
this.chart.setOption({
|
||||
tooltip: {trigger: 'axis'},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: this.xAxis
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [{
|
||||
type: 'bar',
|
||||
data: this.data,
|
||||
smooth: true
|
||||
}]
|
||||
})
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.resize)
|
||||
this.chart && this.chart.dispose()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,372 @@
|
||||
<template>
|
||||
<div class="analysis-container">
|
||||
<!-- 图表区域 -->
|
||||
<div class="chart-container" v-loading="loading">
|
||||
<div id="chart" ref="chart" class="chart"></div>
|
||||
</div>
|
||||
|
||||
<!-- 详情表格对话框 -->
|
||||
<el-dialog
|
||||
title="项目详情"
|
||||
:visible.sync="dialogVisible"
|
||||
width="80%"
|
||||
:before-close="handleDialogClose">
|
||||
<el-table
|
||||
:data="selectedData ? selectedData.items : []"
|
||||
border
|
||||
height="400px"
|
||||
style="width: 100%">
|
||||
<el-table-column prop="projectName" label="项目名称" width="300"></el-table-column>
|
||||
<el-table-column prop="projectNum" label="项目编号"></el-table-column>
|
||||
<el-table-column
|
||||
prop="originalFunds"
|
||||
label="合同额"
|
||||
:formatter="formatMoney('originalFunds')">
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="totalIncomeCny"
|
||||
label="营收额度"
|
||||
:formatter="formatMoney('totalIncomeCny')">
|
||||
</el-table-column>
|
||||
<el-table-column prop="beginTime" label="开始时间"></el-table-column>
|
||||
<el-table-column prop="projectStatus" label="项目状态" :formatter="formatStatus"></el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listCustomer } from '@/api/oa/customer';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: 'ProjectAnalysis',
|
||||
props: {
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
chartType: {
|
||||
type: String,
|
||||
default: 'pie'
|
||||
},
|
||||
summaryField: {
|
||||
type: String,
|
||||
default: 'count'
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
customerList: [],
|
||||
chart: null, // echarts实例
|
||||
selectedData: null, // 选中的数据
|
||||
chartData: {
|
||||
categories: [],
|
||||
values: []
|
||||
},
|
||||
dialogVisible: false // 控制对话框显示
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 获取客户ID到客户名称的映射
|
||||
customerMap() {
|
||||
return this.customerList.reduce((map, customer) => {
|
||||
map[customer.customerId] = customer.name;
|
||||
return map;
|
||||
}, {});
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
list: {
|
||||
handler(newVal) {
|
||||
if (newVal.length > 0) {
|
||||
this.processChartData();
|
||||
this.renderChart();
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
},
|
||||
// 监听props变化
|
||||
chartType: {
|
||||
handler() {
|
||||
this.renderChart();
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
summaryField: {
|
||||
handler() {
|
||||
this.loading = true;
|
||||
this.processChartData();
|
||||
this.renderChart();
|
||||
this.loading = false;
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getCustomerList();
|
||||
this.initChart();
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.chart) {
|
||||
this.chart.dispose();
|
||||
}
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
methods: {
|
||||
getCustomerList() {
|
||||
listCustomer({ pageNum: 1, pageSize: 999 }).then(res => {
|
||||
this.customerList = res.rows;
|
||||
this.processChartData();
|
||||
this.renderChart();
|
||||
});
|
||||
},
|
||||
|
||||
initChart() {
|
||||
this.chart = echarts.init(this.$refs.chart);
|
||||
// 监听窗口大小变化,重绘图表
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
|
||||
handleResize() {
|
||||
if (this.chart) {
|
||||
this.chart.resize();
|
||||
}
|
||||
},
|
||||
|
||||
// 处理图表数据:按客户分组统计
|
||||
processChartData() {
|
||||
// 按客户分组(没有客户ID的归为"未分配")
|
||||
const groupedData = this.list.reduce((groups, item) => {
|
||||
const customerId = item.customerId || 'unassigned';
|
||||
const customerName = this.customerMap[customerId] || '未分配客户';
|
||||
|
||||
if (!groups[customerId]) {
|
||||
groups[customerId] = {
|
||||
name: customerName,
|
||||
count: 0,
|
||||
profitLoss: 0,
|
||||
totalIncomeCny: 0,
|
||||
items: []
|
||||
};
|
||||
}
|
||||
|
||||
groups[customerId].count += 1;
|
||||
groups[customerId].totalIncomeCny += Number(item.totalIncomeCny || 0);
|
||||
groups[customerId].profitLoss += Number(item.profitLoss || 0);
|
||||
groups[customerId].items.push(item);
|
||||
|
||||
return groups;
|
||||
}, {});
|
||||
|
||||
// 转换为数组并排序
|
||||
const dataArray = Object.values(groupedData).sort((a, b) => {
|
||||
return b[this.summaryField] - a[this.summaryField];
|
||||
});
|
||||
|
||||
console.log('processChartData',this.list , groupedData, dataArray)
|
||||
|
||||
// 更新图表数据
|
||||
this.chartData.categories = dataArray.map(item => item.name);
|
||||
this.chartData.values = dataArray.map(item => ({
|
||||
name: item.name,
|
||||
value: item[this.summaryField],
|
||||
items: item.items
|
||||
}));
|
||||
},
|
||||
|
||||
// 渲染图表
|
||||
renderChart() {
|
||||
if (!this.chart) return;
|
||||
|
||||
const { categories, values } = this.chartData;
|
||||
let option = {};
|
||||
|
||||
if (this.chartType === 'pie') {
|
||||
// 饼图配置
|
||||
option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: params => {
|
||||
const value = params.value;
|
||||
const total = values.reduce((sum, item) => sum + item.value, 0);
|
||||
const percent = ((value / total) * 100).toFixed(2);
|
||||
|
||||
if (this.summaryField === 'count') {
|
||||
return `${params.name}: ${value} 个 (${percent}%)`;
|
||||
} else {
|
||||
return `${params.name}: ${this.formatNumber(value)} 元 (${percent}%)`;
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 10,
|
||||
data: categories
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: this.getFieldLabel(),
|
||||
type: 'pie',
|
||||
radius: '70%',
|
||||
center: ['50%', '60%'],
|
||||
data: values,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
} else {
|
||||
// 柱状图配置
|
||||
option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
formatter: params => {
|
||||
const value = params[0].value;
|
||||
if (this.summaryField === 'count') {
|
||||
return `${params[0].name}: ${value} 个`;
|
||||
} else {
|
||||
return `${params[0].name}: ${this.formatNumber(value)} 元`;
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: categories,
|
||||
axisTick: {
|
||||
alignWithLabel: true
|
||||
}
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: value => {
|
||||
if (this.summaryField === 'count') {
|
||||
return value;
|
||||
} else {
|
||||
return this.formatNumber(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: this.getFieldLabel(),
|
||||
type: 'bar',
|
||||
barWidth: '60%',
|
||||
data: values
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
this.chart.setOption(option);
|
||||
|
||||
// 绑定点击事件
|
||||
this.chart.off('click');
|
||||
this.chart.on('click', params => {
|
||||
this.selectedData = values.find(item => item.name === params.name);
|
||||
this.dialogVisible = true; // 显示对话框
|
||||
});
|
||||
},
|
||||
|
||||
// 处理图表类型变化
|
||||
handleChartTypeChange() {
|
||||
this.renderChart();
|
||||
},
|
||||
|
||||
// 处理汇总字段变化
|
||||
handleSummaryFieldChange() {
|
||||
this.processChartData();
|
||||
this.renderChart();
|
||||
},
|
||||
|
||||
// 清除选择
|
||||
clearSelection() {
|
||||
this.selectedData = null;
|
||||
this.dialogVisible = false; // 关闭对话框
|
||||
},
|
||||
|
||||
// 对话框关闭时的处理
|
||||
handleDialogClose() {
|
||||
this.selectedData = null;
|
||||
this.dialogVisible = false;
|
||||
},
|
||||
|
||||
// 获取字段标签
|
||||
getFieldLabel() {
|
||||
const labels = {
|
||||
'count': '项目数量',
|
||||
'originalFunds': '合同额 (元)',
|
||||
'totalIncomeCny': '营收额度 (元)'
|
||||
};
|
||||
return labels[this.summaryField];
|
||||
},
|
||||
|
||||
// 格式化数字(千分位)
|
||||
formatNumber(num) {
|
||||
return num.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
},
|
||||
|
||||
// 格式化金额显示
|
||||
formatMoney(field) {
|
||||
return (row) => {
|
||||
const value = Number(row[field] || 0);
|
||||
return this.formatNumber(value);
|
||||
};
|
||||
},
|
||||
|
||||
// 格式化项目状态
|
||||
formatStatus(row) {
|
||||
const statusMap = {
|
||||
'1': '进行中',
|
||||
'0': '未开始',
|
||||
'2': '已完成',
|
||||
'3': '已终止'
|
||||
};
|
||||
return statusMap[row.projectStatus] || '未知';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.analysis-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<el-table
|
||||
:data="projects"
|
||||
stripe
|
||||
style="width: 100%;"
|
||||
:row-class-name="rowClassName"
|
||||
>
|
||||
<el-table-column prop="name" label="项目名称" />
|
||||
<el-table-column prop="amount" label="合同金额" />
|
||||
<el-table-column prop="due" label="到期时间" />
|
||||
<el-table-column prop="status" label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="tagType(row.status)">
|
||||
{{ row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="remainTime" label="剩余时间">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.remainTime > 0">剩余{{ row.remainTime }}天</span>
|
||||
<span v-else-if="row.remainTime === 0">今日过期</span>
|
||||
<span v-else>过期{{ Math.abs(row.remainTime) }}天</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-view"
|
||||
@click="toProject(scope.row)"
|
||||
>查看详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ExpiringProjectsTable',
|
||||
props: {
|
||||
projects: { type: Array, required: true }
|
||||
},
|
||||
methods: {
|
||||
tagType(status) {
|
||||
if (status === '已逾期') return 'danger'
|
||||
if (status === '即将到期') return 'warning'
|
||||
if (status === '进行中') return 'success'
|
||||
return 'info'
|
||||
},
|
||||
rowClassName({ row }) {
|
||||
return row.status === '已逾期' ? 'expired-row' : ''
|
||||
},
|
||||
toProject(row){
|
||||
this.$router.push({path:'/project/project',query:{projectName:row.name}})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.expired-row {
|
||||
background-color: #fff1f0; /* 浅红底突出过期行 */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<!-- src/components/StatsCard.vue -->
|
||||
<template>
|
||||
<el-card :body-style="{ padding: '20px',height: '130px' }">
|
||||
<div class="stats">
|
||||
<div class="value">{{ value }}</div>
|
||||
<div class="title">{{ title }}</div>
|
||||
<div class="rate" :class="{ positive: rate >= 0, negative: rate < 0 }" v-if="rate!==12976">
|
||||
<i :class="rate >= 0 ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i>
|
||||
{{ Math.abs(rate) }}%
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'StatsCard',
|
||||
props: {
|
||||
title: { type: String, required: true },
|
||||
value: { type: [String, Number], required: true },
|
||||
rate: { type: Number, required: true }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stats .value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.stats .title {
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.stats .rate {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.stats .positive {
|
||||
color: #67C23A;
|
||||
}
|
||||
.stats .negative {
|
||||
color: #F56C6C;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div ref="chart" class="chart" style="width: 100%; height: 300px"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
export default {
|
||||
name: 'StatusChart',
|
||||
data () {
|
||||
return {
|
||||
chart: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 从vuex获取项目列表
|
||||
projects () {
|
||||
return this.$store.getters.projectList || [];
|
||||
},
|
||||
// 按规则统计各状态的项目数量
|
||||
chartData () {
|
||||
// 初始化各状态计数
|
||||
const statusCount = {
|
||||
'已完成': 0,
|
||||
'已逾期': 0,
|
||||
'即将到期': 0,
|
||||
'进行中': 0
|
||||
};
|
||||
|
||||
// 获取当前日期(仅保留年月日,消除时分秒影响)
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
this.projects.forEach(project => {
|
||||
// 优先判断已完成状态
|
||||
if (project.projectStatus == 1) {
|
||||
statusCount['已完成']++;
|
||||
} else {
|
||||
// 处理未完成项目的时间判断
|
||||
if (project.finishTime) {
|
||||
try {
|
||||
// 将finishTime转换为日期对象(兼容YYYY-MM-dd hh:mm:ss格式)
|
||||
const finishDate = new Date(project.finishTime);
|
||||
finishDate.setHours(0, 0, 0, 0); // 统一时间维度
|
||||
|
||||
// 计算间隔天数(毫秒转天)
|
||||
const timeDiff = finishDate - today;
|
||||
const dayDiff = timeDiff / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (dayDiff < 0) {
|
||||
// 已逾期:截止日期已过
|
||||
statusCount['已逾期']++;
|
||||
} else if (dayDiff < 10) {
|
||||
// 即将到期:截止日期在3天内
|
||||
statusCount['即将到期']++;
|
||||
} else {
|
||||
// 其他情况:进行中
|
||||
statusCount['进行中']++;
|
||||
}
|
||||
} catch (e) {
|
||||
// 日期格式错误时默认归为进行中
|
||||
console.error('日期格式错误:', project.finishTime, e);
|
||||
statusCount['进行中']++;
|
||||
}
|
||||
} else {
|
||||
// 无截止日期的未完成项目归为进行中
|
||||
statusCount['进行中']++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 转换为ECharts所需的数据格式(过滤数量为0的状态)
|
||||
return Object.entries(statusCount)
|
||||
.filter(([_, value]) => value > 0)
|
||||
.map(([name, value]) => {
|
||||
// 状态颜色映射
|
||||
const colorMap = {
|
||||
'已完成': '#1890ff', // 蓝色
|
||||
'已逾期': '#ff4d4f', // 红色
|
||||
'即将到期': '#faad14', // 黄色
|
||||
'进行中': '#52c41a' // 绿色
|
||||
};
|
||||
return {
|
||||
name,
|
||||
value,
|
||||
itemStyle: {
|
||||
color: colorMap[name]
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.chart = echarts.init(this.$refs.chart);
|
||||
this.updateChart();
|
||||
window.addEventListener('resize', this.resize);
|
||||
},
|
||||
watch: {
|
||||
// 监听项目列表变化,更新图表
|
||||
projects: {
|
||||
handler: 'updateChart',
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resize () {
|
||||
this.chart && this.chart.resize();
|
||||
},
|
||||
updateChart () {
|
||||
if (!this.chart) return;
|
||||
|
||||
this.chart.setOption({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c}个 ({d}%)' // 显示数量和百分比
|
||||
},
|
||||
legend: {
|
||||
bottom: 10,
|
||||
textStyle: { fontSize: 12 }
|
||||
},
|
||||
series: [{
|
||||
name: '项目状态',
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: { show: false },
|
||||
emphasis: {
|
||||
label: { show: true, fontSize: '16', fontWeight: 'bold' }
|
||||
},
|
||||
data: this.chartData // 使用计算后的图表数据
|
||||
}]
|
||||
});
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('resize', this.resize);
|
||||
this.chart && this.chart.dispose();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<!-- src/components/TrendChart.vue -->
|
||||
<template>
|
||||
<div ref="chart" class="chart" style="width: 100%; height: 300px"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
export default {
|
||||
name: 'TrendChart',
|
||||
props: {
|
||||
xAxis: { type: Array, required: true },
|
||||
data: { type: Array, required: true }
|
||||
},
|
||||
computed: {
|
||||
chartOptions() {
|
||||
return {
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: { type: 'category', data: this.xAxis },
|
||||
yAxis: { type: 'value' },
|
||||
series: [{
|
||||
data: this.data,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
lineStyle: { width: 2 }
|
||||
}]
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.chart = echarts.init(this.$refs.chart)
|
||||
this.chart.setOption(this.chartOptions)
|
||||
window.addEventListener('resize', this.resize)
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
// 只要 props 变了就调用
|
||||
xAxis: 'updateChart',
|
||||
data: 'updateChart'
|
||||
},
|
||||
methods: {
|
||||
resize() {
|
||||
this.chart && this.chart.resize()
|
||||
},
|
||||
updateChart() {
|
||||
if (!this.chart) return
|
||||
this.chart.setOption({
|
||||
tooltip: {trigger: 'axis'},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: this.xAxis
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [{
|
||||
type: 'line',
|
||||
data: this.data,
|
||||
smooth: true
|
||||
}]
|
||||
})
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.resize)
|
||||
this.chart && this.chart.dispose()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
257
ruoyi-ui/src/views/oa/project/dashboard/index.vue
Normal file
257
ruoyi-ui/src/views/oa/project/dashboard/index.vue
Normal file
@@ -0,0 +1,257 @@
|
||||
<!-- src/views/Dashboard.vue -->
|
||||
<template>
|
||||
<div class="dashboard" style="padding: 20px; background: #fff;">
|
||||
<!-- header -->
|
||||
<el-row type="flex" justify="space-between" align="middle" class="mb-20" :gutter="20"
|
||||
style="background:#fff; padding:20px; border-radius:4px;">
|
||||
<el-col>
|
||||
<h2 style="margin:0">项目中心数据看板</h2>
|
||||
<small>最后更新时间:{{ lastUpdate }}</small>
|
||||
</el-col>
|
||||
<el-col>
|
||||
<el-date-picker v-model="startDate" type="date" placeholder="开始日期" size="small" format="yyyy/MM/dd"
|
||||
value-format="yyyy/MM/dd" />
|
||||
<span style="margin: 0 8px;">至</span>
|
||||
<el-date-picker v-model="endDate" type="date" placeholder="结束日期" size="small" format="yyyy/MM/dd"
|
||||
value-format="yyyy/MM/dd" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 四个统计卡片 -->
|
||||
<el-row :gutter="20" style="margin-bottom:20px">
|
||||
<el-col :span="6" v-for="stat in stats" :key="stat.title">
|
||||
<StatsCard :title="stat.title" :value="stat.value" :rate="stat.rate" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 三张图 -->
|
||||
<el-row :gutter="20" class="mt-20" style="margin-bottom:20px">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover" class="chart-card">
|
||||
<div slot="header">合同金额趋势</div>
|
||||
<TrendChart :xAxis="months" :data="trendData" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover" class="chart-card">
|
||||
<div slot="header">项目状态分布</div>
|
||||
<StatusChart :data="statusData" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover" class="chart-card">
|
||||
<div slot="header">月度合同数对比</div>
|
||||
<CompareChart :xAxis="months" :data="compareData" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover" class="chart-card">
|
||||
<div slot="header">
|
||||
客户分布
|
||||
<el-select v-model="customerField" placeholder="选择客户字段" size="small">
|
||||
<el-option label="项目数量" value="count" />
|
||||
<el-option label="合同额" value="totalIncomeCny" />
|
||||
<el-option label="营收额度" value="profitLoss" />
|
||||
</el-select>
|
||||
<el-select v-model="customerChartType" placeholder="选择图表类型" size="small">
|
||||
<el-option label="饼图" value="pie" />
|
||||
<el-option label="柱状图" value="bar" />
|
||||
</el-select>
|
||||
</div>
|
||||
<CustomerChart :list="customerData" :chartType="customerChartType" :summaryField="customerField" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 临期项目表 -->
|
||||
<el-card shadow="hover" class="mt-20">
|
||||
<div slot="header">
|
||||
临期项目列表
|
||||
<router-link to="/project/project" style="float: right; color: #409EFF; text-decoration: none;">
|
||||
查看全部
|
||||
</router-link>
|
||||
</div>
|
||||
<ExpiringProjectsTable :projects="projects" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CompareChart from './components/CompareChart.vue'
|
||||
import CustomerChart from './components/CustomerChart.vue'
|
||||
import ExpiringProjectsTable from './components/ExpiringProjectsTable.vue'
|
||||
import StatsCard from './components/StatsCard.vue'
|
||||
import StatusChart from './components/StatusChart.vue'
|
||||
import TrendChart from './components/TrendChart.vue'
|
||||
|
||||
import {
|
||||
getDashboardCharts,
|
||||
getDashboardMetrics,
|
||||
listExpiringProjects,
|
||||
} from '@/api/oa/project'
|
||||
|
||||
import { listProfit } from '@/api/oa/finance/profit'
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
components: {
|
||||
StatsCard,
|
||||
TrendChart,
|
||||
StatusChart,
|
||||
CompareChart,
|
||||
ExpiringProjectsTable,
|
||||
CustomerChart,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
lastUpdate: '',
|
||||
// default to first of month → today
|
||||
startDate: this.firstOfMonth(),
|
||||
endDate: this.today(),
|
||||
|
||||
// your existing refresh logic
|
||||
refreshInterval: 30,
|
||||
timer: null,
|
||||
|
||||
// stats: we'll fill value dynamically, keep rate if you like
|
||||
stats: [
|
||||
{ title: '本月合同总额', value: '', rate: 12.5 },
|
||||
{ title: '合同数量统计', value: '', rate: 8.3 },
|
||||
{ title: '临期项目数量', value: '', rate: 5.2 },
|
||||
{ title: '较上月平均值', value: '', rate: 12976 }
|
||||
],
|
||||
|
||||
// chart & table data
|
||||
months: [],
|
||||
trendData: [],
|
||||
compareData: [],
|
||||
statusData: [],
|
||||
projects: [],
|
||||
customerData: [],
|
||||
customerField: 'count',
|
||||
customerChartType: 'pie'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
firstOfMonth () {
|
||||
const d = new Date()
|
||||
d.setDate(1)
|
||||
return this.formatDate(d)
|
||||
},
|
||||
today () {
|
||||
return this.formatDate(new Date())
|
||||
},
|
||||
formatDate (d) {
|
||||
const yy = d.getFullYear()
|
||||
const mm = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(d.getDate()).padStart(2, '0')
|
||||
return `${yy}/${mm}/${dd}`
|
||||
},
|
||||
formatCurrency (val) {
|
||||
// e.g. "1234567.89" → "¥1,234,567.89"
|
||||
const n = Number(val) || 0
|
||||
return '¥' + n.toLocaleString(undefined, { minimumFractionDigits: 2 })
|
||||
},
|
||||
|
||||
async fetchData () {
|
||||
try {
|
||||
// 1) 卡片指标
|
||||
const { data: metrics } = await getDashboardMetrics({
|
||||
start: this.startDate,
|
||||
end: this.endDate,
|
||||
tradeType: null
|
||||
})
|
||||
// value + rate 都要赋值
|
||||
this.stats[0].value = this.formatCurrency(metrics.totalFunds)
|
||||
this.stats[0].rate = Number(metrics.growthFunds) // 新增:合同总额增长率
|
||||
|
||||
this.stats[1].value = metrics.contractCount
|
||||
this.stats[1].rate = Number(metrics.growthCount) // 新增:合同数量增长率
|
||||
|
||||
this.stats[2].value = metrics.expiringCount
|
||||
this.stats[2].rate = Number(metrics.growthExpiring) // 新增:临期项目增长率
|
||||
|
||||
this.stats[3].value = metrics.completionRate + '%'
|
||||
|
||||
// 2) 三张图:直接用今年的数据
|
||||
const year = new Date().getFullYear()
|
||||
const startOfYear = `${year}/01/01`
|
||||
const today = this.formatDate(new Date())
|
||||
|
||||
const { data: charts } = await getDashboardCharts({
|
||||
start: startOfYear,
|
||||
end: today,
|
||||
tradeType: null
|
||||
})
|
||||
// 下面和你之前一样填充
|
||||
this.months = charts.contractAmountTrend.map(p => p.month)
|
||||
this.trendData = charts.contractAmountTrend.map(p => p.value)
|
||||
this.compareData = charts.monthlyContractComparison.map(p => p.value)
|
||||
|
||||
|
||||
|
||||
// 3) 临期项目列表
|
||||
const { data: list } = await listExpiringProjects({ days: 7, tradeType: null })
|
||||
this.projects = list.map(p => ({
|
||||
projectId: p.projectId,
|
||||
name: p.projectName,
|
||||
amount: this.formatCurrency(p.funds),
|
||||
due: p.finishTime.split('T')[0], // "2024-01-25"
|
||||
status: p.status,
|
||||
remainTime: p.remainTime
|
||||
}))
|
||||
|
||||
// 4) 客户分布
|
||||
const { rows: profitList } = await listProfit({
|
||||
pageSize: 9999,
|
||||
pageNum: 1
|
||||
})
|
||||
this.customerData = profitList
|
||||
|
||||
// 使用项目列表汇总
|
||||
this.statusData = charts.projectStatusDistribution.map(s => ({
|
||||
name: s.status,
|
||||
value: s.count
|
||||
}))
|
||||
|
||||
// 更新时间
|
||||
this.lastUpdate = new Date().toLocaleString()
|
||||
} catch (e) {
|
||||
console.error('Dashboard fetchData error', e)
|
||||
}
|
||||
},
|
||||
|
||||
resetTimer () {
|
||||
clearInterval(this.timer)
|
||||
this.timer = setInterval(this.fetchData, this.refreshInterval * 1000)
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
startDate () {
|
||||
this.fetchData()
|
||||
},
|
||||
endDate () {
|
||||
this.fetchData()
|
||||
},
|
||||
refreshInterval () {
|
||||
this.resetTimer()
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.fetchData()
|
||||
this.resetTimer()
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
clearInterval(this.timer)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.chart-card {
|
||||
height: 400px;
|
||||
}
|
||||
</style>
|
||||
135
ruoyi-ui/src/views/oa/project/pace/components/FormDialog.vue
Normal file
135
ruoyi-ui/src/views/oa/project/pace/components/FormDialog.vue
Normal 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>
|
||||
170
ruoyi-ui/src/views/oa/project/pace/components/MenuSelect.vue
Normal file
170
ruoyi-ui/src/views/oa/project/pace/components/MenuSelect.vue
Normal 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>
|
||||
744
ruoyi-ui/src/views/oa/project/pace/components/StepTable.vue
Normal file
744
ruoyi-ui/src/views/oa/project/pace/components/StepTable.vue
Normal 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>
|
||||
111
ruoyi-ui/src/views/oa/project/pace/components/postpone.vue
Normal file
111
ruoyi-ui/src/views/oa/project/pace/components/postpone.vue
Normal 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>
|
||||
421
ruoyi-ui/src/views/oa/project/pace/components/step.vue
Normal file
421
ruoyi-ui/src/views/oa/project/pace/components/step.vue
Normal 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>
|
||||
365
ruoyi-ui/src/views/oa/project/pace/components/xmind.vue
Normal file
365
ruoyi-ui/src/views/oa/project/pace/components/xmind.vue
Normal 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>
|
||||
237
ruoyi-ui/src/views/oa/project/pace/file.vue
Normal file
237
ruoyi-ui/src/views/oa/project/pace/file.vue
Normal 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>
|
||||
383
ruoyi-ui/src/views/oa/project/pace/index.vue
Normal file
383
ruoyi-ui/src/views/oa/project/pace/index.vue
Normal 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)
|
||||
/* 2‑5 回写缓存 + 更新响应式数据 */
|
||||
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>
|
||||
306
ruoyi-ui/src/views/oa/project/pace/myStep.vue
Normal file
306
ruoyi-ui/src/views/oa/project/pace/myStep.vue
Normal 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>
|
||||
301
ruoyi-ui/src/views/oa/project/pace/postpone.vue
Normal file
301
ruoyi-ui/src/views/oa/project/pace/postpone.vue
Normal 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>
|
||||
342
ruoyi-ui/src/views/oa/project/pace/step.vue
Normal file
342
ruoyi-ui/src/views/oa/project/pace/step.vue
Normal 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>
|
||||
1423
ruoyi-ui/src/views/oa/project/probox.vue
Normal file
1423
ruoyi-ui/src/views/oa/project/probox.vue
Normal file
File diff suppressed because it is too large
Load Diff
864
ruoyi-ui/src/views/oa/project/product/index.vue
Normal file
864
ruoyi-ui/src/views/oa/project/product/index.vue
Normal file
@@ -0,0 +1,864 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<el-form-item label="项目名称" prop="projectName">
|
||||
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目编号" prop="projectNum">
|
||||
<el-input v-model="queryParams.projectNum" placeholder="请输入项目编号" clearable @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目类型" prop="projectType">
|
||||
<el-select v-model="queryParams.projectType" placeholder="请选择项目类型" clearable @change="handleQuery">
|
||||
<el-option v-for="dict in dict.type.sys_project_type" :key="dict.value" :label="dict.label"
|
||||
:value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="贸易类型" prop="tradeType">
|
||||
<el-select v-model="queryParams.tradeType" placeholder="请选择项目类型" clearable @change="handleQuery">
|
||||
<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" :filter-method="filterCode">
|
||||
<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="状态" prop="projectType">
|
||||
<el-select v-model="queryParams.productStatus" placeholder="请选择状态" clearable @change="handleQuery">
|
||||
<el-option v-for="dict in dict.type.sys_project_status" :key="dict.value" :label="dict.label"
|
||||
:value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="签约公司" prop="signingCompany">
|
||||
<el-select v-model="queryParams.signingCompany" placeholder="请选择签约公司" style="width: 100%" clearable>
|
||||
<el-option v-for="dict in dict.type.signing_company" :key="dict.value" :label="dict.label"
|
||||
:value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="签约客户" prop="customerId">
|
||||
<el-select v-model="queryParams.customerId" placeholder="请选择签约客户" filterable style="width: 100%" clearable>
|
||||
<el-option v-for="item in customerList" :key="item.customerId" :label="item.name" :value="item.customerId" />
|
||||
</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-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-table v-loading="loading" :data="projectList" @selection-change="handleSelectionChange">
|
||||
<el-table-column label="项目类型" prop="tradeType" width="100">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.sys_trade_type" :value="scope.row.tradeType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="项目代号" prop="projectCode" width="100" 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="项目名称" align="left" prop="projectName">
|
||||
<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="项目编号" align="left" prop="projectNum" />
|
||||
<el-table-column label="签约公司" align="left" prop="signingCompany">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.signing_company" :value="scope.row.signingCompany" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="客户名称" align="center" prop="customerName">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" @click="toDetail(scope.row)">{{ scope.row.customerName }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="项目总金额" align="center" prop="funds">
|
||||
<template slot-scope="scope">
|
||||
<div>{{ convertToTenThousand(scope.row.funds) }}</div>
|
||||
<div v-if="scope.row.prePay > 0">
|
||||
<el-tag type="warning">预付款:{{ convertToTenThousand(scope.row.prePay) }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" align="center" prop="functionary" />
|
||||
<el-table-column label="开始时间" align="center" prop="beginTime">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.beginTime, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="剩余时间" align="center" prop="remainTime">
|
||||
<template slot-scope="scope">
|
||||
<div v-if="scope.row.remainTime >= 0">
|
||||
<div v-if="scope.row.productStatus == 0">
|
||||
<span v-if="scope.row.remainTime > 5">剩余{{ scope.row.remainTime }}天</span>
|
||||
<el-tag v-else-if="scope.row.remainTime <= 5 && scope.row.remainTime > 3" type="warning">
|
||||
剩余{{ scope.row.remainTime }}天
|
||||
</el-tag>
|
||||
<el-tag v-else type="danger">剩余{{ scope.row.remainTime }}天</el-tag>
|
||||
</div>
|
||||
<div v-else>-</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-tag type="danger" v-if="scope.row.productStatus == 0">过期{{
|
||||
Math.abs(scope.row.remainTime)
|
||||
}}天
|
||||
</el-tag>
|
||||
<div v-else>-</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="productStatus">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.sys_project_status" :value="scope.row.productStatus.toString()" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否延期" align="center" prop="remark">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.isPostpone === 0">否</span>
|
||||
<span v-if="scope.row.isPostpone === 1">是</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="置顶">
|
||||
<template slot-scope="scope">
|
||||
<el-switch :active-value="1" :inactive-value="0" v-model="scope.row.isTop"
|
||||
@change="handleRowChange(scope.row)"></el-switch>
|
||||
</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" v-if="scope.row.productStatus == 0" icon="el-icon-check"
|
||||
@click="handleClosure(scope.row)">生产结项
|
||||
</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-view" @click="handleDetail(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="detailShow" @close="closeDialog" width="76%" append-to-body>
|
||||
<ProjectInfo :info="form" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listCustomer } from "@/api/oa/customer";
|
||||
import { findContractByProjectId, getOaContract, selectContractByProjectId } from "@/api/oa/oaContract";
|
||||
import { addProject, delProject, getProject, listProject, updateProject } from "@/api/oa/project";
|
||||
import { listProjectSchedule } from "@/api/oa/projectSchedule";
|
||||
import { listPage } from "@/api/oa/projectScheduleStep";
|
||||
import { listTask } from "@/api/oa/task";
|
||||
import { listByIds } from "@/api/system/oss";
|
||||
|
||||
import ProjectInfo from "@/components/fad-service/ProjectInfo/index.vue";
|
||||
import FilePreview from "@/components/FilePreview/index.vue";
|
||||
import PostPone from "@/views/oa/project/components/PostPone.vue";
|
||||
import Dict from "@/views/system/dict/index.vue";
|
||||
|
||||
export default {
|
||||
name: "Project",
|
||||
components: { FilePreview, Dict, PostPone, ProjectInfo },
|
||||
inject: ['$folder'],
|
||||
dicts: [
|
||||
'sys_project_status',
|
||||
'sys_project_type',
|
||||
'sys_sort_grade',
|
||||
'sys_work_type',
|
||||
'sys_sort_grade',
|
||||
'sys_trade_type',
|
||||
'sys_project_code',
|
||||
'signing_company'],
|
||||
data () {
|
||||
return {
|
||||
folderInstance: null,
|
||||
// 按钮loading
|
||||
buttonLoading: false,
|
||||
// 遮罩层
|
||||
loading: true,
|
||||
// 选中数组
|
||||
ids: [],
|
||||
// 非单个禁用
|
||||
single: true,
|
||||
// 非多个禁用
|
||||
multiple: true,
|
||||
// 显示搜索条件
|
||||
showSearch: true,
|
||||
// 总条数
|
||||
total: 0,
|
||||
// 项目管理表格数据
|
||||
projectList: [],
|
||||
//tabs标签
|
||||
tabContract: 'tab01',
|
||||
// 弹出层标题
|
||||
title: "",
|
||||
titleContract: "",
|
||||
//编辑弹出层
|
||||
editShow: false,
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
//合同项目信息
|
||||
projectId: '',
|
||||
desTitle: '8888',
|
||||
projectNum: '',
|
||||
contractForm: {},
|
||||
//附件
|
||||
fileList: [],
|
||||
//详情lable背景
|
||||
lableBg: "background: #f0f9eb; width:150px; text-align: center;",
|
||||
|
||||
//采购合同列表
|
||||
oaContractList: [],
|
||||
dialogContract: false,
|
||||
searchTime: [],
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
projectName: undefined,
|
||||
projectNum: undefined,
|
||||
beginTime: undefined,
|
||||
finishTime: undefined,
|
||||
signingCompany: undefined,
|
||||
customerId: undefined,
|
||||
},
|
||||
// 表单参数
|
||||
form: {},
|
||||
periodItem: '',
|
||||
// 表单校验
|
||||
rules: {
|
||||
projectId: [
|
||||
{ required: true, message: "ID不能为空", trigger: "blur" }
|
||||
],
|
||||
projectName: [
|
||||
{ required: true, message: "项目名称不能为空", trigger: "blur" }
|
||||
],
|
||||
projectNum: [
|
||||
{ required: true, message: "项目编号不能为空", trigger: "blur" }
|
||||
],
|
||||
projectType: [
|
||||
{ required: true, message: "项目类型不能为空", trigger: "change" }
|
||||
],
|
||||
functionary: [
|
||||
{ required: true, message: "项目负责人不能为空", trigger: "blur" }
|
||||
],
|
||||
address: [
|
||||
{ required: true, message: "项目地址不能为空", trigger: "blur" }
|
||||
],
|
||||
funds: [
|
||||
{ required: true, message: '项目总金额不能为空', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
const regex = /(^[1-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9]\.[0-9]([0-9]))/;
|
||||
if (!regex.test(value)) {
|
||||
callback(new Error('请输入正确的金额格式'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
beginTime: [
|
||||
{ required: true, message: "开始日期不能为空", trigger: "blur" }
|
||||
],
|
||||
finishTime: [
|
||||
{ required: true, message: "结束日期不能为空", trigger: "blur" }
|
||||
],
|
||||
contractId: [
|
||||
{ required: true, message: "关联合同ID不能为空", trigger: "blur" }
|
||||
],
|
||||
},
|
||||
dictBackup: [],
|
||||
otherFiles: '',
|
||||
detailShow: false,
|
||||
otherFilesLoading: false,
|
||||
currentProjectId: '',
|
||||
postPoneOpen: false,
|
||||
customerList: [],
|
||||
paceFiles: [],
|
||||
paceFilesLoading: [],
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
// 监听整个 query 对象,如果有多个 query 参数也一起触发
|
||||
'$route.query': {
|
||||
handler () {
|
||||
this.initFromRoute()
|
||||
},
|
||||
immediate: false,
|
||||
deep: true
|
||||
},
|
||||
},
|
||||
created () {
|
||||
this.initFromRoute()
|
||||
this.getDicts();
|
||||
this.getCustomerList();
|
||||
this.dictBackup = this.dict.type.sys_project_code;
|
||||
this.folderInstance = this.$folder();
|
||||
},
|
||||
methods: {
|
||||
getCustomerList () {
|
||||
listCustomer({ pageNum: 1, pageSize: 999 }).then(response => {
|
||||
this.customerList = response.rows;
|
||||
}).catch(() => {
|
||||
this.customerList = [];
|
||||
});
|
||||
},
|
||||
handleRowChange (row) {
|
||||
this.loading = true;
|
||||
const payload = {
|
||||
...row,
|
||||
projectId: row.projectId,
|
||||
isTop: row.isTop ? 1 : 0,
|
||||
}
|
||||
updateProject(payload).then(response => {
|
||||
this.$modal.msgSuccess("更新成功");
|
||||
this.getList();
|
||||
}).catch(() => {
|
||||
this.$modal.msgError("更新失败");
|
||||
});
|
||||
},
|
||||
initFromRoute () {
|
||||
// 每次路由 query 变化都重新赋值并拉列表
|
||||
this.queryParams.projectName = this.$route.query.projectName
|
||||
// 新增逻辑:如果有projectId参数,拉取单个项目
|
||||
if (this.$route.query.projectId && this.$route.query.projectId !== '') {
|
||||
this.getSingleProject(this.$route.query.projectId);
|
||||
} else {
|
||||
this.getList();
|
||||
}
|
||||
},
|
||||
// 新增:获取单个项目详情
|
||||
getSingleProject (projectId) {
|
||||
this.loading = true;
|
||||
getProject(projectId).then(response => {
|
||||
this.projectList = [response.data];
|
||||
this.total = 1;
|
||||
this.loading = false;
|
||||
}).catch(() => {
|
||||
this.projectList = [];
|
||||
this.total = 0;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
/** 输入时同时按 label / value 本地过滤 */
|
||||
filterCode (query) {
|
||||
query = (query || '').toLowerCase();
|
||||
this.dict.type.sys_project_code = this.dictBackup.filter(item => {
|
||||
return item.label.toLowerCase().indexOf(query) > -1 || item.value.toLowerCase().indexOf(query) > -1;
|
||||
});
|
||||
},
|
||||
toDetail (row) {
|
||||
console.log(row)
|
||||
this.$router.push('/customer/detail/' + row.customerId);
|
||||
},
|
||||
handleDetail (row) {
|
||||
this.loading = true;
|
||||
this.detailShow = true;
|
||||
this.reset();
|
||||
const projectId = row.projectId
|
||||
this.projectId = row.projectId;
|
||||
this.handleContract(row);
|
||||
getProject(projectId).then(response => {
|
||||
console.log(response.data);
|
||||
this.loading = false;
|
||||
this.form = response.data;
|
||||
let period = [];
|
||||
period.push(response.data.beginTime);
|
||||
period.push(response.data.finishTime);
|
||||
this.periodItem = period;
|
||||
this.form.signingCompany = this.form.signingCompany.toString();
|
||||
|
||||
this.open = true;
|
||||
this.title = "项目名称:" + response.data.projectName;
|
||||
});
|
||||
},
|
||||
handlePostPone (row) {
|
||||
this.currentProjectId = row.projectId;
|
||||
this.postPoneOpen = true;
|
||||
},
|
||||
/** 查询项目管理列表 */
|
||||
getList () {
|
||||
this.loading = true;
|
||||
this.queryParams.params = {};
|
||||
if (null != this.searchTime && '' != this.searchTime) {
|
||||
this.queryParams.params["beginCreateTime"] = this.getRealDate(this.searchTime[0]);
|
||||
this.queryParams.params["endCreateTime"] = this.getRealDate(this.searchTime[1]);
|
||||
}
|
||||
listProject(this.queryParams).then(response => {
|
||||
this.projectList = response.rows.map(item => {
|
||||
if (item.projectCode) {
|
||||
item.projectCodeType = item.projectCode.split('-')[0];
|
||||
item.projectCodeNumber = item.projectCode.split('-')[1];
|
||||
} else {
|
||||
item.projectCodeType = undefined;
|
||||
item.projectCodeNumber = undefined;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
this.total = response.total;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
|
||||
/**日期转字符串**/
|
||||
getRealDate (startDate) {
|
||||
// 时间转换
|
||||
var datejson = new Date(startDate).toJSON();
|
||||
var date = new Date(+new Date(datejson)
|
||||
+ 8 * 3600 * 1000).toISOString().replace(/T/g, ' ').replace(/\.[\d]{3}Z/, '')
|
||||
return date;
|
||||
},
|
||||
// 取消按钮
|
||||
cancel () {
|
||||
this.open = false;
|
||||
this.reset();
|
||||
},
|
||||
// 表单重置
|
||||
reset () {
|
||||
this.form = {
|
||||
projectId: undefined,
|
||||
projectName: undefined,
|
||||
projectNum: undefined,
|
||||
projectType: undefined,
|
||||
projectCodeType: undefined,
|
||||
projectCodeNumber: undefined,
|
||||
address: undefined,
|
||||
functionary: undefined,
|
||||
beginTime: undefined,
|
||||
finishTime: undefined,
|
||||
delivery: undefined,
|
||||
guarantee: undefined,
|
||||
introduction: undefined,
|
||||
projectGrade: undefined,
|
||||
productStatus: undefined,
|
||||
contractId: undefined,
|
||||
invoiceName: undefined,
|
||||
invoiceNumber: undefined,
|
||||
invoiceAddress: undefined,
|
||||
invoiceBank: undefined,
|
||||
accessory: undefined,
|
||||
remark: undefined,
|
||||
tradeType: 1,
|
||||
prePay: undefined,
|
||||
projectCode: undefined,
|
||||
closureFiles: undefined,
|
||||
signingCompany: undefined,
|
||||
customerId: undefined,
|
||||
};
|
||||
this.resetForm("form");
|
||||
this.contractForm = {};
|
||||
this.fileList = [];
|
||||
},
|
||||
selectPrePay () {
|
||||
this.handleQuery()
|
||||
},
|
||||
/** 搜索按钮操作 */
|
||||
handleQuery () {
|
||||
this.queryParams.pageNum = 1;
|
||||
this.getList();
|
||||
},
|
||||
/** 重置按钮操作 */
|
||||
resetQuery () {
|
||||
this.searchTime = [];
|
||||
this.resetForm("queryForm");
|
||||
this.handleQuery();
|
||||
},
|
||||
// 多选框选中数据
|
||||
handleSelectionChange (selection) {
|
||||
this.ids = selection.map(item => item.projectId)
|
||||
this.single = selection.length !== 1
|
||||
this.multiple = !selection.length
|
||||
},
|
||||
/** 新增按钮操作 */
|
||||
handleAdd () {
|
||||
this.reset();
|
||||
this.periodItem = ''
|
||||
this.editShow = true;
|
||||
// this.open = true;
|
||||
this.title = "添加项目";
|
||||
},
|
||||
/**
|
||||
* 时间范围失去焦点时绑定日期数据
|
||||
* @param e
|
||||
*/
|
||||
getTimeBlur (e) {
|
||||
this.form.beginTime = e.value[0]
|
||||
this.form.finishTime = e.value[1]
|
||||
this.form.postponeTime = e.value[1]
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
handleUpdate (row) {
|
||||
this.loading = true;
|
||||
this.editShow = true;
|
||||
this.reset();
|
||||
const projectId = row.projectId
|
||||
this.projectId = row.projectId;
|
||||
this.handleContract(row);
|
||||
getProject(projectId).then(response => {
|
||||
console.log(response.data);
|
||||
this.loading = false;
|
||||
this.form = response.data;
|
||||
let period = [];
|
||||
period.push(response.data.beginTime);
|
||||
period.push(response.data.finishTime);
|
||||
this.periodItem = period;
|
||||
this.form.signingCompany = this.form.signingCompany.toString();
|
||||
|
||||
this.open = true;
|
||||
this.title = "项目名称:" + response.data.projectName;
|
||||
});
|
||||
},
|
||||
|
||||
/**项目合同管理**/
|
||||
handleContract (row) {
|
||||
this.reset();
|
||||
this.loading = true;
|
||||
this.projectId = row.projectId;
|
||||
this.desTitle = row.projectName;
|
||||
this.projectNum = row.projectNum
|
||||
this.findContract(row.projectId)
|
||||
},
|
||||
findContract (pId) {
|
||||
let data = {
|
||||
projectId: pId,
|
||||
contractType: '2'
|
||||
}
|
||||
findContractByProjectId(data).then(res => {
|
||||
if (res.data == null) {
|
||||
this.contractForm = {
|
||||
contractId: undefined,
|
||||
projectId: undefined,
|
||||
contractNum: undefined,
|
||||
contractName: undefined,
|
||||
firstName: undefined,
|
||||
firstPerson: undefined,
|
||||
firstPhone: undefined,
|
||||
secondName: undefined,
|
||||
secondPerson: undefined,
|
||||
secondPhone: undefined,
|
||||
contractPrice: undefined,
|
||||
signTime: undefined,
|
||||
validity: undefined,
|
||||
contractStatus: undefined,
|
||||
accessory: undefined,
|
||||
remark: undefined,
|
||||
createBy: undefined,
|
||||
createTime: undefined,
|
||||
updateBy: undefined,
|
||||
updateTime: undefined,
|
||||
|
||||
}
|
||||
} else {
|
||||
this.contractForm = res.data;
|
||||
this.getFile(res.data.accessory);
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
handleRowClick (row) {
|
||||
// this.folderInstance.setFiles([row]);
|
||||
this.folderInstance.previewSimple(row);
|
||||
},
|
||||
|
||||
/**采购合同列表**/
|
||||
getContractList (pId, cType) {
|
||||
this.loading = true;
|
||||
let data = {
|
||||
projectId: pId,
|
||||
contractType: cType
|
||||
}
|
||||
selectContractByProjectId(data).then(response => {
|
||||
this.oaContractList = response.data;
|
||||
})
|
||||
},
|
||||
handleClosureFiles () {
|
||||
updateProject(this.form).then(res => {
|
||||
this.$modal.msgSuccess("结项文件已更新");
|
||||
this.getList();
|
||||
})
|
||||
},
|
||||
/**查看采购合同**/
|
||||
handlePreview (row) {
|
||||
this.loading = true;
|
||||
this.dialogContract = true;
|
||||
this.contractForm = {};
|
||||
this.projectId = row.projectId;
|
||||
// this.reset();
|
||||
// this.buyOntractShow = false;
|
||||
this.titleContract = "查看合同";
|
||||
this.getContractById(row.contractId);
|
||||
|
||||
},
|
||||
/**获取合同信息**/
|
||||
getContractById (cId) {
|
||||
getOaContract(cId).then(response => {
|
||||
this.loading = false;
|
||||
this.contractForm = response.data;
|
||||
this.getFile(response.data.accessory);
|
||||
});
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm () {
|
||||
this.$refs["form"].validate(async (valid) => {
|
||||
if (valid) {
|
||||
this.buttonLoading = true;
|
||||
// 只有projectCodeType和projectCodeNumber都有值时才拼接项目代号
|
||||
const projectCode = (this.form.projectCodeType && this.form.projectCodeNumber) ? this.form.projectCodeType + '-' + this.form.projectCodeNumber : undefined;
|
||||
const payload = {
|
||||
...this.form,
|
||||
beginTime: this.periodItem[0],
|
||||
finishTime: this.periodItem[1],
|
||||
customerId: this.form.customerId ? this.form.customerId : 0,
|
||||
projectCode,
|
||||
}
|
||||
//履约保证金转json
|
||||
try {
|
||||
if (this.form.projectId != null) {
|
||||
await updateProject(payload)
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
} else {
|
||||
await addProject(payload)
|
||||
this.$modal.msgSuccess("新增成功");
|
||||
this.editShow = false;
|
||||
this.getList();
|
||||
}
|
||||
// 刷新项目选择器中的数据
|
||||
// this.$store.dispatch('meta/getProjectList')
|
||||
this.getList();
|
||||
} finally {
|
||||
this.buttonLoading = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**项目完成后修改项目状态**/
|
||||
changeProjectStatus () {
|
||||
this.form.productStatus = 1;
|
||||
updateProject(this.form).then(response => {
|
||||
this.$modal.msgSuccess("任务进度完成");
|
||||
this.getList()
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
/** 金额转为万元 **/
|
||||
convertToTenThousand (amount) {
|
||||
const convertToTenThousand = (amount) => `${Number((amount / 10000).toFixed(6))} 万元`;
|
||||
return convertToTenThousand(amount);
|
||||
},
|
||||
|
||||
/**合同选项卡**/
|
||||
handleTabContract (tab, event) {
|
||||
if (tab.index == '1') {
|
||||
this.getContractList(this.projectId, 2);
|
||||
} else if (tab.index == '2') {
|
||||
this.getContractList(this.projectId, 1);
|
||||
} else if (tab.index == '3') {
|
||||
console.log(tab.index, '获取结项文件');
|
||||
} else if (tab.index == '4') {
|
||||
this.otherFilesLoading = true;
|
||||
listTask({
|
||||
projectId: this.projectId,
|
||||
pageSize: 999,
|
||||
}).then(res => {
|
||||
const files = [...new Set(res.rows.map(item => item.accessory).filter(item => item))];
|
||||
if (!files.length) return;
|
||||
// 调用listOss获取文件列表详情
|
||||
listByIds(files).then(res => {
|
||||
this.otherFiles = res.data;
|
||||
console.log(this.otherFiles, res, 'otherFiles');
|
||||
}).finally(() => {
|
||||
this.otherFilesLoading = false;
|
||||
})
|
||||
}).finally(() => {
|
||||
this.otherFilesLoading = false;
|
||||
})
|
||||
} else if (tab.index == '5') {
|
||||
this.paceFilesLoading = true;
|
||||
listProjectSchedule({
|
||||
projectId: this.projectId,
|
||||
pageSize: 1,
|
||||
}).then(res => {
|
||||
if (!res.rows.length) return;
|
||||
const scheduleId = res.rows[0].scheduleId;
|
||||
listPage({
|
||||
scheduleId: scheduleId,
|
||||
pageSize: 999,
|
||||
}).then(res => {
|
||||
const paceFiles = res.rows.map(item => item.relatedDocs).filter(item => item).join(',');
|
||||
console.log(paceFiles, 'paceFiles', res.rows.map(item => item.relatedDocs));
|
||||
if (paceFiles.length == 0) return;
|
||||
listByIds(paceFiles).then(res => {
|
||||
this.paceFiles = res.data;
|
||||
console.log(this.paceFiles, res, 'paceFiles');
|
||||
}).finally(() => {
|
||||
this.paceFilesLoading = false;
|
||||
})
|
||||
}).finally(() => {
|
||||
this.paceFilesLoading = false;
|
||||
})
|
||||
}).finally(() => {
|
||||
this.paceFilesLoading = false;
|
||||
})
|
||||
}
|
||||
},
|
||||
/**获取附件**/
|
||||
getFile (val) {
|
||||
if (val) {
|
||||
listByIds(val).then(res => {
|
||||
this.fileList = res.data;
|
||||
})
|
||||
} else {
|
||||
this.fileList = [];
|
||||
}
|
||||
},
|
||||
|
||||
/** 关闭弹窗 **/
|
||||
closeDialog () {
|
||||
this.editShow = false;
|
||||
this.detailShow = false;
|
||||
this.tabContract = 'tab01';
|
||||
this.getList();
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete (row) {
|
||||
const projectIds = row.projectId || this.ids;
|
||||
this.$modal.confirm('是否确认删除项目管理编号为"' + projectIds + '"的数据项?').then(() => {
|
||||
this.loading = true;
|
||||
return delProject(projectIds);
|
||||
}).then(() => {
|
||||
this.loading = false;
|
||||
this.getList();
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
|
||||
/** 项目状态改为结项 */
|
||||
handleClosure (row) {
|
||||
this.$modal.confirm('是否确认结项"' + row.projectName + '"的数据项?生产结项后将不会出现在考勤页面中').then(() => {
|
||||
this.loading = true;
|
||||
row.productStatus = 1;
|
||||
return updateProject(row)
|
||||
}).then(() => {
|
||||
this.loading = false;
|
||||
// this.$router.push('/project/closure/detail/'+row.projectId)
|
||||
this.$modal.msgSuccess("操作成功")
|
||||
this.getList();
|
||||
}).catch(() => {
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
|
||||
handleDownload (row) {
|
||||
this.$download.oss(row.ossId);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.line {
|
||||
padding-left: 13px
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
padding-left: 70px
|
||||
}
|
||||
|
||||
.block {}
|
||||
|
||||
.el-timeline {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
list-style: none;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
padding: 0
|
||||
}
|
||||
|
||||
.card-col {
|
||||
margin-top: 10px;
|
||||
padding: 10px 15px
|
||||
}
|
||||
|
||||
.el-descriptions__header {
|
||||
margin-bottom: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.el-descriptions__extra {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #cccccc
|
||||
}
|
||||
|
||||
h4,
|
||||
p {
|
||||
line-height: 24px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-weight: 500
|
||||
}
|
||||
|
||||
.message {
|
||||
color: #cccccc;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
/*分割线的样式*/
|
||||
.el-divider {
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
.el-divider--horizontal {
|
||||
display: block;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
margin: 10px 20px 20px 20px;
|
||||
}
|
||||
|
||||
.el-divider__text.is-left {
|
||||
color: #409eff;
|
||||
left: 0;
|
||||
font-weight: bold;
|
||||
margin: 0 10px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
/*分割线的样式 end*/
|
||||
</style>
|
||||
217
ruoyi-ui/src/views/oa/project/report/components/ExportDialog.vue
Normal file
217
ruoyi-ui/src/views/oa/project/report/components/ExportDialog.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<div class="export-container">
|
||||
<!-- 时间范围选择 -->
|
||||
<el-date-picker
|
||||
v-model="exportForm.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="yyyy-MM-dd"
|
||||
style="width: 280px; margin-right: 16px;"
|
||||
:picker-options="pickerOptions"
|
||||
/>
|
||||
|
||||
<!-- 用户选择 -->
|
||||
<el-select
|
||||
v-model="exportForm.userIds"
|
||||
multiple
|
||||
filterable
|
||||
placeholder="选择用户"
|
||||
style="width: 280px; margin-right: 16px;"
|
||||
:loading="userLoading"
|
||||
@visible-change="handleUserSelectVisible"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userList"
|
||||
:key="user.userId"
|
||||
:label="user.nickName"
|
||||
:value="user.userId"
|
||||
>
|
||||
<span>{{ user.nickName }}</span>
|
||||
<span style="float: right; color: #8492a6; font-size: 13px">{{ user.dept && user.dept.deptName }}</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<!-- 导出按钮 -->
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="el-icon-download"
|
||||
@click="handleExport"
|
||||
:loading="exportLoading"
|
||||
:disabled="!canExport"
|
||||
>
|
||||
导出报工
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { exportProjectReport } from '@/api/oa/projectReport'
|
||||
import { selectUser } from '@/api/system/user'
|
||||
|
||||
export default {
|
||||
name: 'ExportDialog',
|
||||
data() {
|
||||
return {
|
||||
exportLoading: false,
|
||||
userLoading: false,
|
||||
|
||||
// 表单数据
|
||||
exportForm: {
|
||||
dateRange: [],
|
||||
userIds: [],
|
||||
options: ['includeDetails', 'includeSummary']
|
||||
},
|
||||
|
||||
// 日期选择器配置
|
||||
pickerOptions: {
|
||||
shortcuts: [
|
||||
{
|
||||
text: '最近一周',
|
||||
onClick(picker) {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
|
||||
picker.$emit('pick', [start, end])
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '最近一个月',
|
||||
onClick(picker) {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
|
||||
picker.$emit('pick', [start, end])
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '最近三个月',
|
||||
onClick(picker) {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)
|
||||
picker.$emit('pick', [start, end])
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// 用户列表
|
||||
userList: []
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
canExport() {
|
||||
return this.exportForm.dateRange &&
|
||||
this.exportForm.dateRange.length === 2 &&
|
||||
this.exportForm.userIds.length > 0
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.initDateRange()
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 初始化默认时间范围(当前月)
|
||||
initDateRange() {
|
||||
const today = new Date()
|
||||
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1)
|
||||
const endOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0)
|
||||
|
||||
this.exportForm.dateRange = [
|
||||
this.formatDate(startOfMonth),
|
||||
this.formatDate(endOfMonth)
|
||||
]
|
||||
},
|
||||
|
||||
formatDate(date) {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
},
|
||||
|
||||
// 用户选择器显示/隐藏事件
|
||||
async handleUserSelectVisible(visible) {
|
||||
if (visible && this.userList.length === 0) {
|
||||
await this.getUserList()
|
||||
}
|
||||
},
|
||||
|
||||
// 获取用户列表
|
||||
async getUserList() {
|
||||
this.userLoading = true
|
||||
try {
|
||||
const params = {
|
||||
pageNum: 1,
|
||||
pageSize: 1000
|
||||
}
|
||||
const response = await selectUser(params)
|
||||
this.userList = response.rows || []
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
this.$message.error('获取用户列表失败')
|
||||
} finally {
|
||||
this.userLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 执行导出
|
||||
async handleExport() {
|
||||
if (!this.canExport) {
|
||||
this.$message.warning('请选择时间范围和用户')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.exportLoading = true
|
||||
|
||||
// 构建导出参数
|
||||
const exportParams = {
|
||||
startDate: this.exportForm.dateRange[0],
|
||||
endDate: this.exportForm.dateRange[1],
|
||||
userIds: this.exportForm.userIds,
|
||||
options: this.exportForm.options.join(',')
|
||||
}
|
||||
|
||||
// 调用导出API
|
||||
const response = await exportProjectReport(exportParams)
|
||||
|
||||
// 处理文件下载
|
||||
const blob = new Blob([response], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
})
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `projectReport_${this.exportForm.dateRange[0]}_${this.exportForm.dateRange[1]}.xlsx`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
this.$message.success('导出成功!')
|
||||
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error)
|
||||
this.$message.error('导出失败,请重试')
|
||||
} finally {
|
||||
this.exportLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.export-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
105
ruoyi-ui/src/views/oa/project/report/components/PieChart.vue
Normal file
105
ruoyi-ui/src/views/oa/project/report/components/PieChart.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: 'PieChart',
|
||||
props: {
|
||||
// 传入的 data 数组,每项是 { projectName: string, count: number, ... }
|
||||
data: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
handler() {
|
||||
this.renderChart();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.chart = echarts.init(this.$refs.chartRef);
|
||||
this.renderChart();
|
||||
window.addEventListener('resize', this.resizeChart);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.resizeChart);
|
||||
this.chart && this.chart.dispose();
|
||||
},
|
||||
methods: {
|
||||
renderChart() {
|
||||
if (!this.chart) return;
|
||||
// 如果没有任何数据,显示“暂无数据”提示
|
||||
if (!this.data || this.data.length === 0) {
|
||||
this.chart.clear();
|
||||
this.chart.showLoading({
|
||||
text: '暂无数据',
|
||||
color: '#999',
|
||||
textColor: '#999',
|
||||
maskColor: 'transparent'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 将后端的 { projectName, count } 转成 { name, value }
|
||||
const pieData = this.data.map(item => ({
|
||||
name: item.projectName,
|
||||
value: item.value
|
||||
}));
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
},
|
||||
// legend: {
|
||||
// orient: 'vertical',
|
||||
// left: '0%',
|
||||
// formatter: name => {
|
||||
// // 最多6个汉字,超出则省略
|
||||
// return name.length > 6 ? name.slice(0, 6) + '…' : name;
|
||||
// },
|
||||
// data: pieData.map(i => i.name)
|
||||
// },
|
||||
series: [
|
||||
{
|
||||
name: '项目分布',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
label: {
|
||||
formatter: '{b}\n{c}'
|
||||
},
|
||||
data: pieData
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.chart.setOption(option, true);
|
||||
},
|
||||
resizeChart() {
|
||||
this.chart && this.chart.resize();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 让“暂无数据”文案垂直居中 */
|
||||
.chart-container .echarts-loading-text {
|
||||
font-size: 14px !important;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:visible.sync="visible"
|
||||
width="860px"
|
||||
custom-class="project-report-detail"
|
||||
:before-close="handleClose"
|
||||
append-to-body
|
||||
>
|
||||
<template #title>
|
||||
<div class="dialog-title flex items-center gap-2">
|
||||
<i class="el-icon-document"></i>
|
||||
<span>项目报工详情</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 打印 / 导出区域 -->
|
||||
<div ref="printArea">
|
||||
<el-scrollbar style="max-height: 70vh; padding-right: 8px">
|
||||
<!-- 基本信息 -->
|
||||
<el-card shadow="never" class="mb-6">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="经办人">{{ data.nickName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="部门">{{ data.deptName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="工作地点">{{ data.workPlace }}</el-descriptions-item>
|
||||
<el-descriptions-item label="是否出差">
|
||||
<el-tag :type="data.isTrip === 1 ? 'success' : 'info'" size="mini">
|
||||
{{ data.isTrip === 1 ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 项目信息 -->
|
||||
<el-card shadow="never" class="mb-6">
|
||||
<template #header>
|
||||
<div class="card-header">项目信息</div>
|
||||
</template>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="项目名称" :span="2">{{ data.projectName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="项目编号">{{ data.projectNum }}</el-descriptions-item>
|
||||
<el-descriptions-item label="项目代号">{{ data.projectCode }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 报工内容(富文本) -->
|
||||
<el-card shadow="never" class="mb-6 rich-card">
|
||||
<template #header>
|
||||
<div class="card-header">报工内容</div>
|
||||
</template>
|
||||
<div class="rich-text" v-html="safeContent"></div>
|
||||
</el-card>
|
||||
|
||||
<!-- 备注 -->
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">备注</div>
|
||||
</template>
|
||||
<p class="text-gray-700 whitespace-pre-line">{{ data.remark || '—' }}</p>
|
||||
</el-card>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<template #footer>
|
||||
<el-button @click="handlePrint" size="mini">打印</el-button>
|
||||
<el-button @click="handleDownload" size="mini">下载 PDF</el-button>
|
||||
<el-button @click="visible = false" type="primary" size="mini">关 闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DOMPurify from 'dompurify'
|
||||
export default {
|
||||
name: 'ProjectReportDetail',
|
||||
props: {
|
||||
value: { type: Boolean, default: false }, // v-model
|
||||
record: { type: Object, required: true }
|
||||
},
|
||||
data() {
|
||||
return { visible: this.value }
|
||||
},
|
||||
computed: {
|
||||
data() {
|
||||
return this.record || {}
|
||||
},
|
||||
safeContent() {
|
||||
return DOMPurify.sanitize(this.data.content || '')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(v) { this.visible = v },
|
||||
visible(v) { this.$emit('input', v) }
|
||||
},
|
||||
methods: {
|
||||
handleClose() { this.visible = false },
|
||||
/** 直接打印弹窗内容 */
|
||||
handlePrint() {
|
||||
const printContent = this.$refs.printArea
|
||||
const printWindow = window.open('', '', 'width=1000,height=800')
|
||||
printWindow.document.write('<html><head><title>打印</title>')
|
||||
printWindow.document.write('<style>@page{size:auto;margin:20mm;}body{font-family:Arial;} .el-scrollbar__wrap{overflow:visible !important;}</style>')
|
||||
printWindow.document.write('</head><body>')
|
||||
printWindow.document.write(printContent.innerHTML)
|
||||
printWindow.document.write('</body></html>')
|
||||
printWindow.document.close()
|
||||
printWindow.focus()
|
||||
printWindow.print()
|
||||
printWindow.close()
|
||||
},
|
||||
/** 下载为 PDF:动态引入 html2pdf.js */
|
||||
async handleDownload() {
|
||||
// const { default: html2pdf } = await import(/* webpackChunkName: "html2pdf" */ 'html2pdf.js')
|
||||
// const opt = {
|
||||
// margin: 10,
|
||||
// filename: `项目报工详情_${this.data.reportId || Date.now()}.pdf`,
|
||||
// html2canvas: { scale: 2, useCORS: true },
|
||||
// jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
|
||||
// }
|
||||
// html2pdf().from(this.$refs.printArea).set(opt).save()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.project-report-detail >>> .el-dialog__header { padding-bottom: 0; }
|
||||
.dialog-title { font-size: 18px; font-weight: 600; }
|
||||
.card-header { font-weight: 600; font-size: 14px; color: #303133; }
|
||||
.rich-card { padding-bottom: 0 !important; }
|
||||
.rich-text {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background-color: #ffffff;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.rich-text h1,
|
||||
.rich-text h2,
|
||||
.rich-text h3,
|
||||
.rich-text h4 { margin: 0.8em 0 0.5em; font-weight: 600; }
|
||||
.rich-text p { margin: 0.5em 0; }
|
||||
.rich-text ul,
|
||||
.rich-text ol { padding-left: 1.4em; margin: 0.5em 0; }
|
||||
.mb-6 { margin-bottom: 24px; }
|
||||
</style>
|
||||
135
ruoyi-ui/src/views/oa/project/report/components/RankList.vue
Normal file
135
ruoyi-ui/src/views/oa/project/report/components/RankList.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 空数据提示 -->
|
||||
<div v-if="!ranking || ranking.length === 0" class="empty-state">
|
||||
暂无数据
|
||||
</div>
|
||||
<!-- 排名列表 -->
|
||||
<div v-else class="rank-list two-column">
|
||||
<!-- 左列:1-5 -->
|
||||
<div class="column left-column">
|
||||
<div
|
||||
v-for="(item, idx) in ranking.slice(0, 5)"
|
||||
:key="idx"
|
||||
class="ranking-item"
|
||||
>
|
||||
<i
|
||||
:class="idx < 3 ? topIcons[idx] : 'el-icon-user'"
|
||||
:style="{ color: iconColors[idx] }"
|
||||
/>
|
||||
<span class="rank-number">{{ idx + 1 }}</span>
|
||||
<span class="nick">{{ item.nickName }}</span>
|
||||
<span class="dept" v-if="item.deptName">{{ item.deptName }}</span>
|
||||
<span class="count">{{ item.count }} 报工</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右列:6-10 -->
|
||||
<div class="column right-column">
|
||||
<div
|
||||
v-for="(item, idx) in ranking.slice(5, 10)"
|
||||
:key="idx + 5"
|
||||
class="ranking-item"
|
||||
>
|
||||
<i
|
||||
:class="(idx + 5) < 3 ? topIcons[idx + 5] : 'el-icon-user'"
|
||||
:style="{ color: iconColors[idx + 5] }"
|
||||
/>
|
||||
<span class="rank-number">{{ idx + 6 }}</span>
|
||||
<span class="nick">{{ item.nickName }}</span>
|
||||
<span class="dept" v-if="item.deptName">/{{ item.deptName }}</span>
|
||||
<span class="count">{{ item.count }} 报工</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RankList',
|
||||
props: {
|
||||
ranking: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
topIcons: [
|
||||
'el-icon-medal-1',
|
||||
'el-icon-medal',
|
||||
'el-icon-medal'
|
||||
],
|
||||
iconColors: [
|
||||
'#E6A23C', '#C0C4CC', '#F56C6C',
|
||||
'#909399', '#909399', '#909399',
|
||||
'#909399', '#909399', '#909399', '#909399'
|
||||
]
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rank-list.two-column {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 250px; /* 撑满父级容器 */
|
||||
|
||||
}
|
||||
.column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between; /* 从头到尾均匀分布 */
|
||||
}
|
||||
.left-column {
|
||||
padding-right: 8px;
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
.right-column {
|
||||
padding-left: 8px;
|
||||
}
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
}
|
||||
/* 每列除最后一项外绘制底部边框 */
|
||||
.left-column .ranking-item:not(:last-child),
|
||||
.right-column .ranking-item:not(:last-child) {
|
||||
border-bottom: 1px solid #ebebeb;
|
||||
}
|
||||
.ranking-item i {
|
||||
font-size: 20px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.rank-number {
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.nick {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
.dept {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.count {
|
||||
font-size: 16px;
|
||||
color: #409EFF;
|
||||
}
|
||||
.empty-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
color: #909399;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: 'TrendChart',
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
handler() {
|
||||
this.renderChart();
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.chart = echarts.init(this.$refs.chartRef);
|
||||
this.renderChart();
|
||||
window.addEventListener('resize', this.resizeChart);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.resizeChart);
|
||||
this.chart && this.chart.dispose();
|
||||
},
|
||||
methods: {
|
||||
renderChart() {
|
||||
if (!this.chart) return;
|
||||
// 如果数据为空,清空图表并显示暂无数据 loading
|
||||
if (!this.data || this.data.length === 0) {
|
||||
this.chart.clear();
|
||||
this.chart.showLoading({
|
||||
text: '暂无数据',
|
||||
color: '#999',
|
||||
textColor: '#999',
|
||||
maskColor: 'transparent'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const option = {
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: this.data.map(i => i.date)
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: this.data.map(i => i.count),
|
||||
type: 'line',
|
||||
areaStyle: {}
|
||||
}
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
}
|
||||
};
|
||||
// 第二个参数 true 避免与之前配置合并
|
||||
this.chart.setOption(option, true);
|
||||
},
|
||||
resizeChart() {
|
||||
this.chart && this.chart.resize();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 居中“暂无数据”文字 */
|
||||
.chart-container .echarts-loading-text {
|
||||
font-size: 14px !important;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%) !important;
|
||||
}
|
||||
</style>
|
||||
398
ruoyi-ui/src/views/oa/project/report/dashboard.vue
Normal file
398
ruoyi-ui/src/views/oa/project/report/dashboard.vue
Normal file
@@ -0,0 +1,398 @@
|
||||
<template>
|
||||
<div class="report-dashboard">
|
||||
<!-- Header -->
|
||||
<el-row type="flex" align="middle" justify="space-between" class="header-row">
|
||||
<el-col>
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="yyyy-MM-dd"
|
||||
/>
|
||||
<el-button type="primary" icon="el-icon-search" class="export-button" @click="handleSearch">
|
||||
搜 索
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- Summary Cards with Skeleton -->
|
||||
<el-skeleton :loading="loading" animated>
|
||||
<template #template>
|
||||
<el-row :gutter="20" class="summary-cards">
|
||||
<el-col :span="6" v-for="n in 4" :key="n">
|
||||
<el-skeleton-item variant="text" style="width: 60%; height: 20px" />
|
||||
<el-skeleton-item variant="rect" style="width: 100%; height: 80px; margin-top: 10px" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
<template #default>
|
||||
<el-row :gutter="20" class="summary-cards">
|
||||
<el-col :span="6" v-for="card in summaryCards" :key="card.title">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>{{ card.title }}</span>
|
||||
</template>
|
||||
<div class="card-content" :class="{ exception: card.exception }">
|
||||
<div class="card-value">{{ card.value }}</div>
|
||||
<div v-if="!card.exception" class="card-change" :class="card.changeClass">
|
||||
{{ card.displayChange }}
|
||||
</div>
|
||||
<div v-else class="card-action">
|
||||
<span class="handle">需要处理</span>
|
||||
</div>
|
||||
<i :class="['card-icon', card.icon]" :style="{ color: card.iconColor }" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
|
||||
<!-- Charts & Ranking with Skeleton -->
|
||||
<el-skeleton :loading="loading" animated>
|
||||
<template #template>
|
||||
<el-row :gutter="20" class="charts-ranking">
|
||||
<el-col :span="8" v-for="n in 3" :key="n">
|
||||
<el-skeleton-item variant="rect" style="width: 100%; height: 300px" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
<template #default>
|
||||
<el-row type="flex" :gutter="20" class="charts-ranking">
|
||||
<el-col :span="8">
|
||||
<el-card
|
||||
class="charts-card"
|
||||
:body-style="{ height: '300px', padding: '16px' }"
|
||||
>
|
||||
<template #header>报工趋势</template>
|
||||
<TrendChart :data="trendData" style="height: 100%;" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card
|
||||
class="charts-card"
|
||||
:body-style="{ height: '300px', padding: '16px' }"
|
||||
>
|
||||
<template #header>项目分布</template>
|
||||
<PieChart :data="pieData" style="height: 100%;" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card
|
||||
class="charts-card"
|
||||
:body-style="{ height: '300px', padding: '16px' }"
|
||||
>
|
||||
<template #header>人员排行榜</template>
|
||||
<RankList :ranking="ranking" style="flex:1; overflow:auto;" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
|
||||
<!-- Table & Tabs -->
|
||||
<el-card class="table-card">
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="数据总结" name="summary" />
|
||||
<el-tab-pane label="项目进度" name="progress" />
|
||||
<el-tab-pane label="历史记录" name="history" />
|
||||
</el-tabs>
|
||||
|
||||
<!-- 数据总结 -->
|
||||
<div v-show="activeTab==='summary'">
|
||||
<!-- 时间检索 -->
|
||||
<el-row align="middle" class="summary-filter" :gutter="12" style="margin-bottom:16px">
|
||||
<el-col :span="4">
|
||||
<el-date-picker
|
||||
v-model="dateRangeSummary"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="yyyy-MM-dd"
|
||||
unlink-panels
|
||||
@change="onSummaryDateChange"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-button type="primary" @click="fetchSummary">筛选</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 数据总结表格 -->
|
||||
<el-table
|
||||
:data="summaryList"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="nickName" label="姓名" />
|
||||
<el-table-column prop="deptName" label="部门"/>
|
||||
<el-table-column prop="inWorkNum" label="国内出差次数"/>
|
||||
<el-table-column prop="outWorkNum" label="国外出差次数" />
|
||||
<el-table-column prop="reportCount" label="报工次数" />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab==='progress'">
|
||||
<el-table :data="projectList" stripe>
|
||||
<el-table-column label="项目代号">
|
||||
<template #default="{row}">
|
||||
<el-tag v-if="row.projectCode==null" type="danger">无</el-tag>
|
||||
<el-tag v-else>{{ row.projectCode }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="projectNum" label="项目编号" />
|
||||
<el-table-column prop="projectName" label="项目名称" />
|
||||
<el-table-column prop="reportCount" label="参与人天" />
|
||||
<el-table-column label="剩余时间" align="center" prop="remainTime">
|
||||
<template slot-scope="scope">
|
||||
<div v-if="scope.row.remainTime>=0">
|
||||
<div v-if="scope.row.projectStatus===0">
|
||||
<span v-if="scope.row.remainTime>5">剩余{{ scope.row.remainTime }}天</span>
|
||||
<el-tag v-else-if="scope.row.remainTime<=5 &&scope.row.remainTime>3" type="warning">
|
||||
剩余{{ scope.row.remainTime }}天
|
||||
</el-tag>
|
||||
<el-tag v-else type="danger">剩余{{ scope.row.remainTime }}天</el-tag>
|
||||
</div>
|
||||
<div v-else>-</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-tag type="danger" v-if="scope.row.projectStatus===0">过期{{
|
||||
Math.abs(scope.row.remainTime)
|
||||
}}天
|
||||
</el-tag>
|
||||
<div v-else>-</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="projectStatus">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.sys_project_status" :value="scope.row.projectStatus"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab==='history'">
|
||||
<div class="history-table-header">
|
||||
<el-col>
|
||||
<export-dialog />
|
||||
</el-col>
|
||||
<el-button type="text" size="small" @click="loadMore">更多
|
||||
<i class="el-icon-arrow-right" style="margin-left: 4px;"></i></el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="filteredHistory" stripe style="margin-top:16px">
|
||||
<el-table-column prop="createTime" label="报工时间">
|
||||
<template slot-scope="scope">
|
||||
<span>{{parseTime(scope.row.createTime,'{y}-{m}-{d}')}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="deptName" label="涉及部门"/>
|
||||
<el-table-column prop="nickName" label="报工人"/>
|
||||
<el-table-column prop="projectName" label="相关项目" />
|
||||
<el-table-column prop="status" label="操作状态">
|
||||
<template #default="{row}">
|
||||
<span>✔️成功</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-view"
|
||||
@click="openDetail(scope.row)"
|
||||
>详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
|
||||
<project-report-detail
|
||||
v-model="detailVisible"
|
||||
:record="currentRow" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
getCardData,
|
||||
getPieData,
|
||||
getProjectData,
|
||||
getProjectReport,
|
||||
getRankData, getSummaryList,
|
||||
getTrendData,
|
||||
listClearProjectReport
|
||||
} from '@/api/oa/projectReport';
|
||||
import ExportDialog from './components/ExportDialog.vue';
|
||||
import PieChart from './components/PieChart.vue';
|
||||
import ProjectReportDetail from "./components/ProjectReportDetail.vue";
|
||||
import RankList from "./components/RankList.vue";
|
||||
import TrendChart from './components/TrendChart.vue';
|
||||
|
||||
export default {
|
||||
name: 'ReportDashboard',
|
||||
dicts:['sys_project_status'],
|
||||
components: {
|
||||
ProjectReportDetail,
|
||||
RankList,
|
||||
TrendChart,
|
||||
PieChart,
|
||||
ExportDialog
|
||||
},
|
||||
data() {
|
||||
const today = new Date();
|
||||
const yyyy = today.getFullYear();
|
||||
const mm = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(today.getDate()).padStart(2, '0');
|
||||
return {
|
||||
loading: true,
|
||||
dateRange: [`${yyyy}-${mm}-01`, `${yyyy}-${mm}-${dd}`],
|
||||
summaryCards: [],
|
||||
trendData: [],
|
||||
dateRangeSummary: [`${yyyy}-${mm}-01`, `${yyyy}-${mm}-${dd}`],
|
||||
pieData: [],
|
||||
ranking: [
|
||||
{ name:'张明华', role:'设备维护主管', score:98, status:'success' },
|
||||
{ name:'李建国', role:'生产线组长', score:95 },
|
||||
{ name:'王丽娜', role:'质检工程师', score:92 },
|
||||
{ name:'陈志远', role:'项目经理', score:88 },
|
||||
{ name:'刘思思', role:'工艺工程师', score:85 },
|
||||
{ name:'赵永康', role:'质检员', score:82 }
|
||||
],
|
||||
summaryList:[],
|
||||
|
||||
|
||||
projectList: [
|
||||
{ name:'智能制造系统升级', progress:95, efficiency:'95%', participants:24, status:'进行中' },
|
||||
{ name:'自动化生产线改造', progress:78, efficiency:'78%', participants:18, status:'延期' },
|
||||
{ name:'设备预防性维护', progress:98, efficiency:'98%', participants:12, status:'进行中' },
|
||||
{ name:'质量管理体系优化', progress:65, efficiency:'65%', participants:15, status:'异常' }
|
||||
],
|
||||
historyList: [ /* ...静态历史... */ ],
|
||||
historyFilter: { keyword:'', type:'' },
|
||||
historyTypes: ['报工提交','数据修改','审核操作'],
|
||||
activeTab: 'summary',
|
||||
currentRow:{},
|
||||
detailVisible: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredHistory() {
|
||||
return this.historyList.filter(item =>
|
||||
(!this.historyFilter.keyword || item.content.includes(this.historyFilter.keyword)) &&
|
||||
(!this.historyFilter.type || item.type === this.historyFilter.type)
|
||||
);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 时间选择变化时,如果想自动刷新也可在这里调用 fetchSummary
|
||||
onSummaryDateChange() {
|
||||
this.fetchSummary();
|
||||
},
|
||||
// 拉取"数据总结"数据的接口方法
|
||||
fetchSummary() {
|
||||
getSummaryList(this.dateRangeSummary[0],this.dateRangeSummary[1]).then(res=>{
|
||||
this.summaryList = res.data
|
||||
})
|
||||
},
|
||||
|
||||
loadMore(){
|
||||
this.$router.push('/hint/projectReport')
|
||||
},
|
||||
/** 查询详情 */
|
||||
openDetail(row) {
|
||||
getProjectReport(row.reportId).then(response => {
|
||||
this.currentRow = response.data; // row 为表格中选中的记录
|
||||
this.detailVisible = true;
|
||||
})
|
||||
|
||||
},
|
||||
handleSearch(){
|
||||
this.getBaseData()
|
||||
},
|
||||
async getBaseData() {
|
||||
this.fetchSummary()
|
||||
const [ cardRes, trendRes, pieRes,rankRes,ProjRes,reportRes ] = await Promise.all([
|
||||
getCardData(),
|
||||
getTrendData(this.dateRange[0], this.dateRange[1]),
|
||||
getPieData(this.dateRange[0], this.dateRange[1]),
|
||||
getRankData(this.dateRange[0], this.dateRange[1]),
|
||||
getProjectData(this.dateRange[0], this.dateRange[1]),
|
||||
listClearProjectReport(this.dateRange[0], this.dateRange[1])
|
||||
]);
|
||||
// 处理卡片
|
||||
const { todayCount, todayCountChange: yc,
|
||||
inProgressProjects, projectChange: yp,
|
||||
completionRate, completionChange: ycr,
|
||||
exceptions
|
||||
} = cardRes.data;
|
||||
const pct = (t,y) => y===0 ? '—' : (((t-y)/y)*100).toFixed(1) + '%';
|
||||
this.summaryCards = [
|
||||
{ title:'今日报工人数', value:todayCount, displayChange:`${pct(todayCount,yc)} 较昨日`, changeClass: todayCount-yc>=0?'up':'down', icon:'el-icon-user', iconColor:'#67C23A' },
|
||||
{ title:'进行中项目', value:inProgressProjects, displayChange:`${pct(inProgressProjects,yp)} 较昨日`, changeClass: inProgressProjects-yp>=0?'up':'down', icon:'el-icon-document', iconColor:'#409EFF' },
|
||||
{ title:'本月报工', value:completionRate, displayChange:`${pct(completionRate,ycr)} 较上月`, changeClass: completionRate-ycr>=0?'up':'down', icon:'el-icon-data-analysis', iconColor:'#E6A23C' },
|
||||
{ title:'异常预警', value:exceptions, exception:true, icon:'el-icon-warning', iconColor:'#F56C6C' }
|
||||
];
|
||||
// 赋值图表数据
|
||||
this.trendData = trendRes.data;
|
||||
this.pieData = pieRes.data;
|
||||
this.ranking = rankRes.data;
|
||||
this.projectList = ProjRes.data;
|
||||
console.log(this.projectList)
|
||||
this.historyList = reportRes.data;
|
||||
this.loading = false;
|
||||
},
|
||||
statusTagType(s) {
|
||||
return s === '进行中' ? 'success'
|
||||
: s === '延期' ? 'warning'
|
||||
: s === '异常' ? 'danger'
|
||||
: '';
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.getBaseData();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.report-dashboard { padding:20px; background:#f5f7fa; }
|
||||
.header-row { margin-bottom:20px; }
|
||||
.export-button { margin-left:10px; }
|
||||
.summary-cards { margin-bottom:20px; }
|
||||
.card-content { position:relative; padding:20px; }
|
||||
.card-value { font-size:28px; margin:10px 0; }
|
||||
.card-change { font-size:12px; }
|
||||
.card-change.up { color:#67C23A; }
|
||||
.card-change.down { color:#F56C6C; }
|
||||
.card-icon { position:absolute; top:20px; right:20px; font-size:28px; }
|
||||
.card-action { margin-top:10px; }
|
||||
.handle { color:#F56C6C; margin-right:10px; }
|
||||
.charts-ranking { margin-bottom:20px; }
|
||||
.charts-card { display:flex; flex-direction:column; }
|
||||
.table-card { margin-top:20px; }
|
||||
.history-filter { margin-bottom:12px; }
|
||||
.status-icon.success { color:#67C23A; margin-right:4px; }
|
||||
.status-icon.danger { color:#F56C6C; margin-right:4px; }
|
||||
.more { float:right; }
|
||||
/* 组件内的 .chart-container 在各自组件中定义 */
|
||||
.history-table-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0 5px;
|
||||
}
|
||||
/* 如果想让按钮 hover 时高亮箭头,可以加: */
|
||||
.history-table-header .el-button:hover .el-icon-arrow-right {
|
||||
color: #409EFF;
|
||||
}
|
||||
</style>
|
||||
562
ruoyi-ui/src/views/oa/project/report/index.vue
Normal file
562
ruoyi-ui/src/views/oa/project/report/index.vue
Normal file
@@ -0,0 +1,562 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="96px">
|
||||
<el-form-item label="项目名称" prop="projectId">
|
||||
<project-select
|
||||
v-model="queryParams.projectId"
|
||||
style="width: 200px; margin-right: 10px;"
|
||||
></project-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="projectCode">
|
||||
<el-input
|
||||
v-model="queryParams.projectCode"
|
||||
placeholder="请输入项目代号"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="报工人" prop="nickName">
|
||||
<el-input
|
||||
v-model="queryParams.nickName"
|
||||
placeholder="请输入报工人"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="经办部门" prop="deptId">
|
||||
<el-select v-model="queryParams.deptId" filterable placeholder="请选择经办部门">
|
||||
<el-option
|
||||
v-for="item in deptList"
|
||||
:key="item.deptId"
|
||||
:label="item.deptName"
|
||||
:value="item.deptId">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="报工时间" prop="createTime">
|
||||
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
type="date"
|
||||
placeholder="请输入报工时间"
|
||||
value-format="yyyy-MM-dd"
|
||||
>
|
||||
</el-date-picker>
|
||||
</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"
|
||||
v-hasPermi="['oa:projectReport:add']"
|
||||
>新增
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
icon="el-icon-edit"
|
||||
size="mini"
|
||||
:disabled="single"
|
||||
@click="handleUpdate"
|
||||
v-hasPermi="['oa:projectReport:edit']"
|
||||
>修改
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
icon="el-icon-delete"
|
||||
size="mini"
|
||||
:disabled="multiple"
|
||||
@click="handleDelete"
|
||||
v-hasPermi="['oa:projectReport:remove']"
|
||||
>删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
icon="el-icon-download"
|
||||
size="mini"
|
||||
@click="handleExport"
|
||||
v-hasPermi="['oa:projectReport:export']"
|
||||
>导出
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="el-icon-plus"
|
||||
size="mini"
|
||||
@click="handleSupp"
|
||||
v-hasPermi="['oa:projectReport:edit']"
|
||||
>补录</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="projectReportList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center"/>
|
||||
<el-table-column label="项目代号" prop="projectCode" width="100" 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="项目名称" align="center" width="150" prop="projectName">
|
||||
<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="项目编号" align="left" prop="projectNum"/>
|
||||
<el-table-column label="经办人" align="center" prop="nickName">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.nickName }}
|
||||
<template v-if="scope.row.deptName!=null">
|
||||
({{ scope.row.deptName }})
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="工作地点" align="center" prop="workPlace"/>
|
||||
<el-table-column label="国内/国外" align="center" prop="workType">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.workType===0?'':'warning'">{{ scope.row.workType===0?'国内':'国外' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报工时间" align="center" prop="createTime">
|
||||
<template slot-scope="scope">
|
||||
<span>{{parseTime(scope.row.createTime,'{y}-{m}-{d}')}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="remark"/>
|
||||
<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-view"
|
||||
@click="openDetail(scope.row)"
|
||||
>详情
|
||||
</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
@click="handleUpdate(scope.row)"
|
||||
v-hasPermi="['oa:projectReport:edit']"
|
||||
>修改
|
||||
</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
v-hasPermi="['oa:projectReport:remove']"
|
||||
>删除
|
||||
</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="800px" append-to-body>
|
||||
<!-- 今日报工提示 -->
|
||||
<el-alert
|
||||
v-if="hasTodayReport"
|
||||
title="今日已报工"
|
||||
type="warning"
|
||||
description="您今天已有报工记录,提交后将修改今日报工内容"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px;">
|
||||
</el-alert>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="96px">
|
||||
|
||||
<el-form-item label="工作地点" prop="workPlace">
|
||||
<el-input v-model="form.workPlace" placeholder="请输入工作地点"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否出差" prop="isTrip">
|
||||
<el-radio-group v-model="form.isTrip">
|
||||
<el-radio :label="1">是</el-radio>
|
||||
<el-radio :label="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<template v-if="form.isTrip===1">
|
||||
<el-form-item label="国内/国外" prop="workType">
|
||||
<el-radio-group v-model="form.workType">
|
||||
<el-radio :label="0">国内</el-radio>
|
||||
<el-radio :label="1">国外</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item label="请选择项目" prop="projectId">
|
||||
<project-select
|
||||
v-model="form.projectId"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="报工内容">
|
||||
<editor v-model="form.content" :min-height="192"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" placeholder="请输入备注"/>
|
||||
</el-form-item>
|
||||
<!-- 只在编辑模式或者补录模式下下显示报工时间 -->
|
||||
<el-form-item v-if="form.reportId || suppVisible" label="报工时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="form.createTime"
|
||||
type="datetime"
|
||||
placeholder="请选择报工时间"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
format="yyyy-MM-dd HH:mm:ss"
|
||||
>
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="suppVisible" label="报工人" prop="userId">
|
||||
<el-select v-model="form.userId" filterable clearable placeholder="请选择报工人">
|
||||
<el-option v-for="item in userList" :key="item.value" :label="item.label" :value="item.value"></el-option>
|
||||
</el-select>
|
||||
</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>
|
||||
|
||||
<project-report-detail
|
||||
v-model="detailVisible"
|
||||
:record="currentRow" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
addProjectReport,
|
||||
delProjectReport,
|
||||
getProjectReport,
|
||||
getTodayProjectReport,
|
||||
listProjectReport,
|
||||
suppProjectReport,
|
||||
updateProjectReport
|
||||
} from "@/api/oa/projectReport";
|
||||
import { listDept } from "@/api/system/dept";
|
||||
import { listUser } from "@/api/system/user";
|
||||
import ProjectSelect from "@/components/fad-service/ProjectSelect";
|
||||
import ProjectReportDetail from "@/views/oa/project/report/components/ProjectReportDetail.vue";
|
||||
|
||||
export default {
|
||||
name: "ProjectReport",
|
||||
components: {ProjectReportDetail, ProjectSelect},
|
||||
data() {
|
||||
return {
|
||||
// 按钮loading
|
||||
buttonLoading: false,
|
||||
detailVisible: false,
|
||||
// 今日报工状态
|
||||
hasTodayReport: false,
|
||||
todayReportId: null,
|
||||
// 遮罩层
|
||||
loading: true,
|
||||
// 选中数组
|
||||
ids: [],
|
||||
// 非单个禁用
|
||||
single: true,
|
||||
// 非多个禁用
|
||||
multiple: true,
|
||||
// 显示搜索条件
|
||||
showSearch: true,
|
||||
// 总条数
|
||||
total: 0,
|
||||
// 项目报工表格数据
|
||||
projectReportList: [],
|
||||
// 弹出层标题
|
||||
title: "",
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
handler: undefined,
|
||||
workPlace: undefined,
|
||||
projectId: null,
|
||||
content: undefined,
|
||||
},
|
||||
deptList: [],
|
||||
currentRow:{},
|
||||
suppVisible: false,
|
||||
userList: [],
|
||||
// 表单参数
|
||||
form: {},
|
||||
// 表单校验
|
||||
rules: {
|
||||
workPlace: [
|
||||
{required: true, message: "工作地点不能为空", trigger: "blur"}
|
||||
],
|
||||
projectId: [
|
||||
{required: true, message: "项目 ID不能为空", trigger: "blur"}
|
||||
],
|
||||
content: [
|
||||
{required: true, message: "报工内容不能为空", trigger: "blur"}
|
||||
],
|
||||
isTrip: [
|
||||
{required: true, message: '请选择是否出差', trigger: 'change'}
|
||||
],
|
||||
workType: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择出差地点',
|
||||
trigger: 'change',
|
||||
validator: (rule, value, callback) => {
|
||||
if (this.form.isTrip === 1 && !value && value !== 0) {
|
||||
callback(new Error('请选择出差地点'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getList();
|
||||
this.getUserList();
|
||||
},
|
||||
methods: {
|
||||
/** 查询详情 */
|
||||
openDetail(row) {
|
||||
getProjectReport(row.reportId).then(response => {
|
||||
this.currentRow = response.data; // row 为表格中选中的记录
|
||||
this.detailVisible = true;
|
||||
})
|
||||
},
|
||||
|
||||
handleSupp() {
|
||||
this.suppVisible = true;
|
||||
this.reset();
|
||||
this.open = true;
|
||||
this.title = "补录项目报工";
|
||||
},
|
||||
|
||||
/** 检查今日报工 */
|
||||
checkTodayReport() {
|
||||
getTodayProjectReport().then(response => {
|
||||
if (response.data && response.data.reportId) {
|
||||
this.hasTodayReport = true;
|
||||
this.todayReportId = response.data.reportId;
|
||||
// 如果有今日报工记录,填充表单数据
|
||||
this.form = {
|
||||
...this.form,
|
||||
...response.data
|
||||
};
|
||||
} else {
|
||||
this.hasTodayReport = false;
|
||||
this.todayReportId = null;
|
||||
}
|
||||
}).catch(() => {
|
||||
this.hasTodayReport = false;
|
||||
this.todayReportId = null;
|
||||
});
|
||||
},
|
||||
|
||||
getDeptList() {
|
||||
listDept().then(res => {
|
||||
this.deptList = res.data;
|
||||
})
|
||||
},
|
||||
getUserList() {
|
||||
listUser({
|
||||
pageNum: 1,
|
||||
pageSize: 1000
|
||||
}).then(res => {
|
||||
this.userList = res.rows.map(item => ({
|
||||
value: item.userId,
|
||||
label: item.nickName
|
||||
}));
|
||||
})
|
||||
},
|
||||
/** 查询项目报工列表 */
|
||||
getList() {
|
||||
this.loading = true;
|
||||
listProjectReport(this.queryParams).then(response => {
|
||||
this.projectReportList = response.rows;
|
||||
this.total = response.total;
|
||||
this.loading = false;
|
||||
this.getDeptList();
|
||||
});
|
||||
|
||||
},
|
||||
// 取消按钮
|
||||
cancel() {
|
||||
this.open = false;
|
||||
this.reset();
|
||||
},
|
||||
// 表单重置
|
||||
reset() {
|
||||
this.form = {
|
||||
reportId: undefined,
|
||||
handler: undefined,
|
||||
workPlace: undefined,
|
||||
projectId: undefined,
|
||||
content: undefined,
|
||||
createTime: undefined,
|
||||
createBy: undefined,
|
||||
userId: undefined,
|
||||
updateTime: undefined,
|
||||
updateBy: undefined,
|
||||
delFlag: undefined,
|
||||
remark: undefined,
|
||||
isTrip: undefined,
|
||||
workType: undefined,
|
||||
};
|
||||
// 重置今日报工状态
|
||||
this.hasTodayReport = false;
|
||||
this.todayReportId = null;
|
||||
// 安全地重置表单,避免字段引用错误
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.form) {
|
||||
this.$refs.form.resetFields();
|
||||
}
|
||||
});
|
||||
},
|
||||
/** 搜索按钮操作 */
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1;
|
||||
this.getList();
|
||||
},
|
||||
/** 重置按钮操作 */
|
||||
resetQuery() {
|
||||
this.resetForm("queryForm");
|
||||
this.handleQuery();
|
||||
},
|
||||
// 多选框选中数据
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.reportId)
|
||||
this.single = selection.length !== 1
|
||||
this.multiple = !selection.length
|
||||
},
|
||||
/** 新增按钮操作 */
|
||||
handleAdd() {
|
||||
this.reset();
|
||||
this.suppVisible = false;
|
||||
this.checkTodayReport();
|
||||
this.open = true;
|
||||
this.title = "添加项目报工";
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
handleUpdate(row) {
|
||||
this.suppVisible = false;
|
||||
this.loading = true;
|
||||
this.reset();
|
||||
const reportId = row.reportId || this.ids
|
||||
getProjectReport(reportId).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.suppVisible) {
|
||||
suppProjectReport(this.form).then(response => {
|
||||
this.$modal.msgSuccess("补录成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
this.suppVisible = false;
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 如果有今日报工记录且当前是新增模式,则更新今日报工
|
||||
if (this.hasTodayReport && !this.form.reportId) {
|
||||
const updateData = { ...this.form, reportId: this.todayReportId };
|
||||
updateProjectReport({ ...updateData, time: this.form.createTime }).then(response => {
|
||||
this.$modal.msgSuccess("修改今日报工成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
} else if (this.form.reportId != null) {
|
||||
updateProjectReport({ ...this.form, time: this.form.createTime }).then(response => {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
} else {
|
||||
addProjectReport(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
const reportIds = row.reportId || this.ids;
|
||||
this.$modal.confirm('是否确认删除项目报工编号为"' + reportIds + '"的数据项?').then(() => {
|
||||
this.loading = true;
|
||||
return delProjectReport(reportIds);
|
||||
}).then(() => {
|
||||
this.loading = false;
|
||||
this.getList();
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
/** 导出按钮操作 */
|
||||
handleExport() {
|
||||
this.download('oa/projectReport/export', {
|
||||
...this.queryParams
|
||||
}, `projectReport_${new Date().getTime()}.xlsx`)
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
476
ruoyi-ui/src/views/oa/project/report/my.vue
Normal file
476
ruoyi-ui/src/views/oa/project/report/my.vue
Normal file
@@ -0,0 +1,476 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="96px">
|
||||
<el-form-item label="项目名称" prop="projectId">
|
||||
<project-select v-model="queryParams.projectId" style="width: 200px; margin-right: 10px;"></project-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="projectCode">
|
||||
<el-input v-model="queryParams.projectCode" placeholder="请输入项目代号" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="报工人" prop="nickName">
|
||||
<el-input v-model="queryParams.nickName" placeholder="请输入报工人" clearable @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="经办部门" prop="deptId">
|
||||
<el-select v-model="queryParams.deptId" filterable placeholder="请选择经办部门">
|
||||
<el-option v-for="item in deptList" :key="item.deptId" :label="item.deptName" :value="item.deptId">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="报工时间" prop="createTime">
|
||||
<el-date-picker v-model="queryParams.createTime" type="date" placeholder="请输入报工时间" value-format="yyyy-MM-dd">
|
||||
</el-date-picker>
|
||||
</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"
|
||||
v-hasPermi="['oa:projectReport:add']">新增
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate"
|
||||
v-hasPermi="['oa:projectReport:edit']">修改
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete"
|
||||
v-hasPermi="['oa:projectReport:remove']">删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
|
||||
v-hasPermi="['oa:projectReport:export']">导出
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleSupp"
|
||||
v-hasPermi="['oa:projectReport:edit']">补录</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<!-- 替换表格为日历视图 -->
|
||||
<el-calendar v-loading="loading" ref="calendar">
|
||||
<!-- 自定义日历单元格内容 -->
|
||||
<template slot="dateCell" slot-scope="{date, data}">
|
||||
<!-- 格式化日期为 yyyy-MM-dd 用于匹配报工记录 -->
|
||||
<div class="calendar-day">
|
||||
<span class="date">{{ data.day.split('-')[2] }}</span>
|
||||
<!-- 显示当天的报工记录 -->
|
||||
<div class="report-item" v-for="item in getDayReports(formatDate(date))" :key="item.reportId"
|
||||
@click="openDetail(item)">
|
||||
<el-tag size="mini" type="info">{{ item.projectName || '无项目' }}</el-tag>
|
||||
<span class="report-desc">{{ item.workPlace }} ({{ item.nickName }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-calendar>
|
||||
|
||||
<!-- 保留分页(日历视图仍需分页逻辑,仅隐藏表格分页展示) -->
|
||||
<!-- <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||||
@pagination="getList" style="margin-top: 20px;" /> -->
|
||||
|
||||
<!-- 添加或修改项目报工对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
|
||||
<!-- 今日报工提示 -->
|
||||
<el-alert v-if="hasTodayReport" title="今日已报工" type="warning" description="您今天已有报工记录,提交后将修改今日报工内容" show-icon
|
||||
:closable="false" style="margin-bottom: 20px;">
|
||||
</el-alert>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="96px">
|
||||
<el-form-item label="工作地点" prop="workPlace">
|
||||
<el-input v-model="form.workPlace" placeholder="请输入工作地点" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否出差" prop="isTrip">
|
||||
<el-radio-group v-model="form.isTrip">
|
||||
<el-radio :label="1">是</el-radio>
|
||||
<el-radio :label="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<template v-if="form.isTrip === 1">
|
||||
<el-form-item label="国内/国外" prop="workType">
|
||||
<el-radio-group v-model="form.workType">
|
||||
<el-radio :label="0">国内</el-radio>
|
||||
<el-radio :label="1">国外</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item label="请选择项目" prop="projectId">
|
||||
<project-select v-model="form.projectId" style="width: 100%;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="报工内容">
|
||||
<editor v-model="form.content" :min-height="192" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
<!-- 只在编辑模式或者补录模式下下显示报工时间 -->
|
||||
<el-form-item v-if="form.reportId || suppVisible" label="报工时间" prop="createTime">
|
||||
<el-date-picker v-model="form.createTime" type="datetime" placeholder="请选择报工时间"
|
||||
value-format="yyyy-MM-dd HH:mm:ss" format="yyyy-MM-dd HH:mm:ss">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="suppVisible" label="报工人" prop="userId">
|
||||
<el-select v-model="form.userId" filterable clearable placeholder="请选择报工人">
|
||||
<el-option v-for="item in userList" :key="item.value" :label="item.label" :value="item.value"></el-option>
|
||||
</el-select>
|
||||
</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>
|
||||
|
||||
<project-report-detail v-model="detailVisible" :record="currentRow" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
addProjectReport,
|
||||
delProjectReport,
|
||||
getProjectReport,
|
||||
getTodayProjectReport,
|
||||
listProjectReport,
|
||||
suppProjectReport,
|
||||
updateProjectReport
|
||||
} from "@/api/oa/projectReport";
|
||||
import { listDept } from "@/api/system/dept";
|
||||
import { listUser } from "@/api/system/user";
|
||||
import ProjectSelect from "@/components/fad-service/ProjectSelect";
|
||||
import ProjectReportDetail from "@/views/oa/project/report/components/ProjectReportDetail.vue";
|
||||
|
||||
export default {
|
||||
name: "ProjectReport",
|
||||
components: { ProjectReportDetail, ProjectSelect },
|
||||
data () {
|
||||
return {
|
||||
// 按钮loading
|
||||
buttonLoading: false,
|
||||
detailVisible: false,
|
||||
// 今日报工状态
|
||||
hasTodayReport: false,
|
||||
todayReportId: null,
|
||||
// 遮罩层
|
||||
loading: true,
|
||||
// 选中数组
|
||||
ids: [],
|
||||
// 非单个禁用
|
||||
single: true,
|
||||
// 非多个禁用
|
||||
multiple: true,
|
||||
// 显示搜索条件
|
||||
showSearch: true,
|
||||
// 总条数
|
||||
total: 0,
|
||||
// 项目报工表格数据(日历视图仍需此数组存储数据)
|
||||
projectReportList: [],
|
||||
// 弹出层标题
|
||||
title: "",
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
// 查询参数 - 核心修改:pageSize改为1000,nickName默认赋值当前用户
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 1000, // 改为1000
|
||||
handler: undefined,
|
||||
workPlace: undefined,
|
||||
projectId: null,
|
||||
content: undefined,
|
||||
nickName: this.$store.getters.nickName // 默认筛选当前用户的报工记录
|
||||
},
|
||||
deptList: [],
|
||||
currentRow: {},
|
||||
suppVisible: false,
|
||||
userList: [],
|
||||
// 表单参数
|
||||
form: {},
|
||||
// 表单校验
|
||||
rules: {
|
||||
workPlace: [
|
||||
{ required: true, message: "工作地点不能为空", trigger: "blur" }
|
||||
],
|
||||
projectId: [
|
||||
{ required: true, message: "项目 ID不能为空", trigger: "blur" }
|
||||
],
|
||||
content: [
|
||||
{ required: true, message: "报工内容不能为空", trigger: "blur" }
|
||||
],
|
||||
isTrip: [
|
||||
{ required: true, message: '请选择是否出差', trigger: 'change' }
|
||||
],
|
||||
workType: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择出差地点',
|
||||
trigger: 'change',
|
||||
validator: (rule, value, callback) => {
|
||||
if (this.form.isTrip === 1 && !value && value !== 0) {
|
||||
callback(new Error('请选择出差地点'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
created () {
|
||||
this.getList();
|
||||
this.getUserList();
|
||||
},
|
||||
methods: {
|
||||
/** 格式化日期为 yyyy-MM-dd 格式 */
|
||||
formatDate (date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
},
|
||||
/** 获取指定日期的报工记录 */
|
||||
getDayReports (dateStr) {
|
||||
return this.projectReportList.filter(item => {
|
||||
// 提取报工记录的日期部分(忽略时分秒)
|
||||
const reportDate = item.createTime ? item.createTime.split(' ')[0] : '';
|
||||
return reportDate === dateStr;
|
||||
});
|
||||
},
|
||||
/** 查询详情 */
|
||||
openDetail (row) {
|
||||
getProjectReport(row.reportId).then(response => {
|
||||
this.currentRow = response.data;
|
||||
this.detailVisible = true;
|
||||
})
|
||||
},
|
||||
|
||||
handleSupp () {
|
||||
this.suppVisible = true;
|
||||
this.reset();
|
||||
this.open = true;
|
||||
this.title = "补录项目报工";
|
||||
},
|
||||
|
||||
/** 检查今日报工 */
|
||||
checkTodayReport () {
|
||||
getTodayProjectReport().then(response => {
|
||||
if (response.data && response.data.reportId) {
|
||||
this.hasTodayReport = true;
|
||||
this.todayReportId = response.data.reportId;
|
||||
this.form = {
|
||||
...this.form,
|
||||
...response.data
|
||||
};
|
||||
} else {
|
||||
this.hasTodayReport = false;
|
||||
this.todayReportId = null;
|
||||
}
|
||||
}).catch(() => {
|
||||
this.hasTodayReport = false;
|
||||
this.todayReportId = null;
|
||||
});
|
||||
},
|
||||
|
||||
getDeptList () {
|
||||
listDept().then(res => {
|
||||
this.deptList = res.data;
|
||||
})
|
||||
},
|
||||
getUserList () {
|
||||
listUser({
|
||||
pageNum: 1,
|
||||
pageSize: 1000
|
||||
}).then(res => {
|
||||
this.userList = res.rows.map(item => ({
|
||||
value: item.userId,
|
||||
label: item.nickName
|
||||
}));
|
||||
})
|
||||
},
|
||||
/** 查询项目报工列表 */
|
||||
getList () {
|
||||
this.loading = true;
|
||||
listProjectReport(this.queryParams).then(response => {
|
||||
this.projectReportList = response.rows;
|
||||
this.total = response.total;
|
||||
this.loading = false;
|
||||
this.getDeptList();
|
||||
});
|
||||
},
|
||||
// 取消按钮
|
||||
cancel () {
|
||||
this.open = false;
|
||||
this.reset();
|
||||
},
|
||||
// 表单重置
|
||||
reset () {
|
||||
this.form = {
|
||||
reportId: undefined,
|
||||
handler: undefined,
|
||||
workPlace: undefined,
|
||||
projectId: undefined,
|
||||
content: undefined,
|
||||
createTime: undefined,
|
||||
createBy: undefined,
|
||||
userId: undefined,
|
||||
updateTime: undefined,
|
||||
updateBy: undefined,
|
||||
delFlag: undefined,
|
||||
remark: undefined,
|
||||
isTrip: undefined,
|
||||
workType: undefined,
|
||||
};
|
||||
this.hasTodayReport = false;
|
||||
this.todayReportId = null;
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.form) {
|
||||
this.$refs.form.resetFields();
|
||||
}
|
||||
});
|
||||
},
|
||||
/** 搜索按钮操作 */
|
||||
handleQuery () {
|
||||
this.queryParams.pageNum = 1;
|
||||
this.getList();
|
||||
},
|
||||
/** 重置按钮操作 - 核心修改:重置后仍保留报工人默认值 */
|
||||
resetQuery () {
|
||||
this.resetForm("queryForm");
|
||||
// 重置后恢复报工人默认值为当前用户
|
||||
this.queryParams.nickName = this.$store.getters.nickName;
|
||||
this.handleQuery();
|
||||
},
|
||||
// 多选框选中数据(日历视图下可保留,用于批量操作)
|
||||
handleSelectionChange (selection) {
|
||||
this.ids = selection.map(item => item.reportId)
|
||||
this.single = selection.length !== 1
|
||||
this.multiple = !selection.length
|
||||
},
|
||||
/** 新增按钮操作 */
|
||||
handleAdd () {
|
||||
this.reset();
|
||||
this.suppVisible = false;
|
||||
this.checkTodayReport();
|
||||
this.open = true;
|
||||
this.title = "添加项目报工";
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
handleUpdate (row) {
|
||||
this.suppVisible = false;
|
||||
this.loading = true;
|
||||
this.reset();
|
||||
const reportId = row.reportId || this.ids
|
||||
getProjectReport(reportId).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.suppVisible) {
|
||||
suppProjectReport(this.form).then(response => {
|
||||
this.$modal.msgSuccess("补录成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
this.suppVisible = false;
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 今日报工更新
|
||||
if (this.hasTodayReport && !this.form.reportId) {
|
||||
const updateData = { ...this.form, reportId: this.todayReportId };
|
||||
updateProjectReport({ ...updateData, time: this.form.createTime }).then(response => {
|
||||
this.$modal.msgSuccess("修改今日报工成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
} else if (this.form.reportId != null) {
|
||||
updateProjectReport({ ...this.form, time: this.form.createTime }).then(response => {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
} else {
|
||||
addProjectReport(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
}).finally(() => {
|
||||
this.buttonLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete (row) {
|
||||
const reportIds = row.reportId || this.ids;
|
||||
this.$modal.confirm('是否确认删除项目报工编号为"' + reportIds + '"的数据项?').then(() => {
|
||||
this.loading = true;
|
||||
return delProjectReport(reportIds);
|
||||
}).then(() => {
|
||||
this.loading = false;
|
||||
this.getList();
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
/** 导出按钮操作 */
|
||||
handleExport () {
|
||||
this.download('oa/projectReport/export', {
|
||||
...this.queryParams
|
||||
}, `projectReport_${new Date().getTime()}.xlsx`)
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 日历单元格样式优化 */
|
||||
.calendar-day {
|
||||
height: 100%;
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.report-item {
|
||||
margin-bottom: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.report-desc {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user