| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371 |
- <script setup>
- import { useDataDictStore } from '@/store/modules/dataDict';
- import { onMounted, reactive, ref, computed, getCurrentInstance, watch } from 'vue';
- import { useDebounceFn } from '@/utils/useDebounceFn.js';
- import tpfTimeRange from '@/components/tpf-time-range/tpf-time-range.vue';
- import deviceTransfer from '@/components/device-transfer/index.vue';
- import { updateRuiDuReportBatch, getRuiDuReportAttrs } from '@/api/ruiDu.js';
- import config from '@/utils/config';
- import dayjs from 'dayjs';
- import { getTenantId, getAccessToken } from '@/utils/auth.js';
- const { appContext } = getCurrentInstance();
- const t = appContext.config.globalProperties.$t;
- const { getIntDictOptions, getStrDictOptions, loadDataDictList } = useDataDictStore();
- const rdStatusRange = ref([]);
- const techniqueRange = ref([]);
- const handleInitSelect = () => {
- rdStatusRange.value = getStrDictOptions('rdStatus').map(item => {
- return {
- ...item,
- text: item.label,
- };
- });
- // 施工工艺
- techniqueRange.value = getIntDictOptions('rq_iot_project_technology_rd').map(item => {
- return {
- ...item,
- text: item.label,
- };
- });
- };
- onMounted(() => {
- loadDataDictList().then(() => {
- handleInitSelect();
- });
- });
- const selectPlaceholder = computed(() => {
- return props.formDisable ? ' ' : t('operation.PleaseSelect');
- });
- const inputPlaceholder = computed(() => {
- return props.formDisable ? ' ' : t('operation.PleaseInput');
- });
- const props = defineProps({
- reportId: {
- type: String,
- default: '',
- },
- reportData: {
- type: Object,
- default: () => {},
- },
- formDisable: {
- type: Boolean,
- default: false, // 是否禁用表单
- },
- });
- const isRequired = computed(() => {
- return props.formDisable ? false : true;
- });
- const reportFormRef = ref(null);
- // 设备选择器
- const deviceTransferRef = ref(null);
- // 已选择的设备(名称)
- const selectedEquipmentNames = ref('');
- // 未选择的设备(名称)
- const unselectedEquipmentNames = ref('');
- const timeRangeRef = ref(null);
- const startTime = ref('00:00');
- const startDefaultTime = ref('06:00');
- const endTime = ref('24:00');
- const endDefaultTime = ref('06:00');
- const timeRange = data => {
- form.startTime = data[0];
- form.endTime = data[1];
- };
- const dailyFuel = ref(0);
- const form = reactive({
- startTime: startDefaultTime.value,
- endTime: endDefaultTime.value,
- platformIds: [],
- deviceIds: [],
- productionStatus: '',
- nextPlan: '',
- externalRental: '',
- faultDowntime: '',
- malfunction: '',
- attachments: [],
- reportFuels: [],
- });
- const formDataBaseRules = reactive({
- // 时间节点 - 开始时间
- startTime: {
- rules: [
- {
- required: true,
- errorMessage: `${t('operation.PleaseSelect')}${t('ruiDu.timeNode')}`,
- },
- ],
- },
- // 时间节点 - 结束时间
- endTime: {
- rules: [
- {
- required: true,
- errorMessage: `${t('operation.PleaseSelect')}${t('ruiDu.timeNode')}`,
- },
- ],
- },
- // 当日生产动态
- productionStatus: {
- rules: [
- {
- required: true,
- errorMessage: `${t('operation.PleaseFillIn')}${t('ruiDu.dailyProductionDynamic')}`,
- },
- ],
- },
- nextPlan: {
- rules: [
- {
- required: true,
- errorMessage: `请输入下步工作计划`,
- },
- ],
- },
- });
- const validate = async () => {
- return await reportFormRef.value.validate();
- };
- const submitForm = async () => {
- const deleteId = wellOptions.value.filter(o => !form.platformIds.includes(o.value));
- deleteId.forEach(o => {
- delete form[o.value];
- });
- await validate();
- // // 处理表单数据
- const formDataCopy = JSON.parse(JSON.stringify(form));
- if (!formDataCopy.dailyFuel && formDataCopy.dailyFuel !== 0) {
- uni.showToast({
- title: '请输入当日油耗',
- icon: 'none',
- });
- return;
- }
- const responseData = [];
- // // 处理施工工艺
- form.platformIds.forEach(id => {
- formDataCopy[id].extProperty.forEach(attr => {
- if (attr.dataType === 'double') {
- attr.actualValue = Number(attr.actualValue);
- }
- });
- const attachments = JSON.parse(JSON.stringify(formDataCopy.attachments)).map(item => {
- item.bizId = id;
- return item;
- });
- let platformWell = props.reportData.platformWell;
- if (platformWell === 1) {
- platformWell = id === props.reportData.id ? 1 : 2;
- }
- const data = {
- id,
- timeRange: ['1970-01-01T00:00:00.008Z', '1970-01-01T00:00:00.008Z'],
- projectDepartment: '',
- costCenter: '',
- dynamicFields: {},
- platformWell,
- companyId: props.reportData.companyId,
- deptId: props.reportData.deptId,
- attachments,
- deviceIds: formDataCopy.deviceIds,
- startTime: formDataCopy.startTime,
- endTime: formDataCopy.endTime,
- extProperty: formDataCopy[id].extProperty,
- externalRental: formDataCopy.externalRental,
- faultDowntime: formDataCopy.faultDowntime,
- malfunction: formDataCopy.malfunction,
- nextPlan: formDataCopy.nextPlan,
- ...(props.reportData.platformWell === 1
- ? { platformId: props.reportData.platforms.find(v => v.reportId === id).id }
- : {}),
- productionStatus: formDataCopy.productionStatus,
- rdStatus: formDataCopy[id].rdStatus,
- techniqueIds: formDataCopy[id].techniqueIds.map(v => v.toString()),
- reportFuels: formDataCopy.reportFuels.map(item => ({
- ...item,
- customFuel: Number(item.customFuel),
- reportId: id,
- })),
- dailyFuel: Number(formDataCopy.dailyFuel),
- };
- responseData.push(data);
- });
- // 提交表单
- updateRuiDuReportBatch(responseData).then(res => {
- // 提交成功
- if (res.code === 0) {
- uni.showToast({ title: t('operation.success'), icon: 'none' });
- // 返回上一页
- uni.navigateBack();
- } else {
- uni.showToast({ title: res.msg, icon: 'none' });
- }
- });
- };
- const handleClickSelectDevice = () => {
- deviceTransferRef.value.open();
- };
- const handleEquipmentNames = deviceIds => {
- form.deviceIds = deviceIds || []; //施工设备
- const { selectedDevices = [] } = props.reportData || {};
- const deviceIdSet = new Set(deviceIds);
- // 已选择的设备(名称)
- selectedEquipmentNames.value = selectedDevices
- .filter(item => deviceIdSet.has(item.id))
- .map(item => item.deviceName)
- .join(',');
- // 未选择的设备(名称)
- unselectedEquipmentNames.value =
- selectedDevices
- .filter(item => !deviceIdSet.has(item.id))
- .map(item => item.deviceName)
- .join(',') || t('ruiDu.allEquipmentConstructed');
- };
- // 设备选择器回调
- const handleTransferChange = selectedIds => {
- // 更新已选择的设备及名称
- handleEquipmentNames(selectedIds);
- };
- const steps = ref([]);
- const formDataFormat = () => {
- // 处理时间范围
- timeRangeFormat();
- // 处理已选择的设备
- if (props.reportData?.deviceIds) {
- const { deviceIds = [] } = props.reportData || {};
- handleEquipmentNames(deviceIds);
- }
- if (props.reportData.platformWell === 1) {
- form.platformIds = props.reportData.platforms?.map(v => v.reportId) ?? [];
- } else {
- form.platformIds = [props.reportData.id];
- }
- // if (props.formDisable) {
- // } else {
- // form.platformIds = [props.reportData.id];
- // }
- if (props.reportData.platformWell === 1) {
- props.reportData.platforms.forEach(p => {
- form[p.reportId] = { rdStatus: p.rdStatus, techniqueIds: p.techniqueIds, extProperty: p.extProperty };
- });
- } else {
- form[props.reportData.id] = {
- rdStatus: props.reportData.rdStatus,
- techniqueIds: props.reportData.techniqueIds,
- extProperty: props.reportData.extProperty,
- };
- }
- // 当日生产动态
- form.productionStatus = props.reportData.productionStatus || ''; //当日生产动态
- // 下步工作计划
- form.nextPlan = props.reportData.nextPlan || ''; //下步工作计划
- // 外租设备
- form.externalRental = props.reportData.externalRental || ''; //外租设备
- // 故障情况
- form.malfunction = props.reportData.malfunction || ''; //故障情况
- // 故障误工H
- form.faultDowntime = props.reportData.faultDowntime || ''; //故障误工H
- // 附件
- form.attachments = props.reportData.attachments || [];
- // 提取变量,方便阅读
- const list1 = props.reportData.reportFuels;
- const list2 = props.reportData.reportedFuels;
- const validList = list1?.length > 0 ? list1 : list2?.length > 0 ? list2 : [];
- form.reportFuels = validList.map(v => ({
- ...v,
- // 这里保持你原有的数值处理逻辑
- customFuel: Number(Number(props.formDisable ? v.customFuel ?? 0 : v.customFuel ?? v.zhbdFuel ?? 0).toFixed(2)),
- }));
- steps.value = (props.reportData.taskProgresses ?? []).map(v => ({ title: v.rdStatusLabel, desc: v.createTime }));
- initDailyFuel();
- // form.
- // 展示用的文件列表
- // attachmentsFileList.value =
- // props.reportData?.attachments?.map(item => ({
- // ...item,
- // name: item.filename,
- // url: item.filePath,
- // })) || [];
- };
- // 初始化时间范围
- const timeRangeFormat = () => {
- if (props.reportData.startTime) {
- const startArr = props.reportData.startTime;
- form.startTime = `${startArr[0].toString().padStart(2, '0')}:${startArr[1].toString().padStart(2, '0')}`;
- }
- if (props.reportData.endTime) {
- const endArr = props.reportData.endTime;
- form.endTime = `${endArr[0].toString().padStart(2, '0')}:${endArr[1].toString().padStart(2, '0')}`;
- }
- };
- watch(
- () => props.reportData,
- val => {
- if (val.id) {
- formDataFormat();
- }
- },
- { deep: true, immediate: true }
- );
- const wellOptions = computed(() => {
- return (
- props.reportData.platforms?.map(v => ({
- text: v.wellName,
- value: v.reportId,
- })) ?? []
- );
- });
- const handleClickTimeRange = () => {
- // 打开时间范围选择器
- timeRangeRef.value.open();
- };
- const uploadRef = ref(null);
- const attachmentList = ref([]);
- const chooseFile = () => {
- uploadRef.value.chooseFile({
- count: 9,
- size: 50,
- success: files => {
- attachmentList.value = attachmentList.value.concat(files);
- attachmentList.value.forEach(file => {
- // 将等待上传和上传失败的文件提交上传到服务器
- // 提示:::如果接口不支持跨域,改成调用this.getTempFilePath(file)
- if (file.status === 'waiting' || file.status === 'fail') {
- uploadHandle(file);
- }
- });
- },
- });
- };
- const uploadHandle = file => {
- uploadRef.value.upload({
- url: config.default.apiUrl + config.default.apiUrlSuffix + '/rq/file/upload',
- file,
- name: 'files',
- header: {
- 'Authorization': getAccessToken() ? 'Bearer ' + getAccessToken() : '',
- 'Tenant-id': getTenantId() ? getTenantId() : '1',
- 'Device-id': 'undefined',
- },
- method: 'post',
- success: e => {
- file.status = 'success';
- const result = JSON.parse(e.result);
- if (result.code !== 0) {
- uni.showToast({
- title: `【${file.name}】${t('operation.uploadFail')}`,
- icon: 'none',
- });
- return;
- }
- form.attachments.push({
- bizId: props.reportId,
- category: 'daily_report',
- filePath: result.data.files[0].filePath,
- filename: result.data.files[0].name,
- // fileSize: data.files[0].size,
- // fileType: data.files[0].type,
- remark: '',
- type: 'attachment',
- });
- },
- fail: e => {
- file.status = 'fail';
- console.error('上传异常:', err);
- uni.showToast({ title: t('operation.uploadFail'), icon: 'none' });
- },
- });
- };
- // const uploadHandle = file => {
- // uploadRef.value.getTempFilePath({
- // file,
- // success: e => {
- // uni.uploadFile({
- // url: config.default.apiUrl + config.default.apiUrlSuffix + '/rq/file/upload',
- // header: {
- // 'Authorization': getAccessToken() ? 'Bearer ' + getAccessToken() : '',
- // 'tenant-id': getTenantId() ? getTenantId() : '1',
- // 'device-id': undefined,
- // },
- // name: 'files',
- // // #ifdef H5
- // file: e.result,
- // // #endif
- // // #ifndef H5
- // filePath: e.result,
- // // #endif
- // success: e => {
- // console.log('e :>> ', e);
- // // file.status = 'success';
- // // const result = JSON.parse(e.result);
- // // if (e.code !== 0) {
- // // uni.showToast({
- // // title: `【${file.name}】${t('operation.uploadFail')}`,
- // // icon: 'none',
- // // });
- // // return;
- // // }
- // // form.attachments.push({
- // // bizId: props.reportId,
- // // category: 'daily_report',
- // // filePath: data.files[0].filePath,
- // // filename: data.files[0].name,
- // // // fileSize: data.files[0].size,
- // // // fileType: data.files[0].type,
- // // remark: '',
- // // type: 'attachment',
- // // });
- // },
- // fail: e => {
- // console.log('e :>> ', e);
- // },
- // });
- // },
- // });
- // };
- // 删除附件
- const deleteFiles = index => {
- // 1. 从formData.attachments中移除选中项
- form.attachments.splice(index, 1);
- };
- function copyToPublicAndOpen(sourcePath, fileName) {
- // 获取 _downloads/ (公共下载目录) 的目录对象
- plus.io.resolveLocalFileSystemURL(
- '_downloads/',
- entryDir => {
- // 获取源文件对象
- plus.io.resolveLocalFileSystemURL(
- sourcePath,
- entryFile => {
- // 执行复制操作:将 sourcePath 复制到 _downloads/ 下,并重命名
- entryFile.copyTo(
- entryDir,
- fileName,
- newEntry => {
- console.log('文件已复制到公共目录:', newEntry.fullPath);
- uni.showToast({
- title: '已保存到下载目录',
- icon: 'none',
- duration: 3000,
- });
- // 3. 预览打开
- // 注意:打开公共目录的文件推荐用 plus.runtime.openFile
- // uni.openDocument 有时对公共路径支持不好
- plus.runtime.openFile(
- newEntry.fullPath,
- {},
- e => {
- console.log('打开成功');
- },
- e => {
- console.error('打开失败', e);
- uni.showToast({ title: '无法打开文件', icon: 'none' });
- }
- );
- },
- e => {
- console.error('复制文件失败:', e);
- uni.showToast({ title: '保存到公共目录失败', icon: 'none' });
- }
- );
- },
- e => {
- console.error('读取源文件失败:', e);
- }
- );
- },
- e => {
- console.error('读取下载目录失败:', e);
- }
- );
- }
- function saveTempFileToDownloads(tempPath, fileName) {
- // 1. 获取系统 Downloads 目录对象
- // "_downloads/" 是 H5+ API 对安卓公共下载目录的映射
- plus.io.resolveLocalFileSystemURL(
- '_downloads/',
- entryDir => {
- // 2. 获取临时文件对象
- plus.io.resolveLocalFileSystemURL(
- tempPath,
- entryFile => {
- // 3. 执行复制:将临时文件复制到 Downloads 目录
- entryFile.copyTo(
- entryDir,
- fileName,
- newEntry => {
- console.log('文件路径:' + newEntry.fullPath);
- uni.showToast({
- title: '已保存至Downloads',
- icon: 'none',
- });
- // 4. (可选) 打开预览
- plus.runtime.openFile(newEntry.fullPath);
- },
- e => {
- console.error('复制失败', e);
- uni.showToast({ title: '保存失败', icon: 'none' });
- }
- );
- },
- e => {
- console.error('读取临时文件失败', e);
- }
- );
- },
- e => {
- console.error('无法访问下载目录', e);
- // 这里如果报错,通常是权限没给或者 Android 11+ 读写受限
- }
- );
- }
- // 下载文件
- const downloadFile = async file => {
- console.log('🚀 ~ downloadFile ~ file:', file);
- const { filePath: fileUrl, name: fileName } = file;
- if (!fileUrl) {
- uni.showToast({ title: t('operation.fileUrlEmpty'), icon: 'none' });
- return;
- }
- // 获取平台
- const platform = uni.getSystemInfoSync().platform;
- console.log('🚀 ~ downloadFile ~ platform:', platform);
- // 判断平台
- if (platform === 'android') {
- uni.downloadFile({
- url: fileUrl,
- success: res => {
- console.log('🚀 ~ downloadFile ~ res:', res);
- if (res.statusCode === 200) {
- saveTempFileToDownloads(res.tempFilePath, fileName);
- // uni.saveFile({
- // tempFilePath: res.tempFilePath,
- // success: res => {
- // // console.log('🚀 ~ downloadFile saveFile ~ res:', res);
- // uni.showToast({
- // title: t('operation.downloadSuccess'),
- // icon: 'none',
- // });
- // copyToPublicAndOpen(res.savedFilePath, fileName);
- // },
- // fail: err => {
- // console.log('🚀 ~ downloadFile saveFile ~ err:', err);
- // uni.showToast({
- // title: t('operation.downloadFail'),
- // icon: 'none',
- // });
- // },
- // });
- }
- },
- fail: err => {
- console.log('🚀 ~ downloadFile ~ err:', err);
- uni.showToast({
- title: t('operation.downloadFail'),
- icon: 'none',
- });
- },
- });
- } else {
- try {
- // 2.1 处理文件名(避免特殊字符乱码,如中文、空格)
- const safeFileName = decodeURIComponent(fileName || '未命名文件'); // 解码URL编码的文件名
- // 2.2 创建隐藏的 <a> 标签(核心:利用 download 属性触发下载)
- const link = document.createElement('a');
- // 关键:设置 download 属性指定文件名(Web 端独有)
- link.download = safeFileName;
- // 设置文件地址(若跨域,需后端配置 CORS + Content-Disposition 响应头)
- link.href = fileUrl;
- // 隐藏 <a> 标签(不影响页面布局)
- link.style.display = 'none';
- // 将 <a> 标签添加到文档中(否则部分浏览器无法触发点击)
- document.body.appendChild(link);
- // 2.3 模拟点击 <a> 标签触发下载
- link.click();
- // 2.4 下载后清理资源(避免内存泄漏)
- setTimeout(() => {
- document.body.removeChild(link); // 移除 <a> 标签
- URL.revokeObjectURL(link.href); // 释放 URL 资源(若使用 Blob 时必需)
- }, 100);
- } catch (err) {
- console.error('🚀 ~ Web 端下载失败:', err);
- // 若直接通过 <a> 标签下载失败(如跨域),尝试通过 Blob 流下载(场景2兼容)
- await downloadFileByBlob(fileUrl, fileName);
- }
- }
- };
- // 兼容场景2:通过 Blob 流下载(解决跨域或后端返回流的情况)
- const downloadFileByBlob = async (fileUrl, fileName) => {
- try {
- // 1. 发起请求获取文件流(注意:需设置 responseType: 'blob')
- const response = await fetch(fileUrl, {
- method: 'GET',
- headers: {
- // 若需要登录态,添加 token(根据项目授权方式调整)
- Authorization: `Bearer ${uni.getStorageSync('token')}`,
- },
- });
- if (!response.ok) {
- throw new Error(`请求失败: ${response.status}`);
- }
- // 2. 将响应转换为 Blob 对象(根据文件类型设置 MIME,如 PDF 为 'application/pdf')
- const blob = await response.blob();
- // 3. 生成 Blob 临时 URL
- const blobUrl = URL.createObjectURL(blob);
- // 4. 用 <a> 标签触发下载(同场景1逻辑)
- const safeFileName = decodeURIComponent(fileName || '未命名文件');
- const link = document.createElement('a');
- link.download = safeFileName;
- link.href = blobUrl;
- link.style.display = 'none';
- document.body.appendChild(link);
- link.click();
- // 5. 清理资源
- setTimeout(() => {
- document.body.removeChild(link);
- URL.revokeObjectURL(blobUrl); // 必须释放 Blob URL,避免内存泄漏
- uni.hideLoading();
- // uni.showToast({ title: t("operation.downloadSuccess"), icon: "success" });
- }, 100);
- } catch (err) {
- console.error('🚀 ~ Blob 下载失败:', err);
- uni.hideLoading();
- // uni.showToast({ title: t("operation.downloadFail"), icon: "error" });
- }
- };
- const removeSelectedItem = (platformId, value) => {
- form[platformId].techniqueIds = form[platformId].techniqueIds.filter(item => item !== value);
- getWorkloadInfoByTechnique(platformId);
- };
- const getWorkloadInfoByTechnique = platformId => {
- const ids = form[platformId].techniqueIds;
- if (!ids.length) {
- form[platformId].extProperty = [];
- return;
- }
- getRuiDuReportAttrs({
- techniqueIds: ids.join(','),
- }).then(res => {
- const { data = [] } = res;
- // 1. 按 "identifier+unit" 去重:用Map保证唯一,key为拼接字段
- const uniqueMap = new Map();
- data.forEach(item => {
- // 生成去重key(identifier和unit都存在才拼接,避免异常)
- const key =
- item.identifier && item.unit ? `${item.identifier}-${item.unit}` : Math.random().toString(36).slice(2, 11); // 异常情况用随机key避免重复
- uniqueMap.set(key, item); // 重复key会覆盖,实现去重
- });
- // 去重后的数组
- const uniqueData = Array.from(uniqueMap.values());
- // 2. 对比formData.extProperty,保留已有actualValue(避免覆盖用户输入)
- const handledData = uniqueData.map(newItem => {
- // 生成当前新项的去重key
- const newKey = newItem.identifier && newItem.unit ? `${newItem.identifier}-${newItem.unit}` : '';
- // 在原有extProperty中找匹配项
- const oldItem = form[platformId].extProperty.find(old => {
- const oldKey = old.identifier && old.unit ? `${old.identifier}-${old.unit}` : '';
- return newKey && oldKey && newKey === oldKey;
- });
- // 有匹配项则复用原有actualValue,无则用新项的(默认空)
- return {
- ...newItem,
- actualValue: oldItem?.actualValue ?? newItem.actualValue,
- };
- });
- // 3. 重新赋值给formData.extProperty(更新页面展示)
- form[platformId].extProperty = handledData;
- });
- };
- defineExpose({
- submitForm,
- });
- // 假设你已经定义了 props
- // const props = defineProps({ reportData: Object });
- // --- 1. 公共工具函数 (保持不变) ---
- const parseNumber = val => {
- let num = parseFloat(val);
- if (isNaN(num)) num = 0;
- if (num < 0) num = 0;
- return Number(num.toFixed(2));
- };
- // --- 2. 防抖逻辑定义 (保持不变) ---
- // 列表变化 -> 算总和
- const handleListChange = useDebounceFn(() => {
- let total = 0;
- form.reportFuels.forEach(item => {
- const formattedVal = parseNumber(item.customFuel);
- if (item.customFuel !== formattedVal) {
- item.customFuel = formattedVal;
- }
- total += formattedVal;
- });
- // 更新 dailyFuel,这会触发下面的 dailyFuel watcher
- dailyFuel.value = parseNumber(total);
- }, 500);
- // dailyFuel 变化 -> 格式化自身 & 同步 form
- const handleDailyFuelChange = useDebounceFn(() => {
- const formattedVal = parseNumber(dailyFuel.value);
- if (dailyFuel.value !== formattedVal) {
- dailyFuel.value = formattedVal;
- }
- form.dailyFuel = formattedVal;
- }, 500);
- // --- 3. 关键修改:初始化逻辑 ---
- const initDailyFuel = () => {
- // 获取 props 中的值 (转为数字以做判断)
- const propVal = props.reportData?.dailyFuel;
- const numPropVal = parseFloat(propVal);
- // 判断规则:如果有值且不是 NaN (根据需求,你也可以加上 > 0 的判断)
- // 这里假设只要 props 里有有效数字,就以 props 为准
- const hasPropValue = !isNaN(numPropVal) && propVal !== null && propVal !== '';
- if (hasPropValue) {
- // 【情况A】Props 有值:直接使用 Props
- const val = parseNumber(numPropVal);
- dailyFuel.value = val;
- form.dailyFuel = val;
- // // 顺便把列表里的每一项也格式化一下(可选)
- // form.reportFuels.forEach(item => {
- // item.customFuel = parseNumber(item.customFuel);
- // });
- } else {
- // 【情况B】Props 没值:根据列表计算初始值
- // 这里我们不使用防抖,直接立即计算一次,确保显示正确
- let total = 0;
- form.reportFuels.forEach(item => {
- // 初始化时顺便把列表里的脏数据格式化了
- const val = parseNumber(item.customFuel);
- item.customFuel = val;
- total += val;
- });
- const finalTotal = parseNumber(total);
- dailyFuel.value = finalTotal;
- form.dailyFuel = finalTotal;
- }
- };
- // --- 4. 监听器 (关键修改) ---
- // 监听列表:【注意】这里去掉了 immediate: true
- // 因为初始化我们已经在上面手动 initDailyFuel() 里做过了
- // 现在只监听用户后续的“修改”操作
- // watch(
- // () => form.reportFuels,
- // () => {
- // handleListChange();
- // },
- // { deep: true } // 只有 deep,没有 immediate
- // );
- // 监听 dailyFuel
- watch(
- () => dailyFuel.value,
- (newVal, oldVal) => {
- // 只有当值真的变了,才触发防抖更新
- // 避免初始化赋值时触发不必要的逻辑
- if (newVal !== oldVal) {
- handleDailyFuelChange();
- }
- }
- );
- </script>
- <template>
- <scroll-view scroll-y="true" class="report-form">
- <scroll-view class="steps" scroll-x :scroll-y="false">
- <uni-steps :options="steps" :active="steps.length - 1" :style="{ width: `${steps.length * 100}px` }" />
- </scroll-view>
- <uni-forms
- ref="reportFormRef"
- labelWidth="140px"
- :modelValue="form"
- :rules="formDataBaseRules"
- err-show-type="toast">
- <uni-forms-item
- v-if="reportData.platformWell === 1"
- class="form-item well-form-item"
- label="平台井"
- :required="isRequired">
- <uni-data-select
- class="form-item-select"
- :clear="false"
- align="right"
- placeholder="请选择平台井"
- :disabled="formDisable"
- :localdata="wellOptions"
- multiple
- v-model="form.platformIds" />
- </uni-forms-item>
- <div class="content">
- <uni-forms-item class="form-item" :label="`${$t('ruiDu.timeNode')}:`" :required="isRequired">
- <view class="item-content" @click="props.formDisable ? '' : handleClickTimeRange()">
- <view class="time-range-item" v-if="form.startTime && form.endTime">
- {{ form.startTime }} 至 {{ form.endTime }}
- </view>
- <view class="time-range-item" v-else>
- {{ selectPlaceholder }}
- </view>
- </view>
- </uni-forms-item>
- <uni-forms-item class="form-item" :label="`${$t('ruiDu.constructionEquipment')}:`">
- <view>
- <uni-row class="item-content">
- <button
- class="mini-btn form-item-btn"
- type="primary"
- :disabled="formDisable"
- v-if="!formDisable"
- @click="handleClickSelectDevice">
- {{ $t('device.selectDevice') }}
- </button>
- </uni-row>
- <uni-row>
- <uni-easyinput
- style="text-align: right"
- type="textarea"
- :autoHeight="true"
- :inputBorder="false"
- :clearable="false"
- :styles="{ disableColor: '#fff' }"
- :placeholder="$t('ruiDu.unselectedEquipment')"
- :disabled="true"
- v-model="selectedEquipmentNames" />
- </uni-row>
- </view>
- </uni-forms-item>
- <uni-forms-item class="form-item" :label="`${$t('ruiDu.unselectedEquipment')}:`">
- <uni-easyinput
- style="text-align: right"
- type="textarea"
- :autoHeight="true"
- :inputBorder="false"
- :clearable="false"
- :styles="{ disableColor: '#fff' }"
- :placeholder="' '"
- :disabled="true"
- v-model="unselectedEquipmentNames" />
- </uni-forms-item>
- <uni-forms-item :required="isRequired" class="form-item" label="当日油耗(L):">
- <uni-easyinput
- class="digit-item"
- type="number"
- :inputBorder="false"
- :clearable="false"
- :styles="{ disableColor: '#fff' }"
- :placeholder="inputPlaceholder"
- :disabled="formDisable"
- v-model="dailyFuel" />
- </uni-forms-item>
- <uni-forms-item
- class="form-item"
- :label="`${$t('ruiDu.dailyProductionDynamic')}:`"
- :required="isRequired"
- name="productionStatus">
- <uni-easyinput
- style="text-align: right"
- type="textarea"
- :autoHeight="true"
- :inputBorder="false"
- :clearable="false"
- :styles="{ disableColor: '#fff' }"
- :placeholder="inputPlaceholder"
- :disabled="formDisable"
- v-model="form.productionStatus"
- :maxlength="1000" />
- </uni-forms-item>
- <!-- 下步工作计划 -->
- <uni-forms-item
- class="form-item"
- :required="isRequired"
- :label="`${$t('ruiDu.nextWorkPlan')}:`"
- name="nextPlan">
- <uni-easyinput
- style="text-align: right"
- type="textarea"
- :autoHeight="true"
- :inputBorder="false"
- :clearable="false"
- :styles="{ disableColor: '#fff' }"
- :placeholder="inputPlaceholder"
- :disabled="formDisable"
- v-model="form.nextPlan"
- :maxlength="1000" />
- </uni-forms-item>
- <!-- 外租设备 -->
- <uni-forms-item class="form-item" :label="`${$t('ruiDu.externalRentalEquipment')}:`" name="externalRental">
- <uni-easyinput
- style="text-align: right"
- type="textarea"
- :autoHeight="true"
- :inputBorder="false"
- :clearable="false"
- :styles="{ disableColor: '#fff' }"
- :placeholder="inputPlaceholder"
- :disabled="formDisable"
- v-model="form.externalRental"
- :maxlength="1000" />
- </uni-forms-item>
- <!-- 故障情况 -->
- <uni-forms-item class="form-item" :label="`${$t('ruiDu.faultSituation')}:`" name="malfunction">
- <uni-easyinput
- style="text-align: right"
- type="textarea"
- :autoHeight="true"
- :inputBorder="false"
- :clearable="false"
- :styles="{ disableColor: '#fff' }"
- :placeholder="inputPlaceholder"
- :disabled="formDisable"
- v-model="form.malfunction"
- :maxlength="1000" />
- </uni-forms-item>
- <!-- 故障误工H -->
- <uni-forms-item class="form-item" :label="`${$t('ruiDu.faultDowntimeH')}:`" name="faultDowntime">
- <uni-easyinput
- class="digit-item"
- type="digit"
- :inputBorder="false"
- :clearable="true"
- :styles="{ disableColor: '#fff' }"
- :placeholder="inputPlaceholder"
- :disabled="formDisable"
- v-model="form.faultDowntime"
- :maxlength="1000" />
- </uni-forms-item>
- <!-- 附件 -->
- <uni-forms-item class="form-item" :label="`${$t('ruiDu.attachment')}:`" :required="false" name="attachments">
- <view v-if="!formDisable" class="file-parent">
- <view class="file-picker-container item-end">
- <view class="file-size-limit">
- {{ $t('ruiDu.fileSizeLimit') }}
- </view>
- <button type="primary" size="mini" class="file-picker-btn" @click="chooseFile">
- {{ $t('ruiDu.selectFile') }}
- </button>
- </view>
- <view class="file-list">
- <view v-for="(file, index) in form.attachments" :key="index">
- {{ file.filename }}
- <button @click="deleteFiles" type="primary" size="mini" class="file-picker-btn">删除文件</button>
- </view>
- </view>
- </view>
- <view class="file-list item-col" v-else>
- <view v-for="(file, index) in form.attachments" :key="index">
- <span>{{ file.filename }}</span>
- <button @click="downloadFile(file)" type="primary" size="mini" class="file-picker-btn">下载文件</button>
- </view>
- </view>
- </uni-forms-item>
- </div>
- <div v-for="(platformId, index) in form.platformIds" :key="platformId" class="content">
- <div class="content-title">{{
- wellOptions.find(item => item.value === platformId)?.text ?? reportData.wellName ?? ''
- }}</div>
- <uni-forms-item
- class="form-item"
- :label="`${$t('ruiDu.constructionStatus')}:`"
- :required="isRequired"
- :name="[platformId, 'rdStatus']"
- :rules="[{ required: true, errorMessage: `${t('operation.PleaseSelect')}${t('ruiDu.constructionStatus')}` }]">
- <uni-data-select
- class="form-item-select items-center"
- :clear="false"
- :align="'right'"
- placement="top"
- :localdata="rdStatusRange"
- :placeholder="selectPlaceholder"
- :disabled="formDisable"
- v-model="form[platformId].rdStatus">
- </uni-data-select>
- </uni-forms-item>
- <uni-forms-item
- class="form-item"
- :label="`${$t('ruiDu.constructionProcess')}:`"
- :required="reportData.virtualProject !== 'Y' ? isRequired : false"
- :name="[platformId, 'techniqueIds']"
- :rules="
- reportData.virtualProject !== 'Y'
- ? [{ required: true, errorMessage: `${t('operation.PleaseSelect')}${t('ruiDu.constructionProcess')}` }]
- : []
- ">
- <uni-data-select
- ref="uniDataSelect"
- mode="underline"
- :multiple="true"
- :clear="false"
- :localdata="techniqueRange"
- :disabled="formDisable"
- placement="top"
- @change="getWorkloadInfoByTechnique(platformId)"
- v-model="form[platformId].techniqueIds">
- <template v-slot:selected="{ selectedItems }" class="form-item">
- <view class="slot-box flex flex-row items-center justify-end">
- <view
- v-for="item in selectedItems"
- :key="item.value"
- class="slot-content-item selected items-center justify-start">
- {{ item.text }}
- <uni-icons
- type="close"
- size="18"
- color="#909399"
- v-if="!formDisable"
- @click="removeSelectedItem(platformId, item.value)"></uni-icons>
- </view>
- <view v-if="selectedItems.length == 0" class="slot-content-item items-center justify-start">
- {{ selectPlaceholder }}
- </view>
- </view>
- </template>
- <template v-slot:option="{ item, itemSelected }">
- <view class="slot-item">
- <uni-list-item class="flex-row items-center justify-between">
- <template v-slot:body>
- <text class="slot-item-text justify-start" :style="{ color: itemSelected ? '#007aff' : '#303133' }">
- {{ item.text }}
- </text>
- </template>
- <template v-slot:footer>
- <uni-icons
- class="items-center justify-center"
- v-if="itemSelected"
- type="checkmarkempty"
- size="20"
- color="#007aff"></uni-icons>
- </template>
- </uni-list-item>
- </view>
- </template>
- <template v-slot:empty>
- <view class="empty-box">
- <view>{{ $t('common.noData') }}</view>
- </view>
- </template>
- </uni-data-select>
- </uni-forms-item>
- <uni-forms-item
- class="form-item"
- v-for="(attr, index) in form[platformId].extProperty"
- :key="index"
- :label="`${attr.name}(${attr.unit}):`"
- :required="attr.required == 1 && isRequired"
- :name="[platformId, 'extProperty', index, 'actualValue']"
- :rules="[{ required: true, errorMessage: `${t('operation.PleaseFillIn')}${attr.name}` }]">
- <uni-easyinput
- v-if="attr.dataType === 'double'"
- class="digit-item"
- type="digit"
- :inputBorder="false"
- :clearable="true"
- :styles="{ disableColor: '#fff' }"
- :placeholder="inputPlaceholder"
- :disabled="formDisable"
- v-model="form[platformId].extProperty[index].actualValue" />
- <uni-easyinput
- v-else
- style="text-align: right"
- :styles="{ disableColor: '#fff' }"
- :inputBorder="false"
- :clearable="true"
- :placeholder="inputPlaceholder"
- :disabled="formDisable"
- v-model="form[platformId].extProperty[index].actualValue"
- :type="'textarea'"></uni-easyinput>
- </uni-forms-item>
- </div>
- <div v-for="(fuel, index) in form.reportFuels" :key="index" class="content">
- <div class="content-title">{{ fuel.deviceCode }}</div>
- <uni-forms-item class="form-item" label="设备名称:">
- <view style="text-align: right; width: 100%; padding-right: 10px; box-sizing: border-box">{{
- fuel.deviceName
- }}</view>
- </uni-forms-item>
- <uni-forms-item class="form-item" label="发生日期:">
- <view style="text-align: right; width: 100%; padding-right: 10px; box-sizing: border-box">{{
- dayjs(fuel.queryDate).format('YYYY-MM-DD')
- }}</view>
- </uni-forms-item>
- <uni-forms-item class="form-item" label="中航北斗油耗(L):">
- <view style="text-align: right; width: 100%; padding-right: 10px; box-sizing: border-box">{{
- (fuel.zhbdFuel ?? 0).toFixed(2)
- }}</view>
- </uni-forms-item>
- <uni-forms-item class="form-item" label="实际油耗(L):">
- <uni-easyinput
- class="digit-item"
- type="number"
- :inputBorder="false"
- :clearable="false"
- :styles="{ disableColor: '#fff' }"
- :placeholder="inputPlaceholder"
- :disabled="formDisable"
- v-model="fuel.customFuel"
- @input="handleListChange" />
- </uni-forms-item>
- </div>
- </uni-forms>
- </scroll-view>
- <lsj-upload ref="uploadRef"></lsj-upload>
- <tpf-time-range
- ref="timeRangeRef"
- :startTime="startTime"
- :startDefaultTime="startDefaultTime"
- :endTime="endTime"
- :endDefaultTime="endDefaultTime"
- @timeRange="timeRange"></tpf-time-range>
- <device-transfer
- ref="deviceTransferRef"
- :allList="reportData.selectedDevices"
- :selected="form.deviceIds"
- @confirm="handleTransferChange" />
- </template>
- <style lang="scss" scoped>
- @import '@/style/work-order-form.scss';
- .report-form {
- height: 100%;
- color: #333;
- }
- .steps {
- :deep(.uni-scroll-view > .uni-scroll-view) {
- padding: 10px 0 20px 0;
- }
- }
- :deep(.uni-textarea-textarea:disabled),
- :deep(.uni-input-input:disabled) {
- color: #333;
- }
- :deep(.uni-date-x) {
- color: #333;
- }
- .digit-item {
- text-align: right;
- :deep(.uni-easyinput__content-input) {
- padding-right: 10px;
- }
- }
- .well-form-item {
- background-color: #fff;
- border-bottom: none;
- padding: 0 10px;
- }
- .form-item-select {
- width: 100%;
- }
- .form-item-btn {
- margin: 10px 0 0 0;
- width: 80px;
- text-align: center;
- }
- .digit-item {
- text-align: right;
- :deep(.uni-easyinput__content-input) {
- padding-right: 10px;
- }
- }
- .file-picker-container {
- width: 100%;
- }
- .file-picker-btn {
- margin-left: unset;
- margin-right: unset;
- }
- .file-size-limit {
- font-size: 10px;
- color: #ff4500;
- padding: 0 10px;
- }
- .file-parent {
- display: flex;
- flex-direction: column;
- align-items: end;
- padding: 10px 0;
- }
- .file-list {
- display: flex;
- flex-direction: column;
- align-items: end;
- justify-content: end;
- max-width: 100%;
- & > view {
- width: 100%;
- padding: 10px;
- padding-right: 0;
- display: flex;
- align-items: center;
- justify-content: space-between;
- & > span {
- flex: 1;
- word-break: break-all;
- overflow-wrap: anywhere;
- margin-right: 10px;
- }
- & > .file-picker-btn {
- flex-shrink: 0;
- margin-left: 0;
- }
- }
- }
- .slot-box {
- width: 100%;
- flex-wrap: wrap;
- }
- .slot-content-item {
- font-size: 12px;
- color: #6a6a6a;
- &.selected {
- background: #f4f4f5;
- // color: #909399;
- margin: 5px;
- padding: 5px;
- border-radius: 3px;
- }
- }
- .slot-item-text {
- width: 90%;
- }
- .content {
- margin-top: 10px;
- padding: 8px;
- background-color: #fff;
- }
- .content-title {
- font-weight: 600;
- height: 44px;
- display: flex;
- align-items: center;
- border-bottom: 1px dashed #cacccf;
- }
- .item-content {
- display: flex;
- align-items: center;
- justify-content: end;
- }
- .item-end {
- display: flex;
- align-items: end;
- justify-content: end;
- }
- .item-col {
- justify-content: end;
- }
- .time-range-item {
- margin: 10px;
- }
- </style>
|