| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="camera-upload"> |
| | | <!-- æç
§/æè§é¢æé® --> |
| | | <view v-if="!disabled" class="camera-buttons"> |
| | | <view class="button-row"> |
| | | <u-button |
| | | type="primary" |
| | | @click="takePhoto" |
| | | :loading="uploading" |
| | | :disabled="fileList.length >= limit" |
| | | :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="takeVideo" |
| | | :loading="uploading" |
| | | :disabled="fileList.length >= limit" |
| | | :customStyle="{ flex: 1 }" |
| | | > |
| | | <u-icon name="video" size="18" color="#fff" style="margin-right: 5px;"></u-icon> |
| | | {{ uploading ? 'ä¸ä¼ ä¸...' : 'æè§é¢' }} |
| | | </u-button> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- æç¤ºä¿¡æ¯ --> |
| | | <view v-if="showTip && !disabled" class="upload-tip"> |
| | | 请使ç¨ç¸æº |
| | | <text v-if="fileSize" class="tip-text"> |
| | | ææå¤§å°ä¸è¶
è¿ <text class="tip-highlight">{{ fileSize }}MB</text> |
| | | </text> |
| | | ç |
| | | <text class="tip-highlight">ç
§çæè§é¢</text> |
| | | </view> |
| | | |
| | | <!-- åªä½æä»¶å表 --> |
| | | <view class="media-list"> |
| | | <view |
| | | v-for="(file, index) in fileList" |
| | | :key="file.uid || index" |
| | | class="media-item" |
| | | > |
| | | <!-- é¢è§åºå --> |
| | | <view class="media-preview" @click="previewMedia(file, index)"> |
| | | <image |
| | | v-if="file.type === 'image'" |
| | | :src="file.url || file.tempFilePath" |
| | | class="preview-image" |
| | | mode="aspectFill" |
| | | ></image> |
| | | <video |
| | | v-else-if="file.type === 'video'" |
| | | :src="file.url || file.tempFilePath" |
| | | class="preview-video" |
| | | :controls="false" |
| | | ></video> |
| | | <view class="media-type-icon"> |
| | | <u-icon |
| | | :name="file.type === 'image' ? 'photo' : 'video'" |
| | | size="12" |
| | | color="#fff" |
| | | ></u-icon> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- æä½æé® --> |
| | | <view class="media-actions" v-if="!disabled"> |
| | | <u-button |
| | | type="error" |
| | | size="mini" |
| | | @click="handleDelete(index)" |
| | | :customStyle="{ |
| | | minWidth: '40px', |
| | | height: '24px', |
| | | fontSize: '10px', |
| | | padding: '0 8px' |
| | | }" |
| | | > |
| | | å é¤ |
| | | </u-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- ä¸ä¼ è¿åº¦ --> |
| | | <view v-if="uploading" class="upload-progress"> |
| | | <u-line-progress |
| | | :percentage="uploadProgress" |
| | | :showText="true" |
| | | activeColor="#409eff" |
| | | ></u-line-progress> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'; |
| | | import { getToken } from "@/utils/auth"; |
| | | |
| | | // Props å®ä¹ |
| | | const props = defineProps({ |
| | | modelValue: [String, Object, Array], |
| | | action: { type: String, default: "/common/minioUploads" }, |
| | | data: { type: Object }, |
| | | limit: { type: Number, default: 5 }, |
| | | fileSize: { type: Number, default: 10 }, // é»è®¤10MBï¼éåè§é¢ |
| | | fileType: { |
| | | type: Array, |
| | | default: () => ["jpg", "jpeg", "png", "mp4", "mov"] |
| | | }, |
| | | isShowTip: { type: Boolean, default: true }, |
| | | disabled: { type: Boolean, default: false }, |
| | | drag: { type: Boolean, default: false }, // æç
§ä¸éè¦ææ½ |
| | | statusType: { type: Number, default: "" }, // ç¨äºåºåä¸åç¶æçä¸ä¼ |
| | | maxVideoDuration: { type: Number, default: 30 }, // æå¤§è§é¢æ¶é¿ï¼ç§ï¼ |
| | | }); |
| | | |
| | | // äºä»¶å®ä¹ |
| | | const emit = defineEmits(['update:modelValue']); |
| | | |
| | | // ååºå¼æ°æ® |
| | | const number = ref(0); |
| | | const uploadList = ref([]); |
| | | const fileList = ref([]); |
| | | const uploading = ref(false); |
| | | const uploadProgress = ref(0); |
| | | |
| | | // 计ç®å±æ§ |
| | | const uploadFileUrl = computed(() => { |
| | | // è·ååºç¡APIå°åï¼éé
uniappç¯å¢ |
| | | let baseUrl = ''; |
| | | |
| | | // å°è¯å¤ç§æ¹å¼è·åbaseUrl |
| | | if (process.env.VUE_APP_BASE_API) { |
| | | baseUrl = process.env.VUE_APP_BASE_API; |
| | | } else if (process.env.NODE_ENV === 'development') { |
| | | baseUrl = 'http://192.168.1.147:9036'; |
| | | } else { |
| | | baseUrl = 'http://192.168.1.147:9036'; |
| | | } |
| | | |
| | | const fullUrl = baseUrl + props.action; |
| | | return fullUrl; |
| | | }); |
| | | const headers = computed(() => { |
| | | const token = getToken(); |
| | | return token ? { Authorization: "Bearer " + token } : {}; |
| | | }); |
| | | const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize)); |
| | | // åå§ååç¼è¾åå§åæ¹æ³ |
| | | const init = () => { |
| | | fileList.value = []; |
| | | uploadList.value = []; |
| | | number.value = 0; |
| | | }; |
| | | |
| | | const editInit = (val) => { |
| | | fileList.value = []; |
| | | val.storageBlobDTO.forEach((element) => { |
| | | // ç¡®ä¿æä»¶æ°æ®å
嫿æå¿
è¦å段ï¼å
æ¬id |
| | | const fileData = { |
| | | ...element, |
| | | id: element.id, // ä¿çæå¡å¨è¿åçid |
| | | url: element.url || element.downloadUrl, |
| | | bucketFilename: element.bucketFilename || element.name, |
| | | downloadUrl: element.downloadUrl || element.url, |
| | | type: element.type || (element.url && element.url.includes('video') ? 'video' : 'image'), |
| | | name: element.name || element.bucketFilename || `æä»¶_${Date.now()}`, |
| | | size: element.size || 0, |
| | | createTime: element.createTime || new Date().getTime(), |
| | | uid: element.uid || new Date().getTime() + Math.random() |
| | | }; |
| | | fileList.value.push(fileData); |
| | | uploadedSuccessfully(); |
| | | }); |
| | | }; |
| | | |
| | | // æµè¯æå¡å¨è¿æ¥ |
| | | const testServerConnection = () => { |
| | | return new Promise((resolve) => { |
| | | uni.request({ |
| | | url: uploadFileUrl.value.replace('/common/minioUploads', '/common/test'), |
| | | method: 'GET', |
| | | timeout: 5000, |
| | | success: (res) => { |
| | | resolve(true); |
| | | }, |
| | | fail: (err) => { |
| | | resolve(false); |
| | | } |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | // ç»ä»¶éæ¯æ¶çæ¸
ç |
| | | onUnmounted(() => { |
| | | // æ¸
çä¸ä¼ ç¶æ |
| | | if (uploading.value) { |
| | | uploading.value = false |
| | | } |
| | | |
| | | // éèå¯è½æ¾ç¤ºçå è½½æç¤º |
| | | uni.hideLoading() |
| | | uni.hideToast() |
| | | }) |
| | | |
| | | // æ´é²æ¹æ³ |
| | | defineExpose({ init, editInit, testServerConnection }); |
| | | |
| | | // çå¬ modelValue åå |
| | | watch( |
| | | () => props.modelValue, |
| | | (val) => { |
| | | if (val) { |
| | | let temp = 1; |
| | | let list = []; |
| | | |
| | | if (Array.isArray(val)) { |
| | | list = val; |
| | | } else if (typeof val === "string") { |
| | | list = val.split(",").map(url => ({ url: url.trim() })); |
| | | } |
| | | |
| | | fileList.value = list.map((item) => { |
| | | if (typeof item === "string") { |
| | | item = { name: item, url: item }; |
| | | } |
| | | // ç¡®ä¿æ¯ä¸ªæä»¶é½æå¿
è¦ç屿§ï¼å
æ¬id |
| | | return { |
| | | ...item, |
| | | id: item.id, // ä¿çidåæ®µ |
| | | uid: item.uid || new Date().getTime() + temp++, |
| | | type: item.type || (item.url && item.url.includes('video') ? 'video' : 'image'), |
| | | name: item.name || item.bucketFilename || `æä»¶_${Date.now()}`, |
| | | size: item.size || 0, |
| | | createTime: item.createTime || new Date().getTime() |
| | | }; |
| | | }); |
| | | } else { |
| | | fileList.value = []; |
| | | } |
| | | }, |
| | | { deep: true, immediate: true } |
| | | ); |
| | | |
| | | // æç
§ |
| | | const takePhoto = () => { |
| | | if (fileList.value.length >= props.limit) { |
| | | uni.showToast({ |
| | | title: `æå¤åªè½ææ${props.limit}个æä»¶`, |
| | | icon: 'none' |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | uni.chooseImage({ |
| | | count: 1, |
| | | sizeType: ['compressed', 'original'], |
| | | sourceType: ['camera'], |
| | | success: (res) => { |
| | | try { |
| | | if (!res.tempFilePaths || res.tempFilePaths.length === 0) { |
| | | throw new Error('æªè·åå°å¾çæä»¶'); |
| | | } |
| | | |
| | | const tempFilePath = res.tempFilePaths[0]; |
| | | const tempFile = res.tempFiles && res.tempFiles[0] ? res.tempFiles[0] : {}; |
| | | |
| | | const file = { |
| | | tempFilePath: tempFilePath, |
| | | type: 'image', |
| | | name: `photo_${Date.now()}.jpg`, |
| | | size: tempFile.size || 0, |
| | | createTime: new Date().getTime(), |
| | | uid: Date.now() + Math.random() |
| | | }; |
| | | |
| | | handleBeforeUpload(file); |
| | | } catch (error) { |
| | | console.error('å¤çæç
§ç»æå¤±è´¥:', error); |
| | | uni.showToast({ |
| | | title: 'å¤çå¾ç失败', |
| | | icon: 'error' |
| | | }); |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error('æç
§å¤±è´¥:', err); |
| | | uni.showToast({ |
| | | title: 'æç
§å¤±è´¥: ' + (err.errMsg || 'æªç¥é误'), |
| | | icon: 'error' |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // æè§é¢ |
| | | const takeVideo = () => { |
| | | if (fileList.value.length >= props.limit) { |
| | | uni.showToast({ |
| | | title: `æå¤åªè½ææ${props.limit}个æä»¶`, |
| | | icon: 'none' |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | uni.chooseVideo({ |
| | | sourceType: ['camera'], |
| | | maxDuration: props.maxVideoDuration, |
| | | camera: 'back', |
| | | success: (res) => { |
| | | try { |
| | | if (!res.tempFilePath) { |
| | | throw new Error('æªè·åå°è§é¢æä»¶'); |
| | | } |
| | | |
| | | const file = { |
| | | tempFilePath: res.tempFilePath, |
| | | type: 'video', |
| | | name: `video_${Date.now()}.mp4`, |
| | | size: res.size || 0, |
| | | duration: res.duration || 0, |
| | | createTime: new Date().getTime(), |
| | | uid: Date.now() + Math.random() |
| | | }; |
| | | |
| | | handleBeforeUpload(file); |
| | | } catch (error) { |
| | | console.error('å¤çæè§é¢ç»æå¤±è´¥:', error); |
| | | uni.showToast({ |
| | | title: 'å¤çè§é¢å¤±è´¥', |
| | | icon: 'error' |
| | | }); |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error('æè§é¢å¤±è´¥:', err); |
| | | uni.showToast({ |
| | | title: 'æè§é¢å¤±è´¥: ' + (err.errMsg || 'æªç¥é误'), |
| | | icon: 'error' |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // æä»¶ä¸ä¼ å¤ç |
| | | const uploadFile = (file) => { |
| | | uploading.value = true; |
| | | uploadProgress.value = 0; |
| | | number.value++; // å¢å ä¸ä¼ è®¡æ° |
| | | |
| | | // ç¡®ä¿æä»¶è·¯å¾æ£ç¡® |
| | | const filePath = file.tempFilePath || file.path; |
| | | if (!filePath) { |
| | | handleUploadError('æä»¶è·¯å¾ä¸åå¨'); |
| | | return; |
| | | } |
| | | |
| | | // ç¡®ä¿tokenåå¨ |
| | | const token = getToken(); |
| | | if (!token) { |
| | | handleUploadError('ç¨æ·æªç»å½'); |
| | | return; |
| | | } |
| | | |
| | | // åå¤ä¸ä¼ åæ° |
| | | const uploadParams = { |
| | | url: uploadFileUrl.value, |
| | | filePath: filePath, |
| | | name: 'files', |
| | | formData: { |
| | | type: props.statusType || 0, |
| | | ...(props.data || {}) |
| | | }, |
| | | header: { |
| | | 'Authorization': `Bearer ${token}` |
| | | } |
| | | }; |
| | | |
| | | const uploadTask = uni.uploadFile({ |
| | | ...uploadParams, |
| | | success: (res) => { |
| | | try { |
| | | if (res.statusCode === 200) { |
| | | const response = JSON.parse(res.data); |
| | | if (response.code === 200) { |
| | | handleUploadSuccess(response, file); |
| | | uni.showToast({ |
| | | title: 'ä¸ä¼ æå', |
| | | icon: 'success' |
| | | }); |
| | | emit("update:modelValue", fileList.value); |
| | | } else { |
| | | handleUploadError(response.msg || 'æå¡å¨è¿åé误'); |
| | | } |
| | | } else { |
| | | handleUploadError(`æå¡å¨é误ï¼ç¶æç : ${res.statusCode}`); |
| | | } |
| | | } catch (e) { |
| | | console.error('è§£æååºå¤±è´¥:', e); |
| | | console.error('åå§ååºæ°æ®:', res.data); |
| | | handleUploadError('ååºæ°æ®è§£æå¤±è´¥: ' + e.message); |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error('ä¸ä¼ 失败:', err.errMsg || err); |
| | | number.value--; // ä¸ä¼ 失败æ¶åå°è®¡æ° |
| | | |
| | | let errorMessage = 'ä¸ä¼ 失败'; |
| | | if (err.errMsg) { |
| | | if (err.errMsg.includes('statusCode: null')) { |
| | | errorMessage = 'ç½ç»è¿æ¥å¤±è´¥ï¼è¯·æ£æ¥ç½ç»è®¾ç½®'; |
| | | } else if (err.errMsg.includes('timeout')) { |
| | | errorMessage = 'ä¸ä¼ è¶
æ¶ï¼è¯·éè¯'; |
| | | } else if (err.errMsg.includes('fail')) { |
| | | errorMessage = 'ä¸ä¼ 失败ï¼è¯·æ£æ¥ç½ç»è¿æ¥'; |
| | | } else { |
| | | errorMessage = err.errMsg; |
| | | } |
| | | } |
| | | |
| | | handleUploadError(errorMessage); |
| | | }, |
| | | complete: () => { |
| | | uploading.value = false; |
| | | uploadProgress.value = 0; |
| | | } |
| | | }); |
| | | |
| | | // çå¬ä¸ä¼ è¿åº¦ |
| | | if (uploadTask && uploadTask.onProgressUpdate) { |
| | | uploadTask.onProgressUpdate((res) => { |
| | | uploadProgress.value = res.progress; |
| | | }); |
| | | } |
| | | }; |
| | | // è·ååªä½æä»¶å |
| | | const getMediaName = (file) => { |
| | | if (file.bucketFilename) { |
| | | return file.bucketFilename.length > 15 |
| | | ? file.bucketFilename.substring(0, 15) + '...' |
| | | : file.bucketFilename; |
| | | } |
| | | if (file.name) { |
| | | return file.name.length > 15 |
| | | ? file.name.substring(0, 15) + '...' |
| | | : file.name; |
| | | } |
| | | return file.type === 'image' ? 'ç
§ç' : 'è§é¢'; |
| | | }; |
| | | |
| | | // æ ¼å¼åæä»¶å¤§å° |
| | | const formatFileSize = (size) => { |
| | | if (!size) return ''; |
| | | if (size < 1024) return size + 'B'; |
| | | if (size < 1024 * 1024) return (size / 1024).toFixed(1) + 'KB'; |
| | | return (size / (1024 * 1024)).toFixed(1) + 'MB'; |
| | | }; |
| | | |
| | | // æ ¼å¼åæ¶é´ |
| | | const formatTime = (timestamp) => { |
| | | if (!timestamp) return ''; |
| | | const date = new Date(timestamp); |
| | | const now = new Date(); |
| | | const diff = now - date; |
| | | |
| | | if (diff < 60000) return 'åå'; |
| | | if (diff < 3600000) return Math.floor(diff / 60000) + 'åéå'; |
| | | if (diff < 86400000) return Math.floor(diff / 3600000) + 'å°æ¶å'; |
| | | |
| | | return date.toLocaleDateString(); |
| | | }; |
| | | |
| | | // é¢è§åªä½æä»¶ |
| | | const previewMedia = (file, index) => { |
| | | if (file.type === 'image') { |
| | | // é¢è§å¾ç |
| | | const urls = fileList.value |
| | | .filter(item => item.type === 'image') |
| | | .map(item => item.url || item.tempFilePath); |
| | | |
| | | uni.previewImage({ |
| | | urls: urls, |
| | | current: file.url || file.tempFilePath |
| | | }); |
| | | } else if (file.type === 'video') { |
| | | // é¢è§è§é¢ |
| | | uni.previewVideo({ |
| | | sources: [{ |
| | | src: file.url || file.tempFilePath, |
| | | type: 'mp4' |
| | | }], |
| | | current: 0 |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // ä¸è½½æä»¶ |
| | | const handleDownload = (index) => { |
| | | const file = fileList.value[index]; |
| | | const url = file.url || file.downloadUrl; |
| | | |
| | | if (!url) { |
| | | uni.showToast({ |
| | | title: 'æä»¶é¾æ¥ä¸åå¨ï¼æ æ³ä¸è½½', |
| | | icon: 'none' |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | // 使ç¨uniappçä¸è½½API |
| | | uni.downloadFile({ |
| | | url: url, |
| | | success: (res) => { |
| | | if (res.statusCode === 200) { |
| | | // ä¿åå°ç¸åææä»¶ç³»ç» |
| | | uni.saveFile({ |
| | | tempFilePath: res.tempFilePath, |
| | | success: (saveRes) => { |
| | | uni.showToast({ |
| | | title: 'ä¸è½½æå', |
| | | icon: 'success' |
| | | }); |
| | | }, |
| | | fail: (err) => { |
| | | console.error('ä¿åæä»¶å¤±è´¥:', err); |
| | | uni.showToast({ |
| | | title: 'ä¿åæä»¶å¤±è´¥', |
| | | icon: 'error' |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error('ä¸è½½å¤±è´¥:', err); |
| | | uni.showToast({ |
| | | title: 'ä¸è½½å¤±è´¥', |
| | | icon: 'error' |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // æ£æ¥ç½ç»è¿æ¥ |
| | | const checkNetworkConnection = () => { |
| | | return new Promise((resolve) => { |
| | | uni.getNetworkType({ |
| | | success: (res) => { |
| | | if (res.networkType === 'none') { |
| | | resolve(false); |
| | | } else { |
| | | resolve(true); |
| | | } |
| | | }, |
| | | fail: () => { |
| | | resolve(false); |
| | | } |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | // ä¸ä¼ åæ ¡éª |
| | | const handleBeforeUpload = async (file) => { |
| | | // æ£æ¥ç½ç»è¿æ¥ |
| | | const hasNetwork = await checkNetworkConnection(); |
| | | if (!hasNetwork) { |
| | | uni.showToast({ |
| | | title: 'ç½ç»è¿æ¥ä¸å¯ç¨ï¼è¯·æ£æ¥ç½ç»è®¾ç½®', |
| | | icon: 'none' |
| | | }); |
| | | return false; |
| | | } |
| | | |
| | | // æ ¡éªæä»¶å¤§å° |
| | | if (props.fileSize && file.size) { |
| | | const isLt = file.size / 1024 / 1024 < props.fileSize; |
| | | if (!isLt) { |
| | | uni.showToast({ |
| | | title: `æä»¶å¤§å°ä¸è½è¶
è¿ ${props.fileSize} MB!`, |
| | | icon: 'none' |
| | | }); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | // æ ¡éªè§é¢æ¶é¿ |
| | | if (file.type === 'video' && file.duration && file.duration > props.maxVideoDuration) { |
| | | uni.showToast({ |
| | | title: `è§é¢æ¶é¿ä¸è½è¶
è¿ ${props.maxVideoDuration} ç§!`, |
| | | icon: 'none' |
| | | }); |
| | | return false; |
| | | } |
| | | |
| | | // æ ¡éªæä»¶ç±»å |
| | | if (props.fileType && Array.isArray(props.fileType) && props.fileType.length > 0) { |
| | | const fileName = file.name || ''; |
| | | const fileExtension = fileName ? fileName.split('.').pop().toLowerCase() : ''; |
| | | |
| | | // æ ¹æ®æä»¶ç±»åç¡®å®ææçæ©å±å |
| | | let expectedTypes = []; |
| | | if (file.type === 'image') { |
| | | expectedTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp']; |
| | | } else if (file.type === 'video') { |
| | | expectedTypes = ['mp4', 'mov', 'avi', 'wmv']; |
| | | } |
| | | |
| | | // æ£æ¥æä»¶æ©å±åæ¯å¦å¨å
许çç±»åä¸ |
| | | if (fileExtension && expectedTypes.length > 0) { |
| | | const isAllowed = expectedTypes.some(type => |
| | | props.fileType.includes(type) && type === fileExtension |
| | | ); |
| | | |
| | | if (!isAllowed) { |
| | | uni.showToast({ |
| | | title: `æä»¶æ ¼å¼ä¸æ¯æï¼è¯·ææ ${expectedTypes.join('/')} æ ¼å¼çæä»¶`, |
| | | icon: 'none' |
| | | }); |
| | | return false; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // æ ¡éªéè¿ï¼å¼å§ä¸ä¼ |
| | | uploadFile(file); |
| | | return true; |
| | | }; |
| | | |
| | | // ä¸ä¼ 失败å¤ç |
| | | const handleUploadError = (message = 'ä¸ä¼ æä»¶å¤±è´¥', showRetry = true) => { |
| | | if (showRetry) { |
| | | uni.showModal({ |
| | | title: 'ä¸ä¼ 失败', |
| | | content: message + 'ï¼æ¯å¦éè¯ï¼', |
| | | success: (res) => { |
| | | if (res.confirm) { |
| | | // ç¨æ·éæ©éè¯ï¼è¿éå¯ä»¥éæ°è§¦åä¸ä¼ |
| | | } |
| | | } |
| | | }); |
| | | } else { |
| | | uni.showToast({ |
| | | title: message, |
| | | icon: 'error' |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // ä¸ä¼ æååè° |
| | | const handleUploadSuccess = (res, file) => { |
| | | if (res.code === 200 && res.data && Array.isArray(res.data) && res.data.length > 0) { |
| | | const uploadedFile = res.data[0]; |
| | | // ç¡®ä¿ä¸ä¼ çæä»¶æ°æ®å®æ´ï¼å
å«id |
| | | const fileData = { |
| | | ...file, |
| | | id: uploadedFile.id, // æ·»å æå¡å¨è¿åçid |
| | | url: uploadedFile.url || uploadedFile.downloadUrl, |
| | | bucketFilename: uploadedFile.bucketFilename || file.name, |
| | | downloadUrl: uploadedFile.downloadUrl || uploadedFile.url, |
| | | size: uploadedFile.size || file.size, |
| | | createTime: uploadedFile.createTime || new Date().getTime() |
| | | }; |
| | | |
| | | uploadList.value.push(fileData); |
| | | uploadedSuccessfully(); |
| | | } else { |
| | | number.value--; // ä¸ä¼ 失败æ¶åå°è®¡æ° |
| | | handleUploadError(res.msg || 'ä¸ä¼ 失败'); |
| | | } |
| | | }; |
| | | |
| | | // å 餿件 |
| | | const handleDelete = (index) => { |
| | | uni.showModal({ |
| | | title: '确认å é¤', |
| | | content: 'ç¡®å®è¦å é¤è¿ä¸ªæä»¶åï¼', |
| | | success: (res) => { |
| | | if (res.confirm) { |
| | | fileList.value.splice(index, 1); |
| | | emit("update:modelValue", listToString(fileList.value)); |
| | | uni.showToast({ |
| | | title: 'å 餿å', |
| | | icon: 'success' |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // ä¸ä¼ ç»æå¤ç |
| | | const uploadedSuccessfully = () => { |
| | | if (number.value > 0 && uploadList.value.length === number.value) { |
| | | // åå¹¶å·²åå¨çæä»¶ååä¸ä¼ çæä»¶ |
| | | const existingFiles = fileList.value.filter((f) => f.url !== undefined); |
| | | fileList.value = [...existingFiles, ...uploadList.value]; |
| | | |
| | | // éç½®ç¶æ |
| | | uploadList.value = []; |
| | | number.value = 0; |
| | | |
| | | // è§¦åæ´æ°äºä»¶ï¼ä¼ é宿´çæä»¶å表 |
| | | emit("update:modelValue", fileList.value); |
| | | } |
| | | }; |
| | | |
| | | const listToString = (list, separator = ",") => { |
| | | const strs = list |
| | | .filter(item => item.url) |
| | | .map(item => item.url) |
| | | .join(separator); |
| | | return strs; |
| | | }; |
| | | </script> |
| | | <style scoped lang="scss"> |
| | | .camera-upload { |
| | | width: 100%; |
| | | } |
| | | |
| | | .camera-buttons { |
| | | margin-bottom: 15px; |
| | | |
| | | .button-row { |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | } |
| | | |
| | | .upload-tip { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-bottom: 15px; |
| | | text-align: center; |
| | | line-height: 1.5; |
| | | |
| | | .tip-text { |
| | | margin: 0 2px; |
| | | } |
| | | |
| | | .tip-highlight { |
| | | color: #f56c6c; |
| | | font-weight: bold; |
| | | } |
| | | } |
| | | |
| | | .media-list { |
| | | margin-top: 10px; |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .media-item { |
| | | position: relative; |
| | | width: 80px; |
| | | height: 80px; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | background-color: #f5f5f5; |
| | | border: 2px solid #e9ecef; |
| | | transition: all 0.3s ease; |
| | | |
| | | &:hover { |
| | | border-color: #409eff; |
| | | transform: scale(1.02); |
| | | } |
| | | } |
| | | |
| | | .media-preview { |
| | | position: relative; |
| | | width: 100%; |
| | | height: 100%; |
| | | cursor: pointer; |
| | | |
| | | .preview-image, .preview-video { |
| | | width: 100%; |
| | | height: 100%; |
| | | object-fit: cover; |
| | | } |
| | | |
| | | .media-type-icon { |
| | | position: absolute; |
| | | top: 4px; |
| | | right: 4px; |
| | | width: 20px; |
| | | height: 20px; |
| | | background-color: rgba(0, 0, 0, 0.6); |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | } |
| | | |
| | | .media-actions { |
| | | position: absolute; |
| | | bottom: 0; |
| | | left: 0; |
| | | right: 0; |
| | | background: linear-gradient(transparent, rgba(0, 0, 0, 0.7)); |
| | | padding: 4px; |
| | | display: flex; |
| | | justify-content: center; |
| | | opacity: 0; |
| | | transition: opacity 0.3s ease; |
| | | |
| | | .media-item:hover & { |
| | | opacity: 1; |
| | | } |
| | | } |
| | | |
| | | .upload-progress { |
| | | margin-top: 15px; |
| | | padding: 0 10px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view> |
| | | <!-- å¼¹çª --> |
| | | <u-popup |
| | | v-model="dialogVisitable" |
| | | mode="center" |
| | | width="90%" |
| | | height="80%" |
| | | border-radius="20" |
| | | @close="cancel" |
| | | > |
| | | <view class="popup-content"> |
| | | <view class="popup-header"> |
| | | <text class="popup-title">æ¥çéä»¶</text> |
| | | <u-icon |
| | | name="close" |
| | | size="24" |
| | | color="#999" |
| | | @click="cancel" |
| | | ></u-icon> |
| | | </view> |
| | | |
| | | <view class="upload-container"> |
| | | <view class="form-container"> |
| | | <view class="title">å·¡æ£éä»¶</view> |
| | | |
| | | <!-- å¾çå表 --> |
| | | <view v-if="beforeProductionImgs.length > 0" class="media-section"> |
| | | <view class="section-title">å¾ç</view> |
| | | <view class="image-grid"> |
| | | <view |
| | | v-for="(item, index) in beforeProductionImgs" |
| | | :key="index" |
| | | class="image-item" |
| | | @click="previewImage(item, index)" |
| | | > |
| | | <image |
| | | :src="item" |
| | | mode="aspectFill" |
| | | class="image-preview" |
| | | /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- è§é¢å表 --> |
| | | <view v-if="beforeProductionVideos.length > 0" class="media-section"> |
| | | <view class="section-title">è§é¢</view> |
| | | <view class="video-grid"> |
| | | <view |
| | | v-for="(videoUrl, index) in beforeProductionVideos" |
| | | :key="index" |
| | | class="video-item" |
| | | @click="previewVideo(videoUrl)" |
| | | > |
| | | <view class="video-preview"> |
| | | <u-icon name="play-circle-fill" size="40" color="#fff"></u-icon> |
| | | </view> |
| | | <view class="video-tip">ç¹å»ææ¾</view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- ç©ºç¶æ --> |
| | | <view v-if="beforeProductionImgs.length === 0 && beforeProductionVideos.length === 0" class="empty-state"> |
| | | <u-empty |
| | | mode="data" |
| | | text="ææ éä»¶" |
| | | :iconSize="60" |
| | | ></u-empty> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </u-popup> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onUnmounted } from 'vue' |
| | | |
| | | // æ§å¶å¼¹çªæ¾ç¤º |
| | | const dialogVisitable = ref(false) |
| | | // å¾çæ°ç» |
| | | const beforeProductionImgs = ref([]) |
| | | // è§é¢æ°ç» |
| | | const beforeProductionVideos = ref([]) |
| | | |
| | | // æå¼å¼¹çªå¹¶å è½½æ°æ® |
| | | const openDialog = async (row) => { |
| | | console.log('æå¼éä»¶æ¥çå¼¹çªï¼æ°æ®:', row) |
| | | |
| | | // å¤çæ°æ®ï¼å离å¾çåè§é¢ |
| | | const { images: beforeImgs, videos: beforeVids } = processItems(row.storageBlobDTO || []) |
| | | |
| | | beforeProductionImgs.value = beforeImgs |
| | | beforeProductionVideos.value = beforeVids |
| | | dialogVisitable.value = true |
| | | } |
| | | |
| | | // é¢è§å¾ç |
| | | const previewImage = (url, index) => { |
| | | uni.previewImage({ |
| | | urls: beforeProductionImgs.value, |
| | | current: index, |
| | | fail: (err) => { |
| | | console.error('å¾çé¢è§å¤±è´¥:', err) |
| | | uni.showToast({ |
| | | title: 'å¾çé¢è§å¤±è´¥', |
| | | icon: 'error' |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // é¢è§è§é¢ |
| | | const previewVideo = (url) => { |
| | | uni.previewVideo({ |
| | | sources: [{ |
| | | src: url |
| | | }], |
| | | fail: (err) => { |
| | | console.error('è§é¢é¢è§å¤±è´¥:', err) |
| | | uni.showToast({ |
| | | title: 'è§é¢é¢è§å¤±è´¥', |
| | | icon: 'error' |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // 表åå
³éæ¹æ³ |
| | | const cancel = () => { |
| | | dialogVisitable.value = false |
| | | // éç½®æ°æ® |
| | | beforeProductionImgs.value = [] |
| | | beforeProductionVideos.value = [] |
| | | } |
| | | |
| | | // å¤çæ¯ä¸ç±»æ°æ®ï¼å离å¾çåè§é¢ |
| | | function processItems(items) { |
| | | if (!items || !Array.isArray(items)) { |
| | | return { images: [], videos: [] } |
| | | } |
| | | |
| | | 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 } |
| | | } |
| | | |
| | | // ç»ä»¶éæ¯æ¶çæ¸
ç |
| | | onUnmounted(() => { |
| | | // å
³éå¼¹çª |
| | | dialogVisitable.value = false |
| | | // æ¸
çæ°æ® |
| | | beforeProductionImgs.value = [] |
| | | beforeProductionVideos.value = [] |
| | | }) |
| | | |
| | | // æ´é²æ¹æ³ç»ç¶ç»ä»¶ |
| | | defineExpose({ openDialog }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .popup-content { |
| | | height: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .popup-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 20px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .popup-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | .upload-container { |
| | | flex: 1; |
| | | padding: 20px; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | .form-container { |
| | | width: 100%; |
| | | } |
| | | |
| | | .title { |
| | | font-size: 16px; |
| | | color: #1890ff; |
| | | line-height: 24px; |
| | | font-weight: 600; |
| | | padding-left: 12px; |
| | | position: relative; |
| | | margin: 0 0 20px 0; |
| | | |
| | | &::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 0; |
| | | top: 4px; |
| | | width: 4px; |
| | | height: 16px; |
| | | background-color: #1890ff; |
| | | border-radius: 2px; |
| | | } |
| | | } |
| | | |
| | | .media-section { |
| | | margin-bottom: 30px; |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 14px; |
| | | color: #666; |
| | | margin-bottom: 15px; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .image-grid { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .image-item { |
| | | width: 100px; |
| | | height: 100px; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .image-preview { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | .video-grid { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 15px; |
| | | } |
| | | |
| | | .video-item { |
| | | width: 160px; |
| | | height: 90px; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .video-preview { |
| | | width: 100%; |
| | | height: 100%; |
| | | background-color: #333; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | position: relative; |
| | | } |
| | | |
| | | .video-tip { |
| | | position: absolute; |
| | | bottom: 5px; |
| | | left: 50%; |
| | | transform: translateX(-50%); |
| | | font-size: 12px; |
| | | color: #fff; |
| | | background-color: rgba(0, 0, 0, 0.6); |
| | | padding: 2px 8px; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .empty-state { |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | height: 200px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view> |
| | | <!-- å¼¹çª --> |
| | | <u-popup |
| | | v-model="dialogVisitable" |
| | | mode="center" |
| | | width="90%" |
| | | height="80%" |
| | | border-radius="20" |
| | | @close="cancel" |
| | | > |
| | | <view class="popup-content"> |
| | | <view class="popup-header"> |
| | | <text class="popup-title">æ¥çéä»¶</text> |
| | | <u-icon |
| | | name="close" |
| | | size="24" |
| | | color="#999" |
| | | @click="cancel" |
| | | ></u-icon> |
| | | </view> |
| | | |
| | | <view class="upload-container"> |
| | | <view class="form-container"> |
| | | <view class="title">å·¡æ£éä»¶</view> |
| | | |
| | | <!-- å¾çå表 --> |
| | | <view v-if="beforeProductionImgs.length > 0" class="media-section"> |
| | | <view class="section-title">å¾ç</view> |
| | | <view class="image-grid"> |
| | | <view |
| | | v-for="(item, index) in beforeProductionImgs" |
| | | :key="index" |
| | | class="image-item" |
| | | @click="previewImage(item, index)" |
| | | > |
| | | <image |
| | | :src="item" |
| | | mode="aspectFill" |
| | | class="image-preview" |
| | | /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- è§é¢å表 --> |
| | | <view v-if="beforeProductionVideos.length > 0" class="media-section"> |
| | | <view class="section-title">è§é¢</view> |
| | | <view class="video-grid"> |
| | | <view |
| | | v-for="(videoUrl, index) in beforeProductionVideos" |
| | | :key="index" |
| | | class="video-item" |
| | | @click="previewVideo(videoUrl)" |
| | | > |
| | | <view class="video-preview"> |
| | | <u-icon name="play-circle-fill" size="40" color="#fff"></u-icon> |
| | | </view> |
| | | <view class="video-tip">ç¹å»ææ¾</view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- ç©ºç¶æ --> |
| | | <view v-if="beforeProductionImgs.length === 0 && beforeProductionVideos.length === 0" class="empty-state"> |
| | | <u-empty |
| | | mode="data" |
| | | text="ææ éä»¶" |
| | | :iconSize="60" |
| | | ></u-empty> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </u-popup> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from 'vue' |
| | | |
| | | // æ§å¶å¼¹çªæ¾ç¤º |
| | | const dialogVisitable = ref(false) |
| | | // å¾çæ°ç» |
| | | const beforeProductionImgs = ref([]) |
| | | // è§é¢æ°ç» |
| | | const beforeProductionVideos = ref([]) |
| | | |
| | | // æå¼å¼¹çªå¹¶å è½½æ°æ® |
| | | const openDialog = async (row) => { |
| | | console.log('æå¼éä»¶æ¥çå¼¹çªï¼æ°æ®:', row) |
| | | |
| | | // å¤çæ°æ®ï¼å离å¾çåè§é¢ |
| | | const { images: beforeImgs, videos: beforeVids } = processItems(row.storageBlobDTO || []) |
| | | |
| | | beforeProductionImgs.value = beforeImgs |
| | | beforeProductionVideos.value = beforeVids |
| | | dialogVisitable.value = true |
| | | } |
| | | |
| | | // é¢è§å¾ç |
| | | const previewImage = (url, index) => { |
| | | uni.previewImage({ |
| | | urls: beforeProductionImgs.value, |
| | | current: index, |
| | | fail: (err) => { |
| | | console.error('å¾çé¢è§å¤±è´¥:', err) |
| | | uni.showToast({ |
| | | title: 'å¾çé¢è§å¤±è´¥', |
| | | icon: 'error' |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // é¢è§è§é¢ |
| | | const previewVideo = (url) => { |
| | | uni.previewVideo({ |
| | | sources: [{ |
| | | src: url |
| | | }], |
| | | fail: (err) => { |
| | | console.error('è§é¢é¢è§å¤±è´¥:', err) |
| | | uni.showToast({ |
| | | title: 'è§é¢é¢è§å¤±è´¥', |
| | | icon: 'error' |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // 表åå
³éæ¹æ³ |
| | | const cancel = () => { |
| | | dialogVisitable.value = false |
| | | // éç½®æ°æ® |
| | | beforeProductionImgs.value = [] |
| | | beforeProductionVideos.value = [] |
| | | } |
| | | |
| | | // å¤çæ¯ä¸ç±»æ°æ®ï¼å离å¾çåè§é¢ |
| | | function processItems(items) { |
| | | if (!items || !Array.isArray(items)) { |
| | | return { images: [], videos: [] } |
| | | } |
| | | |
| | | 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"> |
| | | .popup-content { |
| | | height: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .popup-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 20px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .popup-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | .upload-container { |
| | | flex: 1; |
| | | padding: 20px; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | .form-container { |
| | | width: 100%; |
| | | } |
| | | |
| | | .title { |
| | | font-size: 16px; |
| | | color: #1890ff; |
| | | line-height: 24px; |
| | | font-weight: 600; |
| | | padding-left: 12px; |
| | | position: relative; |
| | | margin: 0 0 20px 0; |
| | | |
| | | &::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 0; |
| | | top: 4px; |
| | | width: 4px; |
| | | height: 16px; |
| | | background-color: #1890ff; |
| | | border-radius: 2px; |
| | | } |
| | | } |
| | | |
| | | .media-section { |
| | | margin-bottom: 30px; |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 14px; |
| | | color: #666; |
| | | margin-bottom: 15px; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .image-grid { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .image-item { |
| | | width: 100px; |
| | | height: 100px; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .image-preview { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | .video-grid { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 15px; |
| | | } |
| | | |
| | | .video-item { |
| | | width: 160px; |
| | | height: 90px; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .video-preview { |
| | | width: 100%; |
| | | height: 100%; |
| | | background-color: #333; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | position: relative; |
| | | } |
| | | |
| | | .video-tip { |
| | | position: absolute; |
| | | bottom: 5px; |
| | | left: 50%; |
| | | transform: translateX(-50%); |
| | | font-size: 12px; |
| | | color: #fff; |
| | | background-color: rgba(0, 0, 0, 0.6); |
| | | padding: 2px 8px; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .empty-state { |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | height: 200px; |
| | | } |
| | | </style> |
| | |
| | | // baseUrl: 'https://vue.ruoyi.vip/prod-api', |
| | | // baseUrl: 'http://localhost/prod-api', |
| | | baseUrl: 'http://114.132.189.42:9036', |
| | | // baseUrl: 'http://192.168.1.147:9036', |
| | | //cloudåå°ç½å
³å°å |
| | | // baseUrl: 'http://192.168.10.3:8080', |
| | | // åºç¨ä¿¡æ¯ |
| | |
| | | "navigationBarTitleText": "å·¡æ£ä¸ä¼ ", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/equipmentManagement/faultAnalysis/index", |
| | | "style": { |
| | | "navigationBarTitleText": "æ
éåæè¿½æº¯", |
| | | "navigationStyle": "custom" |
| | | } |
| | | } |
| | | ], |
| | | "subPackages": [ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="fault-analysis-page"> |
| | | <!-- 页é¢å¤´é¨ --> |
| | | <PageHeader title="æ
éåæè¿½æº¯" @back="goBack" /> |
| | | |
| | | <!-- ç»è®¡æ¦è§ --> |
| | | <view class="overview-section"> |
| | | <view class="section-item"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"> |
| | | <up-icon name="file-text" size="16" color="#ffffff"></up-icon> |
| | | </view> |
| | | <text class="item-id">ç»è®¡æ¦è§</text> |
| | | </view> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | |
| | | <view class="overview-content"> |
| | | <view class="overview-item"> |
| | | <view class="overview-number">{{ overviewData.totalFaults }}</view> |
| | | <view class="overview-label">æ
éæ»æ°</view> |
| | | </view> |
| | | <view class="overview-item"> |
| | | <view class="overview-number">{{ overviewData.totalDowntime }}</view> |
| | | <view class="overview-label">æ»åæºæ¶é¿(h)</view> |
| | | </view> |
| | | <view class="overview-item"> |
| | | <view class="overview-number">{{ overviewData.avgRepairTime }}</view> |
| | | <view class="overview-label">å¹³åä¿®å¤æ¶é´(h)</view> |
| | | </view> |
| | | <view class="overview-item"> |
| | | <view class="overview-number">{{ overviewData.faultRate }}%</view> |
| | | <view class="overview-label">æ
éç</view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- æ
éç±»åç»è®¡ --> |
| | | <view class="stat-section"> |
| | | <view class="section-item"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"> |
| | | <up-icon name="file-text" size="16" color="#ffffff"></up-icon> |
| | | </view> |
| | | <text class="item-id">æ
éç±»åç»è®¡</text> |
| | | </view> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | |
| | | <view class="item-details"> |
| | | <view class="detail-row" v-for="(item, index) in faultTypeStats" :key="index"> |
| | | <text class="detail-label">{{ item.name }}</text> |
| | | <text class="detail-value">{{ item.value }}次</text> |
| | | <text class="detail-value highlight">{{ item.percent }}%</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- æ ¹å åæç»è®¡ --> |
| | | <view class="stat-section"> |
| | | <view class="section-item"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"> |
| | | <up-icon name="search" size="16" color="#ffffff"></up-icon> |
| | | </view> |
| | | <text class="item-id">æ ¹å åæç»è®¡</text> |
| | | </view> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | |
| | | <view class="item-details"> |
| | | <view class="detail-row" v-for="(item, index) in rootCauseStats" :key="index"> |
| | | <text class="detail-label">{{ item.name }}</text> |
| | | <text class="detail-value">{{ item.value }}次</text> |
| | | <text class="detail-value highlight">{{ item.percent }}%</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- è¯¦ç»æ°æ®å表 --> |
| | | <view class="table-section"> |
| | | <view class="section-item"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"> |
| | | <up-icon name="list" size="16" color="#ffffff"></up-icon> |
| | | </view> |
| | | <text class="item-id">è¯¦ç»æ°æ®</text> |
| | | </view> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | |
| | | <view class="fault-list"> |
| | | <view v-for="(item, index) in tableData" :key="index" class="fault-item"> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">设å¤åç§°</text> |
| | | <text class="detail-value">{{ item.equipmentName }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æ
éç±»å</text> |
| | | <view class="detail-value"> |
| | | <u-tag :type="getFaultTypeTagType(item.faultType)" size="small"> |
| | | {{ item.faultType }} |
| | | </u-tag> |
| | | </view> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">æ ¹å </text> |
| | | <text class="detail-value">{{ item.rootCause }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åæºæ¶é¿</text> |
| | | <text class="detail-value highlight">{{ item.downtime }}h</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">çç»</text> |
| | | <text class="detail-value">{{ item.team }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">åçæ¶é´</text> |
| | | <text class="detail-value">{{ item.occurTime }}</text> |
| | | </view> |
| | | </view> |
| | | <up-divider v-if="index < tableData.length - 1"></up-divider> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted } from 'vue' |
| | | import PageHeader from '@/components/PageHeader.vue' |
| | | |
| | | // ç»è®¡æ¦è§æ°æ® |
| | | const overviewData = ref({ |
| | | totalFaults: 156, |
| | | totalDowntime: 1248.5, |
| | | avgRepairTime: 8.0, |
| | | faultRate: 3.2 |
| | | }) |
| | | |
| | | // æ
éç±»åç»è®¡ |
| | | const faultTypeStats = ref([ |
| | | { name: 'æºæ¢°æ
é', value: 45, percent: 28.8 }, |
| | | { name: 'çµæ°æ
é', value: 32, percent: 20.5 }, |
| | | { name: 'æ¶²åæ
é', value: 28, percent: 17.9 }, |
| | | { name: 'æ°å¨æ
é', value: 25, percent: 16.0 }, |
| | | { name: 'å
¶ä»æ
é', value: 26, percent: 16.7 } |
| | | ]) |
| | | |
| | | // æ ¹å åæç»è®¡ |
| | | const rootCauseStats = ref([ |
| | | { name: 'æä½ä¸å½', value: 35, percent: 22.4 }, |
| | | { name: '设å¤èå', value: 28, percent: 17.9 }, |
| | | { name: 'ç»´æ¤ä¸è¶³', value: 22, percent: 14.1 }, |
| | | { name: 'ç¯å¢å ç´ ', value: 18, percent: 11.5 }, |
| | | { name: '设计缺é·', value: 15, percent: 9.6 }, |
| | | { name: 'å
¶ä»åå ', value: 38, percent: 24.4 } |
| | | ]) |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | const tableData = ref([ |
| | | { |
| | | equipmentName: 'ç产线A-01', |
| | | faultType: 'æºæ¢°æ
é', |
| | | rootCause: 'è½´æ¿ç£¨æ', |
| | | downtime: 12.5, |
| | | team: 'ç产ä¸ç', |
| | | occurTime: '2024-01-15 14:30' |
| | | }, |
| | | { |
| | | equipmentName: 'æ£æµè®¾å¤B-02', |
| | | faultType: 'çµæ°æ
é', |
| | | rootCause: 'çµè·¯çè·¯', |
| | | downtime: 8.0, |
| | | team: 'ç产äºç', |
| | | occurTime: '2024-01-16 09:15' |
| | | }, |
| | | { |
| | | equipmentName: 'è¾
å©è®¾å¤C-03', |
| | | faultType: 'æ¶²åæ
é', |
| | | rootCause: 'æ²¹ç®¡æ³æ¼', |
| | | downtime: 6.5, |
| | | team: 'ç»´ä¿®ç', |
| | | occurTime: '2024-01-17 16:45' |
| | | }, |
| | | { |
| | | equipmentName: 'ç产线A-02', |
| | | faultType: 'æ°å¨æ
é', |
| | | rootCause: 'æ°åä¸è¶³', |
| | | downtime: 4.0, |
| | | team: 'ç产ä¸ç', |
| | | occurTime: '2024-01-18 11:20' |
| | | }, |
| | | { |
| | | equipmentName: 'æ£æµè®¾å¤B-01', |
| | | faultType: 'æºæ¢°æ
é', |
| | | rootCause: 'ä¼ å¨å¸¦æè£', |
| | | downtime: 15.0, |
| | | team: 'ç产äºç', |
| | | occurTime: '2024-01-19 08:30' |
| | | } |
| | | ]) |
| | | |
| | | // æ¹æ³ |
| | | const goBack = () => { |
| | | uni.navigateBack() |
| | | } |
| | | |
| | | const getFaultTypeTagType = (faultType) => { |
| | | const typeMap = { |
| | | 'æºæ¢°æ
é': 'error', |
| | | 'çµæ°æ
é': 'warning', |
| | | 'æ¶²åæ
é': 'info', |
| | | 'æ°å¨æ
é': 'success', |
| | | 'å
¶ä»æ
é': 'primary' |
| | | } |
| | | return typeMap[faultType] || 'primary' |
| | | } |
| | | |
| | | // çå½å¨æ |
| | | onMounted(() => { |
| | | console.log('æ
éåæé¡µé¢å·²å è½½') |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .fault-analysis-page { |
| | | background-color: #f5f5f5; |
| | | min-height: 100vh; |
| | | } |
| | | |
| | | .overview-section, |
| | | .stat-section, |
| | | .table-section { |
| | | margin: 15px; |
| | | } |
| | | |
| | | .section-item { |
| | | background: #ffffff; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .item-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 15px; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | } |
| | | |
| | | .item-left { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .document-icon { |
| | | width: 28px; |
| | | height: 28px; |
| | | background: rgba(255, 255, 255, 0.2); |
| | | border-radius: 6px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-right: 10px; |
| | | } |
| | | |
| | | .item-id { |
| | | color: #ffffff; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .overview-content { |
| | | display: flex; |
| | | justify-content: space-around; |
| | | padding: 20px 15px; |
| | | } |
| | | |
| | | .overview-item { |
| | | text-align: center; |
| | | flex: 1; |
| | | } |
| | | |
| | | .overview-number { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: #007aff; |
| | | margin-bottom: 5px; |
| | | } |
| | | |
| | | .overview-label { |
| | | font-size: 12px; |
| | | color: #666; |
| | | } |
| | | |
| | | .item-details { |
| | | padding: 15px; |
| | | } |
| | | |
| | | .detail-row { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 12px 0; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .detail-row:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .detail-label { |
| | | font-size: 14px; |
| | | color: #666; |
| | | min-width: 80px; |
| | | } |
| | | |
| | | .detail-value { |
| | | font-size: 14px; |
| | | color: #333; |
| | | font-weight: 500; |
| | | text-align: right; |
| | | } |
| | | |
| | | .detail-value.highlight { |
| | | color: #007aff; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | .fault-list { |
| | | padding: 15px; |
| | | } |
| | | |
| | | .fault-item { |
| | | margin-bottom: 15px; |
| | | } |
| | | |
| | | .fault-item:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | </style> |
| | |
| | | // ç¼è¾æ¨¡å¼ï¼è·å详æ
|
| | | loadForm(id); |
| | | // å¯éï¼è·å忏
é¤åå¨çidï¼é¿å
å½±ååç»æä½ |
| | | // uni.removeStorageSync('repairId'); |
| | | uni.removeStorageSync('repairId'); |
| | | } else { |
| | | // æ°å¢æ¨¡å¼ |
| | | loadForm(); |
| | |
| | | label: '设å¤ä¿å
»', |
| | | }, |
| | | { |
| | | icon: '/static/images/icon/shebeixunjian@2x.png', |
| | | icon: '/static/images/icon/xunjianshangchuan@2x.png', |
| | | label: 'å·¡æ£ä¸ä¼ ', |
| | | }, |
| | | { |
| | | icon: 'flash', |
| | | icon: '/static/images/icon/guzhangfenxi@2x.png', |
| | | label: 'åæè¿½æº¯', |
| | | bgColor: '#ff9800' |
| | | }, |
| | | { |
| | | icon: '/static/images/icon/zhinengpaidan@2x.png', |
| | | label: 'æºè½æ´¾å', |
| | | bgColor: '#ff6b35' |
| | | }, |
| | | { |
| | | icon: 'file-text', |
| | | icon: '/static/images/icon/zuoyezhidao@2x.png', |
| | | label: 'ä½ä¸æå¯¼', |
| | | bgColor: '#4caf50' |
| | | }, |
| | | { |
| | | icon: 'checkmark-circle', |
| | | icon: '/static/images/icon/jieguoyanzheng@2x.png', |
| | | label: 'ç»æéªè¯', |
| | | bgColor: '#9c27b0' |
| | | } |
| | |
| | | url: '/pages/inspectionUpload/index' |
| | | }); |
| | | break; |
| | | case 'åæè¿½æº¯': |
| | | uni.navigateTo({ |
| | | url: '/pages/equipmentManagement/faultAnalysis/index' |
| | | }); |
| | | break; |
| | | case 'æºè½æ´¾å': |
| | | uni.navigateTo({ |
| | | url: '/pages/equipmentManagement/smartDispatch/index' |
| | |
| | | |
| | | <u-form-item label="éä»¶" prop="storageBlobDTO" labelWidth="80"> |
| | | <view class="upload-container"> |
| | | <u-upload |
| | | :fileList="form.storageBlobDTO" |
| | | @afterRead="afterRead" |
| | | @delete="deleteFile" |
| | | name="files" |
| | | multiple |
| | | :maxCount="10" |
| | | :maxSize="50 * 1024 * 1024" |
| | | accept="image/*,video/*" |
| | | :previewFullImage="true" |
| | | :camera="true" |
| | | :gallery="true" |
| | | ></u-upload> |
| | | <view class="upload-actions"> |
| | | <u-button |
| | | type="primary" |
| | | size="small" |
| | | @click="chooseImage" |
| | | :customStyle="{ marginRight: '10px' }" |
| | | > |
| | | æç
§ |
| | | </u-button> |
| | | <u-button |
| | | type="success" |
| | | size="small" |
| | | @click="chooseVideo" |
| | | > |
| | | å½å |
| | | </u-button> |
| | | </view> |
| | | <ImageUpload |
| | | v-model="form.storageBlobDTO" |
| | | :limit="10" |
| | | :fileSize="50" |
| | | :fileType="['jpg', 'jpeg', 'png', 'mp4', 'mov']" |
| | | :maxVideoDuration="60" |
| | | :statusType="0" |
| | | @update:modelValue="handleStorageBlobUpdate" |
| | | /> |
| | | </view> |
| | | </u-form-item> |
| | | |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { reactive, ref, onMounted, nextTick } from 'vue' |
| | | import { reactive, ref, onMounted, onUnmounted, nextTick } from 'vue' |
| | | import { addOrEditQrCodeRecord } from '@/api/inspectionUpload/index.js' |
| | | import useUserStore from '@/store/modules/user.ts' |
| | | import ImageUpload from '@/components/imageUpload/index.vue' |
| | | |
| | | const emit = defineEmits(['closeDia']) |
| | | |
| | |
| | | const userStore = useUserStore() |
| | | const userInfo = ref({}) |
| | | const locationLoading = ref(false) |
| | | |
| | | // 请æ±åæ¶æ å¿ |
| | | let isRequestCancelled = false |
| | | |
| | | // è·åå½åæ¶é´ |
| | | function getCurrentDateTime() { |
| | |
| | | onMounted(async () => { |
| | | try { |
| | | const res = await userStore.getInfo() |
| | | userInfo.value = res.user |
| | | form.scannerName = userInfo.value.nickName |
| | | form.scannerId = userInfo.value.userId |
| | | form.scanTime = getCurrentDateTime() |
| | | // æ£æ¥ç»ä»¶æ¯å¦è¿åå¨ |
| | | if (!isRequestCancelled && userInfo.value !== undefined) { |
| | | userInfo.value = res.user |
| | | form.scannerName = userInfo.value.nickName |
| | | form.scannerId = userInfo.value.userId |
| | | form.scanTime = getCurrentDateTime() |
| | | } |
| | | } catch (error) { |
| | | console.error('è·åç¨æ·ä¿¡æ¯å¤±è´¥:', error) |
| | | } |
| | | }) |
| | | |
| | | // æä»¶ä¸ä¼ å¤ç |
| | | const afterRead = (event) => { |
| | | const { file } = event |
| | | console.log('æä»¶éæ©:', file) |
| | | |
| | | // ç´æ¥æ·»å å°æä»¶å表ï¼ä¸ä¸ä¼ å°æå¡å¨ |
| | | const fileItem = { |
| | | url: file.url, |
| | | name: file.name || `æä»¶_${Date.now()}`, |
| | | status: 'success', |
| | | size: file.size || 0, |
| | | type: file.type || 'image/jpeg' |
| | | } |
| | | |
| | | form.storageBlobDTO.push(fileItem) |
| | | |
| | | uni.showToast({ |
| | | title: 'æä»¶æ·»å æå', |
| | | icon: 'success' |
| | | }) |
| | | } |
| | | |
| | | // æç
§ |
| | | const chooseImage = () => { |
| | | uni.chooseImage({ |
| | | count: 1, |
| | | sizeType: ['original', 'compressed'], |
| | | sourceType: ['camera'], |
| | | success: (res) => { |
| | | console.log('æç
§æå:', res) |
| | | const tempFilePath = res.tempFilePaths[0] |
| | | |
| | | const fileItem = { |
| | | url: tempFilePath, |
| | | name: `ç
§ç_${Date.now()}.jpg`, |
| | | status: 'success', |
| | | type: 'image/jpeg' |
| | | } |
| | | |
| | | form.storageBlobDTO.push(fileItem) |
| | | |
| | | uni.showToast({ |
| | | title: 'æç
§æå', |
| | | icon: 'success' |
| | | }) |
| | | }, |
| | | fail: (err) => { |
| | | console.error('æç
§å¤±è´¥:', err) |
| | | uni.showToast({ |
| | | title: 'æç
§å¤±è´¥', |
| | | icon: 'error' |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // å½å |
| | | const chooseVideo = () => { |
| | | uni.chooseVideo({ |
| | | sourceType: ['camera'], |
| | | maxDuration: 60, // æå¤§60ç§ |
| | | camera: 'back', |
| | | success: (res) => { |
| | | console.log('å½åæå:', res) |
| | | const tempFilePath = res.tempFilePath |
| | | |
| | | const fileItem = { |
| | | url: tempFilePath, |
| | | name: `è§é¢_${Date.now()}.mp4`, |
| | | status: 'success', |
| | | type: 'video/mp4', |
| | | duration: res.duration, |
| | | size: res.size |
| | | } |
| | | |
| | | form.storageBlobDTO.push(fileItem) |
| | | |
| | | uni.showToast({ |
| | | title: 'å½åæå', |
| | | icon: 'success' |
| | | }) |
| | | }, |
| | | fail: (err) => { |
| | | console.error('å½å失败:', err) |
| | | uni.showToast({ |
| | | title: 'å½å失败', |
| | | icon: 'error' |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // å 餿件 |
| | | const deleteFile = (event) => { |
| | | const { index } = event |
| | | form.storageBlobDTO.splice(index, 1) |
| | | // å¤çstorageBlobDTOæ°æ®æ´æ° |
| | | const handleStorageBlobUpdate = (value) => { |
| | | form.storageBlobDTO = value || [] |
| | | } |
| | | |
| | | // è·åå½åä½ç½® |
| | | const getCurrentLocation = () => { |
| | | // æ£æ¥ç»ä»¶æ¯å¦è¿åå¨ |
| | | if (isRequestCancelled) return |
| | | |
| | | locationLoading.value = true |
| | | uni.showLoading({ title: 'è·åä½ç½®ä¸...' }) |
| | | |
| | | uni.getLocation({ |
| | | type: 'gcj02', |
| | | success: (res) => { |
| | | // æ£æ¥ç»ä»¶æ¯å¦è¿åå¨ |
| | | if (isRequestCancelled) { |
| | | uni.hideLoading() |
| | | return |
| | | } |
| | | |
| | | // 使ç¨éå°çç¼ç è·åå°åä¿¡æ¯ |
| | | uni.request({ |
| | | url: `https://restapi.amap.com/v3/geocode/regeo?key=c120a5dc69a9f61839f7763e6057005f&location=${res.longitude},${res.latitude}&radius=1000&extensions=all`, |
| | | success: (geoRes) => { |
| | | // æ£æ¥ç»ä»¶æ¯å¦è¿åå¨ |
| | | if (isRequestCancelled) { |
| | | uni.hideLoading() |
| | | return |
| | | } |
| | | |
| | | uni.hideLoading() |
| | | locationLoading.value = false |
| | | |
| | |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | // æ£æ¥ç»ä»¶æ¯å¦è¿åå¨ |
| | | if (isRequestCancelled) { |
| | | uni.hideLoading() |
| | | return |
| | | } |
| | | |
| | | uni.hideLoading() |
| | | locationLoading.value = false |
| | | console.error('éå°çç¼ç 失败:', err) |
| | |
| | | }) |
| | | }, |
| | | fail: (err) => { |
| | | // æ£æ¥ç»ä»¶æ¯å¦è¿åå¨ |
| | | if (isRequestCancelled) { |
| | | uni.hideLoading() |
| | | return |
| | | } |
| | | |
| | | uni.hideLoading() |
| | | locationLoading.value = false |
| | | uni.showToast({ |
| | |
| | | // æå¼å¼¹æ¡ |
| | | const openDialog = async (row) => { |
| | | console.log('å¼¹æ¡æ¥æ¶å°çæ°æ®:', row) |
| | | console.log('å¼¹æ¡æå¼åç¶æ:', dialogVisitable.value) |
| | | |
| | | dialogVisitable.value = true |
| | | form.deviceName = row.deviceName || '' |
| | | form.location = row.location || '' |
| | | form.qrCodeId = row.qrCodeId || row.id || '' |
| | | form.qrCodeId = row.qrCodeId |
| | | form.storageBlobDTO = [] |
| | | |
| | | console.log('å¼¹æ¡æå¼åç¶æ:', dialogVisitable.value) |
| | | console.log('å¼¹æ¡è¡¨åæ°æ®:', form) |
| | | |
| | | // å¼ºå¶æ´æ°è§å¾ |
| | |
| | | // æäº¤è¡¨å |
| | | const submitForm = async () => { |
| | | try { |
| | | // æ£æ¥ç»ä»¶æ¯å¦è¿åå¨ |
| | | if (isRequestCancelled) return |
| | | |
| | | console.log('å¼å§æäº¤è¡¨åï¼å½åè¡¨åæ°æ®:', form) |
| | | |
| | | // 表åéªè¯ |
| | |
| | | scannerName: form.scannerName, |
| | | scannerId: form.scannerId, |
| | | scanTime: form.scanTime, |
| | | storageBlobDTO: form.storageBlobDTO, |
| | | storageBlobDTO: form.storageBlobDTO.map(file => ({ |
| | | id: file.id, // æ·»å idåæ®µ |
| | | url: file.url, |
| | | bucketFilename: file.bucketFilename || file.name, |
| | | downloadUrl: file.downloadUrl || file.url, |
| | | type: 0, |
| | | size: file.size, |
| | | createTime: file.createTime || new Date().getTime() |
| | | })), |
| | | qrCode: { |
| | | id: form.qrCodeId || form.qrCode.id |
| | | id: form.qrCodeId |
| | | } |
| | | } |
| | | |
| | | console.log('åå¤æäº¤çæ°æ®:', submitData) |
| | | |
| | | const response = await addOrEditQrCodeRecord(submitData) |
| | | |
| | | // æ£æ¥ç»ä»¶æ¯å¦è¿åå¨ |
| | | if (isRequestCancelled) return |
| | | |
| | | console.log('æäº¤ååº:', response) |
| | | |
| | | uni.showToast({ |
| | |
| | | |
| | | cancel() |
| | | } catch (error) { |
| | | // æ£æ¥ç»ä»¶æ¯å¦è¿åå¨ |
| | | if (isRequestCancelled) return |
| | | |
| | | console.error('æäº¤å¤±è´¥:', error) |
| | | |
| | | // æ¾ç¤ºæ´è¯¦ç»çéè¯¯ä¿¡æ¯ |
| | |
| | | dialogVisitable.value = false |
| | | emit('closeDia') |
| | | } |
| | | |
| | | // ç»ä»¶éæ¯æ¶çæ¸
ç |
| | | onUnmounted(() => { |
| | | // è®¾ç½®åæ¶æ å¿ï¼é»æ¢åç»ç弿¥æä½ |
| | | isRequestCancelled = true |
| | | |
| | | // æ¸
çç¶æ |
| | | if (locationLoading.value) { |
| | | locationLoading.value = false |
| | | } |
| | | |
| | | // å
³éå¼¹çª |
| | | if (dialogVisitable.value) { |
| | | dialogVisitable.value = false |
| | | } |
| | | |
| | | // éèå¯è½æ¾ç¤ºçå è½½æç¤º |
| | | uni.hideLoading() |
| | | }) |
| | | |
| | | defineExpose({ openDialog }) |
| | | </script> |
| | |
| | | |
| | | .upload-container { |
| | | width: 100%; |
| | | } |
| | | |
| | | .upload-actions { |
| | | display: flex; |
| | | justify-content: flex-start; |
| | | margin-top: 10px; |
| | | gap: 10px; |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <view class="inspection-upload-page"> |
| | | <!-- 页é¢å¤´é¨ --> |
| | | <PageHeader title="å·¡æ£ä¸ä¼ " /> |
| | | |
| | | <!-- æ ç¾é¡µ --> |
| | | <view class="tabs-container"> |
| | | <view class="custom-tabs"> |
| | | <view |
| | | v-for="(tab, index) in tabs" |
| | | :key="index" |
| | | class="tab-item" |
| | | :class="{ 'tab-active': currentTabIndex === index }" |
| | | @click="handleTabChange(index)" |
| | | > |
| | | {{ tab.name }} |
| | | </view> |
| | | <view class="tab-line" :style="{ left: currentTabIndex * 50 + '%' }"></view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- æ«ç 模å --> |
| | | <view v-if="activeTab === 'qrCode'" class="scan-section"> |
| | | <view class="scan-controls"> |
| | | <u-button |
| | | :type="isScanning ? 'error' : 'primary'" |
| | | :loading="scanLoading" |
| | | @click="toggleScan" |
| | | > |
| | | {{ scanButtonText }} |
| | | </u-button> |
| | | </view> |
| | | |
| | | <!-- æ«ç åºå --> |
| | | <view v-show="isScanning" class="qr-scan-container"> |
| | | <camera |
| | | class="qr-camera" |
| | | device-position="back" |
| | | flash="off" |
| | | @scancode="handleScanCode" |
| | | @error="handleCameraError" |
| | | ></camera> |
| | | <view class="scan-overlay"> |
| | | <view class="scan-frame"></view> |
| | | <view class="scan-tip">请å°äºç»´ç æ¾å
¥æ¡å
</view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- ç¶ææç¤º --> |
| | | <view class="status-info"> |
| | | <u-alert |
| | | v-if="cameraError" |
| | | :title="cameraError" |
| | | type="error" |
| | | :showIcon="true" |
| | | :closable="true" |
| | | @close="cameraError = ''" |
| | | ></u-alert> |
| | | <view v-if="isScanning" class="scanning-text"> |
| | | <u-loading-icon mode="circle" color="#1890ff" size="20"></u-loading-icon> |
| | | <text class="scanning-label">æ£å¨æ«æäºç»´ç ...</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <PageHeader title="å·¡æ£ä¸ä¼ " @back="goBack"/> |
| | | |
| | | <!-- æ°æ®å表 --> |
| | | <view class="table-section"> |
| | | <!-- ç产巡æ£å表 --> |
| | | <view v-if="activeTab === 'task'" class="task-list"> |
| | | <view class="task-list"> |
| | | <view |
| | | v-for="(item, index) in tableData" |
| | | v-for="(item, index) in taskTableData" |
| | | :key="index" |
| | | class="task-item" |
| | | @click="handleAdd(item)" |
| | |
| | | > |
| | | ä¸ä¼ |
| | | </u-button> |
| | | </view> |
| | | </view> |
| | | <view class="task-details"> |
| | | <view class="detail-item"> |
| | | <text class="detail-label">夿³¨ï¼</text> |
| | | <text class="detail-value">{{ item.remarks || 'æ ' }}</text> |
| | | </view> |
| | | <view class="detail-item"> |
| | | <text class="detail-label">æ§è¡äººï¼</text> |
| | | <text class="detail-value">{{ item.inspector }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- ç°åºå·¡æ£å表 --> |
| | | <view v-if="activeTab === 'qrCode'" class="qr-list"> |
| | | <view |
| | | v-for="(item, index) in tableData" |
| | | :key="index" |
| | | class="qr-item" |
| | | @click="viewFile(item)" |
| | | > |
| | | <view class="qr-header"> |
| | | <view class="qr-info"> |
| | | <text class="device-name">{{ item.qrCode?.deviceName }}</text> |
| | | <text class="device-location">{{ item.qrCode?.location }}</text> |
| | | </view> |
| | | <view class="qr-actions"> |
| | | <u-button |
| | | type="primary" |
| | | type="info" |
| | | size="small" |
| | | @click.stop="viewFile(item)" |
| | | @click.stop="startScanForTask(item)" |
| | | :customStyle="{ |
| | | borderRadius: '15px', |
| | | height: '30px', |
| | | fontSize: '12px' |
| | | }" |
| | | > |
| | | æ¥çéä»¶ |
| | | æ«ç |
| | | </u-button> |
| | | </view> |
| | | </view> |
| | | <view class="qr-details"> |
| | | <view class="task-details"> |
| | | <view class="detail-item"> |
| | | <text class="detail-label">å·¡æ£äººï¼</text> |
| | | <text class="detail-value">{{ item.scanner }}</text> |
| | | <text class="detail-label">夿³¨</text> |
| | | <text class="detail-value">{{ item.remarks || 'æ ' }}</text> |
| | | </view> |
| | | <view class="detail-item"> |
| | | <text class="detail-label">å·¡æ£æ¶é´ï¼</text> |
| | | <text class="detail-value">{{ item.scanTime }}</text> |
| | | <text class="detail-label">æ§è¡äºº</text> |
| | | <text class="detail-value">{{ item.inspector }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- ç©ºç¶æ --> |
| | | <view v-if="tableData.length === 0 && !tableLoading" class="empty-state"> |
| | | <u-empty |
| | | mode="data" |
| | | text="ææ æ°æ®" |
| | | :iconSize="80" |
| | | ></u-empty> |
| | | </view> |
| | | |
| | | <!-- å è½½ç¶æ --> |
| | | <view v-if="tableLoading" class="loading-state"> |
| | | <u-loading-icon mode="circle" color="#1890ff" size="40"></u-loading-icon> |
| | | <text class="loading-text">å è½½ä¸...</text> |
| | | <view v-if="taskTableData.length === 0" class="no-data"> |
| | | <text>ææ æ°æ®</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- å页 --> |
| | | <view v-if="total > 0" class="pagination-container"> |
| | | <u-pagination |
| | | :total="total" |
| | | :current="pageNum" |
| | | :pageSize="pageSize" |
| | | @change="handlePageChange" |
| | | :showTotal="true" |
| | | :showSizer="false" |
| | | :showJumper="false" |
| | | ></u-pagination> |
| | | <!-- æ«ç åºå - å
¨å±å¼¹çª --> |
| | | <view v-if="isScanning" class="qr-scan-overlay"> |
| | | <view class="qr-scan-container"> |
| | | <view class="scan-header"> |
| | | <text class="scan-title">æ«æäºç»´ç </text> |
| | | <u-button |
| | | type="error" |
| | | size="small" |
| | | @click.stop="stopScan" |
| | | :customStyle="{ |
| | | borderRadius: '15px', |
| | | height: '30px', |
| | | fontSize: '12px' |
| | | }" |
| | | > |
| | | å
³é |
| | | </u-button> |
| | | </view> |
| | | <camera |
| | | class="qr-camera" |
| | | device-position="back" |
| | | flash="off" |
| | | @scancode="handleScanCode" |
| | | @error="handleCameraError" |
| | | ></camera> |
| | | <view class="scan-frame-wrapper"> |
| | | <view class="scan-frame"></view> |
| | | <view class="scan-tip">请å°äºç»´ç æ¾å
¥æ¡å
</view> |
| | | </view> |
| | | <u-alert |
| | | v-if="cameraError" |
| | | :title="cameraError" |
| | | type="error" |
| | | :showIcon="true" |
| | | :closable="true" |
| | | @close="cameraError = ''" |
| | | :customStyle="{ |
| | | margin: '10px 0' |
| | | }" |
| | | ></u-alert> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- å¼¹çªç»ä»¶ --> |
| | | <form-dia ref="formDia" @closeDia="handleQuery"></form-dia> |
| | | <qr-code-form-dia ref="qrCodeFormDia" @closeDia="handleQuery"></qr-code-form-dia> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { onMounted, ref, reactive, computed, nextTick } from 'vue' |
| | | import { onMounted, onUnmounted, ref, nextTick } from 'vue' |
| | | import { onShow } from '@dcloudio/uni-app' |
| | | import PageHeader from '@/components/PageHeader.vue' |
| | | import FormDia from './components/formDia.vue' |
| | | import QrCodeFormDia from './components/qrCodeFormDia.vue' |
| | | import { qrCodeScanRecordList } from '@/api/inspectionUpload/index.js' |
| | | import { getInspectionTaskList } from '@/api/equipmentManagement/inspection.js' |
| | | import { getLedgerById } from '@/api/equipmentManagement/ledger.js' |
| | | import {inspectionTaskList} from "@/api/inspectionManagement"; |
| | | // import ViewQrCodeFiles from '@/pages/inspectionManagement/components/viewQrCodeFiles.vue' |
| | | |
| | | // ç»ä»¶å¼ç¨ |
| | | const formDia = ref() |
| | | const qrCodeFormDia = ref() |
| | | |
| | | // å½åæ ç¾ |
| | | const activeTab = ref('task') |
| | | |
| | | const tabName = ref('task') |
| | | const currentTabIndex = ref(0) |
| | | |
| | | // æ ç¾é¡µæ°æ® |
| | | const tabs = reactive([ |
| | | { name: 'ç产巡æ£' }, |
| | | { name: 'ç°åºå·¡æ£' } |
| | | ]) |
| | | // å è½½æç¤ºæ¹æ³ |
| | | const showLoadingToast = (message) => { |
| | | uni.showLoading({ |
| | | title: message, |
| | | mask: true |
| | | }) |
| | | } |
| | | const closeToast = () => { |
| | | uni.hideLoading() |
| | | } |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | const tableData = ref([]) |
| | | const tableLoading = ref(false) |
| | | const total = ref(0) |
| | | const pageNum = ref(1) |
| | | const pageSize = ref(10) |
| | | const taskTableData = ref([]) // çäº§å·¡æ£æ°æ® |
| | | |
| | | // å½åæ«æçä»»å¡ |
| | | const currentScanningTask = ref(null) |
| | | |
| | | // 请æ±åæ¶æ å¿ï¼ç¨äºåæ¶æ£å¨è¿è¡çè¯·æ± |
| | | let isRequestCancelled = false |
| | | |
| | | // æ«ç ç¸å
³ç¶æ |
| | | const isScanning = ref(false) |
| | | const scanLoading = ref(false) |
| | | const cameraError = ref('') |
| | | |
| | | // 计ç®å±æ§ |
| | | const scanButtonText = computed(() => { |
| | | if (scanLoading.value) return 'æ£å¨åå§å...' |
| | | return isScanning.value ? '忢æ«ç ' : 'å¼å§æ«ç ' |
| | | }) |
| | | |
| | | // çå½å¨æ |
| | | onMounted(() => { |
| | | // å»¶è¿åå§åï¼ç¡®ä¿DOM已渲æ |
| | | nextTick(() => { |
| | | handleTabClick({ props: { name: 'task' } }) |
| | | getList() |
| | | }) |
| | | }) |
| | | |
| | |
| | | getList() |
| | | }) |
| | | |
| | | // æ ç¾é¡µåæ¢ |
| | | const handleTabChange = (index) => { |
| | | currentTabIndex.value = index |
| | | const tabNames = ['task', 'qrCode'] |
| | | activeTab.value = tabNames[index] |
| | | tabName.value = tabNames[index] |
| | | tableData.value = [] |
| | | pageNum.value = 1 |
| | | getList() |
| | | } |
| | | // ç»ä»¶éæ¯æ¶çæ¸
ç |
| | | onUnmounted(() => { |
| | | // è®¾ç½®åæ¶æ å¿ï¼é»æ¢åç»ç弿¥æä½ |
| | | isRequestCancelled = true |
| | | |
| | | // 忢æ«ç |
| | | if (isScanning.value) { |
| | | isScanning.value = false |
| | | } |
| | | }) |
| | | |
| | | // æ ç¾é¡µç¹å»ï¼å
¼å®¹æ§æ¹æ³ï¼ |
| | | const handleTabClick = (tab) => { |
| | | tabName.value = tab.props.name |
| | | activeTab.value = tab.props.name |
| | | tableData.value = [] |
| | | getList() |
| | | // è¿åä¸ä¸é¡µ |
| | | const goBack = () => { |
| | | uni.navigateBack() |
| | | } |
| | | |
| | | // æ¥è¯¢æ°æ® |
| | | const handleQuery = () => { |
| | | pageNum.value = 1 |
| | | pageSize.value = 10 |
| | | getList() |
| | | } |
| | | |
| | | // è·ååè¡¨æ°æ® |
| | | const getList = () => { |
| | | tableLoading.value = true |
| | | if (tabName.value === "task") { |
| | | inspectionTaskList({size: pageSize.value, current: pageNum.value}).then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records; |
| | | total.value = res.data.total; |
| | | }) |
| | | } else { |
| | | qrCodeScanRecordList({size: pageSize.value, current: pageNum.value}).then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records; |
| | | total.value = res.data.total; |
| | | }) |
| | | } |
| | | // æ¾ç¤ºå è½½æç¤º |
| | | showLoadingToast('å è½½ä¸...') |
| | | |
| | | // è®¾ç½®åæ¶æ å¿ |
| | | isRequestCancelled = false |
| | | |
| | | inspectionTaskList({}).then(res => { |
| | | // æ£æ¥ç»ä»¶æ¯å¦è¿åå¨ä¸è¯·æ±æªè¢«åæ¶ |
| | | if (!isRequestCancelled) { |
| | | console.log('ç产巡æ£APIè¿åæ°æ®:', res); |
| | | |
| | | // å¤çä¸åçæ°æ®ç»æ |
| | | let records = []; |
| | | if (res && res.data) { |
| | | // å°è¯å¤ç§å¯è½çæ°æ®ç»æ |
| | | if (Array.isArray(res.data.records)) { |
| | | records = res.data.records; |
| | | } else if (Array.isArray(res.data.rows)) { |
| | | records = res.data.rows; |
| | | } else if (Array.isArray(res.data)) { |
| | | records = res.data; |
| | | } else if (Array.isArray(res.data.list)) { |
| | | records = res.data.list; |
| | | } |
| | | } |
| | | |
| | | if (records.length > 0) { |
| | | taskTableData.value = records; |
| | | console.log('çäº§å·¡æ£æ°æ®è®¾ç½®æåï¼è®°å½æ°:', records.length); |
| | | } else { |
| | | console.warn('çäº§å·¡æ£æ°æ®ä¸ºç©ºææ ¼å¼ä¸æ£ç¡®:', res); |
| | | taskTableData.value = []; |
| | | uni.showToast({ |
| | | title: 'ææ å·¡æ£ä»»å¡æ°æ®', |
| | | icon: 'none' |
| | | }); |
| | | } |
| | | } |
| | | // å
³éå è½½æç¤º |
| | | closeToast() |
| | | }).catch(err => { |
| | | // æ£æ¥ç»ä»¶æ¯å¦è¿åå¨ä¸è¯·æ±æªè¢«åæ¶ |
| | | if (!isRequestCancelled) { |
| | | console.error('è·åçäº§å·¡æ£æ°æ®å¤±è´¥:', err); |
| | | taskTableData.value = []; |
| | | // æ·»å é误æç¤º |
| | | uni.showToast({ |
| | | title: 'è·åæ°æ®å¤±è´¥', |
| | | icon: 'error' |
| | | }) |
| | | } |
| | | // å
³éå è½½æç¤º |
| | | closeToast() |
| | | }) |
| | | } |
| | | |
| | | // å页åå |
| | | const handlePageChange = (page) => { |
| | | pageNum.value = page |
| | | getList() |
| | | } |
| | | |
| | | // ä¸ä¼ |
| | | const handleAdd = (row) => { |
| | | nextTick(() => { |
| | | formDia.value?.openDialog(row) |
| | | // æ£æ¥ç»ä»¶æ¯å¦è¿åå¨ |
| | | if (formDia.value && formDia.value.openDialog) { |
| | | formDia.value.openDialog(row) |
| | | } else { |
| | | console.error('ä¸ä¼ ç»ä»¶å¼ç¨ä¸åå¨') |
| | | uni.showToast({ |
| | | title: 'ç»ä»¶æªåå¤å¥½', |
| | | icon: 'error' |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // æ¥çéä»¶ |
| | | const viewFile = (row) => { |
| | | console.log('æ¥çéä»¶:', row) |
| | | uni.showToast({ |
| | | title: 'æ¥çéä»¶åè½å¼åä¸', |
| | | icon: 'none' |
| | | }) |
| | | } |
| | | |
| | | // æ«ç ç¸å
³æ¹æ³ |
| | | const toggleScan = async () => { |
| | | if (isScanning.value) { |
| | | await stopScan() |
| | | } else { |
| | | await startScan() |
| | | } |
| | | } |
| | | |
| | | const startScan = async () => { |
| | | // 为æå®ä»»å¡å¼å§æ«ç |
| | | const startScanForTask = async (task) => { |
| | | try { |
| | | scanLoading.value = true |
| | | // è®°å½å½åæ«æçä»»å¡ |
| | | currentScanningTask.value = task |
| | | console.log('为任å¡å¼å§æ«ç :', task.taskName) |
| | | |
| | | // æ¾ç¤ºæ«æçé¢ |
| | | isScanning.value = true |
| | | |
| | | // 使ç¨uniappçæ«ç API |
| | | uni.scanCode({ |
| | | success: (res) => { |
| | |
| | | title: 'æ«ç 失败', |
| | | icon: 'error' |
| | | }) |
| | | // å
³éæ«æçé¢ |
| | | isScanning.value = false |
| | | }, |
| | | complete: () => { |
| | | scanLoading.value = false |
| | | // æ«ç 宿åå
³éæ«æçé¢ |
| | | setTimeout(() => { |
| | | isScanning.value = false |
| | | }, 1000) |
| | | } |
| | | }) |
| | | } catch (e) { |
| | | console.error('å¯å¨æ«ç 失败:', e) |
| | | scanLoading.value = false |
| | | uni.showToast({ |
| | | title: 'å¯å¨æ«ç 失败', |
| | | icon: 'error' |
| | | }) |
| | | isScanning.value = false |
| | | } |
| | | } |
| | | |
| | | const stopScan = async () => { |
| | | // 忢æ«ç |
| | | const stopScan = () => { |
| | | isScanning.value = false |
| | | currentScanningTask.value = null |
| | | } |
| | | |
| | | // æ«ç æåå¤ç |
| | | const handleScanSuccess = async (result) => { |
| | | try { |
| | | console.log('å¤çæ«ç ç»æ:', result) |
| | | console.log('å½åå
³èä»»å¡:', currentScanningTask.value?.taskName) |
| | | |
| | | uni.showToast({ |
| | | title: 'è¯å«æå', |
| | |
| | | |
| | | // è§£æäºç»´ç æ°æ® |
| | | let qrData |
| | | let deviceId = '' |
| | | |
| | | try { |
| | | qrData = JSON.parse(result.result) |
| | | console.log('è§£æçäºç»´ç æ°æ®:', qrData) |
| | | deviceId = qrData.deviceId || qrData.qrCodeId |
| | | } catch (e) { |
| | | // 妿䏿¯JSONæ ¼å¼ï¼ç´æ¥ä½¿ç¨æ«ç ç»æä½ä¸ºè®¾å¤åç§° |
| | | qrData = { |
| | | deviceName: result.result, |
| | | location: '', |
| | | qrCodeId: result.result // æ·»å äºç»´ç ID |
| | | // 妿䏿¯JSONæ ¼å¼ï¼å°è¯ä»URL䏿ådeviceId |
| | | const url = result.result |
| | | |
| | | if (url.includes('deviceId=')) { |
| | | // ä»URL䏿ådeviceId |
| | | const match = url.match(/deviceId=(\d+)/) |
| | | if (match && match[1]) { |
| | | deviceId = match[1] |
| | | } |
| | | } |
| | | console.log('使ç¨é»è®¤æ°æ®æ ¼å¼:', qrData) |
| | | |
| | | qrData = { |
| | | deviceName: deviceId ? `设å¤${deviceId}` : result.result, |
| | | location: '', |
| | | qrCodeId: deviceId // ä½¿ç¨æåçdeviceIdæåå§ç»æ |
| | | } |
| | | } |
| | | |
| | | // 妿æè®¾å¤IDï¼å°è¯ä»APIè·åçå®ç设å¤åç§° |
| | | if (deviceId) { |
| | | try { |
| | | console.log('æ£å¨æ¥è¯¢è®¾å¤ä¿¡æ¯ï¼è®¾å¤ID:', deviceId) |
| | | const response = await getLedgerById(deviceId) |
| | | console.log('设å¤ä¿¡æ¯æ¥è¯¢ç»æ:', response) |
| | | |
| | | if (response.code === 200 && response.data) { |
| | | qrData.deviceName = response.data.deviceName || `设å¤${deviceId}` |
| | | qrData.location = response.data.storageLocation || '' |
| | | console.log('è·åå°è®¾å¤åç§°:', qrData.deviceName) |
| | | } else { |
| | | console.warn('设å¤ä¿¡æ¯æ¥è¯¢å¤±è´¥ï¼ä½¿ç¨é»è®¤åç§°') |
| | | qrData.deviceName = qrData.deviceName || `设å¤${deviceId}` |
| | | } |
| | | } catch (apiError) { |
| | | console.error('æ¥è¯¢è®¾å¤ä¿¡æ¯å¤±è´¥:', apiError) |
| | | // APIè°ç¨å¤±è´¥æ¶ä½¿ç¨é»è®¤åç§° |
| | | qrData.deviceName = qrData.deviceName || `设å¤${deviceId}` |
| | | } |
| | | } |
| | | |
| | | // ç¡®ä¿æ°æ®å®æ´æ§ |
| | |
| | | qrData.deviceName = result.result |
| | | } |
| | | if (!qrData.qrCodeId) { |
| | | qrData.qrCodeId = result.result |
| | | qrData.qrCodeId = deviceId || result.result |
| | | } |
| | | |
| | | callBackendAPI(qrData) |
| | | // å°æ«ç æ°æ®ä¸ä»»å¡å
³è |
| | | if (currentScanningTask.value) { |
| | | // å
³èä»»å¡ä¿¡æ¯ |
| | | const taskData = { |
| | | ...currentScanningTask.value, |
| | | qrCodeData: qrData |
| | | } |
| | | |
| | | // æå¼ä¸ä¼ å¼¹çªï¼ä¼ éå
³èåç任塿°æ® |
| | | nextTick(() => { |
| | | if (formDia.value && formDia.value.openDialog) { |
| | | formDia.value.openDialog(taskData) |
| | | } else { |
| | | console.error('ä¸ä¼ ç»ä»¶å¼ç¨ä¸åå¨') |
| | | uni.showToast({ |
| | | title: 'ç»ä»¶æªåå¤å¥½', |
| | | icon: 'error' |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | } catch (error) { |
| | | console.error('å¤çæ«ç ç»æå¤±è´¥:', error) |
| | | uni.showToast({ |
| | | title: error.message || 'æ°æ®è§£æå¤±è´¥', |
| | | icon: 'error' |
| | | }) |
| | | } finally { |
| | | // å
³éæ«æçé¢ |
| | | isScanning.value = false |
| | | } |
| | | } |
| | | |
| | | const callBackendAPI = (result) => { |
| | | console.log('å夿å¼å¼¹æ¡ï¼æ°æ®:', result) |
| | | console.log('å¼¹æ¡ç»ä»¶å¼ç¨:', qrCodeFormDia.value) |
| | | |
| | | // ç¡®ä¿ç»ä»¶å¼ç¨åå¨ |
| | | if (qrCodeFormDia.value) { |
| | | console.log('ç´æ¥è°ç¨å¼¹æ¡openDialogæ¹æ³') |
| | | qrCodeFormDia.value.openDialog(result) |
| | | } else { |
| | | // 妿ç»ä»¶å¼ç¨ä¸åå¨ï¼çå¾
ä¸ä¸ä¸ªtick |
| | | console.log('ç»ä»¶å¼ç¨ä¸åå¨ï¼çå¾
nextTick') |
| | | nextTick(() => { |
| | | console.log('nextTickåå¼¹æ¡ç»ä»¶å¼ç¨:', qrCodeFormDia.value) |
| | | if (qrCodeFormDia.value) { |
| | | console.log('nextTickåè°ç¨å¼¹æ¡openDialogæ¹æ³') |
| | | qrCodeFormDia.value.openDialog(result) |
| | | } else { |
| | | console.error('å¼¹æ¡ç»ä»¶å¼ç¨ä¸åå¨') |
| | | uni.showToast({ |
| | | title: 'å¼¹æ¡ç»ä»¶æªåå¤å¥½', |
| | | icon: 'error' |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | } |
| | | |
| | | |
| | | // æ«ç å¤ç |
| | | const handleScanCode = (result) => { |
| | | console.log('æ«ç ç»æ:', result) |
| | | handleScanSuccess(result) |
| | | } |
| | | |
| | | // æå头é误å¤ç |
| | |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | // 导å
¥é宿¨¡åå
Œ
±æ ·å¼ |
| | | @import '@/styles/sales-common.scss'; |
| | | |
| | | // 页é¢å®¹å¨æ ·å¼ |
| | | .inspection-upload-page { |
| | | min-height: 100vh; |
| | | background-color: #f5f5f5; |
| | | } |
| | | |
| | | .tabs-container { |
| | | background-color: #fff; |
| | | margin: 0; |
| | | border-bottom: 1px solid #e8e8e8; |
| | | } |
| | | |
| | | .custom-tabs { |
| | | display: flex; |
| | | background: #f8f9fa; |
| | | position: relative; |
| | | background-color: #fff; |
| | | width: 100%; |
| | | } |
| | | |
| | | .tab-item { |
| | | // åè¡¨å®¹å¨æ ·å¼ |
| | | .table-section { |
| | | padding: 20px; |
| | | } |
| | | |
| | | // ä»»å¡åè¡¨æ ·å¼ - 使ç¨éå®å°è´¦çæ ·å¼è§è |
| | | .task-list { |
| | | .task-item { |
| | | background: #ffffff; |
| | | border-radius: 12px; |
| | | margin-bottom: 16px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
| | | padding: 0 16px; |
| | | |
| | | &:active { |
| | | transform: scale(0.98); |
| | | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 项ç®å¤´é¨æ ·å¼ |
| | | .task-header { |
| | | padding: 16px 0; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .task-info { |
| | | flex: 1; |
| | | text-align: center; |
| | | padding: 20px 0; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .task-name { |
| | | font-size: 14px; |
| | | color: #333; |
| | | font-weight: 500; |
| | | color: #606266; |
| | | transition: all 0.3s ease; |
| | | cursor: pointer; |
| | | position: relative; |
| | | z-index: 2; |
| | | margin-bottom: 0; |
| | | line-height: 1.4; |
| | | } |
| | | |
| | | .tab-item.tab-active { |
| | | color: #1890ff; |
| | | font-weight: 600; |
| | | .task-location { |
| | | font-size: 12px; |
| | | color: #666; |
| | | margin-top: 4px; |
| | | } |
| | | |
| | | .tab-line { |
| | | position: absolute; |
| | | bottom: 0; |
| | | width: 50%; |
| | | height: 3px; |
| | | background-color: #1890ff; |
| | | transition: left 0.3s ease; |
| | | } |
| | | |
| | | .scan-section { |
| | | background-color: #fff; |
| | | padding: 10px; |
| | | } |
| | | |
| | | .scan-controls { |
| | | // 任塿使鮿 ·å¼ |
| | | .task-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | margin-left: 0; |
| | | } |
| | | |
| | | // ä»»å¡è¯¦æ
æ ·å¼ - 使ç¨éå®å°è´¦ç详æ
è¡æ ·å¼ |
| | | .task-details { |
| | | padding: 16px 0; |
| | | |
| | | .detail-item { |
| | | display: flex; |
| | | align-items: flex-end; |
| | | justify-content: space-between; |
| | | margin-bottom: 8px; |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .detail-label { |
| | | font-size: 12px; |
| | | color: #777777; |
| | | min-width: 60px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .detail-value { |
| | | font-size: 12px; |
| | | color: #000000; |
| | | text-align: right; |
| | | flex: 1; |
| | | margin-left: 16px; |
| | | line-height: 1.4; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // æ æ°æ®æç¤ºæ ·å¼ - 使ç¨éå®å°è´¦çæ ·å¼ |
| | | .no-data { |
| | | padding: 40px 0; |
| | | text-align: center; |
| | | color: #999; |
| | | background: none; |
| | | margin: 0; |
| | | } |
| | | |
| | | .no-data text { |
| | | font-size: 14px; |
| | | color: #999; |
| | | } |
| | | |
| | | /* æ«ç å¼¹çªæ ·å¼ */ |
| | | .qr-scan-overlay { |
| | | position: fixed; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background-color: rgba(0, 0, 0, 0.8); |
| | | z-index: 9999; |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | | align-items: center; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .qr-scan-container { |
| | | position: relative; |
| | | width: 100%; |
| | | max-width: 500px; |
| | | margin: 0 auto; |
| | | background: #000; |
| | | border-radius: 8px; |
| | | max-width: 400px; |
| | | background-color: #000; |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .scan-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 15px; |
| | | background-color: rgba(0, 0, 0, 0.7); |
| | | } |
| | | |
| | | .scan-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #fff; |
| | | } |
| | | |
| | | .qr-camera { |
| | | width: 100%; |
| | | height: 400px; |
| | | } |
| | | |
| | | .scan-frame-wrapper { |
| | | position: relative; |
| | | width: 100%; |
| | | height: 300px; |
| | | } |
| | | |
| | | .scan-overlay { |
| | | .scan-frame { |
| | | position: absolute; |
| | | top: 50%; |
| | | left: 50%; |
| | | transform: translate(-50%, -50%); |
| | | width: 70%; |
| | | height: 70%; |
| | | width: 80%; |
| | | height: 80%; |
| | | border: 3px solid #1890ff; |
| | | border-radius: 8px; |
| | | box-shadow: 0 0 20px rgba(24, 144, 255, 0.3); |
| | | animation: pulse 2s infinite; |
| | | } |
| | | |
| | | .scan-frame { |
| | | width: 100%; |
| | | height: 100%; |
| | | border: 2px solid #fff; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .scan-tip { |
| | | position: absolute; |
| | | bottom: -30px; |
| | | bottom: 10px; |
| | | left: 50%; |
| | | transform: translateX(-50%); |
| | | color: #fff; |
| | | font-size: 14px; |
| | | text-align: center; |
| | | background-color: rgba(0, 0, 0, 0.6); |
| | | padding: 5px 15px; |
| | | border-radius: 20px; |
| | | } |
| | | |
| | | @keyframes pulse { |
| | |
| | | 100% { opacity: 0.8; } |
| | | } |
| | | |
| | | .status-info { |
| | | margin-top: 16px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .scanning-text { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: #1890ff; |
| | | margin-top: 8px; |
| | | } |
| | | |
| | | .scanning-label { |
| | | margin-left: 8px; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .table-section { |
| | | padding: 0 15px; |
| | | } |
| | | |
| | | .task-list, .qr-list { |
| | | .task-item, .qr-item { |
| | | background-color: #fff; |
| | | border-radius: 8px; |
| | | margin-bottom: 10px; |
| | | padding: 15px; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| | | } |
| | | } |
| | | |
| | | .task-header, .qr-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: flex-start; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .task-info, .qr-info { |
| | | flex: 1; |
| | | } |
| | | |
| | | .task-name, .device-name { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .task-location, .device-location { |
| | | font-size: 14px; |
| | | color: #666; |
| | | } |
| | | |
| | | .task-actions, .qr-actions { |
| | | margin-left: 10px; |
| | | } |
| | | |
| | | .task-details, .qr-details { |
| | | .detail-item { |
| | | display: flex; |
| | | margin-bottom: 6px; |
| | | |
| | | .detail-label { |
| | | font-size: 14px; |
| | | color: #666; |
| | | min-width: 60px; |
| | | } |
| | | |
| | | .detail-value { |
| | | font-size: 14px; |
| | | color: #333; |
| | | flex: 1; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .empty-state, .loading-state { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | padding: 40px 20px; |
| | | background-color: #fff; |
| | | border-radius: 8px; |
| | | margin: 10px 15px; |
| | | } |
| | | |
| | | .loading-text { |
| | | margin-top: 10px; |
| | | font-size: 14px; |
| | | color: #666; |
| | | } |
| | | |
| | | .pagination-container { |
| | | padding: 20px 15px; |
| | | background-color: #fff; |
| | | margin-top: 10px; |
| | | } |
| | | </style> |