|
|
@@ -0,0 +1,443 @@
|
|
|
+<template>
|
|
|
+ <main class="safety-platform-container__main">
|
|
|
+ <el-form
|
|
|
+ ref="formRef"
|
|
|
+ :model="form"
|
|
|
+ :rules="rules"
|
|
|
+ label-width="150px"
|
|
|
+ style="max-width: 600px"
|
|
|
+ label-position="left"
|
|
|
+ >
|
|
|
+ <el-form-item label="生产安全计划名称:" prop="trainingPlanName" required>
|
|
|
+ <el-input v-model="form.trainingPlanName" placeholder="请输入生产安全计划名称" :disabled="isViewMode || isReviewMode" />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="分类名称:" prop="categoryName" required>
|
|
|
+ <el-select v-model="form.categoryName" placeholder="请选择分类名称" :disabled="isViewMode || isReviewMode">
|
|
|
+ <el-option v-for="item in classifyNameOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="责任部门:" prop="responsibleDeptIds" required>
|
|
|
+ <!-- 新需求,增加全选功能 -->
|
|
|
+ <el-select
|
|
|
+ v-model="form.responsibleDeptIds"
|
|
|
+ multiple
|
|
|
+ :disabled="isViewMode || isReviewMode"
|
|
|
+ collapse-tags
|
|
|
+ collapse-tags-tooltip
|
|
|
+ :max-collapse-tags="3"
|
|
|
+ placeholder="请选择责任部门"
|
|
|
+ filterable
|
|
|
+ >
|
|
|
+ <template #header>
|
|
|
+ <el-checkbox v-model="checkAll" :indeterminate="indeterminate" @change="handleCheckAllChange">
|
|
|
+ 全部单位
|
|
|
+ </el-checkbox>
|
|
|
+ </template>
|
|
|
+ <el-option
|
|
|
+ v-for="item in deptTreeOne"
|
|
|
+ :key="item.id"
|
|
|
+ :label="item.deptName"
|
|
|
+ :value="item.id"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="配合部门:" prop="cooperateDeptIds" required>
|
|
|
+ <el-cascader
|
|
|
+ v-model="form.cooperateDeptIds"
|
|
|
+ :options="deptTree"
|
|
|
+ :props="cascaderProp"
|
|
|
+ clearable
|
|
|
+ collapse-tags
|
|
|
+ :show-all-levels="false"
|
|
|
+ :max-collapse-tags="3"
|
|
|
+ popper-class="cascader-popper--custom"
|
|
|
+ placeholder="请选择配合部门"
|
|
|
+ style="width: 100%"
|
|
|
+ :disabled="isViewMode || isReviewMode"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="计划完成时间:" prop="plannedComplateTime" required>
|
|
|
+ <el-date-picker
|
|
|
+ v-model="form.plannedComplateTime"
|
|
|
+ type="date"
|
|
|
+ placeholder="请选择完成时间"
|
|
|
+ style="width: 100%"
|
|
|
+ :disabled="isViewMode || isReviewMode"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="工作内容:" prop="workContent" required>
|
|
|
+ <el-input
|
|
|
+ v-model="form.workContent"
|
|
|
+ type="textarea"
|
|
|
+ :rows="4"
|
|
|
+ :maxlength="300"
|
|
|
+ placeholder="请填写工作内容"
|
|
|
+ :disabled="isViewMode || isReviewMode"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="附件上传:" v-if="isViewMode || isReviewMode" prop="fileUrl" :required="isViewMode || isReviewMode">
|
|
|
+ <div class="file-list" v-if="form.fileUrl && form.fileUrl.length != 0">
|
|
|
+ <div class="file-item" v-for="file in form.fileUrl" :key="file.fileId">
|
|
|
+ <span class="file-item--name">{{ file.fileName }}</span>
|
|
|
+ <div class="file-item--footer">
|
|
|
+ <el-button link type="primary" @click="previewOnline(file.fileUrl, file.fileType)"
|
|
|
+ >预览</el-button
|
|
|
+ >
|
|
|
+ <el-button link type="primary" @click.stop="downloadFile(file.fileUrl, file.fileName)"
|
|
|
+ >下载</el-button
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-else>
|
|
|
+ 暂无附件
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <PreviewOnline ref="previewOnlineRef" />
|
|
|
+ </main>
|
|
|
+ <footer class="safety-platform-container__footer">
|
|
|
+ <el-button @click="router.back()">返回</el-button>
|
|
|
+ <el-button v-if="!isViewMode && !isReviewMode" type="primary" @click="handleSubmit">
|
|
|
+ {{ isCreateMode ? '提交' : '保存' }}
|
|
|
+ </el-button>
|
|
|
+ <el-button v-if="isReviewMode" type="primary" @click="handleReview('reject')">
|
|
|
+ 审核不通过
|
|
|
+ </el-button>
|
|
|
+ <el-button v-if="isReviewMode" type="primary" @click="handleReview('approve')">
|
|
|
+ 审核通过
|
|
|
+ </el-button>
|
|
|
+ </footer>
|
|
|
+ <el-dialog v-model="reviewDialogVisible" title="审核不通过" width="40%" >
|
|
|
+ <p style="margin-bottom: 12px;">审核不通过的记录,需要填写驳回原因</p>
|
|
|
+ <el-input type="textarea" :maxlength="300" show-word-limit :autosize="{ minRows: 6, maxRows: 8 }" v-model="rejectReason" placeholder="请填写驳回审批原因"></el-input>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="reviewDialogVisible = false">取 消</el-button>
|
|
|
+ <el-button type="primary" @click="handleReviewSubmit">确 定</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+ import { computed, onMounted, ref, reactive } from 'vue';
|
|
|
+ import { useRoute, useRouter } from 'vue-router';
|
|
|
+ import { ElMessage } from 'element-plus';
|
|
|
+ import type { FormInstance } from 'element-plus';
|
|
|
+ import { FORM_RULES } from '../configs/form';
|
|
|
+ import {
|
|
|
+ saveWorkPlan,
|
|
|
+ updateWorkPlan,
|
|
|
+ queryWorkPlanDetail,
|
|
|
+ reviewWorkPlan,
|
|
|
+ queryWorkPlanDepartmentDetail,
|
|
|
+ type SaveWorkPlanRequest,
|
|
|
+ } from '@/api/safety-system-construction-work-plan';
|
|
|
+ import UploadFiles from '@/components/UploadFiles/UploadFiles.vue';
|
|
|
+ import type { FileItem } from '@/components/UploadFiles/types';
|
|
|
+ import { formatAttachmentList } from '@/components/UploadFiles/utils';
|
|
|
+ import PreviewOnline from '@/views/disaster/components/PreviewOnline.vue';
|
|
|
+ import { DeptTree } from '@/types/dept/type';
|
|
|
+ import { getAllDepartments } from '@/api/auth/dept';
|
|
|
+ import { downloadFile } from '@/views/disaster/utils';
|
|
|
+
|
|
|
+ const router = useRouter();
|
|
|
+ const route = useRoute();
|
|
|
+
|
|
|
+ const operate = computed(() => (route.query.operate as string) || 'work-plan-create');
|
|
|
+ const currentId = computed(() => Number(route.query.id));
|
|
|
+ const planId = computed(() => Number(route.query.planId));
|
|
|
+
|
|
|
+ const isCreateMode = computed(() => operate.value === 'work-plan-create');
|
|
|
+ const isEditMode = computed(() => operate.value === 'work-plan-edit');
|
|
|
+ const isViewMode = computed(() => operate.value === 'work-plan-view');
|
|
|
+ const isReviewMode = computed(() => operate.value === 'work-plan-review');
|
|
|
+
|
|
|
+ const form = reactive<SaveWorkPlanRequest>({
|
|
|
+ workContent: '',
|
|
|
+ categoryName: '',
|
|
|
+ trainingPlanName: '',
|
|
|
+ responsibleDeptIds: '',
|
|
|
+ cooperateDeptIds: '',
|
|
|
+ executGroupIds: '',
|
|
|
+ plannedComplateTime: '',
|
|
|
+ fileUrl: '',
|
|
|
+ });
|
|
|
+
|
|
|
+ // 分类名称选项
|
|
|
+ const classifyNameOptions = ref<Array<{ label: string; value: string }>>([
|
|
|
+ { label: '安全综合工作', value: '安全综合工作' },
|
|
|
+ { label: '生产安全工作', value: '生产安全工作' },
|
|
|
+ { label: '安全文化活动', value: '安全文化活动' },
|
|
|
+ ]);
|
|
|
+
|
|
|
+ /** 表单校验规则 */
|
|
|
+ const rules = ref<Record<string, import('element-plus').FormItemRule[]>>(FORM_RULES);
|
|
|
+
|
|
|
+ const cascaderProp = {
|
|
|
+ multiple: true,
|
|
|
+ expandTrigger: 'hover',
|
|
|
+ checkStrictly: true,
|
|
|
+ emitPath: false,
|
|
|
+ value: 'id',
|
|
|
+ label: 'deptName',
|
|
|
+ };
|
|
|
+
|
|
|
+ // 获取级联部门数据
|
|
|
+ const deptTree = ref<DeptTree[]>();
|
|
|
+ const deptTreeOne = ref<DeptTree[]>();
|
|
|
+ const loadDeptTreeData = async () => {
|
|
|
+ const result = await getAllDepartments();
|
|
|
+ deptTree.value = result[0].children;
|
|
|
+ deptTreeOne.value = result?.[0]?.children.map(item => ({
|
|
|
+ ...item,
|
|
|
+ children: []
|
|
|
+ }));
|
|
|
+ //编辑、查看时当一级部门个数和所选长度一致,则是全选
|
|
|
+ if (isEditMode.value || isViewMode.value) {
|
|
|
+ if(deptTreeOne.value.length === form.responsibleDeptIds.length){
|
|
|
+ checkAll.value = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ const checkAll = ref(false);
|
|
|
+ const indeterminate = ref(false);
|
|
|
+ const handleCheckAllChange = () => {
|
|
|
+ if (checkAll.value && deptTreeOne.value) {
|
|
|
+ form.responsibleDeptIds = deptTreeOne.value.map((item) => item.id);
|
|
|
+ } else {
|
|
|
+ form.responsibleDeptIds = [];
|
|
|
+ }
|
|
|
+ };
|
|
|
+ /** 附件 JSON [{file_name, url}] 转 FileItem 列表,供 UploadFiles 使用(新增/编辑/查看/审核统一展示) */
|
|
|
+ function convertAttachmentJsonToFileItems(attachmentStr: string): FileItem[] {
|
|
|
+ if (!attachmentStr || !String(attachmentStr).trim()) return [];
|
|
|
+ try {
|
|
|
+ const arr = JSON.parse(attachmentStr);
|
|
|
+ if (!Array.isArray(arr)) return [];
|
|
|
+ return arr.map((item: any, index: number) => {
|
|
|
+ const fileName = item.file_name || item.fileName || `附件${index + 1}`;
|
|
|
+ const url = item.url || item.fileUrl || '';
|
|
|
+ const ext = (fileName || '').split('.').pop()?.toLowerCase() || '';
|
|
|
+ let fileType = 'pdf';
|
|
|
+ if (['doc', 'docx'].includes(ext)) fileType = 'word';
|
|
|
+ else if (['xls', 'xlsx'].includes(ext)) fileType = 'excel';
|
|
|
+ else if (['ppt', 'pptx'].includes(ext)) fileType = 'ppt';
|
|
|
+ return { fileId: index + 1, fileName, fileType, fileSize: '0', fileUrl: url };
|
|
|
+ });
|
|
|
+ } catch {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const attachmentFileList = computed(() => convertAttachmentJsonToFileItems(form.fileUrl || ''));
|
|
|
+
|
|
|
+ async function handleAttachmentUploadSuccess(files: FileItem[]) {
|
|
|
+ if (!files?.length) {
|
|
|
+ form.fileUrl = '';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const list = await formatAttachmentList(files);
|
|
|
+ const jsonArr = (list || [])
|
|
|
+ .map((r) => ({
|
|
|
+ file_name: r.fileName,
|
|
|
+ url: r.fileUrl || '',
|
|
|
+ }))
|
|
|
+ .filter((x) => x.url);
|
|
|
+ form.fileUrl = JSON.stringify(jsonArr);
|
|
|
+ handleSubmit();
|
|
|
+ } catch (e) {
|
|
|
+ console.error('附件上传失败:', e);
|
|
|
+ ElMessage.error('附件上传失败,请重试');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const reviewDialogVisible = ref(false);
|
|
|
+ const rejectReason = ref(''); // 驳回原因
|
|
|
+ // 审核
|
|
|
+ const handleReview = async (action: 'approve' | 'reject') => {
|
|
|
+ if (!currentId.value) return;
|
|
|
+ if(action === 'approve'){
|
|
|
+ // 直接审核通过,无需填写驳回原因
|
|
|
+ try {
|
|
|
+ await reviewWorkPlan({
|
|
|
+ id: currentId.value,
|
|
|
+ approveType: 1, // 1=审核通过
|
|
|
+ });
|
|
|
+ ElMessage.success('审核通过');
|
|
|
+ router.back();
|
|
|
+ } catch (e) {
|
|
|
+ console.error('审核失败:', e);
|
|
|
+ ElMessage.error('审核失败,请重试');
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ reviewDialogVisible.value = true;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ // 提交审核结果(驳回)
|
|
|
+ const handleReviewSubmit = async () => {
|
|
|
+ if (!currentId.value) return;
|
|
|
+ if (!rejectReason.value.trim()) {
|
|
|
+ ElMessage.warning('请填写驳回原因');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ await reviewWorkPlan({
|
|
|
+ id: currentId.value,
|
|
|
+ approveType: 0, // 0=审核不通过
|
|
|
+ refuseReason: rejectReason.value.trim(),
|
|
|
+ });
|
|
|
+ ElMessage.success('审核已驳回');
|
|
|
+ router.back();
|
|
|
+ } catch (e) {
|
|
|
+ console.error('提交审核结果失败:', e);
|
|
|
+ ElMessage.error('提交审核结果失败,请重试');
|
|
|
+ }
|
|
|
+ };
|
|
|
+ const previewOnlineRef = ref<InstanceType<typeof PreviewOnline>>();
|
|
|
+ const handlePreview = (url: string) => {
|
|
|
+ if (url) {
|
|
|
+ // 根据文件扩展名判断文件类型
|
|
|
+ const extension = url.split('.').pop()?.toLowerCase() || '';
|
|
|
+ let fileType: 'pdf' | 'word' | 'excel' | 'ppt' = 'pdf';
|
|
|
+ if (extension === 'doc' || extension === 'docx') {
|
|
|
+ fileType = 'word';
|
|
|
+ } else if (extension === 'xls' || extension === 'xlsx') {
|
|
|
+ fileType = 'excel';
|
|
|
+ } else if (extension === 'ppt' || extension === 'pptx') {
|
|
|
+ fileType = 'ppt';
|
|
|
+ }
|
|
|
+ previewOnlineRef.value?.open(url, fileType);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const previewOnline = (url: string | undefined, type) => {
|
|
|
+ if (url) {
|
|
|
+ previewOnlineRef.value?.open(url, type);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ // 时间格式化
|
|
|
+ const formatDate = (date) => {
|
|
|
+ if (!date) return '';
|
|
|
+ const dateObj = typeof date === 'object' && date instanceof Date ? date : new Date(date);
|
|
|
+ if (isNaN(dateObj.getTime())) return '';
|
|
|
+ const year = dateObj.getFullYear();
|
|
|
+ const month = String(dateObj.getMonth() + 1).padStart(2, '0');
|
|
|
+ const day = String(dateObj.getDate()).padStart(2, '0');
|
|
|
+ return `${year}-${month}-${day}`; // 输出:****-**-**
|
|
|
+ };
|
|
|
+ //转部门格式 “2,3“ -> [2,3]
|
|
|
+ const parseDeptIds = (ids: string | string[] | number[] | undefined | null): number[] => {
|
|
|
+ if (!ids) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Array.isArray(ids)) {
|
|
|
+ return ids.map((v: any) => Number(v)).filter((id) => !isNaN(id));
|
|
|
+ }
|
|
|
+
|
|
|
+ return String(ids)
|
|
|
+ .split(',')
|
|
|
+ .map(Number)
|
|
|
+ .filter((id) => !isNaN(id));
|
|
|
+ };
|
|
|
+
|
|
|
+ const formRef = ref<FormInstance | null>(null);
|
|
|
+
|
|
|
+ const handleValidate = async (): Promise<boolean> => {
|
|
|
+ if (!formRef.value) return false;
|
|
|
+ try {
|
|
|
+ await formRef.value.validate();
|
|
|
+ return true;
|
|
|
+ } catch {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const getDetail = async () => {
|
|
|
+ if (!currentId.value) return;
|
|
|
+ if(planId.value){
|
|
|
+ const res = await queryWorkPlanDepartmentDetail(currentId.value);
|
|
|
+ if (res) {
|
|
|
+ // 映射接口字段到表单字段
|
|
|
+ const cooperateDeptIdsArray = parseDeptIds(res.cooperateDeptIds);
|
|
|
+ const responsibleDeptIdsArray = parseDeptIds(res.responsibleDeptIds);
|
|
|
+ Object.assign(form, {
|
|
|
+ ...res,
|
|
|
+ cooperateDeptIds: cooperateDeptIdsArray,
|
|
|
+ responsibleDeptIds: responsibleDeptIdsArray,
|
|
|
+ fileUrl: JSON.parse(res.fileUrl || '[]'),
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ } else {
|
|
|
+ const res = await queryWorkPlanDetail(currentId.value);
|
|
|
+ if (res) {
|
|
|
+ // 映射接口字段到表单字段
|
|
|
+ const cooperateDeptIdsArray = parseDeptIds(res.cooperateDeptIds);
|
|
|
+ const responsibleDeptIdsArray = parseDeptIds(res.responsibleDeptIds);
|
|
|
+ Object.assign(form, {
|
|
|
+ ...res,
|
|
|
+ cooperateDeptIds: cooperateDeptIdsArray,
|
|
|
+ responsibleDeptIds: responsibleDeptIdsArray,
|
|
|
+ fileUrl: JSON.parse(res.fileUrl || '[]'),
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSubmit = async () => {
|
|
|
+ const res = await handleValidate();
|
|
|
+ if (!res) return;
|
|
|
+ try {
|
|
|
+ if (isViewMode.value && form.fileUrl) {
|
|
|
+ // 处理附件格式
|
|
|
+ form.fileUrl = JSON.parse(form.fileUrl)[0].url;
|
|
|
+ }
|
|
|
+ const basePayload = {
|
|
|
+ workContent: form.workContent,
|
|
|
+ categoryName: form.categoryName,
|
|
|
+ trainingPlanName: form.trainingPlanName,
|
|
|
+ executGroupIds: '',
|
|
|
+ plannedComplateTime: formatDate(form.plannedComplateTime),
|
|
|
+ cooperateDeptIds: form.cooperateDeptIds.toString(),
|
|
|
+ responsibleDeptIds: form.responsibleDeptIds.toString(),
|
|
|
+ fileUrl: form.fileUrl,
|
|
|
+ };
|
|
|
+ // console.log(basePayload, 'basePayload')
|
|
|
+ if (isCreateMode.value) {
|
|
|
+ await saveWorkPlan(basePayload);
|
|
|
+ ElMessage.success('创建成功');
|
|
|
+ } else if ((isEditMode.value && currentId.value) || (isViewMode && currentId.value)) {
|
|
|
+ await updateWorkPlan({
|
|
|
+ id: currentId.value,
|
|
|
+ ...basePayload,
|
|
|
+ });
|
|
|
+ ElMessage.success('保存成功');
|
|
|
+ }
|
|
|
+
|
|
|
+ router.back();
|
|
|
+ } catch (e) {
|
|
|
+ console.error('保存物品库存失败:', e);
|
|
|
+ ElMessage.error('保存失败,请重试');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ onMounted(() => {
|
|
|
+ loadDeptTreeData();
|
|
|
+ // cloneRuleFormData();
|
|
|
+ // beforeRouteLeave();
|
|
|
+ if (isEditMode.value || isViewMode.value || isReviewMode.value) {
|
|
|
+ getDetail();
|
|
|
+ }
|
|
|
+ });
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+ @use '@/styles/page-details-layout.scss' as *;
|
|
|
+</style>
|