1 Commits

Author SHA1 Message Date
JiangSheng
9f2b8e6468 feat: 报告模版工况绑定 2026-03-09 09:54:11 +08:00
8 changed files with 363 additions and 19 deletions

View File

@@ -26,17 +26,20 @@
@change="onFormChangeFun"
>
<template #form-nodeName>
<nodeNameMixed
<NodeNameMixed
v-model="formData.nodeName"
:nodeType="formData.nodeType"
:editable="true"
/>
</template>
<template #form-flowTemplate>
<flowTemplateSelect v-model="selectedFlowTemplate" />
<FlowTemplateSelect v-model="selectedFlowTemplate" />
</template>
<template #form-reportTemplate>
<ReportTemplateSelect v-model="formData.reportTemplate" />
</template>
<template #form-standard>
<knowledgeSelect v-model="standard" v-model:modelName="formData.standardName" />
<KnowledgeSelect v-model="standard" v-model:modelName="formData.standardName" />
</template>
</TableForm>
</template>
@@ -58,15 +61,18 @@ import { getTagKeyMap, NODE_TYPE } from '@/utils/enum/node';
import { disposeTaskMembers } from '@/utils/task';
import { disposeTagKey } from '@/views/task/projectDetail/components/project';
import { isCategoryType, isCategoryNodeType, validateCategoryLevel } from '@/utils/node';
import flowTemplateSelect from './flowTemplateSelect.vue';
import knowledgeSelect from './knowledgeSelect.vue';
import nodeNameMixed from './nodeNameMixed.vue';
import FlowTemplateSelect from './flowTemplateSelect.vue';
import ReportTemplateSelect from './reportTemplateSelect.vue';
import KnowledgeSelect from './knowledgeSelect.vue';
import NodeNameMixed from './nodeNameMixed.vue';
import { TABLE_NAME } from '@/utils/enum/tableName';
import { useDict } from '@/utils/useDict';
import { useI18n } from 'vue-i18n';
import { useTaskStore } from '@/stores/taskPool';
import { useReportStore } from '@/stores/reportTemplate';
const taskStore = useTaskStore();
const reportStore = useReportStore();
enum OPERATION_TYPE {
ADD = 'add',
EDIT = 'edit',
@@ -138,6 +144,7 @@ watch(
}
});
taskStore.fetchTemplates(true);
reportStore.fetchTemplates(true);
}
}
);

View File

