✨ 运单详情打卡详情支持编辑打卡记录

打卡时间轴每条记录后面加「编辑」文本按钮,弹窗回显当前填写的信息,可改可存;
打卡状态也在可编辑之列。

- 字段按过程配置取舍:打卡定位决定地址/经纬度,上传货量类型决定重量/体积/数量,
  上传凭证决定照片;记录里已有值的项即使配置后来改小也保留,否则历史数据再也改不掉
- 接单/回单是确认节点,过程配置里没有打卡定位开关,不采信其模板默认的 location
- 未打卡保存即清除该节点打卡流水,弹窗有对应提示;在途打卡表只有单张照片字段,
  上传时只留一张
- 入口与运单编辑同权限(waybill_manage_edit),管理员天然可见
This commit is contained in:
2026-10-06 04:41:22 +08:00
parent 462f9bbea2
commit 1699ae873f
2 changed files with 404 additions and 1 deletions
+8
View File
@@ -34,6 +34,14 @@ export const getPunchRecords = waybillId =>
params: { waybillId },
});
/** 编辑打卡记录(含打卡状态;punched=false 表示取消打卡) */
export const savePunchRecord = data =>
request({
url: `${baseUrl}/punch-record/save`,
method: 'post',
data,
});
/** 运单车辆实时定位 */
export const locateVehicle = id =>
request({
@@ -2215,6 +2215,14 @@
}}</el-link
>
</template>
<el-link
v-if="canEditWaybillPunchRecord"
class="waybill-manage-page__punch-record-edit-link"
type="primary"
:underline="false"
@click="openWaybillPunchRecordEdit(record)"
>编辑</el-link
>
</div>
<template v-if="record.punched">
<div
@@ -2762,6 +2770,168 @@
</template>
</el-dialog>
<el-dialog
v-model="waybillPunchRecordEditDialog.visible"
title="编辑打卡记录"
append-to-body
destroy-on-close
width="720px"
top="8vh"
class="waybill-manage-page__punch-edit-dialog"
>
<el-form
ref="waybillPunchRecordEditFormRef"
:model="waybillPunchEditForm"
:rules="waybillPunchEditRules"
label-position="right"
label-width="96px"
>
<section-card title="打卡信息">
<el-row :gutter="18">
<el-col :span="12">
<el-form-item label="过程节点">
<span class="waybill-manage-page__punch-edit-readonly">{{
waybillPunchEditRecord.nodeName || waybillPunchEditRecord.nodeCode || '-'
}}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="打卡状态" prop="punched">
<el-radio-group v-model="waybillPunchEditForm.punched">
<el-radio :label="true">已打卡</el-radio>
<el-radio :label="false">未打卡</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col v-if="waybillPunchEditPunchDisabled" :span="24">
<el-alert
type="info"
:closable="false"
show-icon
title="该节点过程配置为不打卡,标记已打卡仅作数据补录"
/>
</el-col>
<el-col v-if="!waybillPunchEditForm.punched" :span="24">
<el-alert
type="warning"
:closable="false"
show-icon
title="保存后该节点的打卡流水将被清除,时间轴上显示为「未打卡」"
/>
</el-col>
<template v-else>
<el-col :span="12">
<el-form-item label="打卡时间" prop="punchTime">
<el-date-picker
v-model="waybillPunchEditForm.punchTime"
type="datetime"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择打卡时间"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col v-if="waybillPunchEditFields.location" :span="24">
<el-form-item label="打卡地址">
<el-input
v-model="waybillPunchEditForm.address"
maxlength="500"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col v-if="waybillPunchEditFields.location" :span="12">
<el-form-item label="经度">
<el-input v-model="waybillPunchEditForm.longitude" placeholder="请输入" />
</el-form-item>
</el-col>
<el-col v-if="waybillPunchEditFields.location" :span="12">
<el-form-item label="纬度">
<el-input v-model="waybillPunchEditForm.latitude" placeholder="请输入" />
</el-form-item>
</el-col>
<el-col v-for="item in waybillPunchEditFields.cargoFields" :key="item.prop" :span="8">
<el-form-item :label="item.label">
<el-input
v-model="waybillPunchEditForm[item.prop]"
maxlength="32"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注">
<el-input
v-model="waybillPunchEditForm.remark"
type="textarea"
:rows="2"
maxlength="500"
show-word-limit
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col v-if="waybillPunchEditFields.voucher" :span="24">
<el-form-item label="打卡照片">
<div class="waybill-manage-page__punch-edit-photos">
<div
v-for="(photo, index) in waybillPunchEditForm.photos"
:key="`${photo.url}-${index}`"
class="waybill-manage-page__punch-edit-photo"
>
<el-image
:src="photo.url"
:preview-src-list="waybillPunchEditPhotoUrls"
:initial-index="index"
fit="cover"
/>
<div class="waybill-manage-page__punch-edit-photo-label">
{{ photo.type || '凭证' }}
</div>
<el-link
type="danger"
:underline="false"
@click="removeWaybillPunchEditPhoto(index)"
>删除</el-link
>
</div>
<el-upload
action="/api/blade-resource/oss/endpoint/put-file"
name="file"
:headers="waybillExceptionUploadHeaders"
:show-file-list="false"
:before-upload="beforeWaybillPunchPhotoUpload"
:on-success="handleWaybillPunchPhotoSuccess"
:on-error="handleWaybillPunchPhotoError"
>
<div class="waybill-manage-page__punch-edit-photo-add">+ 添加</div>
</el-upload>
</div>
<div
v-if="waybillPunchEditRecord.type === 'enroute'"
class="waybill-manage-page__punch-edit-photo-tip"
>
在途打卡只保留 1 张照片
</div>
</el-form-item>
</el-col>
</template>
</el-row>
</section-card>
</el-form>
<template #footer>
<el-button @click="waybillPunchRecordEditDialog.visible = false">取消</el-button>
<el-button
type="primary"
:loading="waybillPunchRecordEditDialog.submitting"
@click="submitWaybillPunchRecordEdit"
>
保存
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="attachmentDocumentPreviewVisible"
:title="attachmentPreviewFile.name || '附件预览'"
@@ -3492,7 +3662,12 @@ import {
getList as getProcessConfigList,
getVoucherImages as getProcessConfigVoucherImages,
} from '@/api/business/process-config';
import { getPunchRecords as getWaybillPunchRecords, locateVehicle, trackVehicle } from '@/api/business/waybill-manage';
import {
getPunchRecords as getWaybillPunchRecords,
locateVehicle,
savePunchRecord,
trackVehicle,
} from '@/api/business/waybill-manage';
import VoucherExecutionDetail from './voucher-execution-detail.vue';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import {
@@ -3790,6 +3965,23 @@ export default {
{ max: 500, message: '上报说明不能超过500个字符', trigger: 'blur' },
],
},
waybillPunchRecordEditDialog: {
visible: false,
submitting: false,
},
waybillPunchEditRecord: {},
waybillPunchEditForm: {
punched: true,
punchTime: '',
address: '',
longitude: '',
latitude: '',
weight: '',
volume: '',
quantity: '',
remark: '',
photos: [],
},
waybillRouteChangeBox: false,
waybillRouteChangeTab: 'change',
waybillRouteChangeRows: [],
@@ -4143,6 +4335,49 @@ export default {
addBtn: this.canCreate,
};
},
// 打卡记录编辑入口与运单编辑同权限:能改运单的人才能订正它的打卡流水
canEditWaybillPunchRecord() {
return this.hasPermission(`${this.config.permission}_edit`) && !this.readonlyScope;
},
waybillPunchEditNode() {
return this.findWaybillPunchEditNodeConfig(this.waybillPunchEditRecord);
},
// 编辑弹窗的字段按过程配置取舍:配置里没采的项不展示。
// 记录里已有值的项即使配置后来改小也保留——否则历史数据里的货量/照片再也改不掉。
waybillPunchEditFields() {
const record = this.waybillPunchEditRecord || {};
const node = this.waybillPunchEditNode;
const hasValue = value => value !== undefined && value !== null && String(value).trim() !== '';
// 接单/回单是确认节点,过程配置里没有「打卡定位」开关,其 location 是模板默认值,不采信
const isConfirmNode = ['accept', 'return'].includes(String(node?.key || ''));
const cargoTypes = Array.isArray(node?.cargoTypes) ? node.cargoTypes : [];
const cargoFields = [
{ prop: 'weight', label: '重量(吨)', cargoType: '重量' },
{ prop: 'volume', label: '体积(方)', cargoType: '体积' },
{ prop: 'quantity', label: '数量(件)', cargoType: '数量' },
].filter(item => cargoTypes.includes(item.cargoType) || hasValue(record[item.prop]));
return {
location:
!isConfirmNode &&
(Boolean(node?.location) ||
hasValue(record.address) ||
hasValue(record.longitude) ||
hasValue(record.latitude)),
cargoFields,
voucher: Boolean(node?.uploadVoucher) || (record.photos || []).length > 0,
};
},
// 过程配置为「不打卡」的节点,补录只是数据订正,司机端不会因此出现该节点的打卡
waybillPunchEditPunchDisabled() {
return this.waybillPunchEditNode?.punch === false;
},
waybillPunchEditRules() {
if (!this.waybillPunchEditForm.punched) return {};
return { punchTime: [{ required: true, message: '请选择打卡时间', trigger: 'change' }] };
},
waybillPunchEditPhotoUrls() {
return (this.waybillPunchEditForm.photos || []).map(item => item.url).filter(Boolean);
},
// 打卡时间轴按打卡时间先后展示:已打卡记录按打卡时间升序在前,
// 未打卡记录保持流程节点原顺序排在其后。
orderedWaybillPunchRecords() {
@@ -5596,6 +5831,105 @@ export default {
this.waybillPunchRecordsLoading = false;
});
},
// 打卡记录 → 过程配置节点:nodeCode 命中优先,回退 nodeName(与后端 listPunchRecords 同口径)
findWaybillPunchEditNodeConfig(record = {}) {
const nodes = Array.isArray(this.waybillDetailProcessNodes)
? this.waybillDetailProcessNodes
: [];
const nodeCode = String(record?.nodeCode || '')
.trim()
.toLowerCase();
const nodeName = String(record?.nodeName || '').trim();
return (
nodes.find(node => nodeCode && String(node.key || '').trim().toLowerCase() === nodeCode) ||
nodes.find(node => nodeName && String(node.name || '').trim() === nodeName) ||
null
);
},
openWaybillPunchRecordEdit(record = {}) {
if (!this.detailRow?.id || !record) return;
this.waybillPunchEditRecord = { ...record };
this.waybillPunchEditForm = {
punched: record.punched !== false,
punchTime: record.punchTime || this.$dayjs().format('YYYY-MM-DD HH:mm:ss'),
address: record.address || '',
longitude: record.longitude || '',
latitude: record.latitude || '',
weight: record.weight || '',
volume: record.volume || '',
quantity: record.quantity || '',
remark: record.remark || '',
photos: (record.photos || [])
.filter(photo => photo?.url)
.map(photo => ({ url: photo.url, type: photo.voucherType || photo.type || '' })),
};
this.waybillPunchRecordEditDialog.visible = true;
},
beforeWaybillPunchPhotoUpload(file) {
const validType = ['image/jpeg', 'image/png', 'image/jpg', 'image/bmp'].includes(file.type);
if (!validType) {
this.$message.error('仅支持 JPG、PNG、BMP 图片');
return false;
}
if (file.size / 1024 / 1024 >= 5) {
this.$message.error('图片大小不能超过 5MB');
return false;
}
return true;
},
handleWaybillPunchPhotoSuccess(res) {
if (res.code !== 200 || !res.data) {
this.$message.error(res.msg || '上传失败');
return;
}
const url = res.data.link || res.data.url || res.data.domain || '';
if (!url) return;
const photos = [...this.waybillPunchEditForm.photos, { url, type: '' }];
// 在途打卡表只有一个照片字段(单张),多传的保存时会被丢掉,这里先只留一张
this.waybillPunchEditForm.photos =
this.waybillPunchEditRecord.type === 'enroute' ? photos.slice(0, 1) : photos;
},
handleWaybillPunchPhotoError() {
this.$message.error('上传失败');
},
removeWaybillPunchEditPhoto(index) {
const photos = [...this.waybillPunchEditForm.photos];
photos.splice(index, 1);
this.waybillPunchEditForm.photos = photos;
},
async submitWaybillPunchRecordEdit() {
await this.$refs.waybillPunchRecordEditFormRef.validate();
const record = this.waybillPunchEditRecord;
const form = this.waybillPunchEditForm;
const keepDetail = form.punched;
const submit =
typeof this.api?.savePunchRecord === 'function' ? this.api.savePunchRecord : savePunchRecord;
this.waybillPunchRecordEditDialog.submitting = true;
try {
await submit({
waybillId: this.detailRow.id,
type: record.type === 'enroute' ? 'enroute' : 'node',
nodeCode: record.nodeCode || '',
nodeName: record.nodeName || '',
punched: form.punched,
// 未打卡时后端会清空该节点流水,明细项不再上传
punchTime: keepDetail ? form.punchTime : '',
address: keepDetail ? form.address : '',
longitude: keepDetail ? form.longitude : '',
latitude: keepDetail ? form.latitude : '',
weight: keepDetail ? form.weight : '',
volume: keepDetail ? form.volume : '',
quantity: keepDetail ? form.quantity : '',
remark: keepDetail ? form.remark : '',
photos: keepDetail ? form.photos.map(photo => ({ url: photo.url, type: photo.type })) : [],
});
this.$message.success('保存成功');
this.waybillPunchRecordEditDialog.visible = false;
this.loadWaybillPunchRecords();
} finally {
this.waybillPunchRecordEditDialog.submitting = false;
}
},
previewDriverUploadPhoto(photos, index) {
const list = (photos || []).map(item => item?.url).filter(Boolean);
if (!list.length) return;
@@ -11212,6 +11546,67 @@ export default {
font-weight: 400;
}
// 编辑入口推到行尾:它是整条记录的操作,与节点名旁的异常链接区分开
&__punch-record-edit-link {
margin-left: auto;
font-size: 13px;
font-weight: 400;
}
&__punch-edit-readonly {
color: #303133;
}
&__punch-edit-photos {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
&__punch-edit-photo {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
width: 96px;
.el-image {
display: block;
width: 96px;
height: 96px;
border: 1px solid #eff1f7;
border-radius: 4px;
overflow: hidden;
}
}
&__punch-edit-photo-label {
font-size: 12px;
line-height: 1.4;
color: #606266;
text-align: center;
word-break: break-all;
}
&__punch-edit-photo-add {
display: flex;
align-items: center;
justify-content: center;
width: 96px;
height: 96px;
border: 1px dashed #dcdfe6;
border-radius: 4px;
font-size: 13px;
color: #909399;
cursor: pointer;
}
&__punch-edit-photo-tip {
margin-top: 4px;
font-size: 12px;
color: #909399;
}
&__punch-record-line {
font-size: 13px;
color: #606266;