| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085 |
- <template>
- <ele-modal
- :visible.sync="visible"
- :title="dialogTitle"
- width="60vw"
- append-to-body
- :maxable="true"
- @close="cancel"
- >
- <!-- 查看态:作业票信息 / 审批流程 切换 -->
- <div class="switch" v-if="isView">
- <div class="switch_left">
- <ul>
- <li
- :class="{ active: activeComp === 'main' }"
- @click="activeComp = 'main'"
- >
- 作业票信息
- </li>
- <li
- :class="{ active: activeComp === 'bpm' }"
- @click="activeComp = 'bpm'"
- >
- 审批流程
- </li>
- </ul>
- </div>
- </div>
- <el-form
- ref="form"
- v-show="activeComp === 'main'"
- v-loading="detailLoading"
- element-loading-text="正在加载作业票详情..."
- :model="form"
- :rules="rules"
- label-width="120px"
- :disabled="isView"
- class="el-form-box"
- >
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="所属危险作业:">
- <el-input v-model="form.workName" disabled placeholder="-" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="作业类型:" prop="workType">
- <el-select
- v-model="form.workType"
- placeholder="请选择作业类型"
- style="width: 100%"
- :disabled="isEdit"
- @change="onWorkTypeChange"
- >
- <el-option
- v-for="item in workTypeList"
- :key="item.value"
- :label="item.label"
- :value="item.value"
- />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="申请单位:" prop="applyDeptId">
- <dept-select
- v-model="form.applyDeptId"
- placeholder="请选择申请单位"
- disabled
- style="width: 100%"
- @changeGroup="onDeptChange"
- />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="申请人:" prop="applicantId">
- <person-select
- ref="personRef"
- v-model="form.applicantId"
- :init="false"
- :selectList="personList"
- placeholder="请选择申请人"
- style="width: 100%"
- disabled
- @selfChange="onPersonChange"
- />
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="作业地点:" prop="workLocation">
- <area-select
- v-model="form.areaId"
- :data="areaTreeList"
- placeholder="请选择作业地点"
- style="width: 100%"
- @checkedKeys="onAreaChange"
- />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="作业开始时间:" prop="startTime">
- <el-date-picker
- v-model="form.startTime"
- type="datetime"
- placeholder="选择开始时间"
- style="width: 100%"
- value-format="yyyy-MM-dd HH:mm:ss"
- />
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="作业结束时间:" prop="endTime">
- <el-date-picker
- v-model="form.endTime"
- type="datetime"
- placeholder="选择结束时间"
- style="width: 100%"
- value-format="yyyy-MM-dd HH:mm:ss"
- />
- </el-form-item>
- </el-col>
- </el-row>
- <!-- 作业位置标记(每张底图可分别打点,由后端 map_point 字段单独保存) -->
- <el-row :gutter="20">
- <el-col :span="24">
- <el-form-item label="作业位置标记:">
- <div class="map-marker-field">
- <map-marker-preview :markers="mapMarkers" />
- <el-button
- v-if="!isView"
- size="mini"
- icon="el-icon-edit"
- @click="openMapMarker"
- >
- {{ mapMarkers.length ? '重新标记' : '标记位置' }}
- </el-button>
- </div>
- </el-form-item>
- </el-col>
- </el-row>
- <el-form-item label="作业票模板:">
- <el-input
- v-model="templateName"
- disabled
- :placeholder="
- form.workType ? '按作业类型自动匹配模板' : '请先选择作业类型'
- "
- >
- <el-button
- v-if="!frontendTemplate"
- slot="append"
- @click="openTemplate"
- >选择模板</el-button
- >
- </el-input>
- <div v-if="form.workType && !frontendTemplate" class="template-tip">
- 该作业类型未配置前端模板,可从发布规则中选择
- </div>
- </el-form-item>
- <!-- 模板内容 -->
- <el-form-item label="作业票内容:" v-show="showProcess">
- <workTicketForm
- v-if="frontendTemplate"
- ref="ticketFormRef"
- :template="frontendTemplate"
- :defaults="templateDefaults"
- :is-view="isView"
- :show-required="fullCheckEnabled"
- />
- <experimentationProcessNew
- v-else-if="!rawExtension"
- ref="experimentationProcessRef"
- :edit="false"
- ></experimentationProcessNew>
- <!-- 未匹配到前端模板时,降级展示已保存的原始作业票内容 -->
- <div v-else class="raw-extension">
- <div
- v-for="(value, key) in displayRawExtension"
- :key="key"
- class="raw-extension-row"
- >
- <span class="raw-extension-key">{{ key }}</span>
- <span class="raw-extension-value">{{ value }}</span>
- </div>
- <div
- v-if="!Object.keys(displayRawExtension).length"
- class="raw-extension-row"
- >
- 暂无作业票内容
- </div>
- </div>
- </el-form-item>
- <el-form-item label="备注:">
- <el-input
- v-model="form.remark"
- type="textarea"
- :rows="3"
- placeholder="请输入备注"
- />
- </el-form-item>
- </el-form>
- <!-- 审批流程(审批记录 + 高亮流程图) -->
- <div v-if="activeComp === 'bpm'" class="flow-box">
- <bpmDetail v-if="form.processInstanceId" :id="form.processInstanceId" />
- <div v-else class="flow-empty">该作业票暂无审批流程</div>
- </div>
- <template v-slot:footer>
- <el-button
- v-if="!isView"
- type="primary"
- @click="save()"
- :loading="loading"
- :disabled="submitLoading"
- >保存</el-button
- >
- <el-button
- v-if="canSubmit"
- type="success"
- @click="submit()"
- :loading="submitLoading"
- :disabled="loading"
- >提交</el-button
- >
- <el-button @click="cancel">{{ isView ? '关闭' : '取消' }}</el-button>
- </template>
- <releaseRules ref="releaseRulesRef" @success="onReleaseRulesSuccess" />
- <map-marker-dialog ref="mapMarkerRef" @confirm="onMapMarkerConfirm" />
- </ele-modal>
- </template>
- <script>
- import AreaSelect from '@/components/RegionsSelect/area-cascader.vue';
- import DeptSelect from '@/components/CommomSelect/dept-select.vue';
- import releaseRules from '@/components/releaseRules/index.vue';
- import personSelect from '@/components/CommomSelect/person-select.vue';
- import experimentationProcessNew from '@/components/releaseRules/experimentationProcessNew.vue';
- import workTicketForm from './components/workTicketForm.vue';
- import mapMarkerDialog from './components/mapMarkerDialog.vue';
- import mapMarkerPreview from './components/mapMarkerPreview.vue';
- import areaMixin from '@/mixins/areaMixin';
- import dictMixins from '@/mixins/dictMixins';
- import dictEnum from '@/enum/dict';
- import {
- getWorkTicketTemplate,
- getUnfilledRequiredFields
- } from './templates';
- import {
- FULL_CHECK_ENABLED,
- FULL_CHECK_KEY,
- REJECTED_STATUS,
- canSubmitStatus,
- getProcessDefinitionKey,
- getProcessInstanceId
- } from './constants';
- import { buildWorkTypeOptions, resolveWorkTypeLabel } from './workTypes';
- import { parseExtension, parseMapMarkers, buildMapPoint } from './mapMarker';
- import bpmDetail from '@/views/bpm/processInstance/detail.vue';
- import {
- ticketSave,
- ticketUpdate,
- ticketGetById,
- ticketSubmit,
- ticketListByWorkId,
- ticketMapPoint
- } from '@/api/dangerousWork';
- import { listOrganizations, getUserPage } from '@/api/system/organization';
- // 作业票表单初始值
- const defForm = {
- id: '',
- workId: '',
- workCode: '',
- workName: '',
- workType: '',
- applyDeptId: '',
- applyDeptName: '',
- applicantId: '',
- applicantName: '',
- areaId: '',
- workLocation: '',
- startTime: '',
- endTime: '',
- remark: '',
- // 作业票状态:1-待提交、2-审批中、3-已通过、4-已驳回
- status: '',
- // 审批流程实例 id,用于查看审批流程
- processInstanceId: ''
- };
- export default {
- components: {
- AreaSelect,
- DeptSelect,
- personSelect,
- releaseRules,
- experimentationProcessNew,
- workTicketForm,
- mapMarkerDialog,
- mapMarkerPreview,
- bpmDetail
- },
- mixins: [areaMixin, dictMixins],
- data() {
- return {
- visible: false,
- loading: false,
- // 提交审批中
- submitLoading: false,
- // 新增申请保存后记录作业票 id,提交失败重试时复用
- newTicketId: '',
- // 查询作业票详情中
- detailLoading: false,
- // 详情请求序号:用于丢弃过期响应(快速切换作业票时)
- detailSeq: 0,
- dialogType: 'add',
- // 弹窗内容页签:main-作业票信息、bpm-审批流程
- activeComp: 'main',
- form: { ...defForm },
- personList: [],
- // 匹配到的前端作业票模板
- frontendTemplate: null,
- // 未匹配到前端模板时的原始作业票内容(降级展示)
- rawExtension: null,
- // 作业位置标记(由后端 map_point 字段单独保存)
- mapMarkers: [],
- // 作业票未保存时暂存的地图位置标记,保存后补存
- pendingMarkers: null,
- // 选中的作业票模板(发布规则)
- templateData: {},
- templateName: '',
- rules: {
- workType: [
- { required: true, message: '请选择作业类型', trigger: 'change' }
- ],
- applyDeptId: [
- { required: true, message: '请选择申请单位', trigger: 'change' }
- ],
- applicantId: [
- { required: true, message: '请选择申请人', trigger: 'change' }
- ],
- workLocation: [
- { required: true, message: '请选择作业地点', trigger: 'change' }
- ],
- startTime: [
- { required: true, message: '请选择作业开始时间', trigger: 'change' }
- ],
- endTime: [
- { required: true, message: '请选择作业结束时间', trigger: 'change' }
- ]
- }
- };
- },
- computed: {
- dialogTitle() {
- if (this.dialogType === 'edit') {
- return '修改作业票';
- }
- if (this.dialogType === 'view') {
- return '查看作业票';
- }
- return '申请作业票';
- },
- isView() {
- return this.dialogType === 'view';
- },
- // 编辑模式:作业类型不允许变更(模板与已填内容强相关)
- isEdit() {
- return this.dialogType === 'edit';
- },
- // 当前作业类型名称
- currentWorkTypeName() {
- const option = this.workTypeList.find(
- (item) => String(item.value) === String(this.form.workType)
- );
- return (
- option?.label ||
- resolveWorkTypeLabel(
- this.form.workType,
- this.dict[dictEnum['危险作业类型']]
- )
- );
- },
- // 是否可提交:
- // - 新增申请:作业票还没保存,提交时内部会先保存再发起审批流
- // - 编辑:仅待提交(草稿)与已驳回状态允许提交,已驳回为重新提交
- canSubmit() {
- if (this.isView) {
- return false;
- }
- if (this.isEdit) {
- return !!this.form.id && canSubmitStatus(this.form.status);
- }
- return true;
- },
- // 作业类型下拉:优先取后端字典,字典没有时使用内置作业类型兜底
- // 与作业票管理弹窗共用同一套构建逻辑,保证申请页与列表页展示一致
- workTypeList() {
- return buildWorkTypeOptions(this.dict[dictEnum['危险作业类型']]);
- },
- // 是否展示模板内容
- showProcess() {
- return (
- !!this.frontendTemplate ||
- !!this.templateData?.tempJson?.tempJson ||
- !!this.rawExtension
- );
- },
- // 降级展示的原始作业票内容:整体校验标记属于控制参数,不作为票面内容展示
- displayRawExtension() {
- const raw = { ...(this.rawExtension || {}) };
- delete raw[FULL_CHECK_KEY];
- return raw;
- },
- // 是否整体校验作业票内容(按环境区分,不可人工调整)
- fullCheckEnabled() {
- return FULL_CHECK_ENABLED;
- },
- // 模板默认值:取自所属危险作业
- // 作业计划开始/结束时间默认留空,由申请人按实际计划填写
- templateDefaults() {
- const option = this.workTypeList.find(
- (item) => String(item.value) === String(this.form.workType)
- );
- return {
- apply_dept: {
- value: this.form.applyDeptId,
- name: this.form.applyDeptName
- },
- apply_time: this.getCurrentTime(),
- work_type: option?.label || this.currentWorkTypeName,
- work_site: this.form.workLocation,
- lifting_site: this.form.workLocation
- };
- }
- },
- created() {
- this.requestDict('危险作业类型');
- },
- methods: {
- // 部门变更时重置人员
- onDeptChange(deptId) {
- this.form.applicantId = '';
- this.form.applicantName = '';
- if (deptId) {
- this.loadPersonList(deptId);
- }
- this.$nextTick(() => {
- this.$refs.form.validateField('applicantId');
- });
- },
- // 人员选择回调
- onPersonChange(_val, item) {
- this.form.applicantName = item ? item.name : '';
- },
- // 区域选择回调
- onAreaChange(nodeInfo) {
- this.form.workLocation =
- nodeInfo && nodeInfo[0]?.pathLabels?.join('/')
- ? nodeInfo[0].pathLabels.join('/')
- : '';
- },
- // 作业类型变更后按作业类型匹配作业票模板
- onWorkTypeChange(val) {
- this.frontendTemplate = null;
- this.templateData = {};
- this.templateName = '';
- this.$refs.experimentationProcessRef?.init('[]');
- if (!val) {
- return;
- }
- // 作业类型名称,用于按名称兜底匹配模板
- const option = this.workTypeList.find(
- (item) => String(item.value) === String(val)
- );
- const workTypeName =
- option?.label ||
- resolveWorkTypeLabel(val, this.dict[dictEnum['危险作业类型']]);
- const template = getWorkTicketTemplate(val, workTypeName);
- if (template) {
- // 命中前端内置模板,直接按作业类型套用
- this.frontendTemplate = template;
- this.templateName = template.name;
- return;
- }
- // 未配置前端模板时,回退到发布规则中选择
- this.openTemplate();
- },
- // 当前时间 yyyy-MM-dd HH:mm:ss
- getCurrentTime() {
- const date = new Date();
- const pad = (n) => (n < 10 ? `0${n}` : `${n}`);
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(
- date.getDate()
- )} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(
- date.getSeconds()
- )}`;
- },
- // 打开模板选择
- openTemplate() {
- if (!this.form.workType) {
- this.$message.warning('请先选择作业类型');
- return;
- }
- this.$refs.releaseRulesRef.open(this.form.workType);
- },
- // 模板选择回调
- onReleaseRulesSuccess(data) {
- this.frontendTemplate = null;
- this.templateData = data || {};
- this.templateName = data?.name || data?.code || '';
- if (data?.tempJson?.tempJson) {
- this.$refs.experimentationProcessRef.init(data.tempJson.tempJson);
- } else {
- this.$refs.experimentationProcessRef?.init('[]');
- }
- },
- // 查询申请单位名称
- async loadDeptName(deptId) {
- if (!deptId) return;
- const userInfo = this.$store.state.user.info || {};
- let deptName = '';
- try {
- const list = await listOrganizations({});
- const dept = (list || []).find((item) => item.id == deptId);
- if (dept) {
- deptName = dept.name;
- }
- } catch (e) {
- console.error('查询申请单位失败', e);
- }
- if (!deptName) {
- deptName = userInfo.deptName || userInfo.groupName || '';
- }
- this.form.applyDeptName = deptName;
- },
- // 加载人员列表,并把申请人补进下拉,保证能回显姓名
- async loadPersonList(deptId, applicant) {
- const list = [];
- if (deptId) {
- try {
- const res = await getUserPage({
- pageNum: 1,
- size: -1,
- groupId: deptId
- });
- list.push(...(res?.list || []));
- } catch (e) {
- console.error('查询人员列表失败', e);
- }
- }
- if (applicant?.id && !list.find((item) => item.id == applicant.id)) {
- list.unshift({ id: applicant.id, name: applicant.name });
- }
- this.personList = list;
- },
- // 打开弹窗,work 为所属的危险作业(申请作业票)
- async open(work) {
- this.visible = true;
- this.dialogType = 'add';
- this.activeComp = 'main';
- this.newTicketId = '';
- const detail = work || {};
- this.resetTicketForm();
- this.form = {
- ...defForm,
- // id 是作业票自身 id(编辑时才有值),detail 是所属危险作业,不能混用
- id: '',
- workId: detail.id || '',
- workCode: detail.workCode || '',
- workName: detail.workName || '',
- applyDeptId: detail.applyDeptId || '',
- applyDeptName: detail.applyDeptName || '',
- applicantId: detail.applicantId || '',
- applicantName: detail.applicantName || '',
- workLocation: detail.workLocation || '',
- areaId: this.getAreaIdByName(detail.workLocation),
- startTime: detail.startTime || '',
- endTime: detail.endTime || ''
- };
- if (!this.form.applyDeptName) {
- this.loadDeptName(this.form.applyDeptId);
- }
- this.loadPersonList(this.form.applyDeptId, {
- id: this.form.applicantId,
- name: this.form.applicantName
- });
- this.$nextTick(() => {
- this.$refs.form && this.$refs.form.clearValidate();
- });
- },
- // 打开已有作业票(查看/编辑)
- // 详情以接口返回为准:列表行数据不含作业票内容(extensionJson),必须查详情
- // @param ticket 作业票对象(至少包含 id)
- // @param work 所属危险作业(用于展示所属作业名称,详情接口不返回)
- // @param mode view-查看 / edit-编辑
- async openTicket(ticket, work, mode) {
- this.visible = true;
- this.dialogType = mode || 'view';
- this.activeComp = 'main';
- this.newTicketId = '';
- this.detailLoading = true;
- const seq = ++this.detailSeq;
- // 先清空上一次的数据,避免详情返回前展示旧内容
- this.resetTicketForm();
- let detail = ticket || {};
- try {
- if (detail.id) {
- const data = await ticketGetById(detail.id, {
- showErrorToast: false
- });
- // 已有更新的请求,丢弃过期响应
- if (seq !== this.detailSeq) {
- return;
- }
- detail = data || detail;
- }
- } catch (e) {
- if (seq !== this.detailSeq) {
- return;
- }
- this.detailLoading = false;
- this.$message.error(e?.message || '查询作业票详情失败');
- this.cancel();
- return;
- }
- this.detailLoading = false;
- this.fillTicketForm(detail, work || {}, ticket);
- },
- // 重置作业票表单状态
- resetTicketForm() {
- this.form = { ...defForm };
- this.templateData = {};
- this.templateName = '';
- this.frontendTemplate = null;
- this.rawExtension = null;
- this.mapMarkers = [];
- this.pendingMarkers = null;
- this.$refs.experimentationProcessRef?.init('[]');
- },
- // 用作业票详情回填表单
- // @param detail 作业票详情
- // @param workDetail 所属危险作业(详情接口不返回作业名称)
- // @param row 列表行数据,详情未返回状态时用于兜底
- fillTicketForm(detail, workDetail, row) {
- this.form = {
- ...defForm,
- id: detail.id || '',
- workId: detail.workId || workDetail.id || '',
- workCode: workDetail.workCode || '',
- workName: workDetail.workName || '',
- applyDeptId: detail.applyDeptId || '',
- applyDeptName: detail.applyDeptName || '',
- applicantId: detail.applicantId || '',
- applicantName: detail.applicantName || '',
- workLocation: detail.workLocation || '',
- areaId: this.getAreaIdByName(detail.workLocation),
- startTime: detail.startTime || '',
- endTime: detail.endTime || '',
- workType: detail.workType || '',
- status: detail.status ?? row?.status ?? '',
- // 流程实例 id:详情没有时用列表行数据兜底
- processInstanceId:
- getProcessInstanceId(detail) || getProcessInstanceId(row)
- };
- // 解析已保存的作业票内容
- const extension = parseExtension(detail.extensionJson);
- this.form.remark = extension.remark || '';
- // 回显作业位置标记(每张底图一个标记点)
- this.mapMarkers = parseMapMarkers(
- detail?.mapPoint ?? detail?.map_point
- );
- // 按作业类型匹配前端内置模板,匹配到则结构化回显
- const option = this.workTypeList.find(
- (item) => String(item.value) === String(detail.workType)
- );
- const workTypeName =
- option?.label ||
- resolveWorkTypeLabel(
- detail.workType,
- this.dict[dictEnum['危险作业类型']]
- );
- const template = getWorkTicketTemplate(detail.workType, workTypeName);
- if (template) {
- this.frontendTemplate = template;
- this.templateName = template.name;
- // 模板表单是 v-if 渲染的:模板变化时组件会重建并清空表单,
- // 必须等它挂载初始化完成后再回填已保存内容
- this.$nextTick(() => {
- this.$refs.ticketFormRef?.fillValues(extension);
- });
- } else {
- // 未匹配到前端模板(发布规则模板),降级展示原始内容
- this.rawExtension = extension;
- }
- if (!this.form.applyDeptName) {
- this.loadDeptName(this.form.applyDeptId);
- }
- this.loadPersonList(this.form.applyDeptId, {
- id: this.form.applicantId,
- name: this.form.applicantName
- });
- this.$nextTick(() => {
- this.$refs.form && this.$refs.form.clearValidate();
- });
- },
- // 打开地图位置标记弹框
- openMapMarker() {
- this.$refs.mapMarkerRef.open(this.mapMarkers);
- },
- // 地图位置标记确认:调用后端专用接口保存(整段覆盖)
- async onMapMarkerConfirm(markers) {
- const list = markers || [];
- this.mapMarkers = list;
- // 作业票还没保存时没有 id,先暂存,等作业票保存后补存
- if (!this.form.id) {
- this.pendingMarkers = list;
- this.$message.success(
- list.length
- ? '地图位置已标记,保存作业票后生效'
- : '已清除地图位置标记'
- );
- return;
- }
- try {
- await ticketMapPoint({
- ticketId: this.form.id,
- // 没有标记时传 null,后端按空值清除标记
- mapPoint: buildMapPoint(list)
- });
- this.$message.success(
- list.length ? '地图位置标记成功' : '已清除地图位置标记'
- );
- } catch (e) {
- // 失败提示由响应拦截器统一处理
- }
- },
- // 保存暂存的地图位置标记:新增作业票保存后补存(编辑态标记已即时保存)
- // 标记保存失败不影响作业票本身的保存/提交,失败提示由响应拦截器统一处理
- // @param ticketId 作业票 id
- async flushPendingMarkers(ticketId) {
- if (!ticketId || !this.pendingMarkers) {
- return;
- }
- try {
- await ticketMapPoint({
- ticketId,
- mapPoint: buildMapPoint(this.pendingMarkers)
- });
- this.pendingMarkers = null;
- } catch (e) {
- // 忽略:作业票已保存,标记可稍后重新标记
- }
- },
- cancel() {
- this.form = { ...defForm };
- this.personList = [];
- this.templateData = {};
- this.templateName = '';
- this.frontendTemplate = null;
- this.rawExtension = null;
- this.mapMarkers = [];
- this.pendingMarkers = null;
- this.detailLoading = false;
- this.submitLoading = false;
- this.newTicketId = '';
- this.visible = false;
- this.$refs.form && this.$refs.form.clearValidate();
- },
- // 收集模板中填写的作业票内容,作为扩展数据提交
- getExtension() {
- // 降级场景(未匹配到前端模板)以已保存的原始内容为基础,避免覆盖丢失
- const extension = { ...(this.rawExtension || {}) };
- try {
- if (this.frontendTemplate) {
- // 前端内置模板:由 workTicketForm 收集
- Object.assign(
- extension,
- this.$refs.ticketFormRef?.getExtension() || {}
- );
- } else if (!this.rawExtension) {
- // 发布规则模板:由 experimentationProcessNew 收集
- const { data } = this.$refs.experimentationProcessRef.getObjValue();
- Object.assign(extension, data || {});
- }
- } catch (e) {
- console.error('获取作业票内容失败', e);
- }
- if (this.form.remark) {
- extension.remark = this.form.remark;
- } else {
- delete extension.remark;
- }
- // 整体校验标记随作业票内容一起保存,便于后端/审批流识别提交时的校验模式
- extension[FULL_CHECK_KEY] = FULL_CHECK_ENABLED;
- return extension;
- },
- // 业务校验并组装作业票保存参数
- // 校验不通过时给出提示并返回 null
- buildSaveParams() {
- // 整体校验开启:必须选择作业票模板,并完整填写作业票内容
- if (FULL_CHECK_ENABLED) {
- if (
- !this.frontendTemplate &&
- !this.templateData?.id &&
- !this.rawExtension
- ) {
- this.$message.warning('请选择作业票模板');
- return null;
- }
- }
- const extension = this.getExtension();
- // 前端内置模板按源系统表单做必填校验(整体校验开启时才校验)
- if (FULL_CHECK_ENABLED && this.frontendTemplate) {
- const unfilled = getUnfilledRequiredFields(
- this.frontendTemplate,
- extension
- );
- if (unfilled.length) {
- this.$message.warning(`请填写作业票内容:${unfilled.join('、')}`);
- return null;
- }
- }
- // 单独新增作业票时,所属危险作业必填
- if (!this.isEdit && !this.form.workId) {
- this.$message.warning('请先选择所属危险作业');
- return null;
- }
- const commonParams = {
- workType: String(this.form.workType),
- applyDeptId: this.form.applyDeptId,
- applyDeptName: this.form.applyDeptName,
- applicantId: this.form.applicantId,
- applicantName: this.form.applicantName,
- workLocation: this.form.workLocation,
- startTime: this.form.startTime,
- endTime: this.form.endTime,
- // 后端扩展数据字段为 extensionJson,且要求字符串(JSON 字符串)
- extensionJson: JSON.stringify(extension)
- };
- // 修改接口不接收 workId(所属危险作业不可变更)
- return this.isEdit
- ? { id: this.form.id, ...commonParams }
- : { workId: this.form.workId, ...commonParams };
- },
- save() {
- if (this.isView) {
- this.cancel();
- return;
- }
- this.$refs.form.validate((valid) => {
- if (!valid) {
- return;
- }
- const params = this.buildSaveParams();
- if (!params) {
- return;
- }
- this.loading = true;
- const request = this.isEdit
- ? ticketUpdate(params)
- : ticketSave(params);
- request
- .then(async (res) => {
- // 新增作业票:保存后补存暂存的地图位置标记
- if (this.pendingMarkers && !this.form.id) {
- const ticketId = await this.resolveSavedTicketId(res, params);
- if (ticketId) {
- await this.flushPendingMarkers(ticketId);
- } else {
- this.$message.warning(
- '作业票已保存,但未获取到作业票ID,地图位置标记未保存,请在作业票列表中重新标记'
- );
- }
- }
- this.$message.success(this.isEdit ? '保存成功' : '申请成功');
- this.cancel();
- this.$emit('reload');
- })
- .catch(() => {})
- .finally(() => {
- this.loading = false;
- });
- });
- },
- // 提交作业票并发起审批流:审批流编码按作业类型取
- // 新增时先保存拿到作业票 id,编辑时直接用已有 id,最终与列表提交走同一个接口
- submit() {
- this.$refs.form.validate((valid) => {
- if (!valid) {
- return;
- }
- const params = this.buildSaveParams();
- if (!params) {
- return;
- }
- const processDefinitionKey = getProcessDefinitionKey(
- this.form.workType,
- this.currentWorkTypeName
- );
- if (!processDefinitionKey) {
- this.$message.warning(
- '该作业类型暂未接入审批流,请联系管理员配置审批流编码'
- );
- return;
- }
- this.$confirm(this.getSubmitTip(), '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning'
- })
- .then(() => this.submitTicket(params, processDefinitionKey))
- .catch(() => {});
- });
- },
- // 提交确认文案:已驳回的作业票重新提交会重新发起一轮审批流
- getSubmitTip() {
- if (Number(this.form.status) === REJECTED_STATUS) {
- return '该作业票已被驳回,重新提交后将重新发起审批流,确认提交吗?';
- }
- if (this.isEdit) {
- return '提交前将自动保存作业票内容,提交后作业票不可再修改,确认提交吗?';
- }
- return '提交后将保存作业票并发起审批流,提交后不可再修改,确认提交吗?';
- },
- // 保存作业票并发起审批流
- async submitTicket(params, processDefinitionKey) {
- this.submitLoading = true;
- try {
- // form.id 为空时是新增;newTicketId 记录本次新增已保存的 id,
- // 提交失败重试时复用,避免重复建票
- let ticketId = this.form.id || this.newTicketId;
- if (ticketId) {
- // 已保存过:先保存最新内容,保证审批中的票与页面一致
- // 修改接口不接收 workId(所属危险作业不可变更)
- const { workId, ...updateParams } = params;
- await ticketUpdate({ ...updateParams, id: ticketId });
- } else {
- // 新增:先保存拿到作业票 id
- const saveRes = await ticketSave(params);
- ticketId = await this.resolveSavedTicketId(saveRes, params);
- if (!ticketId) {
- this.$message.warning(
- '作业票已保存,但未获取到作业票ID,请在作业票列表中点击「提交」发起审批'
- );
- this.cancel();
- this.$emit('reload');
- return;
- }
- this.newTicketId = ticketId;
- // 新增作业票:补存暂存的地图位置标记
- await this.flushPendingMarkers(ticketId);
- }
- await ticketSubmit({ id: ticketId, processDefinitionKey });
- this.$message.success('提交成功,已发起审批');
- this.cancel();
- this.$emit('reload');
- } catch (e) {
- // 失败提示由响应拦截器统一处理
- } finally {
- this.submitLoading = false;
- }
- },
- // 取新建作业票的 id:优先用保存响应,没有则按作业票信息回查
- async resolveSavedTicketId(saveRes, params) {
- const payload = saveRes?.data ?? saveRes;
- const id =
- this.extractId(payload) ||
- this.extractId(payload?.data) ||
- this.extractId(payload?.ticket);
- if (id) {
- return id;
- }
- // 兜底:按所属危险作业回查作业票列表,取与本次申请一致的最新一条
- try {
- const list = await ticketListByWorkId(params.workId);
- const matched = (list || [])
- .filter(
- (item) =>
- String(item.applicantId) === String(params.applicantId) &&
- String(item.workType) === String(params.workType) &&
- String(item.startTime || '') === String(params.startTime || '')
- )
- .sort((a, b) => (a.createTime < b.createTime ? 1 : -1));
- return matched.length ? matched[0].id : '';
- } catch (e) {
- return '';
- }
- },
- // 从响应数据中取作业票 id
- extractId(source) {
- if (source === undefined || source === null) {
- return '';
- }
- if (typeof source === 'number') {
- return source;
- }
- if (typeof source === 'string') {
- return /^\d+$/.test(source) ? source : '';
- }
- if (typeof source === 'object') {
- const value = source.id ?? source.ticketId;
- if (value !== undefined && value !== null && value !== '') {
- return value;
- }
- }
- return '';
- }
- }
- };
- </script>
- <style lang="scss" scoped>
- .el-form-box {
- max-height: calc(100vh - 300px);
- overflow-y: auto;
- padding-right: 10px;
- }
- .template-tip {
- color: #909399;
- font-size: 12px;
- line-height: 20px;
- }
- /* 作业位置标记:缩略图与按钮并排 */
- .map-marker-field {
- display: flex;
- align-items: flex-start;
- gap: 12px;
- }
- /* 未匹配到前端模板时的原始作业票内容 */
- .raw-extension {
- width: 100%;
- max-height: 300px;
- overflow-y: auto;
- padding: 8px 12px;
- background: #f6f7f9;
- border-radius: 4px;
- }
- .raw-extension-row {
- display: flex;
- font-size: 13px;
- line-height: 24px;
- word-break: break-all;
- }
- .raw-extension-key {
- flex: 0 0 180px;
- color: #909399;
- }
- .raw-extension-value {
- flex: 1;
- color: #303133;
- }
- /* 审批流程 */
- .flow-box {
- max-height: calc(100vh - 300px);
- overflow-y: auto;
- }
- .flow-empty {
- padding: 40px 0;
- font-size: 14px;
- color: #909399;
- text-align: center;
- }
- </style>
|