@@ -0,0 +1,210 @@
<template>
<div class="full">
<el-select-v2
v-if="editable"
v-model="selected"
:options="options"
class="full"
:clearable="clearable"
:size="size"
:multiple="multiple"
:collapse-tags="multiple"
:collapse-tags-tooltip="multiple"
:fit-input-width="false"
@change="onChange"
/>
<span v-else class="plain-label">
<template v-if="selectedTemplates.length === 1">
<el-icon
class="view"
@click="previewReport(selectedTemplates[0].uuid)"
:title="$t('通用.预览')"
>
<View />
</el-icon>
<OverflowTooltip :content="selectedTemplates[0].templateName" max-width="120px" />
</template>
<template v-else-if="selectedTemplates.length >= 2">
<el-popover
placement="bottom"
trigger="hover"
:width="400"
popper-class="report-template-select-popover"
>
<template #reference>
<span class="link-text">
{{ $t('工况库.关联N个报告模版', { count: selectedTemplates.length }) }}
</span>
</template>
<div class="template-list">
<div v-for="item in selectedTemplates" :key="item.uuid" class="template-item">
<el-icon class="view" :title="$t('通用.预览')" @click="previewReport(item.uuid)">
<View />
</el-icon>
<OverflowTooltip :content="item.templateName" class="item-name" />
</div>
</div>
</el-popover>
</template>
</span>
<ReportViewDialog
v-if="reportViewVisible"
v-model:showDialog="reportViewVisible"
:reportFileId="currentPreviewFileId"
:reportUuid="currentPreviewUuid"
/>
</div>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue';
import { useReportStore } from '@/stores/reportTemplate';
import ReportViewDialog from '@/components/common/report/reportViewDialog.vue';
import OverflowTooltip from '@/components/common/text/overflowTooltip.vue';
import { View } from '@element-plus/icons-vue';
interface Props {
modelValue?: any;
clearable?: boolean;
editable?: boolean;
size?: '' | 'large' | 'default' | 'small';
multiple?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
clearable: false,
editable: true,
size: 'default',
multiple: true,
});
const emits = defineEmits(['update:modelValue', 'change']);
const parseValue = (value: string): string[] | string => {
if (!value) return props.multiple ? [] : '';
if (props.multiple) {
return value.split(',').filter((v) => v.trim());
}
return value;
};
const stringifyValue = (value: string[] | string): string => {
if (props.multiple && Array.isArray(value)) {
return value.join(',');
}
return String(value || '');
};
const selected = ref<string[] | string>(parseValue(props.modelValue));
watch(
() => props.modelValue,
(v) => {
selected.value = parseValue(v);
}
);
watch(selected, (v) => {
const stringValue = stringifyValue(v);
emits('update:modelValue', stringValue);
});
const reportStore = useReportStore();
const options = computed(() => reportStore.options);
const selectedCodes = computed(() => {
if (props.multiple && Array.isArray(selected.value)) {
return selected.value;
}
return selected.value ? [selected.value] : [];
});
const selectedTemplates = computed(() => {
return reportStore.templates.filter((t: any) => selectedCodes.value.includes(t.templateCode));
});
const onChange = (val: string[] | string) => {
emits('change', stringifyValue(val));
};
const reportViewVisible = ref(false);
const currentPreviewUuid = ref('');
const currentPreviewFileId = ref('');
const previewReport = (uuid: string) => {
const template = reportStore.templates.find((t: any) => t.uuid === uuid);
if (template) {
currentPreviewUuid.value = uuid;
currentPreviewFileId.value = template.fileId || '';
reportViewVisible.value = true;
}
};
</script>
<style scoped lang="scss">
.full {
width: 100%;
}
.plain-label {
display: inline-flex;
align-items: center;
gap: 6px;
vertical-align: middle;
line-height: 1;
max-width: 100%;
.view {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
cursor: pointer;
color: var(--el-color-primary);
flex-shrink: 0;
}
.link-text {
color: var(--el-color-primary);
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
.template-list {
max-height: 400px;
overflow-y: auto;
.template-item {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid var(--el-border-color-lighter);
&:last-child {
border-bottom: none;
}
.item-name {
flex: 1;
min-width: 0;
}
.view {
flex-shrink: 0;
cursor: pointer;
color: var(--el-color-primary);
margin-right: 8px;
}
}
}
</style>
<style lang="scss">
.report-template-select-popover {
max-height: 450px;
overflow: hidden;
}
</style>

View File

@@ -322,6 +322,21 @@
<template #flowTemplate-edit="{ row }">
<flowTemplateSelect v-model="row.flowTemplate" size="small" />
</template>
<!-- 报告模板 -->
<template #reportTemplate="{ row, icon }">
<TreeEditItem
v-if="[NODE_TYPE.TASK].includes(row.nodeType) || isMixedNodeType(row.nodeType)"
:data="row.reportTemplate"
:editMode="editMode"
:icon="icon"
hideTitle
>
<reportTemplateSelect v-model="row.reportTemplate" :editable="false" size="small" />
</TreeEditItem>
</template>
<template #reportTemplate-edit="{ row }">
<reportTemplateSelect v-model="row.reportTemplate" size="small" />
</template>
<!-- 标准规范 -->
<template #standard="{ row, icon }">
<TreeEditItem
@@ -565,9 +580,11 @@ import dictLabel from './dictLabel.vue';
import nodeNameMixed from './nodeNameMixed.vue';
import { poolCategoryTypeOptions } from '@/utils/project';
import flowTemplateSelect from './flowTemplateSelect.vue';
import reportTemplateSelect from './reportTemplateSelect.vue';
import knowledgeSelect from './knowledgeSelect.vue';
import uploadImg from '@/components/common/table/uploadImg.vue';
import { useTaskStore } from '@/stores/taskPool';
import { useReportStore } from '@/stores/reportTemplate';
import userSelect from '../userSelect/index.vue';
import { disposeMemberList } from '@/views/task/projectDetail/components/project';
import { getMemberListIds } from '@/utils/task';
@@ -614,6 +631,7 @@ const { TASK_ACHIEVE_STATUS, RESULT_ACHIEVE_STATUS, TASK_APPROVE_STATUS, TASK_PR
const treeData = ref<any>([]);
const taskStore = useTaskStore();
const reportStore = useReportStore();
const treeDataFormatFun = (data: any, build: any, pId: any) => {
data.forEach((item: any) => {
@@ -721,6 +739,7 @@ const seeApproveDetailFun = async (row: any) => {
onMounted(() => {
initProjectDicts();
taskStore.fetchTemplates();
reportStore.fetchTemplates();
});
defineExpose({

View File

@@ -0,0 +1,57 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { queryReportTemplateApi } from '@/api/capability/report';
import { REPORT_TEMPLATE_PUBLIC_STATUS, FLOW_USE_STATUS } from '@/utils/enum/report';
interface ReportTemplate {
templateName?: string;
templateCode?: string;
[k: string]: any;
}
export const useReportStore = defineStore('reportTemplate', () => {
const templates = ref<ReportTemplate[]>([]);
const loaded = ref(false);
let pendingPromise: Promise<ReportTemplate[]> | null = null;
const options = computed(() =>
templates.value.map((item) => ({
label: item.templateName || '',
value: item.templateCode || '',
}))
);
const fetchTemplates = async (force = false) => {
if (!force && loaded.value && templates.value.length) return templates.value;
if (pendingPromise) return pendingPromise;
pendingPromise = (async () => {
try {
const req = {
current: 1,
size: 99999,
type: REPORT_TEMPLATE_PUBLIC_STATUS.PUBLIC,
templateStatus: FLOW_USE_STATUS.USED,
};
const res: any = await queryReportTemplateApi(req);
if (res && res.code === 200) {
const list = res.data?.data || res.data || [];
templates.value = Array.isArray(list) ? list : [];
} else {
templates.value = [];
}
loaded.value = true;
} catch {
templates.value = [];
loaded.value = true;
} finally {
pendingPromise = null;
}
return templates.value;
})();
return pendingPromise;
};
return { templates, options, fetchTemplates, loaded };
});

View File

@@ -108,6 +108,7 @@ export const poolNodePropPickMap = {
'days',
'flowTemplate',
'standard',
'reportTemplate',
'analyseTarget',
'confidence',
'analyseSoftware',

View File

@@ -87,6 +87,9 @@ const lang = {
: 'Project Detail',
: 'Task Creation',
仿: 'Task Execution',
: 'Work Report List',
: 'I Confirmed',
: 'I Responsible',
: 'Application Center',
: 'Data Management',
: 'Data Overview',
@@ -201,6 +204,7 @@ const lang = {
: 'No modified data to submit for approval',
: 'Performance Pool List',
N个流程模版: 'Associated {count} Flow Templates',
N个报告模版: 'Associated {count} Report Templates',
N个执行规范: 'Associated {count} Standards',
N个工况: 'Associated {count} Tasks',
: 'Please select task pool',

View File

@@ -86,6 +86,9 @@ const lang = {
: '项目详情',
: '任务创建',
仿: '仿真执行',
: '报工列表',
: '我确认的',
: '我负责的',
: '应用中心',
: '数据管理',
: '数据总览',
@@ -197,6 +200,7 @@ const lang = {
: '没有修改的数据需要提交审批',
: '指标库列表',
N个流程模版: '关联{count}个流程模版',
N个报告模版: '关联{count}个报告模版',
N个执行规范: '关联{count}个执行规范',
N个工况: '关联{count}个工况',
: '请选择工况库',

View File

@@ -45,7 +45,7 @@
<el-button @click="closeFun">取消</el-button>
<el-button
type="primary"
v-if="dialogType !== REPORT_OPERATION_TYPE.UPGRADE"
v-if="!(dialogType === REPORT_OPERATION_TYPE.UPGRADE && hasChangeLoadcase)"
@click="confirmFun('ok')"
:loading="loadingInterface"
>确定</el-button
@@ -99,12 +99,14 @@ const confirmFun = async (type: string) => {
if (await tableFormRef.value.validateFun()) {
loadingInterface.value = true;
const flowForm = tableFormRef.value.getFormDataFun();
flowForm.simulationPoolInfoList = simulationPoolInfoList.value;
if (
props.dialogType === REPORT_OPERATION_TYPE.CREATE ||
props.dialogType === REPORT_OPERATION_TYPE.COPY
) {
const formData = new FormData();
formData.append('templateVersion', 'V1.0');
formData.append('simulationPoolInfoListStr', JSON.stringify(simulationPoolInfoList.value));
for (const key in flowForm) {
if (key === 'file') {
// 就算复制模板,如果上传了新的文件就不复制文件了
@@ -122,7 +124,8 @@ const confirmFun = async (type: string) => {
key !== 'templateStatus' &&
key !== 'creatorName' &&
key !== 'createTime' &&
key !== 'approveType'
key !== 'approveType' &&
key !== 'simulationPoolInfoList'
) {
formData.append(key, flowForm[key] ? flowForm[key] : '');
}
@@ -130,17 +133,27 @@ const confirmFun = async (type: string) => {
}
const flowUuid = await createFlow(formData);
emits('confirm', { type, uuid: flowUuid });
} else if (props.dialogType === REPORT_OPERATION_TYPE.UPGRADE) {
emits('confirm', {
type,
uuid: flowForm.uuid,
comment: flowForm.comment,
file: flowForm.file,
fileId: flowForm.file[0].id,
});
} else {
await editReport(flowForm);
emits('confirm', { type, uuid: flowForm.uuid });
if (type === 'ok') {
await editReport(flowForm);
emits('confirm', { type, uuid: flowForm.uuid, tVersion: props.templateVersion });
}
if (type === 'next') {
if (oldComment.value !== flowForm.comment) {
// 升版时只更新描述,不更新工况,工况在提交审批时传递给接口
await editReport({ ...flowForm, simulationPoolInfoList: oldSimulationPoolInfoList.value });
}
emits('confirm', {
type,
uuid: flowForm.uuid,
comment: flowForm.comment,
file: flowForm.file,
fileId: flowForm.file[0].id,
tVersion: props.templateVersion,
// 不更新工况,工况在提交审批时传递给升版接口
simulationPoolInfoList: hasChangeLoadcase.value ? simulationPoolInfoList.value : null,
});
}
}
loadingInterface.value = false;
}
@@ -188,6 +201,7 @@ const editReport = async (params: any) => {
formData.append('extras', params.extras);
formData.append('fileId', oldFormData.value.fileId);
formData.append('templateContent', oldFormData.value.templateContent);
formData.append('simulationPoolInfoListStr', JSON.stringify(params.simulationPoolInfoList));
if (params.file && params.file.length > 0) {
if (params.file[0].raw) {
formData.append('file', params.file[0].raw);
@@ -217,12 +231,17 @@ const closeFun = () => {
const oldFormData = ref<any>({});
const editRowInfo = ref<any>({});
const editRowInfo = ref({});
const oldSimulationPoolInfoList = ref<any[]>([]);
const oldComment = ref('');
const setEditForm = (val: any) => {
oldFormData.value = val;
// nextTick(() => {
simulationPoolInfoList.value = val.simulationPoolInfoList || [];
oldSimulationPoolInfoList.value = val.simulationPoolInfoList || [];
oldComment.value = val.comment;
// tableFormRef.value.setFormDataFun({ ...val });
editRowInfo.value = { ...val };
// });
@@ -245,6 +264,29 @@ const setOptionsFun = (key: string, options: any[]) => {
tableFormRef.value.setOptionsFun(key, options);
};
const hasChangeLoadcase = ref(false);
watch(
() => simulationPoolInfoList.value,
() => {
const oldIds: any[] = [];
oldSimulationPoolInfoList.value.forEach((item) => {
oldIds.push(...item.simulationPoolTaskIds);
});
const newIds: any[] = [];
simulationPoolInfoList.value.forEach((item) => {
newIds.push(...item.simulationPoolTaskIds);
});
oldIds.sort();
newIds.sort();
if (JSON.stringify(oldIds) !== JSON.stringify(newIds)) {
hasChangeLoadcase.value = true;
} else {
hasChangeLoadcase.value = false;
}
}
);
watch(
() => props.showDialog,
async () => {