完成运维记录

master
lynn 7 months ago
parent c9eedafa54
commit f3e70818b4

@ -63,19 +63,19 @@
size="medium"
>
<el-table-column
prop="id"
prop="no"
label="记录编号"
min-width="120"
align="center"
/>
<el-table-column
prop="material"
prop="stocks_item.zichanmingcheng"
label="维护物资"
min-width="120"
align="center"
/>
<el-table-column
prop="plan_date"
prop="planned_maintenance_date"
label="计划维护日期"
min-width="120"
align="center"
@ -86,18 +86,18 @@
align="center"
>
<template slot-scope="scope">
<template v-if="scope.row.status === 'completed'">
{{ scope.row.actual_date }}
<template v-if="scope.row.status === 1">
{{ scope.row.maintenance_date }}
</template>
<template v-else>
<div :class="['status-badge', getBadgeClass(scope.row.plan_date)]" style="display: inline-block;">
{{ getBadgeText(scope.row.plan_date) }}
<div :class="['status-badge', getBadgeClass(scope.row.planned_maintenance_date)]" style="display: inline-block;">
{{ getBadgeText(scope.row.planned_maintenance_date) }}
</div>
</template>
</template>
</el-table-column>
<el-table-column
prop="operator"
prop="responsible_admin.name"
label="负责人"
min-width="100"
align="center"
@ -120,8 +120,8 @@
>
<template slot-scope="scope">
<div style="display: flex; gap: 8px; justify-content: center;">
<Button type="primary" size="small" style="border-radius: 6px;" @click="viewDetail(scope.row)"></Button>
<Button v-if="scope.row.status !== 'completed'" type="primary" size="small" style="border-radius: 6px;" ghost @click="completeMaintenance(scope.row)"></Button>
<Button v-if="scope.row.status === 1" type="primary" size="small" style="border-radius: 6px;" @click="viewDetail(scope.row)"></Button>
<Button v-if="scope.row.status === 0" type="primary" size="small" style="border-radius: 6px;" ghost @click="completeMaintenance(scope.row)"></Button>
</div>
</template>
</el-table-column>
@ -149,28 +149,38 @@
<button class="close-button" @click="closeModal">&times;</button>
</div>
<div class="modal-body">
<form @submit.prevent="submitMaintenance">
<div class="form-group">
<label for="actual-date">实际维护日期:</label>
<input type="date" id="actual-date" v-model="form.actual_date" required>
</div>
<div class="form-group">
<label for="maintenance-notes">备注:</label>
<textarea id="maintenance-notes" v-model="form.notes" rows="4" required></textarea>
</div>
<div class="form-group">
<label for="maintenance-photos">上传图片:</label>
<input type="file" id="maintenance-photos" multiple accept="image/*" @change="handleFileUpload">
<small class="form-text">可选</small>
</div>
<div class="form-group signature-area">
<label>签名:</label>
<Form ref="maintenanceForm" :model="form" :rules="formRules" :label-width="120">
<FormItem label="实际维护日期" prop="actual_date">
<DatePicker v-model="form.actual_date" type="date" placeholder="请选择实际维护日期" style="width: 100%"></DatePicker>
</FormItem>
<FormItem label="维护备注" prop="notes">
<Input v-model="form.notes" type="textarea" :rows="4" placeholder="请输入维护备注"></Input>
</FormItem>
<FormItem label="上传图片" prop="photos">
<Upload
ref="upload"
:before-upload="handleBeforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:on-remove="handleRemove"
:max-size="2048"
multiple
type="drag"
:action="baseUrl + 'api/admin/upload-file'"
>
<div style="padding: 20px 0">
<Icon type="ios-cloud-upload" size="52" style="color: #3399ff"></Icon>
<p>点击或拖拽文件到此处上传</p>
</div>
</Upload>
</FormItem>
<FormItem label="签名" prop="signature">
<div class="signature-pad-wrapper">
<canvas ref="signaturePad" class="signature-canvas"></canvas>
<Button size="small" style="margin-top: 8px;" @click="clearSignature"></Button>
</div>
</div>
</form>
</FormItem>
</Form>
</div>
<div class="modal-footer">
<Button @click="closeModal"></Button>
@ -191,23 +201,23 @@
<div class="modal-body">
<div class="form-group">
<label>记录编号:</label>
<div class="form-value">{{ currentRecord.id }}</div>
<div class="form-value">{{ currentRecord.no }}</div>
</div>
<div class="form-group">
<label>维护物资:</label>
<div class="form-value">{{ currentRecord.material }}</div>
<div class="form-value">{{ currentRecord.stocks_item_name }}</div>
</div>
<div class="form-group">
<label>计划维护日期:</label>
<div class="form-value">{{ currentRecord.plan_date }}</div>
<div class="form-value">{{ currentRecord.planned_maintenance_date }}</div>
</div>
<div class="form-group">
<label>实际维护日期:</label>
<div class="form-value">{{ currentRecord.actual_date || '-' }}</div>
<div class="form-value">{{ currentRecord.maintenance_date || '-' }}</div>
</div>
<div class="form-group">
<label>负责人:</label>
<div class="form-value">{{ currentRecord.operator }}</div>
<div class="form-value">{{ currentRecord.responsible_admin_name }}</div>
</div>
<div class="form-group">
<label>状态:</label>
@ -218,30 +228,51 @@
<div class="form-value">{{ currentRecord.maintenance_notes || '-' }}</div>
</div>
<div class="form-group">
<label>维护图片:</label>
<div class="photo-gallery" v-if="currentRecord.maintenance_photos && currentRecord.maintenance_photos.length">
<div v-for="(photo, index) in currentRecord.maintenance_photos" :key="index" class="photo-item">
<img :src="photo" alt="维护图片" class="photo-preview">
</div>
<label>维护照片:</label>
<div class="photo-gallery" v-if="currentRecord.files && currentRecord.files.length">
<img
v-for="(file, idx) in currentRecord.files"
:key="'file-' + idx"
:src="file.url"
class="photo-preview"
@click="previewImage(file.url)"
style="cursor:pointer;"
alt="维护图片"
/>
</div>
<div class="form-value" v-else>-</div>
</div>
<div class="form-group">
<label>维护人签名:</label>
<div class="form-value">{{ currentRecord.maintenance_signature || '-' }}</div>
<label>签名照片:</label>
<div class="photo-gallery" v-if="currentRecord.sign && currentRecord.sign.url">
<img
:src="currentRecord.sign.url"
class="sign-preview"
@click="previewImage(currentRecord.sign.url)"
style="cursor:pointer;"
alt="签名图片"
/>
</div>
<div class="form-value" v-else>-</div>
</div>
</div>
<div class="modal-footer">
<div class="modal-footer" style="justify-content: center;">
<Button @click="closeViewModal"></Button>
</div>
</div>
</div>
</div>
<!-- 预览弹窗 -->
<div v-if="previewUrl" class="image-preview-modal" @click="closePreview">
<img :src="previewUrl" class="image-preview-large" />
</div>
</div>
</template>
<script>
import { Button, Select, Option, DatePicker, Input } from 'view-design';
import { Button, Select, Option, DatePicker, Input, Form, FormItem, Upload, Icon } from 'view-design';
import { getOperationList, saveOperation, getOperationDetail } from '@/api/maintenance/maintenance';
export default {
components: {
@ -249,10 +280,15 @@ export default {
Select,
Option,
DatePicker,
Input
Input,
Form,
FormItem,
Upload,
Icon
},
data() {
return {
baseUrl: process.env.VUE_APP_BASE_API || window.location.origin + '/',
select: {
page: 1,
page_size: 10,
@ -267,62 +303,7 @@ export default {
pending: 0,
completed: 0
},
list: [
{
id: 'WX20230815001',
material: '3号水泵',
plan_date: '2023-08-14',
actual_date: '2023-08-15',
operator: '张三',
status: 'completed',
maintenance_notes: '水泵运行正常,已更换密封圈',
maintenance_photos: ['/path/to/photo1.jpg', '/path/to/photo2.jpg'],
maintenance_signature: '张三'
},
{
id: 'WX20230816002',
material: '柴油发电机',
plan_date: '2025-05-15', //
actual_date: '',
operator: '李四',
status: 'pending'
},
{
id: 'WX20230818003',
material: '防汛挡板',
plan_date: '2023-08-17',
actual_date: '2023-08-18',
operator: '王五',
status: 'completed',
maintenance_notes: '挡板完好,已进行防腐处理',
maintenance_photos: ['/path/to/photo3.jpg'],
maintenance_signature: '王五'
},
{
id: 'WX20241225004',
material: '冲锋舟',
plan_date: '2024-12-25', //
actual_date: '',
operator: '赵六',
status: 'pending'
},
{
id: 'WX20230701005',
material: '救生衣检查',
plan_date: '2023-07-01', //
actual_date: '',
operator: '孙七',
status: 'pending'
},
{
id: 'WX20240810006',
material: '应急照明灯',
plan_date: '2024-08-10', //
actual_date: '',
operator: '周八',
status: 'pending'
}
],
list: [],
showModal: false,
showViewModal: false,
currentRecord: {},
@ -331,9 +312,57 @@ export default {
notes: '',
photos: []
},
formRules: {
actual_date: [
{
required: true,
message: '请选择实际维护日期',
trigger: 'blur',
validator: (rule, value, callback) => {
if (!value) {
callback(new Error('请选择实际维护日期'));
} else {
callback();
}
}
}
],
notes: [],
photos: [
{
required: true,
type: 'array',
min: 1,
message: '请上传维护图片',
trigger: 'change',
validator: (rule, value, callback) => {
if (!value || value.length === 0) {
callback(new Error('请上传维护图片'));
} else {
callback();
}
}
}
],
signature: [
{
required: true,
message: '请签名',
trigger: 'change',
validator: (rule, value, callback) => {
if (!this.getSignatureDataUrl()) {
callback(new Error('请签名'));
} else {
callback();
}
}
}
]
},
currentUser: '张三',
total: 0,
tableHeight: 550
tableHeight: 550,
previewUrl: ''
}
},
created() {
@ -350,66 +379,25 @@ export default {
methods: {
async getList() {
try {
// TODO: API
this.list = [
{
id: 'WX20230815001',
material: '3号水泵',
plan_date: '2023-08-14',
actual_date: '2023-08-15',
operator: '张三',
status: 'completed',
maintenance_notes: '水泵运行正常,已更换密封圈',
maintenance_photos: ['/path/to/photo1.jpg', '/path/to/photo2.jpg'],
maintenance_signature: '张三'
},
{
id: 'WX20230816002',
material: '柴油发电机',
plan_date: '2025-08-15', //
actual_date: '',
operator: '李四',
status: 'pending'
},
{
id: 'WX20230818003',
material: '防汛挡板',
plan_date: '2023-08-17',
actual_date: '2023-08-18',
operator: '王五',
status: 'completed'
},
{
id: 'WX20241225004',
material: '冲锋舟',
plan_date: '2024-12-25', //
actual_date: '',
operator: '赵六',
status: 'pending'
},
{
id: 'WX20230701005',
material: '救生衣检查',
plan_date: '2023-07-01', //
actual_date: '',
operator: '孙七',
status: 'pending'
},
{
id: 'WX20240810006',
material: '应急照明灯',
plan_date: '2024-08-10', //
actual_date: '',
operator: '周八',
status: 'pending'
}
];
this.total = this.list.length;
this.stats = {
total: this.list.length,
pending: this.list.filter(item => item.status === 'pending').length,
completed: this.list.filter(item => item.status === 'completed').length
const params = {
page: this.select.page,
page_size: this.select.page_size,
warehouse: this.select.warehouse,
status: this.select.status,
date_start: this.select.date_start,
date_end: this.select.date_end,
keyword: this.select.keyword
};
const res = await getOperationList(params);
if (res && res.list) {
this.list = res.list.data;
this.total = res.list.total;
this.stats = {
total: res.chart.total,
pending: res.chart.wait,
completed: res.chart.done
};
}
} catch (e) {
this.$message.error('获取维护记录失败');
}
@ -456,17 +444,35 @@ export default {
return diffDays;
},
viewDetail(item) {
this.currentRecord = { ...item };
this.showViewModal = true;
async viewDetail(item) {
try {
// 1.
const detail = await getOperationDetail({ id: item.id });
// 2.
this.currentRecord = {
...detail,
stocks_item_name: detail.stocks_item?.zichanmingcheng || '-',
responsible_admin_name: detail.responsible_admin?.name || '-'
};
// 3.
this.showViewModal = true;
} catch (e) {
this.$message.error('获取维护详情失败');
}
},
completeMaintenance(item) {
this.showModal = true
this.showModal = true;
this.currentRecord = { ...item };
this.form = {
actual_date: new Date().toISOString().split('T')[0],
notes: '',
photos: []
}
};
this.$nextTick(() => {
if (this.$refs.maintenanceForm) {
this.$refs.maintenanceForm.resetFields();
}
});
},
closeModal() {
this.showModal = false
@ -476,18 +482,78 @@ export default {
photos: []
}
},
handleFileUpload(event) {
this.form.photos = Array.from(event.target.files)
handleBeforeUpload(file) {
//
return true; // true
},
handleUploadSuccess(response, file) {
// response.id idresponse.url
this.form.photos.push({ id: response.id, url: response.url });
},
handleUploadError(error, file) {
//
this.$message.error('上传图片失败');
},
handleRemove(file) {
// file.url
const index = this.form.photos.findIndex(photo => photo.url === file.url);
if (index !== -1) {
this.form.photos.splice(index, 1);
}
},
async submitMaintenance() {
try {
// TODO:
this.closeModal()
this.getList()
this.$refs.maintenanceForm.validate(async (valid) => {
if (valid) {
// 1. canvassign_id
const signatureDataUrl = this.getSignatureDataUrl();
let sign_id = '';
if (signatureDataUrl) {
// base64Blob
const blob = this.dataURLtoBlob(signatureDataUrl);
const formData = new FormData();
formData.append('file', blob, 'signature.png');
//
const res = await this.$axios.post(this.baseUrl + 'api/admin/upload-file', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
sign_id = res.data?.id || res.id;
}
// 2.
const params = {
id: this.currentRecord.id,
maintenance_date: this.formatDate(this.form.actual_date),
maintenance_content: this.form.notes,
file_ids: this.form.photos.map(photo => photo.id),
sign_id: sign_id,
status: 1
};
await saveOperation(params);
this.closeModal();
this.getList();
}
});
} catch (e) {
this.$message.error('提交维护记录失败')
this.$message.error('提交维护记录失败');
}
},
formatDate(date) {
if (!date) return '';
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
},
dataURLtoBlob(dataurl) {
// base64Blob
const arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]);
let n = bstr.length, u8arr = new Uint8Array(n);
while(n--){
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], {type:mime});
},
getBadgeClass(planDate) {
const diffDays = this.getDateDifferenceInDays(planDate);
return diffDays >= 0 ? 'badge-blue' : 'badge-red';
@ -498,11 +564,10 @@ export default {
},
getStatusText(status) {
const statusMap = {
'pending': '待处理',
'completed': '已完成',
'ongoing': '进行中'
'0': '待处理',
'1': '已完成'
};
return statusMap[status] || status;
return statusMap[status] || '未知状态';
},
closeViewModal() {
this.showViewModal = false;
@ -577,6 +642,12 @@ export default {
return this.signaturePadCanvas.toDataURL('image/png');
}
return '';
},
previewImage(url) {
this.previewUrl = url;
},
closePreview() {
this.previewUrl = '';
}
},
watch: {
@ -611,10 +682,11 @@ export default {
}
.content-wrapper {
background-color: #fff;
background: #f4f6fa !important;
border-radius: 10px;
border: 1.5px solid #e0e3e8 !important;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
padding: 20px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
margin-top: 10px;
}
@ -672,25 +744,36 @@ export default {
display: flex;
flex-direction: column;
min-height: 0;
height: calc(100vh - 200px);
}
.el-table th {
background: #f5f7fa !important;
color: #333;
font-weight: bold;
font-size: 15px;
.el-table {
border-radius: 10px;
overflow: hidden;
width: 100%;
height: 100%;
}
.el-table__body-wrapper {
height: calc(100% - 40px);
overflow-y: auto;
}
.el-table__header-wrapper {
height: 40px;
}
.el-table td {
font-size: 14px;
height: 48px;
padding: 8px 0;
}
.el-table {
border-radius: 10px;
overflow: hidden;
width: 100%;
height: 100%;
.el-table th {
background: #f5f7fa !important;
color: #333;
font-weight: bold;
font-size: 15px;
padding: 8px 0;
}
.status-badge {
@ -731,7 +814,7 @@ export default {
position: relative;
margin: auto;
pointer-events: none;
max-width: 600px;
max-width: 900px;
width: 90%;
}
@ -741,28 +824,30 @@ export default {
flex-direction: column;
width: 100%;
pointer-events: auto;
background-color: #fff;
background: #f7f9fa !important;
background-clip: padding-box;
border: 1px solid rgba(0,0,0,.2);
border-radius: 0.3rem;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
border: 2px solid #e0e3e8 !important;
outline: 0;
box-shadow: 0 0.5rem 1rem rgba(0,0,0,.5);
}
.modal-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 1rem 1rem;
border-bottom: 1px solid #dee2e6;
border-top-left-radius: calc(0.3rem - 1px);
border-top-right-radius: calc(0.3rem - 1px);
padding: 1.2rem 1.5rem;
background: #f5f7fa;
border-bottom: 1px solid #eaeaea;
border-radius: 12px 12px 0 0;
}
.modal-title {
margin-bottom: 0;
line-height: 1.5;
font-size: 1.25rem;
font-size: 1.35rem;
font-weight: 700;
color: #222;
}
.close-button {
@ -770,48 +855,47 @@ export default {
margin: -1rem -1rem -1rem auto;
background-color: transparent;
border: 0;
font-size: 1.5rem;
font-size: 2rem;
font-weight: 700;
line-height: 1;
color: #000;
color: #888;
text-shadow: 0 1px 0 #fff;
opacity: .5;
opacity: 0.7;
cursor: pointer;
transition: color 0.2s, opacity 0.2s;
}
.close-button:hover {
opacity: .75;
color: #ed4014;
opacity: 1;
}
.modal-body {
position: relative;
flex: 1 1 auto;
padding: 1rem;
}
.modal-footer {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
padding: 0.75rem;
border-top: 1px solid #dee2e6;
border-bottom-right-radius: calc(0.3rem - 1px);
border-bottom-left-radius: calc(0.3rem - 1px);
gap: 0.5rem;
background: #fcfcfd;
padding: 2rem 1.5rem 1.5rem 1.5rem;
border-radius: 0 0 12px 12px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px 32px;
}
.form-group {
width: 100%;
background: #fff !important;
border: 1.5px solid #e0e3e8 !important;
box-shadow: 0 1px 2px rgba(0,0,0,0.03);
border-radius: 6px;
padding: 12px 16px;
margin-bottom: 0;
display: flex;
flex-direction: column;
min-width: 0;
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #888;
font-size: 0.98rem;
margin-bottom: 4px;
font-weight: 500;
font-size: 0.9rem;
}
.form-group input[type="date"],
@ -824,7 +908,7 @@ export default {
font-weight: 400;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-color: #fff !important;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: 0.25rem;
@ -870,34 +954,132 @@ export default {
}
.form-value {
padding: 8px 12px;
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
min-height: 38px;
color: #222;
font-size: 1.08rem;
font-weight: 500;
background: transparent;
border: none;
min-height: 32px;
padding: 0;
}
.photo-gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
background: #f8f9fa;
border-radius: 6px;
padding: 8px;
margin-top: 4px;
}
.photo-item {
position: relative;
padding-bottom: 100%;
overflow: hidden;
.photo-preview {
width: 64px;
height: 64px;
object-fit: cover;
border-radius: 4px;
border: 1px solid #e9ecef;
border: 1.5px solid #eaeaea;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
cursor: pointer;
transition: transform 0.15s;
}
.photo-preview {
position: absolute;
top: 0;
left: 0;
.photo-preview:hover {
transform: scale(1.08);
box-shadow: 0 4px 16px rgba(0,0,0,0.13);
}
.pagination-container {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
@media (max-width: 900px) {
.modal-dialog {
max-width: 98vw;
}
.modal-body {
padding: 1rem 0.5rem 1rem 0.5rem;
grid-template-columns: 1fr;
}
}
.modal-footer {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
padding: 0.75rem;
border-top: 1px solid #dee2e6;
border-bottom-right-radius: 12px;
border-bottom-left-radius: 12px;
gap: 0.5rem;
}
.image-preview-modal {
position: fixed;
z-index: 2000;
left: 0; top: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.7);
display: flex;
align-items: center;
justify-content: center;
}
.image-preview-large {
max-width: 90vw;
max-height: 90vh;
border-radius: 8px;
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
background: #fff;
}
.sign-preview {
width: 100%;
height: 100%;
object-fit: cover;
max-width: 100%;
height: auto;
object-fit: contain;
border-radius: 4px;
border: 1.5px solid #eaeaea;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
margin-top: 4px;
cursor: pointer;
background: #fff;
}
/* 分割线 */
hr {
border: none;
border-top: 1.5px solid #e0e3e8;
margin: 16px 0;
}
.ivu-input, .ivu-select, .ivu-date-picker, .ivu-btn, .el-table {
border-color: #bfc8d6 !important;
background: #fff !important;
}
.ivu-btn-primary, .el-button--primary {
background: #2d8cf0 !important;
border-color: #2d8cf0 !important;
color: #fff !important;
}
/* 主色条 */
.modal-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 1.2rem 1.5rem;
background: #f5f7fa;
border-bottom: 1px solid #eaeaea;
border-radius: 12px 12px 0 0;
}
/* 分割线 */
hr {
border: none;
border-top: 1.5px solid #e0e3e8;
margin: 16px 0;
}
</style>

@ -483,7 +483,7 @@ export default {
'filter[1][key]': 'wuzileixing',
'filter[1][op]': 'eq',
'filter[1][value]': '一一码'
'filter[1][value]': '一一码'
};
const res = await request({
url: '/api/admin/stocks-item/index',

Loading…
Cancel
Save