Merge branch 'hotfix' of http://carsafe.uicp.cn/Front_Team/SPDM into hotfix

This commit is contained in:
魏保林
2026-03-27 10:51:55 +08:00
7 changed files with 527 additions and 23 deletions

View File

@@ -26,6 +26,7 @@ import Dialog from '@/components/common/dialog/index.vue';
import TableForm from '@/components/common/table/tableForm.vue';
import { uploadBigFile } from '@/utils/file';
import emitter from '@/utils/eventBus';
import { ElMessage } from 'element-plus';
const emit = defineEmits(['finished']);
@@ -81,6 +82,10 @@ watch(
);
const addFileFun = (file: any) => {
if (file.size <= 0) {
ElMessage.warning('大小为0的文件已被过滤');
return false;
}
formData.value.files.push(file);
formData.value.fileTypeDictValue = props.fileType;
formData.value.disciplineDictValue = props.discipline;

View File

@@ -44,8 +44,29 @@
@change="filterChange"
/>
</el-form-item>
<el-form-item v-if="filterItems.includes('workspace') && !projectMultiple" label="工位">
<el-select
v-model="formData.workspace"
:placeholder="!formData.tag1 ? '请先选择项目' : '请选择工位'"
clearable
:disabled="!formData.tag1"
@change="filterChange"
>
<el-option
v-for="item in workspaceOptions"
:key="item.uuid"
:label="item.nodeName"
:value="item.uuid"
/>
</el-select>
</el-form-item>
<el-form-item v-if="filterItems.includes('discipline')" label="学科">
<el-select v-model="formData.discipline" clearable @change="filterChange">
<el-select
v-model="formData.discipline"
placeholder="请选择学科"
clearable
@change="filterChange"
>
<el-option
v-for="item in DISCIPLINE_TYPE.A"
:label="item.label"
@@ -80,6 +101,8 @@ import userSelectByGroup from '@/components/common/userSelect/userSelectByGroup.
import ProjectSelect from '@/components/common/projectSelect/index.vue';
import { userQueryGroupApi } from '@/api/system/user';
import { useDict } from '@/utils/useDict';
import { getChildrenNodeListApi } from '@/api/project/node';
import { debounce } from 'lodash-es';
const { DISCIPLINE_TYPE } = useDict('DISCIPLINE_TYPE');
const props = defineProps({
@@ -129,6 +152,7 @@ const formData = ref<any>({
userGroupId: '',
userId: '',
tag1: '',
workspace: '',
discipline: '',
dateRange: [...props.defaultDateRange],
});
@@ -146,6 +170,7 @@ const formatEmitData = () => {
props.filterItems.includes('projectCode')) && {
tag1: formData.value.tag1,
}),
...(props.filterItems.includes('workspace') && { workspace: formData.value.workspace }),
...(props.filterItems.includes('discipline') && { discipline: formData.value.discipline }),
...(props.filterItems.includes('dateRange') && { dateRange: formData.value.dateRange }),
...props.extraFilters,
@@ -176,11 +201,46 @@ const handleUserGroupChange = () => {
emit('update', formatEmitData());
};
// 筛选条件改变
const filterChange = () => {
const filterChange = debounce(() => {
emit('update', formatEmitData());
}, 300);
// 联动工位,当前项目下的工位
const workspaceOptions = ref<any>([]);
watch(
() => formData.value.tag1,
async (val) => {
formData.value.workspace = '';
workspaceOptions.value = [];
if (val) {
const params = {
nodeId: val,
nodeType: 'workspace',
};
const res = await getChildrenNodeListApi(params);
workspaceOptions.value = formatWorkspaceList(res.data);
}
// 触发更新 会导致更新两次 还是给filterChange加防抖等待一下吧
// await nextTick();
// filterChange();
}
);
// 工位列表格式化分组
const formatWorkspaceList = (list: any) => {
const mergedMap = new Map();
list.forEach((item: any) => {
const { nodeName, uuid } = item;
if (mergedMap.has(nodeName)) {
const existing = mergedMap.get(nodeName);
existing.uuid += `,${uuid}`;
} else {
mergedMap.set(nodeName, { nodeName, uuid });
}
});
return Array.from(mergedMap.values());
};
// 监听插槽中的额外筛选条件
// 【兼容外部的筛选条件】监听插槽中的额外筛选条件
watch(
() => props.extraFilters,
async () => {
@@ -192,7 +252,7 @@ watch(
{ deep: true }
);
const hasFilterField = ref(false);
// 监听formData如果有属性是有值的则hasFilterField为true否则为false
// 【更新筛选图标】 监听formData如果有属性是有值的则hasFilterField为true否则为false
watch(
() => formData.value,
(newVal) => {

View File

@@ -35,11 +35,12 @@
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { ref, watch, nextTick } from 'vue';
import { View, Delete, Download } from '@element-plus/icons-vue';
import FilePreview from '@/components/common/filePreview/index.vue';
import { downloadFileById, fileUploadAllocationIconFun } from '@/utils/file';
import { isArray } from 'lodash-es';
import { ElMessage } from 'element-plus';
interface Props {
modelValue: any;
@@ -85,8 +86,17 @@ const downloadFun = (data: any) => {
downloadFileById(data.fileId);
};
const changeFun = (data: any) => {
const changeFun = (data: any, dataList: any) => {
emit('change', data);
const fileNum = dataList.filter((item: any) => item.name === data.name);
if (fileNum.length > 1) {
ElMessage.warning('不允许上传重名文件');
setTimeout(() => {
nextTick(() => {
removeFun(fileList.value.length - 1);
});
}, 0);
}
};
const removeFun = (index: number) => {

View File

@@ -14,6 +14,12 @@
>
<template v-if="showLeftOptions" #leftOptions>
<div class="operate-box">
<el-button
v-if="paramType === 'run' && canUpdate"
type="primary"
@click="ArchiveRunDataFun"
>归档</el-button
>
<el-button v-if="canUpdate" type="primary" @click="openAddPerformanceWindFun"
>新增</el-button
>
@@ -73,17 +79,25 @@
:export-api="exportPerformanceByScriptApi"
showCheckbox
hidePagination
:actionList="showLeftOptions ? actionList : []"
:actionList="showLeftOptions && canUpdate ? actionList : []"
:export-params="excelParams"
:full-height="fullHeight"
:show-setting="false"
>
<template v-if="showLeftOptions" #leftOptions>
<div class="operate-box">
<el-button type="primary" @click="ArchiveRunDataFun">归档</el-button>
<el-button type="primary" @click="openAddPerformanceWindFun">新增</el-button>
<el-button type="danger" @click="deleteFun">删除</el-button>
<el-button
type="primary"
v-if="paramType === 'run' && canUpdate"
@click="ArchiveRunDataFun"
>归档</el-button
>
<el-button v-if="canUpdate" type="primary" @click="openAddPerformanceWindFun"
>新增</el-button
>
<el-button v-if="canUpdate" type="danger" @click="deleteFun">删除</el-button>
<el-button
v-if="canUpdate"
icon="Upload"
title="导入Excel"
link
@@ -104,9 +118,9 @@
{{ PERFORMANCE_UNIT.O[row.unit] }}
</template>
<template #operate="{ row }">
<!-- <template #operate="{ row }">
<el-button type="danger" link @click="delPerformance(row)">删除</el-button>
</template>
</template> -->
</BaseTable>
</div>
<!-- <div class="operate-box">
@@ -486,18 +500,28 @@ const updatePerformanceFun = async (data: any) => {
// 归档
const ArchiveRunDataFun = async () => {
const param = {
runId: props.runInfo.uuid,
};
try {
const res: any = await syncKeyResultToTaskApi(param);
if (res && res.code === 200) {
ElMessage.success('归档成功');
} else {
ElMessage.warning('归档未成功');
const checkData = baseTableRef.value.tableRef.getCheckboxRecords()
if (checkData?.length) {
const ids = checkData.map((item: any) => {
return item.id;
});
const param = {
runId: props.runInfo.uuid,
performanceIdList: ids,
};
try {
const res: any = await syncKeyResultToTaskApi(param);
if (res && res.code === 200) {
ElMessage.success('归档成功');
} else {
ElMessage.warning('归档未成功');
}
} catch (error) {
console.error(error);
}
} catch (error) {
console.error(error);
} else {
ElMessage.warning('请勾选算例后进行归档');
}
};

View File

@@ -0,0 +1,370 @@
<template>
<div class="dashboard-container">
<div class="charts-container">
<div class="chart-item">
<commonFilterChart
:title="$t('工况库.项目任务进度统计')"
:charts-id="'chart-progress-bar'"
:bar-type="'barChart'"
:nodata="progressBarChartNodata"
:option="progressBarOption"
:showChangeModel="true"
:filterItems="['projectName', 'projectCode', 'discipline', 'workspace']"
@update="progressBarChartUpdate"
>
</commonFilterChart>
</div>
<div class="chart-item">
<commonFilterChart
:title="$t('工况库.项目任务进度统计')"
:charts-id="'chart-progress-pie'"
:bar-type="'pieChart'"
:nodata="progressPieChartNodata"
:option="progressPieOption"
:filterItems="['projectName', 'projectCode', 'discipline', 'workspace']"
@update="progressPieChartUpdate"
>
</commonFilterChart>
</div>
<div class="chart-item">
<commonFilterChart
:title="$t('工况库.项目任务达成统计')"
:charts-id="'chart-achieve-bar'"
:bar-type="'barChart'"
:nodata="achieveBarOption?.xAxis.data.length === 0"
:option="achieveBarOption"
:showChangeModel="true"
:filterItems="['projectName', 'projectCode', 'discipline']"
@update="achieveBarChartUpdate"
>
</commonFilterChart>
</div>
<div class="chart-item">
<commonFilterChart
:title="$t('工况库.项目任务达成统计')"
:charts-id="'chart-achieve-pie'"
:bar-type="'pieChart'"
:nodata="achievePieChartNodata"
:option="achievePieOption"
:filterItems="['projectName', 'projectCode', 'discipline']"
@update="achievePieChartUpdate"
>
</commonFilterChart>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import commonFilterChart from '@/components/common/echartCard/commonFilterChart.vue';
import { ref, provide } from 'vue';
import { getThemeColor } from '@/utils/theme';
import { getTaskCompleteStatisticsByDisciplineApi, queryNodeListApi } from '@/api/project/node';
import { getTaskAchieveStatisticsApi } from '@/api/project/task';
import { useDict } from '@/utils/useDict';
import { getTaskStatusColorList } from '@/utils/enum/task';
import { TASK_CALCULATE_STATUS_OPTIONS } from '@/utils/enum/task';
const { TASK_ACHIEVE_STATUS, RESULT_ACHIEVE_STATUS } = useDict(
'TASK_ACHIEVE_STATUS',
'RESULT_ACHIEVE_STATUS'
);
const statusColorList = getTaskStatusColorList(Object.keys(TASK_ACHIEVE_STATUS.value.O));
// 【1】进度统计柱状图
const progressBarChartNodata = ref(false);
const progressBarOption = ref<any>();
const getCommonOptionFun = async (data: any) => {
let xData: any = [];
let seriesData: any = [];
let legendData: any = [];
const res: any = await getTaskCompleteStatisticsByDisciplineApi({
tag1: data.tag1,
tag5: data.workspace,
discipline: data.discipline,
});
if (res && res.code === 200) {
progressBarChartNodata.value = res.data.allExeStatus.length === 0;
xData = res.data.result.map((item: any) => {
return item.name;
});
legendData = Object.keys(TASK_ACHIEVE_STATUS.value.O).map((item: any) => {
return TASK_ACHIEVE_STATUS.value.O[item];
});
const names = Object.keys(TASK_ACHIEVE_STATUS.value.O);
seriesData = names.map((str) => ({
name: TASK_ACHIEVE_STATUS.value.O[str],
type: 'bar',
emphasis: {
focus: 'series',
},
data: res.data.result.map((item: any) => item?.statusCount[str] || 0),
}));
}
return { xData, seriesData, legendData };
};
const progressBarChartUpdate = async (data: any) => {
const { xData, seriesData, legendData } = await getCommonOptionFun(data);
const option = {
color: statusColorList,
legend: { data: legendData },
grid: { bottom: '50' },
xAxis: { type: 'category', data: xData || [] },
yAxis: { type: 'value', minInterval: 1 },
dataZoom: xData.length > 4,
series: seriesData || [],
};
progressBarOption.value = option;
};
// 【2】进度统计饼图
const progressPieChartNodata = ref(false);
const progressPieOption = ref<any>();
const progressPieChartUpdate = async (data: any) => {
const seriesData: { name: any; value: any }[] = [];
const res: any = await getTaskCompleteStatisticsByDisciplineApi({
tag1: data.tag1,
tag5: data.workspace,
discipline: data.discipline,
});
if (res.code === 200) {
progressPieChartNodata.value = res.data.allExeStatus.length === 0;
// 把res.data.allAchieveStatus数组转换为对象的keyvalue默认是0
const achieveStatusObj: any = {};
Object.keys(TASK_ACHIEVE_STATUS.value.O).forEach((item: any) => {
achieveStatusObj[item] = 0;
});
// 遍历res.data.result数组把每个对象的statusCount属性合并到achieveStatusObj中
res.data.result?.forEach((item: any) => {
for (const key in item.statusCount) {
achieveStatusObj[key] += item.statusCount[key];
}
});
// 遍历achieveStatusObj把每个对象的key转换为中文
for (const key in achieveStatusObj) {
seriesData.push({
name: TASK_ACHIEVE_STATUS.value.O[key],
value: achieveStatusObj[key],
});
}
const option = getPieOptions(statusColorList, seriesData);
progressPieOption.value = option;
}
};
// 完成情况颜色列表:未分析、不合格、合格
const completionStatusColorList = [
'rgb(200, 201, 204)',
getThemeColor('--el-color-danger'),
getThemeColor('--el-color-success'),
];
// 【3】任务达成统计柱状图
const achieveBarOption = ref<any>();
const taskCalculateStatusLegendData = TASK_CALCULATE_STATUS_OPTIONS.map((item) => ({
name: item.label,
}));
const achieveBarChartUpdate = async (data: any) => {
const res: any = await getTaskAchieveStatisticsApi({
resultTagType: 'tag6',
tag1: data.tag1,
discipline: data.discipline,
});
if (res && res.code === 200) {
const xData = res.data.result.map((item: any) => {
return item.name;
});
const seriesData = TASK_CALCULATE_STATUS_OPTIONS.map((item) => ({
name: item.label,
type: 'bar',
emphasis: {
focus: 'series',
},
data: res.data.result.map((resultItem: any) => resultItem?.statusCount[item.value] || 0),
}));
const option = {
color: completionStatusColorList,
legend: { data: taskCalculateStatusLegendData },
grid: { bottom: xData.length > 4 ? '50' : '10' },
xAxis: { data: xData },
yAxis: { minInterval: 1 },
dataZoom: xData.length > 4,
series: seriesData,
};
achieveBarOption.value = { ...option };
}
};
// 【4】任务达成统计饼图
const achievePieChartNodata = ref(false);
const achievePieOption = ref<any>();
const achievePieChartUpdate = async (data: any) => {
const seriesData: { name: any; value: any }[] = [];
const res: any = await getTaskAchieveStatisticsApi({
resultTagType: 'tag6',
tag1: data.tag1,
discipline: data.discipline,
});
if (res.code === 200) {
achievePieChartNodata.value = res.data.allAchieveStatus.length === 0;
const achieveStatusObj: any = {};
Object.keys(RESULT_ACHIEVE_STATUS.value.O).forEach((item: any) => {
achieveStatusObj[item] = 0;
});
// 遍历res.data.result数组把每个对象的statusCount属性合并到achieveStatusObj中
res.data.result?.forEach((item: any) => {
for (const key in item.statusCount) {
achieveStatusObj[key] += item.statusCount[key];
}
});
// 遍历achieveStatusObj把每个对象的key转换为中文
for (const key in achieveStatusObj) {
seriesData.push({
name: RESULT_ACHIEVE_STATUS.value.O[key],
value: achieveStatusObj[key],
});
}
console.log(seriesData, ' seriesData');
const option = getPieOptions(completionStatusColorList, seriesData);
achievePieOption.value = option;
}
};
// 饼图配置 statusColorList 颜色列表 seriesData 数据 [{ name: '分类1', value: 10 },{ name: '分类2', value: 20 }]
const getPieOptions = (statusColorList: any, seriesData: any) => {
const option = {
color: statusColorList,
legend: {
show: true,
icon: 'circle',
itemWidth: 12,
itemHeight: 12,
borderWidth: 0,
top: '0',
formatter: (name: any) => {
const str = seriesData.filter((item: any) => item.name === name)[0];
return `{name|${name}} {value|${str.value}}`;
},
textStyle: {
// color: 'inherit',
rich: {
name: {
fontSize: 14,
align: 'left',
},
value: {
fontSize: 14,
fontWeight: '700',
align: 'right',
},
},
},
// orient: 'vertical', // 方向
itemGap: 14,
},
series: [
{
name: '',
type: 'pie',
radius: ['30%', '55%'],
center: ['60%', '55%'],
emphasis: {
disabled: true, // 禁用emphasis效果
},
itemStyle: {
borderRadius: 0,
borderWidth: 0,
},
label: {
formatter: (params: { name: any; percent: any }) => {
return '{icon|●}{name|' + params.name + '} {value|' + params.percent + '%' + '}';
},
alignTo: 'labelLine',
rich: {
icon: {
fontSize: 20,
color: 'inherit',
},
name: {
fontSize: 14,
padding: [0, 6, 0, 4],
color: 'inherit',
},
value: {
fontSize: 12,
fontWeight: 'bolder',
color: 'inherit',
},
},
},
labelLine: {
lineStyle: {
cap: 'round',
},
},
data: seriesData,
},
],
};
return option;
};
// 统一请求任务列表【以后可以封装公共方法】
const projectListRes = ref(null);
provide('projectProvide', true);
provide('projectListRes', projectListRes);
const getProjectListFun = async () => {
const params = {
nodeType: 'project',
current: 1,
size: 9999,
};
const res = await queryNodeListApi(params);
if (res.code === 200) {
projectListRes.value = res;
}
};
getProjectListFun();
</script>
<style lang="scss" scoped>
.dashboard-container {
height: 100%;
background: var(--el-bg-color-page);
.charts-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 0;
height: 100%;
.chart-item {
min-height: 300px;
overflow: hidden;
&:first-child {
grid-column: 1;
grid-row: 1;
border-right: 16px solid var(--el-bg-color-page);
border-bottom: 16px solid var(--el-bg-color-page);
}
&:nth-child(2) {
grid-column: 2;
grid-row: 1;
border-bottom: 16px solid var(--el-bg-color-page);
}
&:nth-child(3) {
grid-column: 1;
grid-row: 2;
border-right: 16px solid var(--el-bg-color-page);
}
&:nth-child(4) {
grid-column: 2;
grid-row: 2;
}
}
}
}
</style>

View File

@@ -25,6 +25,7 @@ import { ref, computed } from 'vue';
import SimulationLoop from './components/simulationLoop.vue';
// import SimulationLoop from './components/simulationLoopNewBoard.vue'; // 新版仿真闭环看板
import SummaryDashboard from './components/summaryDashboard.vue';
// import SummaryDashboard from './components/summaryNewBoard.vue'; // 新版仿真汇总看板
import dataStatistics from './components/dataStatistics.vue';
import workLoad from '@/views/task/workLoad/index.vue';
import workerLoad from '@/views/task/workerLoad/index.vue';

View File

@@ -258,6 +258,40 @@ const getWorkLoadDataFun = async () => {
const param: any = {
...filterFprmData,
...props.filterData,
idMap: [
{
key: null,
value: 'tag1',
},
{
key: null,
value: 'tag2',
},
{
key: null,
value: 'tag3',
},
{
key: null,
value: 'tag4',
},
{
key: null,
value: 'tag5',
},
{
key: null,
value: 'tag6',
},
{
key: null,
value: 'tag7',
},
{
key: null,
value: 'tag8',
},
],
};
// 根据页面类型去掉没用的参数
if (props.dimension === 'project') {