<template>
|
<view class="upkeep-add">
|
<!-- 使用通用页面头部组件 -->
|
<PageHeader :title="operationType === 'edit' ? '编辑保养计划' : '新增保养计划'" @back="goBack" />
|
|
<!-- 表单内容 -->
|
<u-form ref="formRef" :model="form" :rules="formRules" label-width="110px">
|
<!-- 基本信息 -->
|
<u-form-item label="设备名称" prop="deviceNameText" required border-bottom>
|
<u-input
|
v-model="form.deviceNameText"
|
placeholder="请选择设备名称"
|
readonly
|
@click="showDevicePicker"
|
clearable
|
/>
|
<template #right>
|
<u-icon name="scan" @click="startScan" class="scan-icon" />
|
</template>
|
</u-form-item>
|
|
<u-form-item label="规格型号" prop="deviceModel" border-bottom>
|
<u-input
|
v-model="form.deviceModel"
|
placeholder="请输入规格型号"
|
readonly
|
clearable
|
/>
|
</u-form-item>
|
|
<u-form-item label="计划保养日期" prop="maintenancePlanTime" required border-bottom>
|
<u-input
|
v-model="form.maintenancePlanTime"
|
placeholder="请选择计划保养日期"
|
readonly
|
@click="showDatePicker"
|
clearable
|
/>
|
<template #right>
|
<u-icon name="arrow-right" @click="showDatePicker" />
|
</template>
|
</u-form-item>
|
|
<u-form-item label="保养人" prop="maintenancePerson" required border-bottom>
|
<u-input
|
v-model="form.maintenancePerson"
|
placeholder="请选择保养人"
|
readonly
|
@click="showPersonPicker"
|
clearable
|
/>
|
<template #right>
|
<u-icon name="arrow-right" @click="showPersonPicker" />
|
</template>
|
</u-form-item>
|
|
<u-form-item label="保养部位" prop="maintenanceLocation" required border-bottom>
|
<u-input
|
v-model="form.maintenanceLocation"
|
placeholder="请输入保养部位"
|
clearable
|
/>
|
</u-form-item>
|
|
<u-form-item label="保养内容" prop="maintenanceItems" required border-bottom>
|
<u-input
|
v-model="form.maintenanceItems"
|
placeholder="请输入保养内容"
|
clearable
|
/>
|
</u-form-item>
|
|
<u-form-item label="附件" border-bottom>
|
<view class="attachment-upload">
|
<view class="upload-buttons">
|
<u-button
|
type="primary"
|
@click="chooseAttachment('camera')"
|
:loading="uploading"
|
:disabled="uploading"
|
:customStyle="{ marginRight: '10px', flex: 1 }"
|
>
|
<u-icon name="camera" size="18" color="#fff" style="margin-right: 5px;"></u-icon>
|
{{ uploading ? '上传中...' : '拍照' }}
|
</u-button>
|
<u-button
|
type="success"
|
@click="chooseAttachment('album')"
|
:loading="uploading"
|
:disabled="uploading"
|
:customStyle="{ flex: 1 }"
|
>
|
<u-icon name="photo" size="18" color="#fff" style="margin-right: 5px;"></u-icon>
|
{{ uploading ? '上传中...' : '相册' }}
|
</u-button>
|
</view>
|
<view v-if="attachmentList.length" class="attachment-list">
|
<view
|
v-for="(file, index) in attachmentList"
|
:key="file.id || index"
|
class="attachment-item"
|
>
|
<image
|
:src="getFileAccessUrl(file)"
|
mode="aspectFill"
|
class="attachment-preview"
|
@click="previewAttachment(index)"
|
/>
|
<view class="attachment-delete" @click="removeAttachment(file, index)">
|
<u-icon name="close" size="12" color="#fff" />
|
</view>
|
</view>
|
</view>
|
<view v-else class="attachment-empty">暂无附件,可拍照或从相册选择</view>
|
</view>
|
</u-form-item>
|
|
<!-- 提交按钮 -->
|
<view class="footer-btns">
|
<u-button class="cancel-btn" @click="goBack">取消</u-button>
|
<u-button class="save-btn" @click="sendForm" :loading="loading">保存</u-button>
|
</view>
|
</u-form>
|
|
<!-- 设备选择器 -->
|
<up-action-sheet
|
:show="showDevice"
|
:actions="deviceActions"
|
title="选择设备"
|
@select="onDeviceConfirm"
|
@close="showDevice = false"
|
/>
|
<!-- 保养人选择器 -->
|
<up-action-sheet
|
:show="showPerson"
|
:actions="personActions"
|
title="选择保养人"
|
@select="onPersonConfirm"
|
@close="showPerson = false"
|
/>
|
<up-datetime-picker
|
:show="showDate"
|
v-model="pickerDateValue"
|
@confirm="onDateConfirm"
|
@cancel="showDate = false"
|
mode="date"
|
/>
|
|
</view>
|
</template>
|
|
<script setup>
|
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
import { onShow, onUnload } from '@dcloudio/uni-app';
|
import PageHeader from '@/components/PageHeader.vue';
|
import config from '@/config';
|
import { getToken } from '@/utils/auth';
|
import { getDeviceLedger } from '@/api/equipmentManagement/ledger';
|
import {
|
addUpkeep,
|
editUpkeep,
|
getUpkeepById,
|
listMaintenanceTaskFiles,
|
delMaintenanceTaskFile,
|
} from '@/api/equipmentManagement/upkeep';
|
import { userListNoPageByTenantId } from '@/api/system/user';
|
import useUserStore from '@/store/modules/user';
|
import dayjs from "dayjs";
|
import { formatDateToYMD } from '@/utils/ruoyi';
|
|
const userStore = useUserStore();
|
|
defineOptions({
|
name: "设备保养计划表单",
|
});
|
const showToast = (message) => {
|
uni.showToast({
|
title: message,
|
icon: 'none'
|
})
|
}
|
|
const normalizeId = (raw) => {
|
if (raw === null || raw === undefined) return undefined;
|
const val = String(raw).trim();
|
if (!val || val === 'undefined' || val === 'null') return undefined;
|
return val;
|
};
|
|
// 表单引用
|
const formRef = ref(null);
|
const operationType = ref('add');
|
const loading = ref(false);
|
const uploading = ref(false);
|
const attachmentList = ref([]);
|
const showDevice = ref(false);
|
const showPerson = ref(false);
|
const showDate = ref(false);
|
const pickerDateValue = ref(Date.now());
|
const currentDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]);
|
|
// 设备选项
|
const deviceOptions = ref([]);
|
const deviceNameText = ref('');
|
// 保养人选项
|
const userOptions = ref([]);
|
// 转换为 action-sheet 需要的格式
|
const deviceActions = computed(() => {
|
return deviceOptions.value.map(item => ({
|
text: item.deviceName,
|
value: item.id,
|
data: item
|
}));
|
});
|
const personActions = computed(() => {
|
return userOptions.value.map(item => ({
|
name: item.nickName,
|
value: item.userId,
|
}));
|
});
|
|
// 扫码相关状态
|
const isScanning = ref(false);
|
const scanTimer = ref(null);
|
|
// 表单验证规则
|
const formRules = {
|
deviceLedgerId: [{ required: true, trigger: "change", message: "请选择设备名称" }],
|
maintenancePlanTime: [{ required: true, trigger: "change", message: "请选择计划保养日期" }],
|
maintenancePerson: [{ required: true, trigger: "change", message: "请选择保养人" }],
|
maintenanceLocation: [{ required: true, trigger: "blur", message: "请输入保养部位" }],
|
maintenanceItems: [{ required: true, trigger: "blur", message: "请输入保养内容" }],
|
};
|
|
// 使用 ref 声明表单数据
|
const form = ref({
|
deviceLedgerId: undefined, // 设备ID
|
deviceModel: undefined, // 规格型号
|
maintenancePlanTime: dayjs().format("YYYY-MM-DD"), // 计划保养日期
|
maintenancePerson: userStore.nickName || undefined, // 保养人
|
maintenanceLocation: undefined, // 保养部位
|
maintenanceItems: undefined, // 保养内容
|
tempFileIds: [], // 本次上传附件的临时文件ID,保存时提交
|
});
|
|
// 加载设备列表
|
const loadDeviceName = async () => {
|
try {
|
const { data } = await getDeviceLedger();
|
deviceOptions.value = data || [];
|
} catch (e) {
|
showToast('获取设备列表失败');
|
}
|
};
|
|
// 加载保养人列表
|
const loadUserOptions = async () => {
|
try {
|
const { data } = await userListNoPageByTenantId();
|
userOptions.value = data || [];
|
} catch (e) {
|
showToast('获取保养人列表失败');
|
}
|
};
|
|
// 附件相关
|
const extractTempFileId = (uploadedFile) => {
|
if (!uploadedFile) return undefined;
|
const data = Array.isArray(uploadedFile) ? uploadedFile[0] : uploadedFile;
|
return data?.tempId ?? data?.tempFileId ?? data?.id;
|
};
|
|
const syncTempFileIds = () => {
|
form.value.tempFileIds = attachmentList.value
|
.filter((item) => item.isTempFile)
|
.map((item) => item.tempId ?? item.tempFileId)
|
.filter((v) => v !== undefined && v !== null && v !== '');
|
};
|
|
const getFileAccessUrl = (file = {}) => {
|
if (file?._localPreviewUrl) return file._localPreviewUrl;
|
const url = file.url || file.tempPath || file.tempFilePath || file.path || '';
|
if (!url) return '';
|
if (String(url).startsWith('http') || String(url).startsWith('blob:') || String(url).startsWith('file:') || String(url).startsWith('wxfile:')) {
|
return url;
|
}
|
const path = String(url).startsWith('/') ? url : `/${url}`;
|
return `${config.fileUrl}${path}`;
|
};
|
|
const fetchAttachmentList = async (id) => {
|
if (!id) {
|
attachmentList.value = [];
|
form.value.tempFileIds = [];
|
return;
|
}
|
try {
|
const { code, data } = await listMaintenanceTaskFiles({
|
current: 1,
|
size: 100,
|
deviceMaintenanceId: id,
|
});
|
if (code === 200) {
|
const records = data?.records || [];
|
attachmentList.value = records.map((file) => ({
|
...file,
|
isTempFile: false,
|
}));
|
} else {
|
attachmentList.value = [];
|
}
|
} catch (e) {
|
attachmentList.value = [];
|
}
|
syncTempFileIds();
|
};
|
|
const chooseAttachment = (sourceType) => {
|
const source = sourceType === 'camera' ? ['camera'] : ['album'];
|
const remaining = 9 - attachmentList.value.length;
|
if (remaining <= 0) {
|
showToast('最多上传9张附件');
|
return;
|
}
|
uni.chooseImage({
|
count: Math.min(remaining, 9),
|
sizeType: ['original', 'compressed'],
|
sourceType: source,
|
success: (res) => {
|
const files = res.tempFiles || [];
|
if (!files.length) return;
|
uploadAttachments(files, res.tempFilePaths);
|
},
|
fail: () => {
|
showToast('选择图片失败');
|
},
|
});
|
};
|
|
const handleUploadSuccess = (response, file) => {
|
let uploadedFile = response?.data;
|
if (Array.isArray(uploadedFile)) {
|
uploadedFile = uploadedFile[0];
|
}
|
const tempId = extractTempFileId(uploadedFile);
|
if (!tempId) {
|
showToast('未获取到文件ID');
|
return;
|
}
|
attachmentList.value.push({
|
tempId,
|
tempFileId: tempId,
|
isTempFile: true,
|
url: uploadedFile?.tempPath || uploadedFile?.url || uploadedFile?.downloadUrl || file.tempFilePath || file.path,
|
tempPath: uploadedFile?.tempPath || '',
|
name: uploadedFile?.originalName || uploadedFile?.originalFilename || file.name,
|
_localPreviewUrl: file.tempFilePath || file.path || '',
|
});
|
syncTempFileIds();
|
};
|
|
const uploadAttachments = async (files, tempFilePaths = []) => {
|
const token = getToken();
|
if (!token) {
|
showToast('登录已失效,请重新登录');
|
return;
|
}
|
uploading.value = true;
|
try {
|
for (let i = 0; i < files.length; i++) {
|
const file = files[i];
|
const filePath = file.path || file.tempFilePath || tempFilePaths[i];
|
if (!filePath) continue;
|
await new Promise((resolve, reject) => {
|
uni.uploadFile({
|
url: `${config.baseUrl}/file/upload`,
|
filePath,
|
name: 'file',
|
header: {
|
Authorization: `Bearer ${token}`,
|
},
|
success: (uploadRes) => {
|
try {
|
const parsed = JSON.parse(uploadRes.data || '{}');
|
if (uploadRes.statusCode === 200 && parsed.code === 200) {
|
handleUploadSuccess(parsed, {
|
...file,
|
tempFilePath: filePath,
|
path: filePath,
|
name: file.name || `附件_${Date.now()}_${i}.jpg`,
|
});
|
resolve(parsed);
|
} else {
|
reject(new Error(parsed.msg || '上传失败'));
|
}
|
} catch (err) {
|
reject(new Error('上传响应解析失败'));
|
}
|
},
|
fail: () => reject(new Error('上传失败')),
|
});
|
});
|
}
|
showToast('上传成功');
|
} catch (e) {
|
showToast(e?.message || '上传失败');
|
} finally {
|
uploading.value = false;
|
}
|
};
|
|
const previewAttachment = (index) => {
|
const urls = attachmentList.value
|
.map((item) => getFileAccessUrl(item))
|
.filter(Boolean);
|
if (!urls.length) return;
|
uni.previewImage({
|
urls,
|
current: urls[index] || urls[0],
|
});
|
};
|
|
const removeAttachment = (file, index) => {
|
if (file?.isTempFile) {
|
attachmentList.value.splice(index, 1);
|
syncTempFileIds();
|
return;
|
}
|
if (!file?.id) {
|
attachmentList.value.splice(index, 1);
|
syncTempFileIds();
|
return;
|
}
|
uni.showModal({
|
title: '提示',
|
content: '确认删除该附件吗?',
|
success: async (res) => {
|
if (!res.confirm) return;
|
try {
|
const { code } = await delMaintenanceTaskFile(file.id);
|
if (code === 200) {
|
attachmentList.value.splice(index, 1);
|
showToast('删除成功');
|
} else {
|
showToast('删除失败');
|
}
|
} catch (e) {
|
showToast('删除失败');
|
}
|
},
|
});
|
};
|
|
// 加载表单数据(编辑模式)
|
const loadForm = async (id) => {
|
if (id) {
|
operationType.value = 'edit';
|
try {
|
const { code, data } = await getUpkeepById(id);
|
if (code == 200) {
|
form.value.deviceLedgerId = data.deviceLedgerId;
|
form.value.deviceModel = data.deviceModel;
|
form.value.maintenancePlanTime = dayjs(data.maintenancePlanTime).format("YYYY-MM-DD");
|
form.value.maintenancePerson = data.maintenancePerson;
|
form.value.maintenanceLocation = data.maintenanceLocation;
|
form.value.maintenanceItems = data.maintenanceItems;
|
// 设置设备名称显示
|
const device = deviceOptions.value.find(item => item.id === data.deviceLedgerId);
|
if (device) {
|
form.value.deviceNameText = device.deviceName;
|
}
|
await fetchAttachmentList(id);
|
}
|
} catch (e) {
|
showToast('获取详情失败');
|
}
|
} else {
|
// 新增模式
|
operationType.value = 'add';
|
attachmentList.value = [];
|
form.value.tempFileIds = [];
|
}
|
};
|
|
// 扫描二维码功能
|
const startScan = () => {
|
if (isScanning.value) {
|
showToast('正在扫描中,请稍候...');
|
return;
|
}
|
|
// 调用uni-app的扫码API
|
uni.scanCode({
|
scanType: ['qrCode', 'barCode'],
|
success: (res) => {
|
handleScanResult(res.result);
|
},
|
fail: (err) => {
|
console.error('扫码失败:', err);
|
showToast('扫码失败,请重试');
|
}
|
});
|
};
|
|
// 处理扫码结果
|
const handleScanResult = (scanResult) => {
|
if (!scanResult) {
|
showToast('扫码结果为空');
|
return;
|
}
|
|
isScanning.value = true;
|
showToast('扫码成功');
|
|
// 3秒后处理扫码结果
|
scanTimer.value = setTimeout(() => {
|
processScanResult(scanResult);
|
isScanning.value = false;
|
}, 1000);
|
};
|
function getDeviceIdByRegExp(url) {
|
// 匹配deviceId=后面的数字
|
const reg = /deviceId=(\d+)/;
|
const match = url.match(reg);
|
// 如果匹配到结果,返回数字类型,否则返回null
|
return match ? Number(match[1]) : null;
|
}
|
// 处理扫码结果并匹配设备
|
const processScanResult = (scanResult) => {
|
const deviceId = getDeviceIdByRegExp(scanResult);
|
const matchedDevice = deviceOptions.value.find(item => item.id == deviceId);
|
|
if (matchedDevice) {
|
// 找到匹配的设备,自动填充
|
form.value.deviceLedgerId = matchedDevice.id;
|
form.value.deviceNameText = matchedDevice.deviceName;
|
form.value.deviceModel = matchedDevice.deviceModel;
|
showToast('设备信息已自动填充');
|
} else {
|
// 未找到匹配的设备
|
showToast('未找到匹配的设备,请手动选择');
|
}
|
};
|
|
// 显示设备选择器
|
const showDevicePicker = () => {
|
showDevice.value = true;
|
};
|
|
// 显示保养人选择器
|
const showPersonPicker = () => {
|
if (!userOptions.value.length) {
|
showToast('暂无可选保养人');
|
return;
|
}
|
showPerson.value = true;
|
};
|
|
// 确认保养人选择
|
const onPersonConfirm = (selected) => {
|
const user = userOptions.value.find(item => item.userId === selected.value);
|
form.value.maintenancePerson = user?.nickName || selected.name || '';
|
showPerson.value = false;
|
};
|
|
// 确认设备选择
|
const onDeviceConfirm = (selected) => {
|
// selected 返回的是选中项
|
form.value.deviceLedgerId = selected.value;
|
form.value.deviceNameText = selected.name;
|
const selectedDevice = deviceOptions.value.find(item => item.id === selected.value);
|
if (selectedDevice) {
|
form.value.deviceModel = selectedDevice.deviceModel;
|
}
|
showDevice.value = false;
|
};
|
|
// 显示日期选择器
|
const showDatePicker = () => {
|
showDate.value = true;
|
};
|
|
// 确认日期选择
|
const onDateConfirm = (e) => {
|
form.value.maintenancePlanTime = formatDateToYMD(e.value);
|
showDate.value = false;
|
};
|
|
onShow(() => {
|
// 页面显示时获取参数
|
getPageParams();
|
});
|
|
onMounted(() => {
|
// 页面加载时获取设备列表、保养人列表和参数
|
loadDeviceName();
|
loadUserOptions();
|
getPageParams();
|
});
|
|
// 组件卸载时清理定时器
|
onUnmounted(() => {
|
if (scanTimer.value) {
|
clearTimeout(scanTimer.value);
|
}
|
});
|
|
// 提交表单
|
const sendForm = async () => {
|
try {
|
// 手动验证表单
|
const valid = await formRef.value.validate();
|
if (!valid) return;
|
|
loading.value = true;
|
const id = getPageId();
|
|
syncTempFileIds();
|
// 准备提交数据
|
const submitData = { ...form.value };
|
submitData.tempFileIds = form.value.tempFileIds || [];
|
// 确保日期格式正确
|
if (submitData.maintenancePlanTime && !submitData.maintenancePlanTime.includes(':')) {
|
submitData.maintenancePlanTime = submitData.maintenancePlanTime + ' 00:00:00';
|
}
|
|
const result = id
|
? await editUpkeep({ id: id, ...submitData })
|
: await addUpkeep(submitData);
|
const { code } = result || {};
|
|
if (code == 200) {
|
showToast(`${id ? "编辑" : "新增"}计划成功`);
|
setTimeout(() => {
|
uni.removeStorageSync('repairId');
|
uni.navigateBack();
|
}, 1500);
|
} else {
|
loading.value = false;
|
}
|
} catch (e) {
|
loading.value = false;
|
showToast('表单验证失败');
|
}
|
};
|
|
// 返回上一页
|
const goBack = () => {
|
// 清除存储的id
|
uni.removeStorageSync('repairId');
|
uni.navigateBack();
|
};
|
|
// 获取页面ID
|
const getPageId = () => {
|
return normalizeId(uni.getStorageSync('repairId'));
|
};
|
|
// 获取页面参数
|
const getPageParams = () => {
|
const id = getPageId();
|
if (id) {
|
loadForm(id);
|
} else {
|
operationType.value = 'add';
|
attachmentList.value = [];
|
form.value.tempFileIds = [];
|
loadForm();
|
}
|
};
|
|
onUnload(() => {
|
uni.removeStorageSync('repairId');
|
});
|
</script>
|
|
<style scoped lang="scss">
|
@import '@/static/scss/form-common.scss';
|
.upkeep-add {
|
min-height: 100vh;
|
background: #f8f9fa;
|
padding-bottom: 5rem;
|
}
|
|
.footer-btns {
|
position: fixed;
|
left: 0;
|
right: 0;
|
bottom: 0;
|
background: #fff;
|
display: flex;
|
justify-content: space-around;
|
align-items: center;
|
padding: 0.75rem 0;
|
box-shadow: 0 -0.125rem 0.5rem rgba(0,0,0,0.05);
|
z-index: 1000;
|
}
|
|
.cancel-btn {
|
font-weight: 400;
|
font-size: 1rem;
|
color: #FFFFFF;
|
width: 6.375rem;
|
background: #C7C9CC;
|
box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
|
border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
|
}
|
|
.save-btn {
|
font-weight: 400;
|
font-size: 1rem;
|
color: #FFFFFF;
|
width: 14rem;
|
background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%);
|
box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
|
border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
|
}
|
|
// 响应式调整
|
@media (max-width: 768px) {
|
.submit-section {
|
padding: 12px;
|
}
|
}
|
|
.tip-text {
|
padding: 4px 16px 0 16px;
|
font-size: 12px;
|
color: #888;
|
}
|
|
.scan-icon {
|
color: #1989fa;
|
font-size: 18px;
|
margin-left: 8px;
|
cursor: pointer;
|
}
|
|
.attachment-upload {
|
width: 100%;
|
}
|
|
.upload-buttons {
|
display: flex;
|
margin-bottom: 12px;
|
}
|
|
.attachment-list {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 10px;
|
}
|
|
.attachment-item {
|
position: relative;
|
width: 80px;
|
height: 80px;
|
}
|
|
.attachment-preview {
|
width: 80px;
|
height: 80px;
|
border-radius: 6px;
|
background: #f5f5f5;
|
}
|
|
.attachment-delete {
|
position: absolute;
|
top: -6px;
|
right: -6px;
|
width: 18px;
|
height: 18px;
|
border-radius: 50%;
|
background: rgba(0, 0, 0, 0.65);
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.attachment-empty {
|
font-size: 12px;
|
color: #909399;
|
}
|
</style>
|