| src/pages.json | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/inspectionManagement/components/formDia.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/inspectionManagement/components/qrCodeDia.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/inspectionManagement/components/viewFiles.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/inspectionManagement/components/viewQrCodeFiles.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/inspectionManagement/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/pages.json
@@ -613,6 +613,15 @@ } }, { "path": "pages/inspectionManagement/index", "style": { "navigationBarTitleText": "å·¡æ£ä»»å¡ç®¡ç", "navigationStyle": "custom", "enablePullDownRefresh": true, "backgroundColor": "#f8f8f8" } }, { "path": "pages/equipmentManagement/faultAnalysis/index", "style": { "navigationBarTitleText": "æ éåæè¿½æº¯", src/pages/index.vue
@@ -435,6 +435,10 @@ icon: "/static/images/icon/xunjianshangchuan@2x.png", label: "å·¡æ£ç®¡ç", }, { icon: "/static/images/icon/xunjianshangchuan@2x.png", label: "å·¡æ£ä»»å¡ç®¡ç", } ]); // å¤ç常ç¨åè½ç¹å» @@ -657,6 +661,11 @@ case "å·¡æ£ç®¡ç": uni.navigateTo({ url: "/pages/inspectionUpload/index", }); break; case "å·¡æ£ä»»å¡ç®¡ç": uni.navigateTo({ url: "/pages/inspectionManagement/index", }); break; case "åæè¿½æº¯": @@ -1033,10 +1042,17 @@ { icon: "/static/images/icon/shbeibaoxiu@2x.png", label: "è®¾å¤æ¥ä¿®" }, { icon: "/static/images/icon/shbeibaoyang@2x.png", label: "设å¤ä¿å »" }, { icon: "/static/images/icon/xunjianshangchuan@2x.png", label: "å·¡æ£ç®¡ç" }, { icon: "/static/images/icon/xunjianshangchuan@2x.png", label: "å·¡æ£ä»»å¡ç®¡ç" }, ]; const filteredEquipment = originalEquipment.filter(item => { return allowedMenuTitles.has(item.label); }); if (filteredEquipment.some(i => i.label === "å·¡æ£ç®¡ç")) { const task = originalEquipment.find(i => i.label === "å·¡æ£ä»»å¡ç®¡ç"); if (task && !filteredEquipment.some(i => i.label === "å·¡æ£ä»»å¡ç®¡ç")) { filteredEquipment.push(task); } } equipmentItems.splice(0, equipmentItems.length, ...filteredEquipment); }; src/pages/inspectionManagement/components/formDia.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,549 @@ <template> <u-popup :show="dialogVisitable" mode="center" :round="12" :zIndex="900" @close="cancel"> <view class="popup-content"> <view class="popup-title">{{ operationType === "add" ? "æ°å¢å·¡æ£ä»»å¡" : "ç¼è¾å·¡æ£ä»»å¡" }}</view> <view class="form-body"> <view class="form-item"> <text class="label">设å¤åç§°</text> <picker mode="selector" :range="deviceOptions" range-key="deviceName" :value="deviceIndex" @change="onDeviceChange"> <view class="picker-value">{{ form.taskName || "è¯·éæ©è®¾å¤" }}</view> </picker> </view> <view class="form-item"> <text class="label">å·¡æ£äºº</text> <view class="picker-value inspector-picker" @click="openInspectorPopup"> <text>{{ inspectorText || "è¯·éæ©å·¡æ£äºº" }}</text> <u-icon name="arrow-right" size="14" color="#999" /> </view> <view class="inspector-tags" v-if="form.inspector?.length"> <view v-for="userId in form.inspector" :key="userId" class="inspector-tag" @click="toggleInspector(userId)"> <text>{{ getUserName(userId) }}</text> <u-icon name="close" size="11" color="#1677ff" /> </view> </view> </view> <view class="form-item"> <text class="label">夿³¨</text> <u-textarea v-model="form.remarks" placeholder="请è¾å ¥å¤æ³¨" :height="80" count /> </view> <view class="form-item"> <text class="label">ä»»å¡é¢ç</text> <picker mode="selector" :range="frequencyOptions" range-key="label" :value="frequencyIndex" @change="onFrequencyChange"> <view class="picker-value">{{ currentFrequencyLabel || "è¯·éæ©ä»»å¡é¢ç" }}</view> </picker> </view> <view class="form-item" v-if="form.frequencyType === 'DAILY'"> <text class="label">æ¶é´</text> <picker mode="time" :value="form.frequencyDetail || '08:00'" @change="onDailyTimeChange"> <view class="picker-value">{{ form.frequencyDetail || "è¯·éæ©æ¶é´" }}</view> </picker> </view> <view class="form-item" v-if="form.frequencyType === 'WEEKLY'"> <text class="label">æ¯å¨æ¥æ</text> <picker mode="selector" :range="weekOptions" range-key="label" :value="weekIndex" @change="onWeekChange"> <view class="picker-value">{{ currentWeekLabel || "è¯·éæ©ææ" }}</view> </picker> </view> <view class="form-item" v-if="form.frequencyType === 'WEEKLY'"> <text class="label">æ¯å¨æ¶é´</text> <picker mode="time" :value="form.time || '08:00'" @change="onWeekTimeChange"> <view class="picker-value">{{ form.time || "è¯·éæ©æ¶é´" }}</view> </picker> </view> <view class="form-item" v-if="form.frequencyType === 'MONTHLY'"> <text class="label">æ¯ææ¥æä¸æ¶é´</text> <picker mode="date" fields="day" :value="monthlyDate" @change="onMonthlyDateChange"> <view class="picker-value">{{ monthlyDate || "è¯·éæ©æ¥æ" }}</view> </picker> <picker mode="time" :value="monthlyTime" @change="onMonthlyTimeChange"> <view class="picker-value">{{ monthlyTime || "è¯·éæ©æ¶é´" }}</view> </picker> </view> </view> <view class="popup-footer"> <u-button @click="cancel">åæ¶</u-button> <u-button type="primary" @click="submitForm">ä¿å</u-button> </view> </view> </u-popup> <u-popup :show="showInspectorPopup" mode="bottom" :round="16" :zIndex="1100" @close="closeInspectorPopup"> <view class="inspector-popup"> <view class="inspector-header"> <text class="inspector-title">鿩巡æ£äºº</text> </view> <scroll-view scroll-y class="inspector-list"> <view v-for="item in userList" :key="item.userId" class="inspector-row" @click="toggleInspector(item.userId)"> <text class="inspector-name">{{ item.nickName }}</text> <u-icon v-if="form.inspector.includes(item.userId)" name="checkmark-circle-fill" color="#1677ff" size="20" /> <u-icon v-else name="" color="#d5d8de" size="20" /> </view> </scroll-view> <view class="inspector-footer"> <u-button @click="closeInspectorPopup">åæ¶</u-button> <u-button type="primary" @click="confirmInspector">ç¡®å®</u-button> </view> </view> </u-popup> </template> <script setup> import { computed, reactive, ref } from "vue"; import useUserStore from "@/store/modules/user"; import { addOrEditTimingTask } from "@/api/inspectionManagement/index.js"; import { userListNoPageByTenantId } from "@/api/system/user.js"; import { getDeviceLedger } from "@/api/equipmentManagement/ledger"; const emit = defineEmits(["closeDia"]); const userStore = useUserStore(); const dialogVisitable = ref(false); const operationType = ref("add"); const deviceOptions = ref([]); const userList = ref([]); const showInspectorPopup = ref(false); const defaultForm = () => ({ id: undefined, taskId: undefined, taskName: "", inspector: [], inspectorIds: "", remarks: "", frequencyType: "", frequencyDetail: "", week: "", time: "", registrantId: undefined, }); const form = reactive(defaultForm()); const frequencyOptions = [ { label: "æ¯æ¥", value: "DAILY" }, { label: "æ¯å¨", value: "WEEKLY" }, { label: "æ¯æ", value: "MONTHLY" }, ]; const weekOptions = [ { label: "å¨ä¸", value: "MON" }, { label: "å¨äº", value: "TUE" }, { label: "å¨ä¸", value: "WED" }, { label: "å¨å", value: "THU" }, { label: "å¨äº", value: "FRI" }, { label: "å¨å ", value: "SAT" }, { label: "卿¥", value: "SUN" }, ]; const monthlyDate = ref(""); const monthlyTime = ref(""); const deviceIndex = computed(() => { const index = deviceOptions.value.findIndex(item => item.id === form.taskId); return index >= 0 ? index : 0; }); const frequencyIndex = computed(() => { const index = frequencyOptions.findIndex(item => item.value === form.frequencyType); return index >= 0 ? index : 0; }); const weekIndex = computed(() => { const index = weekOptions.findIndex(item => item.value === form.week); return index >= 0 ? index : 0; }); const currentFrequencyLabel = computed(() => { return frequencyOptions.find(item => item.value === form.frequencyType)?.label || ""; }); const currentWeekLabel = computed(() => { return weekOptions.find(item => item.value === form.week)?.label || ""; }); const inspectorText = computed(() => { if (!form.inspector?.length) return ""; const nameMap = new Map(userList.value.map(item => [item.userId, item.nickName])); return form.inspector.map(id => nameMap.get(id)).filter(Boolean).join("ã"); }); const resetForm = () => { Object.assign(form, defaultForm()); monthlyDate.value = ""; monthlyTime.value = ""; showInspectorPopup.value = false; }; const loadBaseData = async () => { const [userRes, deviceRes] = await Promise.all([ userListNoPageByTenantId(), getDeviceLedger(), ]); userList.value = userRes?.data || []; deviceOptions.value = deviceRes?.data || []; }; const parseWeeklyDetail = detail => { if (!detail || typeof detail !== "string" || !detail.includes(",")) return; const [week, time] = detail.split(","); form.week = week || ""; form.time = time || ""; }; const parseMonthlyDetail = detail => { if (!detail || typeof detail !== "string" || !detail.includes(",")) return; const [day, time] = detail.split(","); if (day) monthlyDate.value = `${new Date().getFullYear()}-${new Date().getMonth() + 1 < 10 ? `0${new Date().getMonth() + 1}` : new Date().getMonth() + 1}-${day.padStart(2, "0")}`; if (time) monthlyTime.value = time; }; const setDeviceModel = id => { const matched = deviceOptions.value.find(item => item.id === id); if (matched) { form.taskId = matched.id; form.taskName = matched.deviceName; } }; const openDialog = async (type, row) => { operationType.value = type; dialogVisitable.value = true; resetForm(); try { await loadBaseData(); if (type === "edit" && row) { Object.assign(form, { ...defaultForm(), ...row, inspector: String(row.inspectorIds || "") .split(",") .map(item => Number(item)) .filter(Boolean), }); if (form.frequencyType === "WEEKLY") parseWeeklyDetail(form.frequencyDetail); if (form.frequencyType === "MONTHLY") parseMonthlyDetail(form.frequencyDetail); } } catch (error) { uni.showToast({ title: "åå§å失败", icon: "none", }); } }; const onDeviceChange = e => { const index = Number(e?.detail?.value || 0); const selected = deviceOptions.value[index]; if (!selected) return; setDeviceModel(selected.id); }; const getUserName = userId => { const user = userList.value.find(item => item.userId === userId); return user?.nickName || ""; }; const openInspectorPopup = () => { showInspectorPopup.value = true; }; const closeInspectorPopup = () => { showInspectorPopup.value = false; }; const toggleInspector = userId => { if (form.inspector.includes(userId)) { form.inspector = form.inspector.filter(id => id !== userId); } else { form.inspector = [...form.inspector, userId]; } }; const confirmInspector = () => { showInspectorPopup.value = false; }; const onFrequencyChange = e => { const index = Number(e?.detail?.value || 0); const selected = frequencyOptions[index]; form.frequencyType = selected?.value || ""; form.frequencyDetail = ""; form.week = ""; form.time = ""; monthlyDate.value = ""; monthlyTime.value = ""; }; const onDailyTimeChange = e => { form.frequencyDetail = e?.detail?.value || ""; }; const onWeekChange = e => { const index = Number(e?.detail?.value || 0); const selected = weekOptions[index]; form.week = selected?.value || ""; }; const onWeekTimeChange = e => { form.time = e?.detail?.value || ""; }; const onMonthlyDateChange = e => { monthlyDate.value = e?.detail?.value || ""; }; const onMonthlyTimeChange = e => { monthlyTime.value = e?.detail?.value || ""; }; const validateForm = () => { if (!form.taskId) { uni.showToast({ title: "è¯·éæ©è®¾å¤", icon: "none" }); return false; } if (!form.inspector.length) { uni.showToast({ title: "è¯·éæ©å·¡æ£äºº", icon: "none" }); return false; } if (!form.frequencyType) { uni.showToast({ title: "è¯·éæ©ä»»å¡é¢ç", icon: "none" }); return false; } if (form.frequencyType === "DAILY" && !form.frequencyDetail) { uni.showToast({ title: "è¯·éæ©æ¶é´", icon: "none" }); return false; } if (form.frequencyType === "WEEKLY" && (!form.week || !form.time)) { uni.showToast({ title: "è¯·éæ©æ¯å¨æ¥æåæ¶é´", icon: "none" }); return false; } if (form.frequencyType === "MONTHLY" && (!monthlyDate.value || !monthlyTime.value)) { uni.showToast({ title: "è¯·éæ©æ¯ææ¥æåæ¶é´", icon: "none" }); return false; } return true; }; const buildFrequencyDetail = () => { if (form.frequencyType === "WEEKLY") return `${form.week},${form.time}`; if (form.frequencyType === "MONTHLY") { const day = monthlyDate.value.split("-")[2] || ""; return `${day},${monthlyTime.value}`; } return form.frequencyDetail; }; const submitForm = async () => { if (!validateForm()) return; try { const userInfo = await userStore.getInfo(); const payload = { ...form, inspectorIds: form.inspector.join(","), frequencyDetail: buildFrequencyDetail(), registrantId: userInfo?.user?.userId, }; delete payload.inspector; delete payload.week; delete payload.time; await addOrEditTimingTask(payload); uni.showToast({ title: "æäº¤æå", icon: "success", }); cancel(); } catch (error) { uni.showToast({ title: "æäº¤å¤±è´¥ï¼è¯·éè¯", icon: "none", }); } }; const cancel = () => { dialogVisitable.value = false; resetForm(); emit("closeDia"); }; defineExpose({ openDialog }); </script> <style scoped lang="scss"> :deep(.uni-picker-container) { z-index: 1200 !important; } .popup-content { width: 88vw; max-height: 80vh; background: #fff; border-radius: 20rpx; overflow: hidden; } .popup-title { text-align: center; font-size: 32rpx; color: #1f1f1f; font-weight: 600; padding: 24rpx 20rpx; border-bottom: 1rpx solid #f0f0f0; } .form-body { padding: 24rpx; max-height: 56vh; overflow-y: auto; } .form-item { margin-bottom: 20rpx; } .label { display: block; margin-bottom: 10rpx; font-size: 24rpx; color: #666; } .picker-value { min-height: 72rpx; background: #f7f8fa; border-radius: 12rpx; padding: 0 20rpx; display: flex; align-items: center; color: #333; font-size: 26rpx; } .inspector-picker { justify-content: space-between; } .inspector-tags { display: flex; flex-wrap: wrap; margin-top: 10rpx; gap: 10rpx; } .inspector-tag { display: flex; align-items: center; gap: 8rpx; background: #edf3ff; color: #1677ff; font-size: 22rpx; border-radius: 999rpx; padding: 6rpx 14rpx; } .popup-footer { display: flex; gap: 16rpx; padding: 20rpx 24rpx 24rpx; border-top: 1rpx solid #f0f0f0; } .inspector-popup { background: #fff; border-radius: 24rpx 24rpx 0 0; overflow: hidden; max-height: 70vh; padding-bottom: env(safe-area-inset-bottom); } .inspector-header { padding: 24rpx; text-align: center; border-bottom: 1rpx solid #f0f0f0; } .inspector-title { color: #1f1f1f; font-size: 30rpx; font-weight: 600; } .inspector-list { max-height: 46vh; padding: 0 24rpx; } .inspector-row { min-height: 82rpx; display: flex; justify-content: space-between; align-items: center; border-bottom: 1rpx solid #f5f5f5; } .inspector-name { font-size: 26rpx; color: #333; } .inspector-footer { display: flex; gap: 16rpx; padding: 20rpx 24rpx 24rpx; } </style> src/pages/inspectionManagement/components/qrCodeDia.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,132 @@ <template> <div> <el-dialog :title="operationType === 'add' ? 'æ°å¢äºç»´ç ' : 'ç¼è¾äºç»´ç '" v-model="dialogVisitable" width="500px" @close="cancel"> <el-form :model="form" :rules="rules" ref="formRef" label-width="120px"> <el-row> <el-col :span="24"> <el-form-item label="设å¤åç§°" prop="deviceName"> <el-input v-model="form.deviceName" placeholder="请è¾å ¥è®¾å¤åç§°" maxlength="30" /> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="24"> <el-form-item label="æå¨ä½ç½®æè¿°" prop="location"> <el-input v-model="form.location" placeholder="请è¾å ¥æå¨ä½ç½®æè¿°" maxlength="30"/> </el-form-item> </el-col> </el-row> </el-form> <div> <el-button type="primary" @click="submitForm">çæå¹¶æå°äºç»´ç </el-button> </div> <div v-if="isShowQrCode" class="print-section" ref="qrCodeContainer" id="qrCodeContainer"> <vue-qrcode :value="qrCodeValue" :width="qrCodeSize"></vue-qrcode> </div> </el-dialog> </div> </template> <script setup> import useUserStore from "@/store/modules/user.js"; import {reactive, ref} from "vue"; import printJS from 'print-js'; import {addOrEditQrCode} from "@/api/inspectionUpload/index.js"; const { proxy } = getCurrentInstance() const emit = defineEmits() const userStore = useUserStore() const dialogVisitable = ref(false); const isShowQrCode = ref(false); const operationType = ref('add'); const qrCodeValue = ref(''); const qrCodeSize = ref(100); const data = reactive({ form: { deviceName: '', location: '', qrCodeId: '', id: '' }, rules: { deviceName: [{ required: true, message: '请è¾å ¥è®¾å¤åç§°', trigger: 'blur' }], location: [{ required: true, message: '请è¾å ¥å°ç¹', trigger: 'blur' }] } }) const { form, rules } = toRefs(data) // æå¼å¼¹æ¡ const openDialog = async (type, row) => { dialogVisitable.value = true qrCodeValue.value = '' isShowQrCode.value = false; if (type === 'edit') { form.value.id = row.id form.value.qrCodeId = row.id form.value.deviceName = row.deviceName form.value.location = row.location // å°è¡¨åæ°æ®è½¬ä¸º JSON å符串ä½ä¸ºäºç»´ç å 容 qrCodeValue.value = JSON.stringify(form.value); isShowQrCode.value = true; } } // æäº¤å并表å const submitForm = () => { proxy.$refs["formRef"].validate(valid => { if (valid) { addOrEditQrCode(form.value).then((res) => { form.value.qrCodeId = res.data }) // å°è¡¨åæ°æ®è½¬ä¸º JSON å符串ä½ä¸ºäºç»´ç å 容 qrCodeValue.value = JSON.stringify(form.value); isShowQrCode.value = true; showQrCode() } }) } const showQrCode = () => { // å»¶è¿æ§è¡æå°ï¼é¿å DOM æ´æ°åå°±è°ç¨æå° setTimeout(() => { printJS({ printable: 'qrCodeContainer',//é¡µé¢ type: "html",//ææ¡£ç±»å maxWidth: 360, style: `@page { margin:0; size: 400px 75px collapse; margin-top:3px; &:first-of-type{ margin-top:0 !important; } } html{ zoom:100%; } @media print{ width: 400px; height: 75px; margin:0; }`, targetStyles: ["*"], // 使ç¨domçæææ ·å¼ï¼å¾éè¦ font_size: '0.20cm', }); }, 300); } // å ³éå并表å const cancel = () => { proxy.resetForm("formRef") dialogVisitable.value = false emit('closeDia') } defineExpose({ openDialog }) </script> <style scoped> .print-section { text-align: center; margin-top: 30px; } </style> src/pages/inspectionManagement/components/viewFiles.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,296 @@ <template> <u-popup :show="dialogVisitable" mode="bottom" :round="16" @close="cancel"> <view class="popup-content"> <view class="popup-header"> <text class="popup-title">æ¥çéä»¶</text> <view class="close-icon" @click="cancel"> <u-icon name="close" size="14" color="#666" /> </view> </view> <view class="popup-body"> <view class="tabs"> <view v-for="tab in tabs" :key="tab.key" class="tab-item" :class="{ active: currentType === tab.key }" @click="currentType = tab.key"> {{ tab.label }} ({{ getCurrentList(tab.key).length }}) </view> </view> <view class="file-list" v-if="getCurrentList(currentType).length"> <view v-for="(file, index) in getCurrentList(currentType)" :key="index" class="file-item" @click="previewFile(file)"> <image v-if="isImageFile(file)" :src="file.url" class="thumb" mode="aspectFill" /> <view v-else class="video-thumb"> <u-icon name="video" size="28" color="#1677ff" /> <text class="video-text">è§é¢</text> </view> <text class="name">{{ file.name || "éä»¶" }}</text> </view> </view> <view v-else class="empty"> <text>ææ éä»¶</text> </view> </view> </view> </u-popup> <u-popup :show="showVideoPopup" mode="center" :round="10" @close="closeVideoPopup"> <view class="video-container"> <video :src="videoUrl" controls autoplay class="video-player" /> </view> </u-popup> </template> <script setup> import { ref } from "vue"; import config from "@/config"; const dialogVisitable = ref(false); const currentType = ref("before"); const showVideoPopup = ref(false); const videoUrl = ref(""); const filesMap = ref({ before: [], after: [], issue: [], }); const tabs = [ { key: "before", label: "ç产å" }, { key: "after", label: "ç产ä¸" }, { key: "issue", label: "ç产å" }, ]; const normalizeUrl = raw => { if (!raw) return ""; const url = String(raw).trim(); if (!url) return ""; if (/^https?:\/\//i.test(url)) return url; if (url.startsWith("/")) return `${config.fileUrl}${url}`; if (/^[a-zA-Z]:\\/.test(url)) { const normalized = url.replace(/\\/g, "/"); const idx = normalized.indexOf("/prod/"); if (idx >= 0) { const relative = normalized.slice(idx + "/prod/".length); return `${config.fileUrl}/${relative}`; } return normalized; } return `${config.fileUrl}/${url.replace(/^\//, "")}`; }; const isImageFile = file => { if (file?.contentType?.startsWith("image/")) return true; const name = String(file?.name || file?.bucketFilename || "").toLowerCase(); return /\.(jpg|jpeg|png|gif|bmp|webp)$/.test(name); }; const normalizeFile = (file, type) => ({ ...file, type, url: normalizeUrl(file?.url || file?.downloadUrl), name: file?.originalFilename || file?.bucketFilename || file?.name, }); const getCurrentList = type => filesMap.value[type] || []; const previewFile = file => { if (isImageFile(file)) { const urls = getCurrentList(currentType.value) .filter(item => isImageFile(item)) .map(item => item.url); uni.previewImage({ urls, current: file.url, }); return; } videoUrl.value = file.url; showVideoPopup.value = true; }; const closeVideoPopup = () => { showVideoPopup.value = false; videoUrl.value = ""; }; const openDialog = row => { const allList = Array.isArray(row?.commonFileList) ? row.commonFileList : []; const beforeList = Array.isArray(row?.commonFileListBefore) ? row.commonFileListBefore : allList.filter(item => item?.type === 10); const afterList = Array.isArray(row?.commonFileListAfter) ? row.commonFileListAfter : allList.filter(item => item?.type === 11); const issueList = Array.isArray(row?.commonFileListIssue) ? row.commonFileListIssue : allList.filter(item => item?.type === 12); filesMap.value = { before: beforeList.map(item => normalizeFile(item, "before")).filter(item => item.url), after: afterList.map(item => normalizeFile(item, "after")).filter(item => item.url), issue: issueList.map(item => normalizeFile(item, "issue")).filter(item => item.url), }; currentType.value = "before"; dialogVisitable.value = true; }; const cancel = () => { dialogVisitable.value = false; closeVideoPopup(); filesMap.value = { before: [], after: [], issue: [], }; }; defineExpose({ openDialog }); </script> <style scoped lang="scss"> .popup-content { width: 100vw; max-height: 82vh; background: #fff; border-radius: 24rpx 24rpx 0 0; overflow: hidden; padding-bottom: env(safe-area-inset-bottom); } .popup-header { padding: 24rpx 20rpx; border-bottom: 1rpx solid #f0f0f0; text-align: center; position: relative; } .popup-title { font-size: 32rpx; color: #1f1f1f; font-weight: 600; } .popup-body { padding: 20rpx 20rpx 26rpx; max-height: 68vh; overflow-y: auto; } .tabs { display: flex; background: #f4f5f8; border-radius: 12rpx; padding: 6rpx; margin-bottom: 20rpx; } .tab-item { flex: 1; text-align: center; padding: 12rpx 0; color: #666; font-size: 24rpx; border-radius: 10rpx; } .tab-item.active { background: #1677ff; color: #fff; font-weight: 600; } .file-list { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16rpx; } .file-item { background: #fafafa; border-radius: 12rpx; padding: 10rpx; } .thumb { width: 100%; height: 180rpx; border-radius: 8rpx; } .video-thumb { width: 100%; height: 180rpx; border-radius: 8rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #edf3ff; } .video-text { font-size: 22rpx; color: #1677ff; margin-top: 6rpx; } .name { margin-top: 8rpx; font-size: 22rpx; color: #333; display: block; word-break: break-all; } .empty { text-align: center; color: #999; padding: 40rpx 0; } .video-container { width: 94vw; background: #000; } .video-player { width: 94vw; height: 55vw; } .close-icon { position: absolute; right: 24rpx; top: 50%; transform: translateY(-50%); width: 44rpx; height: 44rpx; border-radius: 50%; background: #f5f5f5; display: flex; align-items: center; justify-content: center; } </style> src/pages/inspectionManagement/components/viewQrCodeFiles.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,169 @@ <template> <div> <el-dialog title="æ¥çéä»¶" v-model="dialogVisitable" width="800px" @close="cancel"> <div class="upload-container"> <div class="form-container"> <div class="title">å·¡æ£éä»¶</div> <!-- å¾çå表 --> <div style="display: flex; flex-wrap: wrap;"> <img v-for="(item, index) in beforeProductionImgs" :key="index" @click="showMedia(beforeProductionImgs, index, 'image')" :src="item" style="max-width: 100px; height: 100px; margin: 5px;" alt=""> </div> <!-- è§é¢å表 --> <div style="display: flex; flex-wrap: wrap;"> <div v-for="(videoUrl, index) in beforeProductionVideos" :key="index" @click="showMedia(beforeProductionVideos, index, 'video')" style="position: relative; margin: 10px; cursor: pointer;" > <div style="width: 160px; height: 90px; background-color: #333; display: flex; align-items: center; justify-content: center;"> <img src="@/assets/images/video.png" alt="ææ¾" style="width: 30px; height: 30px; opacity: 0.8;" /> </div> <div style="text-align: center; font-size: 12px; color: #666;">ç¹å»ææ¾</div> </div> </div> </div> </div> </el-dialog> <!-- ç»ä¸åªä½æ¥çå¨ --> <div v-if="isMediaViewerVisible" class="media-viewer-overlay" @click.self="closeMediaViewer"> <div class="media-viewer-content" @click.stop> <!-- å¾ç --> <vue-easy-lightbox v-if="mediaType === 'image'" :visible="isMediaViewerVisible" :imgs="mediaList" :index="currentMediaIndex" @hide="closeMediaViewer" ></vue-easy-lightbox> <!-- è§é¢ --> <div v-else-if="mediaType === 'video'" style="position: relative;"> <Video :src="mediaList[currentMediaIndex]" autoplay controls style="max-width: 90vw; max-height: 80vh;" /> </div> </div> </div> </div> </template> <script setup> // æ§å¶å¼¹çªæ¾ç¤º import VueEasyLightbox from "vue-easy-lightbox"; const dialogVisitable = ref(false); // å¾çæ°ç» const beforeProductionImgs = ref([]); // è§é¢æ°ç» const beforeProductionVideos = ref([]); // åªä½æ¥çå¨ç¶æ const isMediaViewerVisible = ref(false); const currentMediaIndex = ref(0); const mediaList = ref([]); // åå¨å½åè¦æ¥ççåªä½å表ï¼å«å¾çåè§é¢å¯¹è±¡ï¼ const mediaType = ref('image'); // image | video // æå¼å¼¹çªå¹¶å è½½æ°æ® const openDialog = async (row) => { const { images: beforeImgs, videos: beforeVids } = processItems(row.storageBlobDTO); beforeProductionImgs.value = beforeImgs; beforeProductionVideos.value = beforeVids; dialogVisitable.value = true; }; // æ¾ç¤ºåªä½ï¼å¾ç or è§é¢ï¼ function showMedia(mediaArray, index, type) { mediaList.value = mediaArray; currentMediaIndex.value = index; mediaType.value = type; isMediaViewerVisible.value = true; } // å ³éåªä½æ¥çå¨ function closeMediaViewer() { isMediaViewerVisible.value = false; mediaList.value = []; mediaType.value = 'image'; } // 表åå ³éæ¹æ³ const cancel = () => { dialogVisitable.value = false; }; // å¤çæ¯ä¸ç±»æ°æ®ï¼å离å¾çåè§é¢ function processItems(items) { const images = []; const videos = []; items.forEach(item => { if (item.contentType?.startsWith('image/')) { images.push(item.url); } else if (item.contentType?.startsWith('video/')) { videos.push(item.url); } }); return { images, videos }; } defineExpose({ openDialog }); </script> <style scoped lang="scss"> .upload-container { display: flex; flex-direction: column; align-items: center; padding: 20px; border: 1px solid #dcdfe6; box-sizing: border-box; .form-container { flex: 1; width: 100%; margin-bottom: 20px; } } .title { font-size: 14px; color: #165dff; line-height: 20px; font-weight: 600; padding-left: 10px; position: relative; margin: 6px 0; &::before { content: ""; position: absolute; left: 0; top: 3px; width: 4px; height: 14px; background-color: #165dff; } } .media-viewer-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.8); z-index: 9999; display: flex; align-items: center; justify-content: center; } .media-viewer-content { position: relative; max-width: 90vw; max-height: 90vh; overflow: hidden; } </style> src/pages/inspectionManagement/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,545 @@ <template> <view class="inspection-management-page"> <PageHeader title="å·¡æ£ä»»å¡ç®¡ç" @back="goBack" /> <view class="toolbar"> <view class="tab-wrap"> <view v-for="tab in tabs" :key="tab.name" class="tab-item" :class="{ active: activeTab === tab.name }" @click="switchTab(tab.name)"> {{ tab.label }} </view> </view> <view class="search-section"> <view class="search-bar"> <view class="search-input"> <up-input class="search-text" placeholder="请è¾å ¥å·¡æ£ä»»å¡åç§°" v-model="queryParams.taskName" clearable /> </view> <view class="search-button" @click="handleQuery"> <up-icon name="search" size="24" color="#999"></up-icon> </view> </view> </view> <view class="meta-bar"> <text class="meta-text">å ± {{ total }} æ¡</text> </view> </view> <view class="list-section"> <uni-swipe-action> <uni-swipe-action-item v-for="item in tableData" :key="item.id" :right-options="activeTab === 'taskManage' ? swipeOptions : []" :disabled="activeTab !== 'taskManage'" @click="onSwipeActionClick($event, item)"> <view class="ledger-item"> <view class="item-header"> <view class="item-left"> <view class="document-icon"> <up-icon name="file-text" size="14" color="#ffffff"></up-icon> </view> <text class="item-id">{{ item.taskName || "--" }}</text> </view> <view class="item-right"> <u-tag :type="getFrequencyTagType(item.frequencyType)" :text="formatFrequency(item.frequencyType) || 'æªç¥é¢æ¬¡'" /> </view> </view> <up-divider></up-divider> <view class="item-details"> <view class="detail-row"> <text class="detail-label">ä»»å¡ç¼å·</text> <text class="detail-value">{{ item.id || "--" }}</text> </view> <view class="detail-row"> <text class="detail-label">æ§è¡å·¡æ£äºº</text> <view class="tag-wrap" v-if="item.inspector?.length"> <uni-tag v-for="(person, index) in item.inspector" :key="index" :text="person" type="primary" size="small" inverted /> </view> <text class="detail-value" v-else>--</text> </view> <view class="detail-row"> <text class="detail-label">å¼å§æ¥æä¸æ¶é´</text> <text class="detail-value highlight">{{ formatFrequencyDetail(item.frequencyDetail) }}</text> </view> <view class="detail-row"> <text class="detail-label">ç»è®°äºº</text> <text class="detail-value">{{ item.registrant || "--" }}</text> </view> <view class="detail-row"> <text class="detail-label">ç»è®°æ¥æ</text> <text class="detail-value">{{ item.createTime || "--" }}</text> </view> <view class="detail-row"> <text class="detail-label">夿³¨</text> <text class="detail-value">{{ item.remarks || "æ " }}</text> </view> <up-divider></up-divider> <view class="card-actions"> <u-button v-if="activeTab === 'taskManage'" type="primary" size="small" class="action-btn" @click.stop="handleAdd(item)">ç¼è¾</u-button> <u-button v-else type="success" size="small" class="action-btn" @click.stop="viewFile(item)">æ¥çéä»¶</u-button> </view> </view> </view> </uni-swipe-action-item> </uni-swipe-action> <uni-load-more :status="loadMoreStatus"></uni-load-more> <view v-if="!loading && tableData.length === 0" class="no-data"> <text>ææ æ°æ®</text> </view> </view> <view v-if="activeTab === 'taskManage'" class="fab-button" @click="handleAdd()"> <up-icon name="plus" size="24" color="#ffffff"></up-icon> </view> <form-dia ref="formDia" @closeDia="handleQuery" /> <view-files ref="viewFiles" /> </view> </template> <script setup> import { ref, reactive, computed, nextTick } from "vue"; import { onShow, onReachBottom, onPullDownRefresh } from "@dcloudio/uni-app"; import PageHeader from "@/components/PageHeader.vue"; import FormDia from "@/pages/inspectionManagement/components/formDia.vue"; import ViewFiles from "@/pages/inspectionManagement/components/viewFiles.vue"; import { delTimingTask, inspectionTaskList, timingTaskList, } from "@/api/inspectionManagement/index.js"; const formDia = ref(); const viewFiles = ref(); const activeTab = ref("taskManage"); const tabs = [ { name: "taskManage", label: "宿¶ä»»å¡ç®¡ç" }, { name: "task", label: "宿¶ä»»å¡è®°å½" }, ]; const queryParams = reactive({ taskName: "", }); const pageParams = reactive({ current: 1, size: 10, }); const total = ref(0); const loading = ref(false); const tableData = ref([]); const swipeOptions = [ { text: "å é¤", style: { backgroundColor: "#ee0a24", }, }, ]; const noMore = computed(() => tableData.value.length >= total.value); const loadMoreStatus = computed(() => { if (loading.value) return "loading"; if (noMore.value) return "noMore"; return "more"; }); const goBack = () => { uni.navigateBack(); }; const formatFrequency = value => { if (value === "DAILY") return "æ¯æ¥"; if (value === "WEEKLY") return "æ¯å¨"; if (value === "MONTHLY") return "æ¯æ"; if (value === "QUARTERLY") return "å£åº¦"; return ""; }; const getFrequencyTagType = value => { if (value === "DAILY") return "success"; if (value === "WEEKLY") return "primary"; if (value === "MONTHLY") return "warning"; return "info"; }; const formatFrequencyDetail = value => { if (!value || typeof value !== "string") return "--"; return value.replace( /MON|TUE|WED|THU|FRI|SAT|SUN/g, item => ({ MON: "å¨ä¸", TUE: "å¨äº", WED: "å¨ä¸", THU: "å¨å", FRI: "å¨äº", SAT: "å¨å ", SUN: "卿¥", })[item] || item ); }; const normalizeInspector = val => { if (!val) return []; if (Array.isArray(val)) return val.filter(Boolean); if (typeof val === "string") { return val .split(",") .map(item => item.trim()) .filter(Boolean); } return [String(val)]; }; const switchTab = tabName => { if (activeTab.value === tabName) return; activeTab.value = tabName; handleQuery(); }; const getList = async () => { if (loading.value) return; loading.value = true; try { const params = { ...queryParams, current: pageParams.current, size: pageParams.size, }; const request = activeTab.value === "task" ? inspectionTaskList : timingTaskList; const res = await request(params); const records = res?.data?.records || []; const normalized = records.map(item => ({ ...item, inspector: normalizeInspector(item.inspector), })); if (pageParams.current === 1) { tableData.value = normalized; } else { tableData.value = [...tableData.value, ...normalized]; } total.value = Number(res?.data?.total || 0); } catch (error) { if (pageParams.current === 1) { tableData.value = []; } uni.showToast({ title: "è·åæ°æ®å¤±è´¥", icon: "none", }); } finally { loading.value = false; } }; const handleQuery = () => { pageParams.current = 1; total.value = 0; getList(); }; const loadMore = () => { if (loading.value || noMore.value) return; pageParams.current += 1; getList(); }; const handleAdd = row => { nextTick(() => { formDia.value?.openDialog(row ? "edit" : "add", row); }); }; const viewFile = row => { nextTick(() => { viewFiles.value?.openDialog(row); }); }; const deleteOne = async row => { if (!row?.id) return; const canDelete = await new Promise(resolve => { uni.showModal({ title: "æç¤º", content: "æ¯å¦ç¡®è®¤å é¤è¯¥å·¡æ£ä»»å¡ï¼", success: modalRes => resolve(Boolean(modalRes.confirm)), fail: () => resolve(false), }); }); if (!canDelete) return; try { await delTimingTask([row.id]); uni.showToast({ title: "å 餿å", icon: "success", }); handleQuery(); } catch (error) { uni.showToast({ title: "å é¤å¤±è´¥", icon: "none", }); } }; const onSwipeActionClick = (event, row) => { if (activeTab.value !== "taskManage") return; if (event?.position !== "right") return; deleteOne(row); }; onShow(() => { handleQuery(); }); onReachBottom(() => { loadMore(); }); onPullDownRefresh(() => { handleQuery(); uni.stopPullDownRefresh(); }); </script> <style scoped> .inspection-management-page { min-height: 100vh; background: #f6f7fb; } .toolbar { padding: 20rpx 24rpx; background: #fff; border-bottom: 1rpx solid #f0f0f0; position: sticky; top: 0; z-index: 10; } .tab-wrap { display: flex; background: #f4f5f8; border-radius: 16rpx; padding: 6rpx; } .tab-item { flex: 1; text-align: center; padding: 14rpx 0; font-size: 26rpx; color: #666; border-radius: 12rpx; } .tab-item.active { background: #1677ff; color: #fff; font-weight: 600; } .search-section { margin-top: 20rpx; } .search-bar { display: flex; align-items: center; background: #f7f8fa; border-radius: 14rpx; padding: 8rpx 12rpx 8rpx 16rpx; border: 1rpx solid #eef1f5; } .search-input { flex: 1; min-width: 0; } .search-text { background: transparent !important; } :deep(.search-text .u-input__content), :deep(.search-text .up-input__content) { background: transparent !important; padding: 0 !important; } .search-button { width: 64rpx; height: 64rpx; border-radius: 12rpx; background: #ffffff; display: flex; align-items: center; justify-content: center; } .meta-bar { margin-top: 16rpx; display: flex; justify-content: space-between; align-items: center; background: #f7f9fc; border-radius: 12rpx; padding: 10rpx 16rpx; } .meta-text { color: #5c6b8a; font-size: 22rpx; } .list-section { padding: 20rpx 24rpx; padding-bottom: calc(132rpx + env(safe-area-inset-bottom)); } .ledger-item { background: #ffffff; border-radius: 20rpx; margin-bottom: 16rpx; overflow: hidden; box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.05); padding: 0 20rpx; } .item-header { display: flex; justify-content: space-between; align-items: center; padding: 22rpx 0; } .item-left { display: flex; align-items: center; gap: 10rpx; flex: 1; min-width: 0; } .item-right { display: flex; align-items: center; } .document-icon { width: 38rpx; height: 38rpx; border-radius: 8rpx; background: #2979ff; display: flex; align-items: center; justify-content: center; } .item-id { font-size: 28rpx; color: #1f1f1f; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .item-details { padding: 18rpx 0 20rpx; } .detail-row { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: 12rpx; } .detail-label { min-width: 160rpx; color: #777; font-size: 24rpx; } .detail-value { flex: 1; color: #333; font-size: 24rpx; text-align: right; word-break: break-all; margin-left: 12rpx; } .detail-value.highlight { color: #2979ff; font-weight: 500; } .tag-wrap { flex: 1; display: flex; gap: 10rpx; flex-wrap: wrap; justify-content: flex-end; } .card-actions { padding-top: 8rpx; display: flex; justify-content: flex-end; } .action-btn { min-width: 140rpx; } .fab-button { position: fixed; bottom: calc(30px + env(safe-area-inset-bottom)); right: 30px; width: 56px; height: 56px; background: #2979ff; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 16px rgba(41, 121, 255, 0.3); z-index: 1000; } .no-data { padding: 40rpx 0; text-align: center; color: #999; } </style>