zhuangyunsheng hai 3 meses
pai
achega
1a5e2c4cb3
Modificáronse 42 ficheiros con 1545 adicións e 1012 borrados
  1. 3 0
      package.json
  2. 20 12
      src/api/model/carwash.js
  3. 25 2
      src/api/model/common.js
  4. 4 0
      src/api/model/system.js
  5. 3 1
      src/components/scTable/index.vue
  6. 113 131
      src/components/scUpload/file.vue
  7. 143 0
      src/components/scUpload/fileViewer.vue
  8. 0 25
      src/components/scUpload/imageViewer.vue
  9. 211 199
      src/components/scUpload/index.vue
  10. 45 0
      src/components/scUpload/main.js
  11. 156 139
      src/components/scUpload/multiple.vue
  12. 45 0
      src/components/scUpload/txtViewer.vue
  13. 0 154
      src/components/scUpload/uploadIndex.vue
  14. 1 1
      src/components/scUpload/videoViewer.vue
  15. 11 11
      src/config/route.js
  16. 1 1
      src/config/upload.js
  17. 37 0
      src/layout/components/probar.vue
  18. 5 1
      src/layout/components/userbar.vue
  19. 13 4
      src/layout/index.vue
  20. 1 4
      src/router/index.js
  21. 22 0
      src/store/modules/project.js
  22. 0 2
      src/style/app.scss
  23. 1 0
      src/style/fix.scss
  24. 7 7
      src/views/basic/project/detail.vue
  25. 2 2
      src/views/basic/project/items.vue
  26. 1 1
      src/views/basic/supplier/detail.vue
  27. 0 188
      src/views/dataMock/carwash/components/form/index.vue
  28. 16 11
      src/views/dataMock/carwash/components/info/detail.vue
  29. 8 11
      src/views/dataMock/carwash/components/info/index.vue
  30. 8 1
      src/views/dataMock/carwash/components/monos.vue
  31. 107 47
      src/views/dataMock/carwash/components/record/detail.vue
  32. 40 13
      src/views/dataMock/carwash/components/record/index.vue
  33. 104 0
      src/views/dataMock/carwash/components/template/detail.vue
  34. 117 0
      src/views/dataMock/carwash/components/template/index.vue
  35. 179 0
      src/views/dataMock/carwash/detail.vue
  36. 29 3
      src/views/dataMock/carwash/index.vue
  37. 2 2
      src/views/dataMock/carwash/main.js
  38. 1 1
      src/views/dataMock/env/components/record.vue
  39. 2 2
      src/views/dataMock/env/detail.vue
  40. 1 1
      src/views/dataMock/standard/components/record.vue
  41. 60 35
      src/views/home/index.vue
  42. 1 0
      src/views/login/index.vue

+ 3 - 0
package.json

@@ -10,6 +10,9 @@
     "dependencies": {
         "@amap/amap-jsapi-loader": "^1.0.1",
         "@element-plus/icons-vue": "2.0.10",
+        "@vue-office/docx": "^1.6.2",
+        "@vue-office/excel": "^1.7.11",
+        "@vue-office/pdf": "^2.0.8",
         "@vxe-ui/plugin-export-xlsx": "^4.0.13",
         "@vxe-ui/plugin-render-element": "^4.0.10",
         "axios": "1.3.4",

+ 20 - 12
src/api/model/carwash.js

@@ -12,45 +12,53 @@ export default {
 
     carInfo: {
         name: "车辆信息",
-        url: `${config.API_URL}/api/carRinseInfo`,
+        url: `${config.API_URL}/ops/carRinse`,
         get: async function (data = {}) {
-            return await http.post(`${this.url}/getPage`, data);
+            return await http.post(`${this.url}/getInfoPage`, data);
         },
 
         add: async function (data = {}) {
-            return await http.post(`${this.url}/save`, data);
+            return await http.post(`${this.url}/saveInfo`, data);
         },
 
         edit: async function (data = {}) {
-            return await http.post(`${this.url}/update`, data);
+            return await http.post(`${this.url}/updateInfo`, data);
         },
 
         del: async function (data = {}) {
-            return await http.post(`${this.url}/remove`, data);
+            return await http.post(`${this.url}/removeInfo`, data);
         }
     },
 
     record: {
         name: "冲洗记录",
-        url: `${config.API_URL}/api/carRinseTemp`,
+        url: `${config.API_URL}/ops/carRinse`,
         get: async function (data = {}) {
             return await http.post(`${this.url}/getPage`, data);
         },
 
         edit: async function (data = {}) {
-            return await http.post(`${this.url}/updateCarRinse`, data);
+            return await http.post(`${this.url}/update`, data);
         },
 
         del: async function (data = {}) {
-            return await http.post(`${this.url}/deleteCarRinse`, data);
+            return await http.post(`${this.url}/remove`, data);
         }
     },
 
-    makeData: {
-        url: `${config.API_URL}/ops/carRinse/makeData`,
-        name: "数据模拟",
+    // makeData: {
+    //     url: `${config.API_URL}/ops/carRinse/makeData`,
+    //     name: "数据模拟",
+    //     add: async function (data = {}) {
+    //         return await http.post(this.url, data);
+    //     }
+    // },
+
+    copyData: {
+        url: `${config.API_URL}/ops/carRinse/copyData`,
+        name: "数据模拟-复制",
         add: async function (data = {}) {
             return await http.post(this.url, data);
         }
-    }
+    },
 }

+ 25 - 2
src/api/model/common.js

@@ -1,3 +1,4 @@
+import axios from "axios";
 import config from "@/config"
 import http from "@/utils/request"
 
@@ -13,8 +14,17 @@ export default {
 			return await http.post(`${this.url}/rm`, { querys: [], expands: { entityID } });
 		},
 
-		get: async function (entityID) {
-			return await http.get(`${this.url}/${entityID}`, {}, { responseType: "blob" });
+        download: async function (entityID, isTxt = false) { // url: string, isTxt: txt解码
+            const config = {
+                responseType: "blob",
+                transformResponse: isTxt && [
+                    async function (data) {
+                        return await transformData(data);
+                    }
+                ] || []
+            }
+
+			return await http.get(`${this.url}/${entityID}`, {}, config);
 		}
 	},
 
@@ -29,4 +39,17 @@ export default {
             return await http.post(`${this.url}/stopTask`, data);
         }
     },
+}
+
+function transformData(data) {
+    return new Promise(resolve => {
+        const reader = new FileReader();
+        reader.readAsText(data, "UTF-8");
+        reader.onload = () => {
+            if (reader.result.includes("�")) {
+                reader.readAsText(data, "GBK");
+                reader.onload = () => resolve(reader.result);
+            } else resolve(reader.result);
+        };
+    });
 }

+ 4 - 0
src/api/model/system.js

@@ -40,6 +40,10 @@ export default {
                 return await http.post(`${this.url}/getList`, data);
             },
 
+            judgment: async function (data = {}) {
+                return await http.post(`${this.url}/getOne`, data);
+            },
+
             add: async function (data = {}) {
                 return await http.post(`${this.url}/save`, data);
             }

+ 3 - 1
src/components/scTable/index.vue

@@ -115,7 +115,7 @@ const gridOptions = ref({
         zoom: true,
         custom: true,
         refresh: {
-            queryMethod: () => getData(),
+            queryMethod: () => getData()
         },
         ...props.toolbarConfig
     },
@@ -203,6 +203,8 @@ const resizeTable = () => {
 const getData = () => {
     if (!props.apiObj) return;
     nextTick(() => {
+        XEUtils.merge(gridOptions.value.formConfig, XEUtils.omit(XEUtils.get(props, "formConfig", {}), "items"));
+
         gridOptions.value.loading = true;
         const reqData = config.framework[props.framework].queryData(gridOptions.value, props.paramsColums);
         props.apiObj[props.apiKey](reqData).then(res => {

+ 113 - 131
src/components/scUpload/file.vue

@@ -26,140 +26,122 @@
 		<span style="display: none!important;"><el-input v-model="value"></el-input></span>
 	</div>
 
-	<sc-image-viewer :showViewer="showPictureViewer" :imageList="previewImageList" teleported @close="showPictureViewer = false"></sc-image-viewer>
-	<sc-video-viewer v-if="showVideoViewer" :videoUrl="previewVideoUrl" hideOnModal @close="showVideoViewer = false"></sc-video-viewer>
+    <file-viewer v-if="showViewer" ref="fileViewer" @closed="showViewer = false"></file-viewer>
 </template>
 
 <script>
-	import config from "@/config/upload";
-
-	export default {
-		props: {
-			modelValue: { type: Array, default: () => [] },
-			tip: { type: String, default: "" },
-			accept: { type: String, default: "" },
-			maxSize: { type: Number, default: 50 },
-			limit: { type: Number, default: 0 },
-			multiple: { type: Boolean, default: true },
-			disabled: { type: Boolean, default: false },
-			hideAdd: { type: Boolean, default: false },
-			onSuccess: { type: Function, default: () => { return true } }
-		},
-
-		data() {
-			return {
-				value: "",
-				defaultFileList: [],
-				
-				showPictureViewer: false,
-				showVideoViewer: false,
-
-				previewImageList: [],
-				previewVideoUrl: ""
-			}
-		},
-
-		watch: {
-			modelValue(val) {
-				if (JSON.stringify(val) != JSON.stringify(this.formatArr(this.defaultFileList))) {
-					this.defaultFileList = val;
-					this.value = val;
-				}
-			},
-
-			defaultFileList: {
-				deep: true,
-				handler(val) {
-					this.$emit("update:modelValue", this.formatArr(val));
-					this.value = val.map(v => v.path).join(",");
-				}
-			}
-		},
-
-		mounted() {
-			this.defaultFileList = this.modelValue;
-			this.value = this.modelValue;
-		},
-
-		methods: {
-			// 格式化数组值
-			formatArr(arr) {
-				return arr.map(item => ({ id: item.id, name: item.name, mineType: item.mineType, path: item.path }));
-			},
-
-			before(file) {
-				const maxSize = file.size / 1024 / 1024 < this.maxSize;
-				if (!maxSize) {
-					this.$message.warning(`上传文件大小不能超过 ${this.maxSize}MB!`);
-					return false;
-				}
-			},
-
-			success(res, file) {
-				let os = this.onSuccess(res, file);
-				if (os != undefined && os == false) return false;
-				
-				file.name = res.fileName;
-				file.path = res.path;
-				file.mineType = res.mineType;
-			},
-
-			error(message) {
-				this.$notify.error({ title: "上传文件未成功", message });
-			},
-
-			beforeRemove({ id, name, path }) { // id, name, path, size
-				return this.$confirm(`是否移除 ${name}? 此操作不可逆!`, "提示", {
-					type: "warning",
-					confirmButtonText: "移除"
-				}).then(() => {
-					const entityID = id || path;
-					this.$API.common.folder.rm(entityID).then(res => {
-						if (res.code == 200) return true;
-						else return false;
-					}).catch(() => {
-						return false;
-					});
-				}).catch(() => {
-					return false;
-				});
-			},
-
-			handleExceed() {
-				this.$message.warning(`当前设置最多上传 ${this.limit} 个文件,请移除后上传!`);
-			},
-
-			async handlePreview(uploadFile) {
-				if (config.imageIncludes(uploadFile.mineType)) {
-					this.showPictureViewer = true;
-					this.previewImageList = ["/api/folder/" + uploadFile.path];
-				} else if (config.videoIncludes(uploadFile.mineType)) {
-					this.showVideoViewer = true;
-					this.previewVideoUrl = "/api/folder/" + uploadFile.path;
-				} else {
-					const res = await this.$API.common.folder.get(uploadFile.path);
-					const a = document.createElement("a");
-					a.href = URL.createObjectURL(res);
-					a.download = uploadFile.name;
-					a.click();
-				}
-			},
-
-			request(param) {
-				const data = new FormData();
-				data.append(param.filename, param.file);
-				this.$API.common.folder.up(data, {
-					onUploadProgress: e => {
-						const percent = parseInt(((e.loaded / e.total) * 100) | 0, 10);
-						param.onProgress({ percent });
-					}
-				}).then(res => {
-					if (res.code == 200) param.onSuccess({ path: res.expands.file, fileName: param.file.name, mineType: param.file.type })
-					else param.onError(res.message || "未知错误");
-				}).catch(err => param.onError(err));
-			}
-		}
-	}
+export default {
+    props: {
+        modelValue: { type: Array, default: () => [] },
+        tip: { type: String, default: "" },
+        accept: { type: String, default: "" },
+        maxSize: { type: Number, default: 50 },
+        limit: { type: Number, default: 0 },
+        multiple: { type: Boolean, default: true },
+        disabled: { type: Boolean, default: false },
+        hideAdd: { type: Boolean, default: false },
+        onSuccess: { type: Function, default: () => { return true } }
+    },
+
+    data() {
+        return {
+            value: "",
+            defaultFileList: [],
+            
+            showViewer: false
+        }
+    },
+
+    watch: {
+        modelValue(val) {
+            if (JSON.stringify(val) != JSON.stringify(this.formatArr(this.defaultFileList))) {
+                this.defaultFileList = val;
+                this.value = val;
+            }
+        },
+
+        defaultFileList: {
+            deep: true,
+            handler(val) {
+                this.$emit("update:modelValue", this.formatArr(val));
+                this.value = val.map(v => v.path).join(",");
+            }
+        }
+    },
+
+    mounted() {
+        this.defaultFileList = this.modelValue;
+        this.value = this.modelValue;
+    },
+
+    methods: {
+        // 格式化数组值
+        formatArr(arr) {
+            return arr.map(item => ({ id: item.id, name: item.name, mineType: item.mineType, path: item.path }));
+        },
+
+        before(file) {
+            const maxSize = file.size / 1024 / 1024 < this.maxSize;
+            if (!maxSize) {
+                this.$message.warning(`上传文件大小不能超过 ${this.maxSize}MB!`);
+                return false;
+            }
+        },
+
+        success(res, file) {
+            let os = this.onSuccess(res, file);
+            if (os != undefined && os == false) return false;
+            
+            file.name = res.fileName;
+            file.path = res.path;
+            file.mineType = res.mineType;
+        },
+
+        error(message) {
+            this.$notify.error({ title: "上传文件未成功", message });
+        },
+
+        beforeRemove({ id, name, path }) { // id, name, path, size
+            return this.$confirm(`是否移除 ${name}? 此操作不可逆!`, "提示", {
+                type: "warning",
+                confirmButtonText: "移除"
+            }).then(() => {
+                const entityID = id || path;
+                this.$API.common.folder.rm(entityID).then(res => {
+                    if (res.code == 200) return true;
+                    else return false;
+                }).catch(() => {
+                    return false;
+                });
+            }).catch(() => {
+                return false;
+            });
+        },
+
+        handleExceed() {
+            this.$message.warning(`当前设置最多上传 ${this.limit} 个文件,请移除后上传!`);
+        },
+
+        handlePreview(uploadFile) {
+            this.showViewer = true;
+            nextTick(() => this.$refs.fileViewer.init(uploadFile));
+        },
+
+        request(param) {
+            const data = new FormData();
+            data.append(param.filename, param.file);
+            this.$API.common.folder.up(data, {
+                onUploadProgress: e => {
+                    const percent = parseInt(((e.loaded / e.total) * 100) | 0, 10);
+                    param.onProgress({ percent });
+                }
+            }).then(res => {
+                if (res.code == 200) param.onSuccess({ path: res.expands.file, fileName: param.file.name, mineType: param.file.type })
+                else param.onError(res.message || "未知错误");
+            }).catch(err => param.onError(err));
+        }
+    }
+}
 </script>
 
 <style lang="scss" scoped>

+ 143 - 0
src/components/scUpload/fileViewer.vue

@@ -0,0 +1,143 @@
+<template>
+	<div class="sc-file-viewer">
+        <el-dialog v-model="visible" fullscreen :show-close="false" @closed="$emit('closed')">
+            <div class="sc-file-viewer__header">
+                <div class="sc-file-viewer__title">
+                    {{ fileName }}
+                    <button class="el-button el-button--primary" aria-label="download" type="button" @click="downloadFile">
+                        <sc-iconify icon="ant-design:download-outlined"></sc-iconify>下载
+					</button>
+                </div>
+				
+                <button class="el-button is-link" aria-label="close" type="button" @click="visible = false">
+                    <sc-iconify icon="ant-design:close-outlined"></sc-iconify>
+                </button>
+            </div>
+            
+            <div v-loading="loading" class="sc-file-viewer__content">
+                <component :is="`vue_office_${fileType.split('.')[0]}`" :src="'/api/folder/' + filePath" :options="options" @rendered="loading = false" @error="loading = false"></component>
+            </div>
+        </el-dialog>
+        
+        <el-image-viewer v-if="showViewer" :url-list="['/api/folder/' + filePath]" teleported @close="showViewer = false"></el-image-viewer>
+	    <video-viewer v-if="showVideoViewer" :videoUrl="filePath" hideOnModal @close="showVideoViewer = false"></video-viewer>
+    </div>
+</template>
+
+<script>
+import { fileTypes, officeOptions } from "./main";
+
+import vue_office_docx from "@vue-office/docx";
+import vue_office_excel from "@vue-office/excel";
+import vue_office_pdf from "@vue-office/pdf";
+import vue_office_txt from "@/components/scUpload/txtViewer";
+import videoViewer from "@/components/scUpload/videoViewer";
+
+import "@vue-office/docx/lib/index.css";
+import "@vue-office/excel/lib/index.css";
+
+export default {
+    emits: ["closed"],
+    components: {
+        vue_office_docx,
+        vue_office_excel,
+        vue_office_pdf,
+        vue_office_txt
+    },
+
+    data() {
+        return {
+            visible: false,
+            loading: false,
+            fileName: null,
+            fileType: null,
+            filePath: null,
+            options: {},
+
+            showViewer: false,
+            showVideoViewer: false
+        }
+    },
+
+    methods: {
+        init(uploadFile) {
+            this.fileName = uploadFile.name;
+            this.filePath = uploadFile.path;
+            this.fileType = fileTypes[uploadFile["mineType"]] || uploadFile["mineType"];
+            if (!fileTypes[uploadFile["mineType"]]) {
+                ElMessage.warning("当前只支持预览 .png/.jpg/.map4/.avi/.txt/.docx/.pdf/.xlsx/.xls 格式文件, 文件已下载");
+                this.downloadFile();
+            } else {
+                if (this.fileType == "image") this.showViewer = true;
+                else if (this.fileType == "video") this.showVideoViewer = true;
+                else {
+                    this.loading = true;
+                    this.visible = true;
+                    this.options = officeOptions[this.fileType.split(".")[0]] || {};
+                    if (this.fileType.includes(".")) this.options.xls = this.fileType.split(".")[1] == "xls";
+                }
+            }
+        },
+
+        downloadFile() {
+            this.$API.common.folder.download(this.filePath).then(res => {
+                const a = document.createElement("a");
+                const blob = new Blob([res.data], { type: this.fileType });
+                a.download = this.fileName;
+                a.href = URL.createObjectURL(blob);
+                a.click();
+            });
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+.sc-file-viewer {
+  --el-overlay-color-lighter: gray;
+  :deep(.el-dialog) {
+    --el-dialog-bg-color: transparent;
+    padding: 0;
+
+    .el-dialog__header {
+      padding: 0;
+    }
+
+    .el-dialog__body {
+      height: 100%;
+      padding: 0;
+
+      .sc-file-viewer__header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: var(--el-dialog-padding-primary);
+        background: #fff;
+
+        .sc-file-viewer__title {
+          line-height: var(--el-dialog-font-line-height);
+          font-size: var(--el-dialog-title-font-size);
+          color: var(--el-text-color-primary);
+
+          button {
+            margin-left: var(--el-dialog-padding-primary);
+
+            svg {
+              margin-right: 6px;
+            }
+          }
+        }
+
+        .el-button.is-link:hover {
+          color: var(--el-color-primary);
+        }
+      }
+
+      .sc-file-viewer__content {
+        height: calc(100% - 64px - 2 * var(--el-dialog-padding-primary));
+        padding: var(--el-dialog-padding-primary);
+      }
+    }
+  }
+}
+</style>

+ 0 - 25
src/components/scUpload/imageViewer.vue

@@ -1,25 +0,0 @@
-<template>
-	<div class="sc-image-viewer">
-		<el-image-viewer v-if="showViewer" v-bind="$attrs" :url-list="imageList" @close="$emit('closed')"></el-image-viewer>
-	</div>
-</template>
-
-<script>
-	export default {
-		emits: ["closed"],
-
-		props: {
-			showViewer: { type: Boolean, default: false },
-			imageList: { type: Array, default: () => [] }
-		},
-
-		data() {
-			return {}
-		},
-
-		methods: {}
-	}
-</script>
-
-<style lang="scss" scoped>
-</style>

+ 211 - 199
src/components/scUpload/index.vue

@@ -15,7 +15,12 @@
 			<sc-video v-if="isVideo(file.mineType)" :src="'/api/folder/' + file.path" showMask @play="videoPlay"></sc-video>
 			
 			<div class="sc-upload__img-actions" v-if="!disabled">
-				<span class="del" @click="handleRemove()"><el-icon><el-icon-delete /></el-icon></span>
+                <el-button class="download" :loading="loading" type="primary" @click="handleDownload">
+                    <sc-iconify icon="ant-design:download-outlined"></sc-iconify>
+                </el-button>
+                <el-button class="del" :loading="loading" type="danger" @click="handleRemove">
+                    <sc-iconify icon="ant-design:delete-outlined"></sc-iconify>
+                </el-button>
 			</div>
 		</div>
 		<el-upload v-if="!file" class="uploader" ref="uploader"
@@ -50,195 +55,207 @@
 		</el-dialog>
 	</div>
 
-	<sc-video-viewer v-if="showVideoViewer" :videoUrl="previewVideoUrl" hideOnModal @close="showVideoViewer = false"></sc-video-viewer>
+    <file-viewer v-if="showViewer" ref="fileViewer" @closed="showViewer = false"></file-viewer>
 </template>
 
 <script>
-	import config from "@/config/upload";
-
-	export default {
-		props: {
-			modelValue: { type: Object, default: () => {} },
-			width: { type: Number, default: 148 },
-			height: { type: Number, default: 148 },
-			title: { type: String, default: "" },
-			accept: { type: String, default: "image/gif, image/jpeg, image/png, video/mp4 , video/avi" },
-			icon: { type: String, default: "el-icon-plus" },
-			maxSize: { type: Number, default: 50 },
-			disabled: { type: Boolean, default: false },
-			round: { type: Boolean, default: false },
-			onSuccess: { type: Function, default: () => { return true } },
-			cropper: { type: Boolean, default: false },
-			compress: { type: Number, default: 1 },
-			aspectRatio: {type: Number, default: NaN }
-		},
-
-		data() {
-			return {
-				value: "{}",
-				file: null,
-				style: {
-					width: this.width + "px",
-					height: this.height + "px"
-				},
-				cropperDialogVisible: false,
-				cropperFile: null,
-
-				showVideoViewer: false,
-				previewVideoUrl: ""
-			}
-		},
-
-		watch: {
-			modelValue(val) {
-				this.value = JSON.stringify(val);
-				this.newFile(val);
-			},
-
-			value(val) {
-				this.$emit("update:modelValue", JSON.parse(val));
-			}
-		},
-
-		mounted() {
-			if (this.modelValue) {
-				this.value = JSON.stringify(this.modelValue);
-				this.newFile(this.modelValue);
-			}
-		},
-		
-		methods: {
-			isImage(type) {
-				return config.imageIncludes(type);
-			},
-
-			isVideo(type) {
-				return config.videoIncludes(type);
-			},
-			
-			newFile(data) {
-				this.file = Object.keys(data).length ? { status: "success", ...data } : null;
-			},
-
-			cropperSave() {
-				this.$refs.cropper.getCropFile(file => {
-
-					file.uid = this.cropperFile.uid;
-					this.cropperFile.raw = file;
-
-					this.file = this.cropperFile;
-					this.file.tempFile = URL.createObjectURL(this.file.raw);
-					this.$refs.uploader.submit();
-
-				}, this.cropperFile.name, this.cropperFile.type);
-				this.cropperDialogVisible = false;
-			},
-
-			cropperClosed() {
-				URL.revokeObjectURL(this.cropperFile.tempCropperFile);
-				delete this.cropperFile.tempCropperFile;
-			},
-
-			handleRemove() {
-				const file = JSON.parse(this.value)
-				this.$confirm(`是否移除 ${file.name}? 此操作不可逆!`, "提示", {
-					type: "warning",
-					confirmButtonText: "移除"
-				}).then(() => {
-					if (file.id) {
-						this.$API.common.folder.rm(file.id).then(res => {
-							if (res.code == 200) this.clearFiles();
-						}).catch(() => {});
-					} else this.clearFiles();
-				}).catch(() => {});
-			},
-
-			clearFiles() {
-				URL.revokeObjectURL(this.file.tempFile);
-				this.value = "{}";
-				this.file = null;
-				this.$nextTick(() => this.$refs.uploader.clearFiles());
-			},
-
-			change(file, files) {
-				if (files.length > 1) files.splice(0, 1);
-
-				if (this.cropper && file.status == "ready") {
-					if (!this.isImage(file.raw.type)) return false;
-					this.cropperFile = file;
-					this.cropperFile.tempCropperFile = URL.createObjectURL(file.raw);
-					this.cropperDialogVisible = true;
-					return false;
-				}
-
-				this.file = file;
-				if (file.status == "ready") file.tempFile = URL.createObjectURL(file.raw);
-			},
-
-			before(file) {
-				if (!this.isImage(file.type) && !this.isVideo(file.type)) {
-					this.$message.warning({ title: "上传文件警告", message: "选择的文件非图像类/视频类文件" });
-					this.clearFiles();
-					return false;
-				}
-				const maxSize = file.size / 1024 / 1024 < this.maxSize;
-				if (!maxSize) {
-					this.$message.warning(`上传文件大小不能超过 ${this.maxSize}MB!`);
-					this.clearFiles();
-					return false;
-				}
-			},
-
-			handleExceed(files) {
-				const file = files[0];
-				file.uid = genFileId();
-				this.$refs.uploader.handleStart(file);
-			},
-
-			success(res, file) {
-				// 释放内存删除blob
-				URL.revokeObjectURL(file.tempFile);
-				delete file.tempFile;
-				let os = this.onSuccess(res, file);
-				if (os != undefined && os == false) {
-					this.$nextTick(() => {
-						this.file = null;
-						this.value = "{}";
-					});
-					return false;
-				}
-				file.name = res.fileName;
-				file.path = res.path;
-				file.mineType = res.mineType;
-				this.value = JSON.stringify({ path: res.path, name: res.fileName, mineType: res.mineType });
-			},
-
-			error(message) {
-				this.$nextTick(() => this.clearFiles());
-				this.$notify.error({ title: "上传文件未成功", message });
-			},
-
-			request(param) {
-				const data = new FormData();
-				data.append(param.filename, param.file);
-
-				this.$API.common.folder.up(data, {
-					onUploadProgress: e => {
-						const percent = parseInt(((e.loaded / e.total) * 100) | 0, 10);
-						param.onProgress({ percent });
-					}
-				}).then(res => {
-					if (res.code == 200) param.onSuccess({ path: res.expands.file, fileName: param.file.name, mineType: param.file.type });
-					else param.onError(res.message || "未知错误");
-				}).catch(err => param.onError(err));
-			},
-
-			videoPlay() {
-				this.showVideoViewer = true;
-				this.previewVideoUrl = "/api/folder/" + this.file.path;
-			}
-		}
-	}
+import { fileTypes } from "./main";
+
+export default {
+    props: {
+        modelValue: { type: Object, default: () => {} },
+        width: { type: Number, default: 148 },
+        height: { type: Number, default: 148 },
+        title: { type: String, default: "" },
+        accept: { type: String, default: "image/gif, image/jpeg, image/png, video/mp4 , video/avi" },
+        icon: { type: String, default: "el-icon-plus" },
+        maxSize: { type: Number, default: 50 },
+        disabled: { type: Boolean, default: false },
+        round: { type: Boolean, default: false },
+        onSuccess: { type: Function, default: () => { return true } },
+        cropper: { type: Boolean, default: false },
+        compress: { type: Number, default: 1 },
+        aspectRatio: {type: Number, default: NaN }
+    },
+
+    data() {
+        return {
+            value: "{}",
+            file: null,
+            style: {
+                width: this.width + "px",
+                height: this.height + "px"
+            },
+            cropperDialogVisible: false,
+            cropperFile: null,
+
+            loading: false,
+            showViewer: false
+        }
+    },
+
+    watch: {
+        modelValue(val) {
+            this.value = JSON.stringify(val);
+            this.newFile(val);
+        },
+
+        value(val) {
+            this.$emit("update:modelValue", JSON.parse(val));
+        }
+    },
+
+    mounted() {
+        if (this.modelValue) {
+            this.value = JSON.stringify(this.modelValue);
+            this.newFile(this.modelValue);
+        }
+    },
+    
+    methods: {
+        isImage(type) {
+            return fileTypes[type] == "image"
+        },
+
+        isVideo(type) {
+            return fileTypes[type] == "video"
+        },
+        
+        newFile(data) {
+            this.file = Object.keys(data).length ? { status: "success", ...data } : null;
+        },
+
+        cropperSave() {
+            this.$refs.cropper.getCropFile(file => {
+
+                file.uid = this.cropperFile.uid;
+                this.cropperFile.raw = file;
+
+                this.file = this.cropperFile;
+                this.file.tempFile = URL.createObjectURL(this.file.raw);
+                this.$refs.uploader.submit();
+
+            }, this.cropperFile.name, this.cropperFile.type);
+            this.cropperDialogVisible = false;
+        },
+
+        cropperClosed() {
+            URL.revokeObjectURL(this.cropperFile.tempCropperFile);
+            delete this.cropperFile.tempCropperFile;
+        },
+
+        handleRemove() {
+            const file = JSON.parse(this.value)
+            this.$confirm(`是否移除 ${file.name}? 此操作不可逆!`, "提示", {
+                type: "warning",
+                confirmButtonText: "移除"
+            }).then(() => {
+                if (file.id) {
+                    this.$API.common.folder.rm(file.id).then(res => {
+                        if (res.code == 200) this.clearFiles();
+                    }).catch(() => {});
+                } else this.clearFiles();
+            }).catch(() => {});
+        },
+
+        clearFiles() {
+            URL.revokeObjectURL(this.file.tempFile);
+            this.value = "{}";
+            this.file = null;
+            this.$nextTick(() => this.$refs.uploader.clearFiles());
+        },
+
+        change(file, files) {
+            if (files.length > 1) files.splice(0, 1);
+
+            if (this.cropper && file.status == "ready") {
+                if (!this.isImage(file.raw.type)) return false;
+                this.cropperFile = file;
+                this.cropperFile.tempCropperFile = URL.createObjectURL(file.raw);
+                this.cropperDialogVisible = true;
+                return false;
+            }
+
+            this.file = file;
+            if (file.status == "ready") file.tempFile = URL.createObjectURL(file.raw);
+        },
+
+        before(file) {
+            if (!this.isImage(file.type) && !this.isVideo(file.type)) {
+                this.$message.warning({ title: "上传文件警告", message: "选择的文件非图像类/视频类文件" });
+                this.clearFiles();
+                return false;
+            }
+            const maxSize = file.size / 1024 / 1024 < this.maxSize;
+            if (!maxSize) {
+                this.$message.warning(`上传文件大小不能超过 ${this.maxSize}MB!`);
+                this.clearFiles();
+                return false;
+            }
+        },
+
+        handleExceed(files) {
+            const file = files[0];
+            file.uid = genFileId();
+            this.$refs.uploader.handleStart(file);
+        },
+
+        success(res, file) {
+            // 释放内存删除blob
+            URL.revokeObjectURL(file.tempFile);
+            delete file.tempFile;
+            let os = this.onSuccess(res, file);
+            if (os != undefined && os == false) {
+                this.$nextTick(() => {
+                    this.file = null;
+                    this.value = "{}";
+                });
+                return false;
+            }
+            file.name = res.fileName;
+            file.path = res.path;
+            file.mineType = res.mineType;
+            this.value = JSON.stringify({ path: res.path, name: res.fileName, mineType: res.mineType });
+        },
+
+        error(message) {
+            this.$nextTick(() => this.clearFiles());
+            this.$notify.error({ title: "上传文件未成功", message });
+        },
+
+        request(param) {
+            const data = new FormData();
+            data.append(param.filename, param.file);
+
+            this.$API.common.folder.up(data, {
+                onUploadProgress: e => {
+                    const percent = parseInt(((e.loaded / e.total) * 100) | 0, 10);
+                    param.onProgress({ percent });
+                }
+            }).then(res => {
+                if (res.code == 200) param.onSuccess({ path: res.expands.file, fileName: param.file.name, mineType: param.file.type });
+                else param.onError(res.message || "未知错误");
+            }).catch(err => param.onError(err));
+        },
+
+        videoPlay() {
+            this.showViewer = true;
+            nextTick(() => this.$refs.fileViewer.init(this.file));
+        },
+
+        handleDownload() {
+            this.loading = true;
+            this.$API.common.folder.download(this.file.path).then(res => {
+                this.loading = false;
+                const a = document.createElement("a");
+                const blob = new Blob([res.data], { type: this.file.mineType });
+                a.download = this.file.name;
+                a.href = URL.createObjectURL(blob);
+                a.click();
+            }).catch(() => this.loading = false);
+        }
+    }
+}
 </script>
 
 <style scoped>
@@ -274,28 +291,23 @@
   border: 1px solid var(--el-border-color);
 }
 .sc-upload__img-actions {
+  z-index: 120;
   position: absolute;
   top: 0;
   right: 0;
   display: none;
 }
-.sc-upload__img-actions span {
-  display: flex;
-  justify-content: center;
-  align-items: center;
+.sc-upload__img-actions .el-button {
   width: 25px;
   height: 25px;
-  cursor: pointer;
-  color: #fff;
-}
-.sc-upload__img-actions span i {
-  font-size: 12px;
+  padding: 0;
+  border-radius: 0;
 }
-.sc-upload__img-actions .del {
-  background: #f56c6c;
+.sc-upload__img-actions .el-button + .el-button {
+ margin-left: 0;
 }
 .sc-upload__img:hover .sc-upload__img-actions {
-  display: block;
+  display: flex;
 }
 .sc-upload__img-slot {
   display: flex;

+ 45 - 0
src/components/scUpload/main.js

@@ -0,0 +1,45 @@
+export const fileTypes = {
+    "image/gif": "image",
+    "image/jpeg": "image",
+    "image/jpg": "image",
+    "image/png": "image",
+    "video/mp4": "video",
+    "video/avi": "video",
+    "application/vnd.ms-excel": "excel.xls",
+    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "excel.xlsx",
+    "application/pdf": "pdf",
+    "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
+    "text/plain": "txt"
+}
+
+export const officeOptions = {
+    excel: {
+        xls: false,       // 预览xlsx文件设为false;预览xls文件设为true
+        minColLength: 0,  // excel最少渲染多少列,如果想实现xlsx文件内容有几列,就渲染几列,可以将此值设置为0.
+        minRowLength: 0,  // excel最少渲染多少行,如果想实现根据xlsx实际函数渲染,可以将此值设置为0.
+        widthOffset: 10,  // 如果渲染出来的结果感觉单元格宽度不够,可以在默认渲染的列表宽度上再加 Npx宽
+        heightOffset: 10, // 在默认渲染的列表高度上再加 Npx高
+        beforeTransformData: workbookData => workbookData, // 底层通过exceljs获取excel文件内容,通过该钩子函数,可以对获取的excel文件内容进行修改,比如某个单元格的数据显示不正确,可以在此自行修改每个单元格的value值。
+        transformData: workbookData => workbookData // 将获取到的excel数据进行处理之后且渲染到页面之前,可通过transformData对即将渲染的数据及样式进行修改,此时每个单元格的text值就是即将渲染到页面上的内容
+    },
+    pdf: {
+        // width: 500, //number,可不传,用来控制pdf预览的宽度,默认根据文档实际宽度计算
+        httpHeaders: {}, //object, Basic authentication headers
+        password: "" //string, 加密pdf的密码
+    },
+    docx: {
+        className: "docx", //class name/prefix for default and document style classes
+        inWrapper: true, //enables rendering of wrapper around document content
+        ignoreWidth: false, //disables rendering width of page
+        ignoreHeight: false, //disables rendering height of page
+        ignoreFonts: false, //disables fonts rendering
+        breakPages: true, //enables page breaking on page breaks
+        ignoreLastRenderedPageBreak: false, //disables page breaking on lastRenderedPageBreak elements
+        experimental: false, //enables experimental features (tab stops calculation)
+        trimXmlDeclaration: true, //if true, xml declaration will be removed from xml documents before parsing
+        useBase64URL: false, //if true, images, fonts, etc. will be converted to base 64 URL, otherwise URL.createObjectURL is used
+        useMathMLPolyfill: false, //includes MathML polyfills for chrome, edge, etc.
+        showChanges: false, //enables experimental rendering of document changes (inserions/deletions)
+        debug: false //enables additional logging
+    }
+}

+ 156 - 139
src/components/scUpload/multiple.vue

@@ -29,7 +29,12 @@
 					<sc-video v-if="isVideo(file.mineType)" :src="'/api/folder/' + file.path" showMask @play="videoPlay(file)"></sc-video>
 
 					<div v-if="!disabled && file.status == 'success'" class="sc-upload__item-actions">
-						<span class="del" @click="handleRemove(file)"><el-icon><el-icon-delete /></el-icon></span>
+						<el-button class="download" :loading="loading" type="primary" @click="handleDownload(file)">
+                            <sc-iconify icon="ant-design:download-outlined"></sc-iconify>
+                        </el-button>
+                        <el-button class="del" :loading="loading" type="danger" @click="handleRemove(file)">
+                            <sc-iconify icon="ant-design:delete-outlined"></sc-iconify>
+                        </el-button>
 					</div>
 					<div v-if="file.status == 'ready' || file.status == 'uploading'" class="sc-upload__item-progress">
 						<el-progress :percentage="file.percentage" text-inside :stroke-width="16" />
@@ -40,147 +45,159 @@
 		<span style="display:none!important"><el-input v-model="value"></el-input></span>
 	</div>
 
-	<sc-video-viewer v-if="showVideoViewer" :videoUrl="previewVideoUrl" hideOnModal @close="showVideoViewer = false"></sc-video-viewer>
+    <file-viewer v-if="showViewer" ref="fileViewer" @closed="showViewer = false"></file-viewer>
 </template>
 
 <script>
-	import config from "@/config/upload";
-
-	export default {
-		props: {
-			modelValue: { type: Array, default: () => [] },
-			tip: { type: String, default: "" },
-			maxSize: { type: Number, default: 50 },
-			limit: { type: Number, default: 0 },
-			multiple: { type: Boolean, default: true },
-			disabled: { type: Boolean, default: false },
-			onSuccess: { type: Function, default: () => { return true } }
-		},
-
-		data() {
-			return {
-				value: "",
-				defaultFileList: [],
-
-				showVideoViewer: false,
-				previewVideoUrl: ""
-			}
-		},
-
-		watch: {
-			modelValue(val) {
-				if (JSON.stringify(val) != JSON.stringify(this.formatArr(this.defaultFileList))) {
-					this.defaultFileList = val;
-					this.value = val;
-				}
-			},
-
-			defaultFileList: {
-				deep: true,
-				handler(val) {
-					this.$emit("update:modelValue", this.formatArr(val));
-					this.value = val.map(v => v.path).join(",");
-				}
-			}
-		},
-
-		computed: {
-			preview() {
-				return this.defaultFileList.map(v => "/api/folder/" + v.path);
-			}
-		},
-
-		mounted() {
-			this.defaultFileList = this.modelValue;
-			this.value = this.modelValue;
-		},
-
-		methods: {
-			isImage(type) {
-				return config.imageIncludes(type);
-			},
-
-			isVideo(type) {
-				return config.videoIncludes(type);
-			},
-
-			// 格式化数组值
-			formatArr(arr) {
-				return arr.map(item => ({ id: item.id, name: item.name, mineType: item.mineType, path: item.path }));
-			},
-
-			before(file) {
-				if (!this.isImage(file.type) && !this.isVideo(file.type)) {
-					this.$message.warning({ title: "上传文件警告", message: "选择的文件非图像类/视频类文件" });
-					return false;
-				}
-
-				const maxSize = file.size / 1024 / 1024 < this.maxSize;
-				if (!maxSize) {
-					this.$message.warning(`上传文件大小不能超过 ${this.maxSize}MB!`);
-					return false;
-				}
-			},
-
-			success(res, file) {
-				let os = this.onSuccess(res, file);
-				if (os != undefined && os == false) return false;
-				
-				file.name = res.fileName;
-				file.path = res.path;
-				file.mineType = res.mineType;
-			},
-
-			error(message) {
-				this.$notify.error({ title: "上传文件未成功", message });
-			},
-
-			beforeRemove({ id, name }) {
-				return this.$confirm(`是否移除 ${name}? 此操作不可逆!`, "提示", {
-					type: "warning",
-					confirmButtonText: "移除"
-				}).then(() => {
-					if (id) {
-						this.$API.common.folder.rm(id).then(res => {
-							if (res.code == 200) return true;
-							else return false;
-						}).catch(() => {
-							return false;
-						});
-					} return true;
-				}).catch(() => {
-					return false;
-				});
-			},
-			
-			handleRemove(file) {
-				this.$refs.uploader.handleRemove(file);
-			},
-
-			handleExceed() {
-				this.$message.warning(`当前设置最多上传 ${this.limit} 个文件,请移除后上传!`);
-			},
-
-			request(param) {
-				const data = new FormData();
-				data.append(param.filename, param.file);
-				this.$API.common.folder.up(data, {
-					onUploadProgress: e => {
-						const percent = parseInt(((e.loaded / e.total) * 100) | 0, 10)
-						param.onProgress({ percent });
-					}
-				}).then(res => {
-					if (res.code == 200) param.onSuccess({ path: res.expands.file, fileName: param.file.name, mineType: param.file.type })
-					else param.onError(res.message || "未知错误");
-				}).catch(err => param.onError(err));
-			},
-
-			videoPlay(file) {
-				this.showVideoViewer = true;
-				this.previewVideoUrl = "/api/folder/" + file.path;
-			}
-		}
-	}
+import { fileTypes } from "./main";
+
+export default {
+    props: {
+        modelValue: { type: Array, default: () => [] },
+        tip: { type: String, default: "" },
+        maxSize: { type: Number, default: 50 },
+        limit: { type: Number, default: 0 },
+        multiple: { type: Boolean, default: true },
+        disabled: { type: Boolean, default: false },
+        onSuccess: { type: Function, default: () => { return true } }
+    },
+
+    data() {
+        return {
+            value: "",
+            defaultFileList: [],
+
+            loading: false,
+            showViewer: false
+        }
+    },
+
+    watch: {
+        modelValue(val) {
+            if (JSON.stringify(val) != JSON.stringify(this.formatArr(this.defaultFileList))) {
+                this.defaultFileList = val;
+                this.value = val;
+            }
+        },
+
+        defaultFileList: {
+            deep: true,
+            handler(val) {
+                this.$emit("update:modelValue", this.formatArr(val));
+                this.value = val.map(v => v.path).join(",");
+            }
+        }
+    },
+
+    computed: {
+        preview() {
+            return this.defaultFileList.map(v => "/api/folder/" + v.path);
+        }
+    },
+
+    mounted() {
+        this.defaultFileList = this.modelValue;
+        this.value = this.modelValue;
+    },
+
+    methods: {
+        isImage(type) {
+            return fileTypes[type] == "image"
+        },
+
+        isVideo(type) {
+            return fileTypes[type] == "video"
+        },
+
+        // 格式化数组值
+        formatArr(arr) {
+            return arr.map(item => ({ id: item.id, name: item.name, mineType: item.mineType, path: item.path }));
+        },
+
+        before(file) {
+            if (!this.isImage(file.type) && !this.isVideo(file.type)) {
+                this.$message.warning({ title: "上传文件警告", message: "选择的文件非图像类/视频类文件" });
+                return false;
+            }
+
+            const maxSize = file.size / 1024 / 1024 < this.maxSize;
+            if (!maxSize) {
+                this.$message.warning(`上传文件大小不能超过 ${this.maxSize}MB!`);
+                return false;
+            }
+        },
+
+        success(res, file) {
+            let os = this.onSuccess(res, file);
+            if (os != undefined && os == false) return false;
+            
+            file.name = res.fileName;
+            file.path = res.path;
+            file.mineType = res.mineType;
+        },
+
+        error(message) {
+            this.$notify.error({ title: "上传文件未成功", message });
+        },
+
+        beforeRemove({ id, name }) {
+            return this.$confirm(`是否移除 ${name}? 此操作不可逆!`, "提示", {
+                type: "warning",
+                confirmButtonText: "移除"
+            }).then(() => {
+                if (id) {
+                    this.$API.common.folder.rm(id).then(res => {
+                        if (res.code == 200) return true;
+                        else return false;
+                    }).catch(() => {
+                        return false;
+                    });
+                } return true;
+            }).catch(() => {
+                return false;
+            });
+        },
+        
+        handleRemove(file) {
+            this.$refs.uploader.handleRemove(file);
+        },
+
+        handleExceed() {
+            this.$message.warning(`当前设置最多上传 ${this.limit} 个文件,请移除后上传!`);
+        },
+
+        request(param) {
+            const data = new FormData();
+            data.append(param.filename, param.file);
+            this.$API.common.folder.up(data, {
+                onUploadProgress: e => {
+                    const percent = parseInt(((e.loaded / e.total) * 100) | 0, 10)
+                    param.onProgress({ percent });
+                }
+            }).then(res => {
+                if (res.code == 200) param.onSuccess({ path: res.expands.file, fileName: param.file.name, mineType: param.file.type })
+                else param.onError(res.message || "未知错误");
+            }).catch(err => param.onError(err));
+        },
+
+        videoPlay(file) {
+            this.showViewer = true;
+            nextTick(() => this.$refs.fileViewer.init(file));
+        },
+
+        handleDownload(file) {
+            this.loading = true;
+            this.$API.common.folder.download(file.path).then(res => {
+                this.loading = false;
+                const a = document.createElement("a");
+                const blob = new Blob([res.data], { type: file.mineType });
+                a.download = this.file.name;
+                a.href = URL.createObjectURL(blob);
+                a.click();
+            }).catch(() => this.loading = false);
+        }
+    }
+}
 </script>
 
 <style scoped>

+ 45 - 0
src/components/scUpload/txtViewer.vue

@@ -0,0 +1,45 @@
+<template>
+    <div class="vue-office-txt">
+        <div class="txt-wrapper">{{ textValue }}</div>
+    </div>
+</template>
+
+<script>
+export default {
+    props: {
+        src: { type: String, default: "" }
+    },
+
+    data() {
+        return {
+            textValue: null
+        }
+    },
+
+    mounted() {
+        this.txtDecode();
+    },
+
+    methods: {
+        txtDecode() {
+            this.$API.common.folder.download(this.src, true).then(res => {
+                this.textValue = res;
+                this.$emit("rendered");
+            }).catch(() => this.$emit("error"));
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+.vue-office-txt {
+  background: gray;
+  padding: 0 20px 16px;
+
+  .txt-wrapper {
+    padding: 30px;
+    background: #fff;
+    white-space: pre-wrap;
+  }
+}
+</style>

+ 0 - 154
src/components/scUpload/uploadIndex.vue

@@ -1,154 +0,0 @@
-<template>
-	<el-main>
-		<el-card shadow="never" header="文件示例">
-			<sc-upload-file v-model="fileurl" :limit="3" :data="{otherData:'demo'}" tip="最多上传3个文件,单个文件不要超过10M,请上传xlsx/docx格式文件">
-				<el-button type="primary" icon="el-icon-upload">上传附件</el-button>
-			</sc-upload-file>
-		</el-card>
-
-		<el-card shadow="never" header="文件示例(值为对象数组,适合保存原始文件名)">
-			<sc-upload-file v-model="fileurlArr" :limit="3" tip="最多上传3个文件,单个文件不要超过10M,请上传xlsx/docx格式文件">
-				<el-button type="primary" icon="el-icon-upload">上传附件</el-button>
-			</sc-upload-file>
-		</el-card>
-
-		<el-card shadow="never" header="图片卡片示例(已开启拖拽排序)">
-			<sc-upload-multiple v-model="fileurl2" draggable :limit="3" tip="最多上传3个文件,单个文件不要超过10M,请上传图像格式文件"></sc-upload-multiple>
-		</el-card>
-
-		<el-card shadow="never" header="单图像示例">
-			<el-space wrap :size="8">
-				<sc-upload v-model="fileurl3"></sc-upload>
-				<sc-upload v-model="fileurl4" title="自定义标题" icon="el-icon-picture"></sc-upload>
-				<sc-upload v-model="fileurl5" :apiObj="uploadApi" accept="image/jpg,image/png" :on-success="success" :width="220">
-					<div class="custom-empty">
-						<el-icon><el-icon-upload /></el-icon>
-						<p>自定义插槽</p>
-					</div>
-				</sc-upload>
-				<sc-upload v-model="fileurl6" round icon="el-icon-avatar" title="开启圆形"></sc-upload>
-				<sc-upload v-model="fileurl7" title="开启剪裁" :cropper="true" :compress="1" :aspectRatio="1/1"></sc-upload>
-			</el-space>
-		</el-card>
-
-
-
-		<el-card shadow="never" header="在验证表单中使用">
-			<el-form ref="ruleForm" :model="form" :rules="rules" label-width="100px">
-				<el-form-item label="身份证" required>
-					<el-space wrap :size="8">
-						<el-form-item prop="file1">
-							<sc-upload v-model="form.file1" title="人像面"></sc-upload>
-						</el-form-item>
-						<el-form-item prop="file2">
-							<sc-upload v-model="form.file2" title="国徽面"></sc-upload>
-						</el-form-item>
-					</el-space>
-				</el-form-item>
-				<el-form-item label="其他凭证" prop="file3">
-					<sc-upload-multiple v-model="form.file3"></sc-upload-multiple>
-				</el-form-item>
-				<el-form-item label="附件" prop="file4">
-					<sc-upload-file v-model="form.file4" :limit="1" drag>
-						<el-icon class="el-icon--upload"><el-icon-upload-filled /></el-icon>
-						    <div class="el-upload__text">
-						      Drop file here or <em>click to upload</em>
-						    </div>
-					</sc-upload-file>
-				</el-form-item>
-				<el-form-item label="日期" prop="date">
-					<el-date-picker type="date" placeholder="选择日期" v-model="form.date"></el-date-picker>
-				</el-form-item>
-				<el-form-item>
-					<el-button type="primary" @click="submitForm">保存</el-button>
-				    <el-button @click="resetForm">重置</el-button>
-				</el-form-item>
-			</el-form>
-		</el-card>
-
-	</el-main>
-</template>
-
-<script>
-	export default {
-		name: 'upload',
-		data() {
-			return {
-				uploadApi: this.$API.common.upload,
-				fileurlArr: [
-					{
-						name: '销售合同模板.xlsx',
-						url: 'http://www.scuiadmin.com/files/220000198611262243.xlsx'
-					},
-					{
-						name: '企业员工联系方式.xlsx',
-						url: 'http://www.scuiadmin.com/files/350000201004261875.xlsx',
-					}
-				],
-				fileurl: "http://www.scuiadmin.com/files/220000198611262243.xlsx,http://www.scuiadmin.com/files/350000201004261875.xlsx",
-				fileurl2: "img/auth_banner.jpg,img/avatar3.gif",
-				fileurl3: "img/auth_banner.jpg",
-				fileurl4: "",
-				fileurl5: "",
-				fileurl6: "",
-				fileurl7: "",
-				form: {
-					file1: "",
-					file2: "",
-					file3: "",
-					file4: "",
-					date: ""
-				},
-				rules: {
-					file1: [
-						{required: true, message: '请上传', trigger: 'change'}
-					],
-					file2: [
-						{required: true, message: '请上传', trigger: 'change'}
-					],
-					file3: [
-						{required: true, message: '请上传', trigger: 'change'}
-					],
-					file4: [
-						{required: true, message: '请上传附件', trigger: 'change'}
-					],
-					date: [
-						{required: true, message: '请选择日期', trigger: 'change'}
-					]
-				}
-			}
-		},
-		methods: {
-			success(response){
-				this.$alert(`success函数钩子,可用于类似OCR返回信息,return false后阻止后续执行,回调参数打开控制台查看`, {
-					title: "提示",
-					type: "success"
-				})
-				console.log(response);
-				return false;
-			},
-			submitForm(){
-				this.$refs.ruleForm.validate((valid) => {
-					if (valid) {
-						alert('请看控制台输出');
-						console.log(this.form);
-					}else{
-						return false;
-					}
-				})
-			},
-			resetForm(){
-				this.$refs.ruleForm.resetFields();
-			}
-		}
-	}
-</script>
-
-<style scoped>
-	.el-card+.el-card {margin-top: 15px;}
-	.imglist .el-col+.el-col {margin-left: 8px;}
-
-	.custom-empty {width: 100%;height: 100%;display: flex;flex-direction: column;align-items: center;justify-content: center;background: #8c939d;border-radius:5px;}
-	.custom-empty i {font-size: 30px;color: #fff;}
-	.custom-empty p {font-size: 12px;font-weight: normal;color: #fff;margin-top: 10px;}
-</style>

+ 1 - 1
src/components/scUpload/videoViewer.vue

@@ -7,7 +7,7 @@
 			</span>
 			
 			<div class="el-image-viewer__canvas">
-				<sc-video class="el-image-viewer__img" :src="videoUrl" autoplay></sc-video>
+				<sc-video class="el-image-viewer__img" :src="'/api/folder/' + videoUrl" autoplay></sc-video>
 			</div>
 		</div>
 	</teleport>

+ 11 - 11
src/config/route.js

@@ -92,13 +92,13 @@ const routes = [
         name: "dataMock",
         path: "/dataMock",
         meta: { title: "数据管理与模拟", icon: "majesticons:data-plus-line" },
-        redirect: "/dataMock/env",
+        redirect: "/dataMock/ugliAi",
         children: [
             {
-                name: "envMock",
-                path: "/dataMock/env",
-                meta: { title: "数据管理与模拟-环境监测", icon: "fluent:earth-leaf-16-regular" },
-                component: "dataMock/env"
+                name: "ugliAiMock",
+                path: "/dataMock/ugliAi",
+                meta: { title: "数据管理与模拟-AI视频危险源识别", icon: "hugeicons:ai-brain-02" },
+                component: "dataMock/ugliAi"
             },
             {
                 name: "standardMock",
@@ -106,17 +106,17 @@ const routes = [
                 meta: { title: "数据管理与模拟-标养室监测", icon: "dashicons:dashboard" },
                 component: "dataMock/standard"
             },
+            {
+                name: "envMock",
+                path: "/dataMock/env",
+                meta: { title: "数据管理与模拟-环境监测", icon: "fluent:earth-leaf-16-regular" },
+                component: "dataMock/env"
+            },
             {
                 name: "carwashMock",
                 path: "/dataMock/carwash",
                 meta: { title: "数据管理与模拟-渣土运输管理", icon: "map:car-wash" },
                 component: "dataMock/carwash"
-            },
-            {
-                name: "ugliAiMock",
-                path: "/dataMock/ugliAi",
-                meta: { title: "数据管理与模拟-AI视频危险源识别", icon: "hugeicons:ai-brain-02" },
-                component: "dataMock/ugliAi"
             }
         ]
     },

+ 1 - 1
src/config/upload.js

@@ -1,7 +1,7 @@
 //上传配置
 export default {
 	imageIncludes: function (type) {
-		return ["image/gif", "image/jpeg", "image/png"].includes(type);
+		return ["image/gif", "image/jpeg", "image/jpg", "image/png"].includes(type);
 	},
 
 	videoIncludes: function (type) {

+ 37 - 0
src/layout/components/probar.vue

@@ -0,0 +1,37 @@
+<template>
+    <el-select v-model="projectId" suffix-icon="" :value-on-clear="null" filterable clearable placeholder="全部项目" @change="projectChange">
+        <el-option v-for="item in projects" :key="item.fpiId" :label="item.projectName" :value="item.fpiId"></el-option>
+    </el-select>
+</template>
+
+<script>
+export default {
+    data() {
+        return {
+            projectId: this.$store.state.project.projectId
+        }
+    },
+
+    computed: {
+        projects() {
+            return this.$store.state.project.projects
+        }
+    },
+
+    methods: {
+        projectChange(e) {
+            this.$store.commit("SET_projectId", e);
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+.el-select {width: 220px;}
+.el-select :deep(.el-select__wrapper.is-filterable) {
+    background-color: transparent;box-shadow: 0 0;
+    
+    .el-select__placeholder, .el-select__input {font-size: 16px;color: #fff;text-align: center;}
+    .el-select__suffix .el-icon {color: #fff;}
+}
+</style>

+ 5 - 1
src/layout/components/userbar.vue

@@ -1,5 +1,8 @@
 <template>
 	<div class="user-bar">
+        <div class="panel-item">
+            <probar></probar>
+        </div>
         <div class="panel-item hidden-sm-and-down" @click="search">
 			<el-icon><el-icon-search /></el-icon>
 		</div>
@@ -36,9 +39,10 @@
 import search from "./search";
 import setting from "./setting";
 import password from "./password";
+import probar from "./probar";
 
 export default {
-    components: { search, setting, password },
+    components: { probar, search, setting, password },
     data() {
         return {
             searchVisible: false,

+ 13 - 4
src/layout/index.vue

@@ -61,7 +61,7 @@ import SideM from "./components/sideM";
 import Tags from "./components/tags";
 import NavMenu from "./components/NavMenu";
 import userbar from "./components/userbar";
-    
+
 export default {
     name: "index",
     components: {
@@ -71,10 +71,9 @@ export default {
         userbar
     },
     data() {
-        return {
-        }
+        return {}
     },
-    computed:{
+    computed: {
         ismobile() {
             return this.$store.state.global.ismobile
         },
@@ -92,8 +91,14 @@ export default {
         }
     },
     created() {
+        this.$router.getProject();
         this.onLayoutResize();
         window.addEventListener("resize", this.onLayoutResize);
+        window.addEventListener("setItemEvent", this.storageChange);
+    },
+
+    unmounted() {
+        window.removeEventListener("setItemEvent", this.storageChange);
     },
 
     methods: {
@@ -101,6 +106,10 @@ export default {
             this.$store.commit("SET_ismobile", document.body.clientWidth < 992);
         },
 
+        storageChange({ key, newValue }) {
+            if (key == "PROJECT") this.$store.commit("SET_projects", XEUtils.toStringJSON(newValue).content);
+        },
+
         // 点击显示
         showMenu(route) {
             this.$router.push({ path: route.path });

+ 1 - 4
src/router/index.js

@@ -72,9 +72,6 @@ router.beforeEach(async (to, from, next) => {
     
     // 加载动态/静态路由
 	if (!isGetRouter) {
-        // 所有项目
-        if (!tool.data.get("PROJECT")) router.getProject()
-
         tool.data.set("MENU", [...userRoutes]);
         XEUtils.arrayEach(XEUtils.toTreeArray(filterAsyncRouter(userRoutes)), item => router.addRoute("layout", item))
 		routes_404_r = router.addRoute(routes_404)
@@ -133,7 +130,7 @@ function loadComponent(component) {
 router.getProject = async fpiId => {
     let projectRes = await api.system.project.all();
     tool.data.set("PROJECT", projectRes);
-    (!tool.data.get("PROJECT_ID") || fpiId && fpiId == tool.data.get("PROJECT_ID")) && tool.data.set("PROJECT_ID", XEUtils.get(XEUtils.first(projectRes), "fpiId"));
+    (!tool.data.get("PROJECT_ID") || fpiId && fpiId == tool.data.get("PROJECT_ID")) && tool.data.set("PROJECT_ID", null);
 }
 
 router.getGates = async (storagePath, start = 0, gates = []) => {

+ 22 - 0
src/store/modules/project.js

@@ -0,0 +1,22 @@
+import TOOL from "@/utils/tool";
+
+export default {
+	state: {
+		projects: TOOL.data.get("PROJECT") || [],
+		projectId: TOOL.data.get("PROJECT_ID") || null
+	},
+	mutations: {
+		SET_projectId(state, key) {
+			state.projectId = key
+		    TOOL.data.set("PROJECT_ID", key)
+		},
+		SET_projects(state, key) {
+			state.projects = key
+		},
+
+        clearProject(state) {
+			state.projects = []
+			state.projectId = null
+		}
+	}
+}

+ 0 - 2
src/style/app.scss

@@ -109,8 +109,6 @@ a,button,input,textarea{-webkit-tap-highlight-color:rgba(0,0,0,0);box-sizing: bo
         .query-action__item .vxe-form--item-inner {white-space: nowrap;}
         .query-action__item .vxe-form--item-trigger-node {min-width: unset;margin-left: 12px;}
     }
-
-    .vxe-table-top__button {padding-bottom: 1em;}
 }
 
 .vxe-table-slot--popper .el-select-dropdown__item, .vxe-table-slot--popper .el-dropdown-menu__item, .vxe-table-slot--popper .el-tree {font-size: inherit;}

+ 1 - 0
src/style/fix.scss

@@ -63,6 +63,7 @@
 .el-checkbox-button__inner {font-size: 12px;}
 .el-sub-menu .el-icon {font-size: 17px;}
 .el-sub-menu .el-sub-menu__icon-arrow {font-size: 12px;}
+.el-form .el-form-item__content .el-date-editor {width: 100%;}
 
 .aminui-tags li:hover {background-color: var(--el-color-primary-light-9);}
 .aminui-tags li.active {background-color: var(--el-color-primary)!important;}

+ 7 - 7
src/views/basic/project/detail.vue

@@ -352,21 +352,21 @@ const districtList = reactive({
 });
 const boundAreas = reactive({});
 
-const open = () => {
+const open = async () => {
     visible.value = true;
-    initMap();
+    await initMap();
 }
 const setData = async fpiId => {
-    open();
+    await open();
     mode.value = "edit";
     API.system.project.detail({ fpiId }).then(res => {
         XEUtils.objectEach(form.value, (_, key) => {
-            if (key == "projectFirmName") XEUtils.set(form.value, "deptId", XEUtils.get(XEUtils.findTree(props.treeSelectProps.data, item => item.name == XEUtils.get(res.project, key)), "item.deptId", null))
+            if (key == "projectFirmName") XEUtils.set(form.value, "deptId", XEUtils.get(XEUtils.findTree(props.treeSelectProps.data, item => item.name == XEUtils.get(res.project, key)), "item.deptId", null));
             else if (key == "folders") {
                 XEUtils.objectEach(form.value.folders, (_, folder_key) => {
-                    XEUtils.set(form.value, `${key}.${folder_key}.entities`, XEUtils.map(XEUtils.get(res.project, `${key}.${folder_key}.entities`), ({ id, mineType, name, path }) => ({ id, mineType, name, path })))
+                    XEUtils.set(form.value, `${key}.${folder_key}.entities`, XEUtils.map(XEUtils.get(res.project, `${key}.${folder_key}.entities`), ({ id, mineType, name, path }) => ({ id, mineType, name, path })));
                 });
-            } else XEUtils.set(form.value, key, XEUtils.get(res.project, key) || XEUtils.get(res.position, key, null))
+            } else XEUtils.set(form.value, key, XEUtils.get(res.project, key) || XEUtils.get(res.position, key, null));
         });
         XEUtils.arrayEach(res.apiList, item => {
             const prefix = XEUtils.get(interfaceDic, item.platformType);
@@ -562,7 +562,7 @@ defineExpose({
 .el-form {
     padding: 0 var(--el-dialog-padding-primary);
 
-    .el-input-number, :deep(.el-date-editor.el-input) {width: 100%;}
+    .el-input-number {width: 100%;}
     .el-input-number :deep(.el-input__inner) {text-align: unset;}
     .el-input-number :deep(.el-input__suffix) {font-size: 12px;}
     

+ 2 - 2
src/views/basic/project/items.vue

@@ -1,6 +1,6 @@
 <template>
-    <el-dialog v-model="visible" :title="`${form.projectName}-验收清单`" width="870" :close-on-click-modal="false" @closed="$emit('closed')">
-        <el-form ref="formRef" :model="form" label-width="110px">
+    <el-dialog v-model="visible" :title="`${form.projectName}-验收清单`" width="860" :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-form ref="formRef" :model="form" label-width="120">
             <el-form-item required>
                 <template #label>
                     <el-tooltip placement="top">

+ 1 - 1
src/views/basic/supplier/detail.vue

@@ -1,6 +1,6 @@
 <template>
     <el-dialog v-model="visible" :title="titleMap[mode]" width="480" :close-on-click-modal="false" @closed="$emit('closed')">
-        <el-form ref="formRef" :model="form" :rules="rules" label-width="140px">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="140">
             <el-form-item label="供应商类型" prop="type">
                 <el-radio-group v-model="form.type">
                     <el-radio label="firm">厂商</el-radio>

+ 0 - 188
src/views/dataMock/carwash/components/form/index.vue

@@ -1,188 +0,0 @@
-<template>
-    <el-main>
-        <el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
-            <el-row>
-                <el-col :md="8" :xs="24">
-                    <el-form-item label="所属项目" prop="fpiId">{{ projectName }}</el-form-item>
-                </el-col>
-                <el-col :md="8" :xs="24">
-                    <el-form-item label="时间范围" prop="dateRange">
-                        <el-date-picker v-model="form.dateRange" type="daterange" :clearable="false" :shortcuts="shortcuts" value-format="YYYY-MM-DD" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
-                    </el-form-item>
-                </el-col>
-                <el-col :md="8" :xs="24">
-                    <el-form-item label="循环次数" prop="cpTimes">
-                        <el-input-number v-model="form.cpTimes" :min="1" :controls="false" placeholder="请输入循环次数"></el-input-number>
-                    </el-form-item>
-                </el-col>
-                <el-col :md="8" :xs="24">
-                    <el-form-item label="车牌号" prop="licensePlate">
-                        <el-select :loading="loading" v-model="form.licensePlate" filterable remote allow-create default-first-option placeholder="请输入车牌号" :remote-method="remoteMethod" @change="licensePlateChange">
-                            <el-option v-for="item in carInfoList" :key="item.id" :value="item.plateNumber"></el-option>
-                        </el-select>
-                    </el-form-item>
-                </el-col>
-                <el-col :md="8" :xs="24">
-                    <el-form-item label="车牌颜色" prop="licensePlateColor">
-                        <el-select v-model="form.licensePlateColor" placeholder="请选择车牌颜色">
-                            <el-option v-for="(item, index) in carWashDic.plateColor.slice(1)" :key="index" :label="item" :value="index + 1"></el-option>
-                        </el-select>
-                    </el-form-item>
-                </el-col>
-                <el-col :md="8" :xs="24">
-                    <el-form-item label="车辆类型" prop="carType">
-                        <el-select v-model="form.carType" placeholder="请选择车辆类型">
-                            <el-option v-for="(item, index) in carWashDic.carType" :key="index" :label="item" :value="index"></el-option>
-                        </el-select>
-                    </el-form-item>
-                </el-col>
-                <el-col :md="8" :xs="24">
-                    <el-form-item label="车辆颜色" prop="vehicleColor">
-                        <el-select v-model="form.vehicleColor" placeholder="请选择车辆颜色">
-                            <el-option v-for="(item, index) in carWashDic.carColor" :key="index" :label="item" :value="index"></el-option>
-                        </el-select>
-                    </el-form-item>
-                </el-col>
-                <el-col :md="8" :xs="24">
-                    <el-form-item label="识别结果" prop="alarmType">
-                        <el-select v-model="form.alarmType" placeholder="请选择识别结果">
-                            <el-option v-for="(item, index) in carWashDic.alarmType.slice(1)" :key="index" :label="item" :value="index + 1"></el-option>
-                        </el-select>
-                    </el-form-item>
-                </el-col>
-            </el-row>
-            <el-row>
-                <el-col :md="8" :xs="24">
-                    <el-form-item label="车身清洗图片" prop="folders[carrinse/attach].entities[0]">
-                        <sc-upload v-model="form.folders['carrinse/attach'].entities[0]" :width="140" :height="180" accept="image/jpeg, image/png"></sc-upload>
-                    </el-form-item>
-                </el-col>
-                <el-col :md="8" :xs="24">
-                    <el-form-item label="后盖密闭图片" prop="folders[carrinse/side].entities[0]">
-                        <sc-upload v-model="form.folders['carrinse/side'].entities[0]" :width="140" :height="180" accept="image/jpeg, image/png"></sc-upload>
-                    </el-form-item>
-                </el-col>
-            </el-row>
-        </el-form>
-    </el-main>
-
-    <el-footer>
-        <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
-        <el-button auto-insert-space @click="cancel">取消</el-button>
-    </el-footer>
-</template>
-
-<script setup>
-import moment from "moment";
-import XEUtils from "xe-utils";
-import API from "@/api";
-import TOOL from "@/utils/tool";
-import scUpload from "@/components/scUpload/index";
-import { carWashDic } from "@/views/dataMock/carwash/main";
-
-const $emit = defineEmits(["success", "closed"]);
-const loading = ref(false);
-const isSaving = ref(false);
-const carInfoList = ref([]);
-
-const shortcuts = [
-    { text: "上周", value: () => [moment().subtract(1, "week"), moment()] },
-    { text: "上月", value: () => [moment().subtract(1, "month"), moment()] },
-    { text: "去年", value: () => [moment().subtract(1, "year"), moment()] }
-]
-
-const projectName = ref(XEUtils.get(XEUtils.find(TOOL.data.get("PROJECT"), item => item.fpiId === TOOL.data.get("PROJECT_ID")), "projectName"))
-const form = ref({
-    fpiId: TOOL.data.get("PROJECT_ID"),
-    dateRange: [],
-    cpTimes: 1,
-    licensePlate: null,
-    licensePlateColor: null,
-    carType: null,
-    vehicleColor: null,
-    alarmType: 3,
-    folders: {
-        "carrinse/attach": {
-            entities: [{}]
-        },
-
-        "carrinse/side": {
-            entities: [{}]
-        }
-    }
-});
-
-const rules = reactive({
-    dateRange: [{ required: true, message: "请选择时间范围" }],
-    cpTimes: [{ required: true, message: "请输入循环次数" }],
-    licensePlate: [{ required: true, message: "请输入车牌号" }],
-    licensePlateColor: [{ required: true, message: "请选择车牌颜色" }],
-    carType: [{ required: true, message: "请选择车辆类型" }],
-    vehicleColor: [{ required: true, message: "请选择车辆颜色" }],
-    alarmType: [{ required: true, message: "请选择识别结果" }],
-    "folders[carrinse/attach].entities[0]": [{ required: true, validator: (rule, value, callback) => {
-        if (XEUtils.isEmpty(value)) return callback(new Error("请上传车身清洗图片"));
-        callback();
-    }}],
-    "folders[carrinse/side].entities[0]": [{ required: true, validator: (rule, value, callback) => {
-        if (XEUtils.isEmpty(value)) return callback(new Error("请上传后盖密闭图片"));
-        callback();
-    }}]
-})
-
-const remoteMethod = plateNumberLike => {
-    if (plateNumberLike) {
-        loading.value = true;
-        API.carwash.carInfo.get({ current: 1, size: 99999, projectId: TOOL.data.get("PROJECT_ID"), plateNumberLike }).then(res => {
-            loading.value = false;
-            carInfoList.value = res.records || [];
-        }).catch(() => loading.value = false);
-    } else carInfoList.value = [];
-}
-
-const licensePlateChange = e => {
-    const carinfo = XEUtils.find(carInfoList.value, item => item.plateNumber == e);
-    if (carinfo) {
-        form.value.carType = carinfo.vehicleType
-        form.value.vehicleColor = carinfo.vehicleColor
-    }
-}
-
-const formRef = ref();
-const cancel = () => formRef.value.resetFields();
-const submit = () => {
-    formRef.value.validate(valid => {
-        if (valid) {
-            const data = XEUtils.omit(form.value, ["dateRange", "folders"]);
-            XEUtils.set(data, "beginCreateDate", XEUtils.first(form.value.dateRange));
-            XEUtils.set(data, "endCreateDate", XEUtils.last(form.value.dateRange));
-            XEUtils.set(data, "folders", {
-                "carrinse/attach": {
-                    entities: form.value.folders["carrinse/attach"].entities.map(item => ({ features: { ticket: item.path } }))
-                },
-                "carrinse/side": {
-                    entities: form.value.folders["carrinse/side"].entities.map(item => ({ features: { ticket: item.path } }))
-                }
-            });
-
-            isSaving.value = true;
-            API.carwash.makeData.add(data).then(res => {
-                isSaving.value = false;
-                ElMessage.success("操作成功");
-                cancel();
-            }).catch(() => isSaving.value = false);
-        } else {
-            return false;
-        }
-    });
-}
-</script>
-
-<style lang="scss" scoped>
-.el-main {background: #fff;}
-
-.el-form-item .el-input-number {width: 100%;}
-.el-form-item .el-input-number :deep(.el-input__inner) {text-align: unset;}
-
-.el-footer {display: flex;justify-content: flex-end;align-items: center;}
-</style>

+ 16 - 11
src/views/dataMock/carwash/components/info/detail.vue

@@ -1,31 +1,33 @@
 <template>
     <el-dialog v-model="visible" :title="titleMap[mode]" width="480" :close-on-click-modal="false" @closed="$emit('closed')">
-        <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
             <el-form-item label="车牌号" prop="plateNumber">
                 <el-input v-model="form.plateNumber" placeholder="请输入车牌号"></el-input>
             </el-form-item>
-            <el-form-item label="车辆类型:" prop="vehicleType">
+            <el-form-item label="车辆类型" prop="vehicleType">
                 <el-select v-model="form.vehicleType" placeholder="请选择车辆类型">
-                    <el-option v-for="(label, key) in carWashDic.carType" :key="key" :label="label" :value="key + ''"></el-option>
+                    <el-option v-for="(label, key) in carWashDic.carType" :key="key" :label="label" :value="key"></el-option>
                 </el-select>
             </el-form-item>
-            <el-form-item label="车辆颜色:" prop="vehicleColor">
+            <el-form-item label="车辆颜色" prop="vehicleColor">
                 <el-select v-model="form.vehicleColor" placeholder="请选择车辆颜色">
-                    <el-option v-for="(label, key) in carWashDic.carColor" :key="key" :label="label" :value="key + ''"></el-option>
+                    <el-option v-for="(label, key) in carWashDic.carColor" :key="key" :label="label" :value="key"></el-option>
                 </el-select>
             </el-form-item>
-            <el-form-item label="使用状态:" prop="isSupervise">
+            <el-form-item label="使用状态" prop="isSupervise">
                 <el-select v-model="form.isSupervise" placeholder="请选择使用状态">
-                    <el-option label="使用中" :value="true"></el-option>
-                    <el-option label="已拆除" :value="false"></el-option>
+                    <el-option v-for="(item, index) in carWashDic.isSupervise" :key="index" :label="item.label" :value="item.value"></el-option>
                 </el-select>
             </el-form-item>
-            <el-form-item label="数据来源:">
+            <el-form-item label="数据来源">
                 <el-select v-model="form.dataSources" clearable placeholder="请选择数据来源">
                     <el-option label="三方系统推送" :value="0"></el-option>
                     <el-option label="其他" :value="1"></el-option>
                 </el-select>
             </el-form-item>
+            <el-form-item label="创建时间" prop="createTime">
+                <el-date-picker v-model="form.createTime" type="datetime" :clearable="false" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择创建时间"></el-date-picker>
+            </el-form-item>
         </el-form>
 
         <template #footer>
@@ -36,6 +38,7 @@
 </template>
 
 <script setup>
+import moment from "moment";
 import XEUtils from "xe-utils";
 import API from "@/api";
 import TOOL from "@/utils/tool";
@@ -58,13 +61,15 @@ const form = ref({
     vehicleType: null,
     vehicleColor: null,
     isSupervise: null,
-    dataSources: null
+    dataSources: null,
+    createTime: moment().format("YYYY-MM-DD HH:mm:ss")
 });
 const rules = reactive({
     plateNumber: [{ required: true, message: "请输入车牌号" }],
     vehicleType: [{ required: true, message: "请选择车辆类型" }],
     vehicleColor: [{ required: true, message: "请选择车辆颜色" }],
-    isSupervise: [{ required: true, message: "请选择使用状态" }]
+    isSupervise: [{ required: true, message: "请选择使用状态" }],
+    createTime: [{ required: true, message: "请选择创建时间" }]
 })
 
 const open = () => visible.value = true;

+ 8 - 11
src/views/dataMock/carwash/components/info/index.vue

@@ -1,13 +1,5 @@
 <template>
     <scTable ref="xGridTable" :apiObj="$API.carwash.carInfo" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns">
-        <template #top>
-            <div class="vxe-table-top__button">
-                <el-button type="primary" @click="table_add">
-                    <template #icon><sc-iconify icon="ant-design:plus-outlined"></sc-iconify></template>新增
-                </el-button>
-            </div>
-        </template>
-
         <template #action="{ row }">
             <el-button type="primary" link @click="table_edit(row)">
                 <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
@@ -60,7 +52,7 @@ const formConfig = reactive({
         mapFormItemInput("plateNumberLike", "车牌号"),
         mapFormItemSelect("vehicleType", "车辆类型", selectConfig),
         mapFormItemSelect("vehicleColor", "车辆颜色", { ...selectConfig, options: carWashDic.carColor.map((label, value) => ({ label, value })) }),
-        mapFormItemSelect("isSupervise", "使用状态", { ...selectConfig, options: carWashDic.isSupervise })
+        mapFormItemSelect("isSupervise", "使用状态", { ...selectConfig, options: carWashDic.isSupervise.map(({ label, value }) => ({ label, value: XEUtils.toValueString(value) })) })
     ]
 })
 
@@ -75,13 +67,14 @@ const paramsColums = reactive([
 
 const columns = reactive([
     { type: "seq", width: 60 },
+    { type: "html", field: "projectName", title: "项目名称", minWidth: 160, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(TOOL.data.get("PROJECT"), item => item.fpiId == row.projectId), "projectName") },
     { type: "html", field: "plateNumber", title: "车牌号", minWidth: 120, sortable: true },
     { type: "html", field: "vehicleType", title: "车辆类型", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(carWashDic.carType, cellValue, "/") },
     { type: "html", field: "vehicleColor", title: "车辆颜色", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(carWashDic.carColor, cellValue, "/") },
     { type: "html", field: "status", title: "使用状态", minWidth: 100, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(carWashDic.isSupervise, item => item.value == row.isSupervise), "label", "/") },
-    { type: "html", field: "dataSources", title: "数据来源", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(carWashDic.dataSources, cellValue, "/") },
+    { type: "html", field: "dataSources", title: "数据来源", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(carWashDic.dataSources, cellValue, "/") },
     { type: "html", field: "createTime", title: "创建时间", minWidth: 160, sortable: true },
-    { title: "操作", width: 140, slots: { default: "action" } }
+    { title: "操作", width: 140, fixed: "right", slots: { default: "action" } }
 ])
 
 // 显示隐藏 筛选表单
@@ -118,4 +111,8 @@ const table_del = ({ id }) => {
         });
     });
 }
+
+defineExpose({
+    table_add
+})
 </script>

+ 8 - 1
src/views/dataMock/carwash/components/monos.vue

@@ -44,7 +44,7 @@ const daterangeConfig = reactive({
 
 const datetimerangeConfig = reactive({
     span: 7,
-    resetValue: [moment().startOf("month").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")],
+    resetValue: () => [moment().startOf("month").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")],
     props: {
         popperClass: "datetime-picker-popper",
         type: "datetimerange",
@@ -62,6 +62,7 @@ const toolbarConfig = reactive({
 const formConfig = reactive({
     data: {
         orderBy: "createTime_desc",
+        parentId: "0",
         projectId: TOOL.data.get("PROJECT_ID"),
         taskType: "car_rinse",
         dateRange: [],
@@ -78,6 +79,7 @@ const formConfig = reactive({
 const paramsColums = reactive([
     { column: "orderBy" },
     { column: "projectId" },
+    { column: "parentId" },
     { column: "taskType" },
     { column: "taskStatus" },
     { column: "planBeginTimeBegin", field: "dateRange[0]" },
@@ -94,6 +96,7 @@ const columns = reactive([
     { type: "html", field: "dateRange", title: "时间范围", minWidth: 190, sortable: true, formatter: ({ cellValue, row }) => cellValue || TOOL.dateFormat(row.planBeginTime, "YYYY-MM-DD") + " 至 " + TOOL.dateFormat(row.planEndTime, "YYYY-MM-DD") },
     { type: "html", field: "createTime", title: "创建时间", minWidth: 160, sortable: true },
     { type: "html", field: "finishTime", title: "任务终止/完成时间", minWidth: 160, sortable: true },
+    { field: "message", title: "失败原因", minWidth: 160 },
     { title: "操作", fixed: "right", minWidth: 100, align: "center", slots: { default: "action" } }
 ])
 
@@ -118,4 +121,8 @@ const table_stop = row => {
         });
     });
 }
+
+defineExpose({
+    refreshTable
+})
 </script>

+ 107 - 47
src/views/dataMock/carwash/components/record/detail.vue

@@ -1,31 +1,61 @@
 <template>
-    <el-dialog v-model="visible" :title="titleMap[mode]" width="480" :close-on-click-modal="false" @closed="$emit('closed')">
-        <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
-            <el-form-item label="车牌号" prop="plateNumber">
-                <el-input v-model="form.plateNumber" placeholder="请输入车牌号"></el-input>
-            </el-form-item>
-            <el-form-item label="车辆类型:" prop="vehicleType">
-                <el-select v-model="form.vehicleType" placeholder="请选择车辆类型">
-                    <el-option v-for="(label, key) in carWashDic.carType" :key="key" :label="label" :value="key + ''"></el-option>
-                </el-select>
-            </el-form-item>
-            <el-form-item label="车辆颜色:" prop="vehicleColor">
-                <el-select v-model="form.vehicleColor" placeholder="请选择车辆颜色">
-                    <el-option v-for="(label, key) in carWashDic.carColor" :key="key" :label="label" :value="key + ''"></el-option>
-                </el-select>
-            </el-form-item>
-            <el-form-item label="使用状态:" prop="isSupervise">
-                <el-select v-model="form.isSupervise" placeholder="请选择使用状态">
-                    <el-option label="使用中" :value="true"></el-option>
-                    <el-option label="已拆除" :value="false"></el-option>
-                </el-select>
-            </el-form-item>
-            <el-form-item label="数据来源:">
-                <el-select v-model="form.dataSources" clearable placeholder="请选择数据来源">
-                    <el-option label="三方系统推送" :value="0"></el-option>
-                    <el-option label="其他" :value="1"></el-option>
-                </el-select>
-            </el-form-item>
+    <el-dialog v-model="visible" title="修改" width="860" :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-row>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="车牌号" prop="licensePlate">
+                        <el-input v-model="form.licensePlate" placeholder="请输入车牌号"></el-input>
+                    </el-form-item>
+                </el-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="车辆类型" prop="carType">
+                        <el-select v-model="form.carType" placeholder="请选择车辆类型">
+                            <el-option v-for="(label, key) in carWashDic.carType" :key="key" :label="label" :value="key"></el-option>
+                        </el-select>
+                    </el-form-item>
+                </el-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="车牌颜色" prop="licensePlateColor">
+                        <el-select v-model="form.licensePlateColor" placeholder="请选择车牌颜色">
+                            <el-option v-for="(label, key) in carWashDic.plateColor" :key="key" :label="label" :value="key"></el-option>
+                        </el-select>
+                    </el-form-item>
+                </el-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="抓拍时间" prop="captureTime">
+                        <el-date-picker v-model="form.captureTime" type="datetime" :clearable="false" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择抓拍时间"></el-date-picker>
+                    </el-form-item>
+                </el-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="车辆入场时间">
+                        <el-date-picker v-model="form.enterTime" type="datetime" :clearable="false" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择车辆入场时间"></el-date-picker>
+                    </el-form-item>
+                </el-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="车辆出场时间">
+                        <el-date-picker v-model="form.leaveTime" type="datetime" :clearable="false" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择车辆出场时间"></el-date-picker>
+                    </el-form-item>
+                </el-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="识别结果" prop="alarmType">
+                        <el-select v-model="form.alarmType" placeholder="请选择识别结果">
+                            <el-option v-for="(item, index) in carWashDic.alarmType.slice(1)" :key="index" :label="item" :value="index + 1"></el-option>
+                        </el-select>
+                    </el-form-item>
+                </el-col>
+            </el-row>
+            <el-row>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="车身清洗图片" prop="folders[carrinse/attach].entities[0]">
+                        <sc-upload v-model="form.folders['carrinse/attach'].entities[0]" :width="140" :height="180" accept="image/jpeg, image/png"></sc-upload>
+                    </el-form-item>
+                </el-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="后盖密闭图片" prop="folders[carrinse/side].entities[0]">
+                        <sc-upload v-model="form.folders['carrinse/side'].entities[0]" :width="140" :height="180" accept="image/jpeg, image/png"></sc-upload>
+                    </el-form-item>
+                </el-col>
+            </el-row>
         </el-form>
 
         <template #footer>
@@ -40,50 +70,80 @@ import XEUtils from "xe-utils";
 import API from "@/api";
 import TOOL from "@/utils/tool";
 import { carWashDic } from "@/views/dataMock/carwash/main";
+import scUpload from "@/components/scUpload/index";
 
 const $emit = defineEmits(["success", "closed"]);
 const visible = ref(false);
 const isSaving = ref(false);
 
-const mode = ref("add");
-const titleMap = reactive({
-    add: "新增",
-    edit: "修改"
-});
-
 const form = ref({
     id: null,
-    projectId: TOOL.data.get("PROJECT_ID"),
-    plateNumber: null,
-    vehicleType: null,
-    vehicleColor: null,
-    isSupervise: null,
-    dataSources: null
+    carType: null,
+    licensePlate: null,
+    licensePlateColor: null,
+    captureTime: null,
+    enterTime: null,
+    leaveTime: null,
+    alarmType: null,
+    folders: {
+        "carrinse/attach": {
+            entities: [{}]
+        },
+
+        "carrinse/side": {
+            entities: [{}]
+        }
+    }
 });
 const rules = reactive({
-    plateNumber: [{ required: true, message: "请输入车牌号" }],
-    vehicleType: [{ required: true, message: "请选择车辆类型" }],
-    vehicleColor: [{ required: true, message: "请选择车辆颜色" }],
-    isSupervise: [{ required: true, message: "请选择使用状态" }]
+    licensePlate: [{ required: true, message: "请输入车牌号" }],
+    carType: [{ required: true, message: "请选择车辆类型" }],
+    licensePlateColor: [{ required: true, message: "请选择车牌颜色" }],
+    captureTime: [{ required: true, message: "请选择抓拍时间" }],
+    alarmType: [{ required: true, message: "请选择识别结果" }],
+    "folders[carrinse/attach].entities[0]": [{ required: true, validator: (rule, value, callback) => {
+        if (XEUtils.isEmpty(value)) return callback(new Error("请上传车身清洗图片"));
+        callback();
+    }}],
+    "folders[carrinse/side].entities[0]": [{ required: true, validator: (rule, value, callback) => {
+        if (XEUtils.isEmpty(value)) return callback(new Error("请上传后盖密闭图片"));
+        callback();
+    }}]
 })
 
 const open = () => visible.value = true;
 const setData = data => {
     open();
-    mode.value = "edit";
-    XEUtils.objectEach(form.value, (_, key) => XEUtils.set(form.value, key, XEUtils.get(data, key)));
+    XEUtils.objectEach(form.value, (_, key) => {
+        if (key == "folders") {
+            XEUtils.objectEach(form.value.folders, (_, folder_key) => {
+                XEUtils.set(form.value, `${key}.${folder_key}.entities`, XEUtils.map(XEUtils.get(XEUtils.toStringJSON(data), `${key}.${folder_key}.entities`), ({ id, mineType, name, path }) => ({ id, mineType, name, path })))
+            });
+        } else XEUtils.set(form.value, key, XEUtils.get(XEUtils.toStringJSON(data), key));
+    });
 }
 
 const formRef = ref();
 const submit = () => {
     formRef.value.validate(valid => {
         if (valid) {
+            const folders = {}, data = XEUtils.omit(form.value, "folders");
+            XEUtils.objectEach(form.value.folders, (item, key) => {
+                const newFiles = item.entities.filter(item => !item.id).map(item => item.path);
+                if (newFiles.length) {
+                    XEUtils.set(folders, key, {
+                        entities: newFiles.map(ticket => ({ features: { ticket } }))
+                    })
+                }
+            })
+            !XEUtils.isEmpty(folders) && XEUtils.set(data, "folders", folders);
+
             isSaving.value = true;
-            API.carwash.carInfo[mode.value](form.value).then(res => {
+            API.carwash.record.edit(data).then(() => {
                 isSaving.value = false;
                 ElMessage.success("操作成功");
                 visible.value = false;
-                $emit("success", mode.value);
+                $emit("success");
             }).catch(() => isSaving.value = false);
         } else {
             return false;

+ 40 - 13
src/views/dataMock/carwash/components/record/index.vue

@@ -1,7 +1,9 @@
 <template>
-    <scTable ref="xGridTable" :apiObj="$API.carwash.record" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns">
+    <scTable ref="xGridTable" batchDel :apiObj="$API.carwash.record" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns" v-bind="props.options">
         <template #default_imgUrl="{ row, column }">
-            <vxe-image v-if="XEUtils.get(row, `folders.${column.field}.entities[0].path`)" :src="'/api/folder/' + XEUtils.get(row, `folders.${column.field}.entities[0].path`)" width="40" height="40"></vxe-image>
+            <template v-if="XEUtils.get(row, `folders.${column.field}.entities[0].path`)">
+                <vxe-image :src="'/api/folder/' + XEUtils.get(row, `folders.${column.field}.entities[0].path`)" width="40" height="40" :toolbar-config="imageToolbar"></vxe-image>
+            </template>
         </template>
         
         <template #action="{ row }">
@@ -13,6 +15,8 @@
             </el-button>
         </template>
     </scTable>
+
+    <record-detail v-if="dialog" ref="recordRef" @success="refreshTable" @closed="dialog = false"></record-detail>
 </template>
 
 <script setup>
@@ -22,6 +26,12 @@ import API from "@/api";
 import TOOL from "@/utils/tool";
 import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker } from "@/components/scTable/helper";
 import { carWashDic } from "@/views/dataMock/carwash/main";
+import recordDetail from "./detail";
+
+const props = defineProps({
+    options: { type: Object, default: () => {} },
+    hideHandler: { type: Boolean, default: false }
+})
 
 const proConfig = reactive({
     storageKey: "PROJECT",
@@ -41,9 +51,8 @@ const selectConfig = reactive({
 
 const datetimerangeConfig = reactive({
     span: 7,
-    resetValue: [moment().startOf("month").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")],
+    resetValue: () => [moment().startOf("month").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")],
     props: {
-        popperClass: "datetime-picker-popper",
         type: "datetimerange",
         startPlaceholder: "开始时间",
         endPlaceholder: "结束时间",
@@ -58,29 +67,31 @@ const toolbarConfig = reactive({
 
 const formConfig = reactive({
     data: {
+        orderBy: "createTime_desc",
         projectId: TOOL.data.get("PROJECT_ID"),
         createTime: [moment().startOf("month").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")]
     },
     items: [
         mapFormItemSelect("projectId", "所属项目", proConfig),
-        mapFormItemInput("licensePlate", "车牌号"),
+        mapFormItemDatePicker("createTime", "抓拍时间", datetimerangeConfig),
+        mapFormItemInput("licensePlate", "车牌号", { span: 5 }),
         mapFormItemSelect("carType", "车辆类型", selectConfig),
         mapFormItemSelect("alarmType", "识别结果", { ...selectConfig, options: carWashDic.alarmType.map((label, value) => ({ label, value })) }),
-        mapFormItemDatePicker("createTime", "抓拍时间", datetimerangeConfig)
     ]
 })
 
 const paramsColums = reactive([
+    { column: "orderBy" },
     { column: "fpiId", field: "projectId" },
     { column: "licensePlate" },
     { column: "carType" },
     { column: "alarmType" },
-    { column: "beginCaptureTime", field: "createTime[0]" },
-    { column: "endCaptureTime", field: "createTime[1]" }
+    { column: "captureTimeBegin", field: "createTime[0]" },
+    { column: "captureTimeEnd", field: "createTime[1]" }
 ])
 
 const columns = reactive([
-    { type: "seq", width: 60 },
+    { type: "seq", fixed: "left", width: 60 },
     { type: "html", field: "captureTime", title: "抓拍时间", minWidth: 160, sortable: true },
     { type: "html", field: "enterTime", title: "车辆入场时间", minWidth: 160, sortable: true },
     { type: "html", field: "leaveTime", title: "车辆出场时间", minWidth: 160, sortable: true },
@@ -90,9 +101,14 @@ const columns = reactive([
     { type: "html", field: "alarmType", title: "识别结果", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(carWashDic.alarmType, cellValue, cellValue) },
     { field: "carrinse/attach", title: "车身清洗图片", minWidth: 110, align: "center", slots: { default: "default_imgUrl" } },
     { field: "carrinse/side", title: "后盖密闭图片", minWidth: 110, align: "center", slots: { default: "default_imgUrl" } },
-    { title: "操作", fixed: "right", width: 140, align: "center", slots: { default: "action" } }
+    { visible: !props.hideHandler, title: "操作", fixed: "right", width: 140, align: "center", slots: { default: "action" } }
 ])
 
+const imageToolbar = reactive({
+    print: false,
+    download: true
+})
+
 // 显示隐藏 筛选表单
 const xGridTable = ref();
 const toggleFormEnabled = () => xGridTable.value.toggleFormEnabled();
@@ -102,16 +118,27 @@ const refreshTable = () => {
     xGridTable.value.searchData();
 }
 
-const table_stop = row => {
-    ElMessageBox.confirm("是否确认终止该任务?", "提示", {
+const recordRef = ref();
+const dialog = ref(false);
+const table_edit = row => {
+    dialog.value = true;
+    nextTick(() => recordRef.value?.setData(row));
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该监控记录?", "删除警告", {
         type: "warning",
         confirmButtonText: "确定",
         cancelButtonText: "取消"
     }).then(() => {
-        API.common.opsTask.stop({ id: row.id }).then(() => {
+        API.carwash.record.del({ id }).then(() => {
             ElMessage.success("操作成功");
             refreshTable();
         });
     });
 }
+
+defineExpose({
+    refreshTable
+})
 </script>

+ 104 - 0
src/views/dataMock/carwash/components/template/detail.vue

@@ -0,0 +1,104 @@
+<template>
+    <el-dialog v-model="visible" :title="titleMap[mode]" width="480" :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-form-item label="车牌号" prop="plateNumber">
+                <el-input v-model="form.plateNumber" placeholder="请输入车牌号"></el-input>
+            </el-form-item>
+            <el-form-item label="车辆类型:" prop="vehicleType">
+                <el-select v-model="form.vehicleType" placeholder="请选择车辆类型">
+                    <el-option v-for="(label, key) in carWashDic.carType" :key="key" :label="label" :value="key + ''"></el-option>
+                </el-select>
+            </el-form-item>
+            <el-form-item label="车辆颜色:" prop="vehicleColor">
+                <el-select v-model="form.vehicleColor" placeholder="请选择车辆颜色">
+                    <el-option v-for="(label, key) in carWashDic.carColor" :key="key" :label="label" :value="key + ''"></el-option>
+                </el-select>
+            </el-form-item>
+            <el-form-item label="使用状态:" prop="isSupervise">
+                <el-select v-model="form.isSupervise" placeholder="请选择使用状态">
+                    <el-option label="使用中" :value="true"></el-option>
+                    <el-option label="已拆除" :value="false"></el-option>
+                </el-select>
+            </el-form-item>
+            <el-form-item label="数据来源:">
+                <el-select v-model="form.dataSources" clearable placeholder="请选择数据来源">
+                    <el-option label="三方系统推送" :value="0"></el-option>
+                    <el-option label="其他" :value="1"></el-option>
+                </el-select>
+            </el-form-item>
+        </el-form>
+
+        <template #footer>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { carWashDic } from "@/views/dataMock/carwash/main";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const mode = ref("add");
+const titleMap = reactive({
+    add: "新增",
+    edit: "修改"
+});
+
+const form = ref({
+    id: null,
+    projectId: TOOL.data.get("PROJECT_ID"),
+    plateNumber: null,
+    vehicleType: null,
+    vehicleColor: null,
+    isSupervise: null,
+    dataSources: null
+});
+const rules = reactive({
+    plateNumber: [{ required: true, message: "请输入车牌号" }],
+    vehicleType: [{ required: true, message: "请选择车辆类型" }],
+    vehicleColor: [{ required: true, message: "请选择车辆颜色" }],
+    isSupervise: [{ required: true, message: "请选择使用状态" }]
+})
+
+const open = () => visible.value = true;
+const setData = data => {
+    open();
+    mode.value = "edit";
+    XEUtils.objectEach(form.value, (_, key) => XEUtils.set(form.value, key, XEUtils.get(data, key)));
+}
+
+const formRef = ref();
+const submit = () => {
+    formRef.value.validate(valid => {
+        if (valid) {
+            isSaving.value = true;
+            API.carwash.carInfo[mode.value](form.value).then(res => {
+                isSaving.value = false;
+                ElMessage.success("操作成功");
+                visible.value = false;
+                $emit("success", mode.value);
+            }).catch(() => isSaving.value = false);
+        } else {
+            return false;
+        }
+    });
+}
+
+defineExpose({
+    open,
+    setData
+})
+</script>
+
+<style scoped>
+.el-form {
+    padding-right: calc(var(--el-dialog-padding-primary) + var(--el-message-close-size, 16px));
+}
+</style>

+ 117 - 0
src/views/dataMock/carwash/components/template/index.vue

@@ -0,0 +1,117 @@
+<template>
+    <scTable ref="xGridTable" :apiObj="$API.carwash.record" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns">
+        <template #default_imgUrl="{ row, column }">
+            <vxe-image v-if="XEUtils.get(row, `folders.${column.field}.entities[0].path`)" :src="'/api/folder/' + XEUtils.get(row, `folders.${column.field}.entities[0].path`)" width="40" height="40"></vxe-image>
+        </template>
+        
+        <template #action="{ row }">
+            <el-button type="primary" link @click="table_edit(row)">
+                <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
+            </el-button>
+            <el-button type="primary" link @click="table_del(row)">
+                <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+            </el-button>
+        </template>
+    </scTable>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker } from "@/components/scTable/helper";
+import { carWashDic } from "@/views/dataMock/carwash/main";
+
+const proConfig = reactive({
+    storageKey: "PROJECT",
+    resetValue: TOOL.data.get("PROJECT_ID"),
+    optionProps: { label: "projectName", value: "fpiId" },
+    events: {
+        change: data => XEUtils.assign(formConfig.data, data)
+    }
+})
+
+const selectConfig = reactive({
+    options: carWashDic.carType.map((label, value) => ({ label, value })),
+    events: {
+        change: data => XEUtils.merge(formConfig.data, data)
+    }
+})
+
+const datetimerangeConfig = reactive({
+    span: 7,
+    resetValue: () => [moment().startOf("month").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")],
+    props: {
+        popperClass: "datetime-picker-popper",
+        type: "datetimerange",
+        startPlaceholder: "开始时间",
+        endPlaceholder: "结束时间",
+        format: "YYYY-MM-DD HH:mm"
+    }
+})
+
+const toolbarConfig = reactive({
+    enabled: true,
+    print: false
+})
+
+const formConfig = reactive({
+    data: {
+        projectId: TOOL.data.get("PROJECT_ID"),
+        createTime: [moment().startOf("month").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")]
+    },
+    items: [
+        mapFormItemSelect("projectId", "所属项目", proConfig),
+        mapFormItemInput("licensePlate", "车牌号"),
+        mapFormItemSelect("carType", "车辆类型", selectConfig),
+        mapFormItemSelect("alarmType", "识别结果", { ...selectConfig, options: carWashDic.alarmType.map((label, value) => ({ label, value })) }),
+        mapFormItemDatePicker("createTime", "抓拍时间", datetimerangeConfig)
+    ]
+})
+
+const paramsColums = reactive([
+    { column: "fpiId", field: "projectId" },
+    { column: "licensePlate" },
+    { column: "carType" },
+    { column: "alarmType" },
+    { column: "beginCaptureTime", field: "createTime[0]" },
+    { column: "endCaptureTime", field: "createTime[1]" }
+])
+
+const columns = reactive([
+    { type: "seq", width: 60 },
+    { type: "html", field: "captureTime", title: "抓拍时间", minWidth: 160, sortable: true },
+    { type: "html", field: "enterTime", title: "车辆入场时间", minWidth: 160, sortable: true },
+    { type: "html", field: "leaveTime", title: "车辆出场时间", minWidth: 160, sortable: true },
+    { type: "html", field: "licensePlate", title: "车牌号", minWidth: 120, sortable: true },
+    { type: "html", field: "licensePlateColor", title: "车牌颜色", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(carWashDic.plateColor, cellValue, cellValue) },
+    { type: "html", field: "carType", title: "车辆类型", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(carWashDic.carType, cellValue, cellValue) },
+    { type: "html", field: "alarmType", title: "识别结果", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(carWashDic.alarmType, cellValue, cellValue) },
+    { field: "carrinse/attach", title: "车身清洗图片", minWidth: 110, align: "center", slots: { default: "default_imgUrl" } },
+    { field: "carrinse/side", title: "后盖密闭图片", minWidth: 110, align: "center", slots: { default: "default_imgUrl" } },
+    { title: "操作", fixed: "right", width: 140, align: "center", slots: { default: "action" } }
+])
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const toggleFormEnabled = () => xGridTable.value.toggleFormEnabled();
+
+const refreshTable = () => {
+    xGridTable.value.reloadColumn(columns);
+    xGridTable.value.searchData();
+}
+
+const table_stop = row => {
+    ElMessageBox.confirm("是否确认终止该任务?", "提示", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.common.opsTask.stop({ id: row.id }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    });
+}
+</script>

+ 179 - 0
src/views/dataMock/carwash/detail.vue

@@ -0,0 +1,179 @@
+<template>
+    <el-dialog v-model="visible" title="数据模拟" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="110">
+            <el-row>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="模拟项目" prop="targetProjectId">
+                        <el-select v-model="form.targetProjectId" filterable placeholder="请选择模拟项目" @change="dataTimeRange">
+                            <el-option v-for="item in $TOOL.data.get('PROJECT')" :key="item.fpiId" :label="item.projectName" :value="item.fpiId"></el-option>
+                        </el-select>
+                    </el-form-item>
+                </el-col>
+                <el-col v-if="form.targetProjectId" :md="12" :xs="24">
+                    <el-form-item label="数据时间范围">
+                        <template v-if="XEUtils.isEmpty(acceptItem)">该项目未配置验收清单,<el-button type="primary" link @click="$router.push('/basic/acceptItems')">去配置</el-button></template>
+                        <template v-else-if="acceptItem.beginTime">{{ $TOOL.dateFormat(acceptItem.beginTime, "YY.M.D") }}<span>-{{ acceptItem.endTime && $TOOL.dateFormat(acceptItem.endTime, "YY.M.D") || "至今" }}</span></template>
+                        <template v-else>该项目未配置数据时间范围,<el-button type="primary" link @click="$router.push('/basic/project')">去配置</el-button></template>
+                    </el-form-item>
+                </el-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="模拟年份" prop="targetYear">
+                        <el-date-picker v-model="form.targetYear" type="year" value-format="YYYY" format="YYYY" placeholder="请选择模拟年份" />
+                    </el-form-item>
+                </el-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="模拟月份">
+                        <el-select v-model="form.targetMonth" filterable placeholder="请选择模拟月份">
+                            <el-option v-for="item in 12" :key="item" :label="item + '月'" :value="XEUtils.padStart(item, 2, '0')"></el-option>
+                        </el-select>
+                    </el-form-item>
+                </el-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="数据处理" prop="isCover">
+                        <el-radio-group v-model="form.isCover">
+                            <el-radio :value="false">重复新增</el-radio>
+                            <el-radio :value="true">数据覆盖</el-radio>
+                        </el-radio-group>
+                    </el-form-item>
+                </el-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item style="margin-bottom: 0;" label="数据来源" prop="source">
+                        <el-radio-group v-model="form.source">
+                            <el-radio value="template">模版项目</el-radio>
+                            <el-radio value="other">其他项目</el-radio>
+                        </el-radio-group>
+                    </el-form-item>
+                </el-col>
+            </el-row>
+
+            <el-divider />
+
+            <el-row>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="数据源项目" prop="sourceProjectId">
+                        <el-select v-model="form.sourceProjectId" filterable placeholder="请选择数据源项目" @change="refreshTable">
+                            <el-option v-for="item in $TOOL.data.get('PROJECT')" :key="item.fpiId" :label="item.projectName" :value="item.fpiId"></el-option>
+                        </el-select>
+                    </el-form-item>
+                </el-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="抓拍时间" prop="sourceTime">
+                        <!-- <vxe-date-range-picker :start-value="form.sourceBeginTime" :end-value="form.sourceEndTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="请选择抓拍时间" @change="refreshTable"></vxe-date-range-picker> -->
+                        <el-date-picker v-model="form.sourceTime" type="datetimerange" :clearable="false" value-format="YYYY-MM-DD HH:mm:ss" :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" :show-footer="false" @change="refreshTable"></el-date-picker>
+                    </el-form-item>
+                </el-col>
+            </el-row>
+
+            <data-table ref="tableRef" hideHandler :options="tableOptions"></data-table>
+        </el-form>
+
+        <template #footer>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import dataTable from "./components/record";
+
+const route = useRoute();
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const form = ref({
+    targetProjectId: TOOL.data.get("PROJECT_ID"),
+    targetYear: null,
+    targetMonth: null,
+    isCover: false,
+    source: "other",
+    sourceProjectId: 11,
+    sourceTime: ["2025-08-01 00:00:00", "2025-08-22 16:00:00"]
+});
+
+const rules = reactive({
+    targetProjectId: [{ required: true, message: "请选择模拟项目" }],
+    targetYear: [{ required: true, message: "请选择模拟年份" }],
+    isCover: [{ required: true }],
+    source: [{ required: true }],
+    sourceProjectId: [{ required: true, message: "请选择数据源项目" }],
+    sourceTime: [{ required: true, message: "请选择数据源抓拍时间" }],
+})
+
+const tableRef = ref();
+const tableOptions = computed(() => ({
+    batchDel: false,
+    maxHeight: 1048,
+    toolbarConfig: { enabled: true, print: false, zoom: false },
+    paramsColums: [
+        { column: "fpiId", field: "sourceProjectId" },
+        { column: "captureTimeBegin", field: "sourceTime[0]" },
+        { column: "captureTimeEnd", field: "sourceTime[1]" }
+    ],
+    formConfig: { enabled: false, data: XEUtils.pick(form.value, "sourceProjectId", "sourceTime") },
+}))
+const refreshTable = () => tableRef.value.refreshTable();
+
+
+const acceptItem = ref({});
+const dataTimeRange = async () => {
+    const query = {
+        projectId: form.value.targetProjectId,
+        itemName: XEUtils.last(route.meta.title.split("-"))
+    }
+    const res = await API.system.project.bindItem.judgment(query);
+    acceptItem.value = res || {};
+}
+
+const open = () => {
+    visible.value = true;
+    TOOL.data.get("PROJECT_ID") && dataTimeRange();
+}
+
+const formRef = ref();
+const submit = () => {
+    formRef.value.validate(valid => {
+        if (valid) {
+            const data = XEUtils.omit(form.value, "source", "sourceTime");
+            XEUtils.set(data, "sourceBeginTime", XEUtils.first(form.value.sourceTime))
+            XEUtils.set(data, "sourceEndTime", XEUtils.last(form.value.sourceTime))
+            
+            isSaving.value = true;
+            API.carwash.copyData.add(data).then(() => {
+                isSaving.value = false;
+                ElMessage.success("操作成功");
+                visible.value = false;
+                $emit("success");
+            }).catch(() => isSaving.value = false);
+        } else {
+            return false;
+        }
+    });
+}
+
+defineExpose({
+    open
+})
+</script>
+
+<style lang="scss" scoped>
+.el-form {padding-right: var(--el-message-close-size, 16px);}
+
+.el-form-item .el-input-number {width: 100%;}
+.el-form-item .el-input-number :deep(.el-input__prefix) {margin-right: 8px;}
+.el-form-item .el-input-number :deep(.el-input__inner) {text-align: unset;}
+.el-form-item .el-input-number + .el-form-item {margin-left: 20px;}
+    
+.el-form-item.step-item {
+    .el-input-number {flex: 1;}
+    .el-form-item {width: 100px;}
+}
+
+.el-form-item .el-radio-group {flex-wrap: nowrap;}
+
+.el-form :deep(.el-main) {padding-right: 0;padding-bottom: 0;}
+</style>

+ 29 - 3
src/views/dataMock/carwash/index.vue

@@ -1,20 +1,46 @@
 <template>
 	<el-container class="is-vertical">
-        <sc-page-header></sc-page-header>
+        <sc-page-header addText="数据模拟" @add="mock_add">
+            <template #extra-right>
+                <el-button v-if="activeName == 'info'" type="primary" @click="table_add">车辆录入</el-button>
+            </template>
+        </sc-page-header>
 
         <el-tabs v-model="activeName">
             <el-tab-pane v-for="(label, key) in workerStates" :key="key" :label="label" :name="key"></el-tab-pane>
         </el-tabs>
 
-        <component :is="allcomp[activeName]" />
+        <component ref="componentRef" :is="allcomp[activeName]" />
 	</el-container>
+
+    <mock-detail v-if="dialog" ref="mockRef" @success="refreshState" @closed="dialog = false"></mock-detail>
 </template>
 
 <script setup>
+import XEUtils from "xe-utils";
+import API from "@/api";
+import TOOL from "@/utils/tool";
 import { workerStates } from "./main";
 import allcomp from "./components";
+import mockDetail from "./detail";
+
+const activeName = ref("info");
+
+const componentRef = ref();
+const mockRef = ref();
+const dialog = ref(false);
+
+const table_add = () => componentRef.value.table_add();
+
+const mock_add = () => {
+    dialog.value = true;
+    nextTick(() => mockRef.value?.open());
+}
 
-const activeName = ref("monos");
+const refreshState = () => {
+    if (activeName.value == "monos") componentRef.value.refreshTable();
+    activeName.value = "monos";
+}
 </script>
 
 <style lang="scss" scoped>

+ 2 - 2
src/views/dataMock/carwash/main.js

@@ -1,8 +1,8 @@
 export const workerStates = {
-    monos: "任务中心",
     info: "车辆信息",
     record: "设备监控",
-    form: "数据模拟"
+    monos: "任务中心",
+    template: "模版项目"
 }
 
 export const carWashDic = {

+ 1 - 1
src/views/dataMock/env/components/record.vue

@@ -21,7 +21,7 @@ const proConfig = reactive({
 
 const daterangeConfig = reactive({
     span: 9,
-    resetValue: [moment().startOf("day").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")],
+    resetValue: () => [moment().startOf("day").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")],
     props: {
         type: "datetimerange",
         clearable: false,

+ 2 - 2
src/views/dataMock/env/detail.vue

@@ -1,6 +1,6 @@
 <template>
-    <el-dialog v-model="visible" title="数据模拟" width="870" :close-on-click-modal="false" @closed="$emit('closed')">
-        <el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
+    <el-dialog v-model="visible" title="数据模拟" width="860" :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
             <el-row>
                 <el-col :md="12" :xs="24">
                     <el-form-item label="时间范围" prop="dateRange">

+ 1 - 1
src/views/dataMock/standard/components/record.vue

@@ -22,7 +22,7 @@ const proConfig = reactive({
 
 const daterangeConfig = reactive({
     span: 9,
-    resetValue: [moment().startOf("day").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")],
+    resetValue: () => [moment().startOf("day").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")],
     props: {
         type: "datetimerange",
         clearable: false,

+ 60 - 35
src/views/home/index.vue

@@ -28,26 +28,47 @@
         </el-card>
 
         <el-card class="home-menu data-mock-card" shadow="never">
-            <template #header>数据管理与模拟
-                <el-input v-model="formData.title" placeholder="输入名称"></el-input>
-                <el-select v-model="formData.projectId" filterable placeholder="选择项目" @change="projectChange">
-                    <el-option v-for="item in projects" :key="item.fpiId" :label="item.projectName" :value="item.fpiId"></el-option>
-                </el-select>
+            <template #header>数据管理
+                <el-input v-model="queryName" clearable placeholder="输入名称"></el-input>
             </template>
+        
+            <el-row v-if="$store.state.project.projectId" v-loading="loading" :gutter="40">
+                <el-col v-if="!acceptItem.length" :xs="24">
+                    <el-empty description="暂无验收项">
+                        <el-button type="primary" @click="$router.push('/basic/project')">维护验收清单</el-button>
+                    </el-empty>
+                </el-col>
 
-            <el-row :gutter="40">
-                <el-col :md="6" :xs="24" v-for="item in filterAcceptItem" :key="item.id">
-                    <div :class="['menu-item', item.beginTime && 'has-time']" @click="dataMockRoute(item.name) && $router.push(dataMockRoute(item.name).path)">
+                <el-col v-else :md="6" :xs="24" v-for="item in filterAcceptItem" :key="item.id">
+                    <div :class="['menu-item', item.beginTime && 'has-time']" @click="formatAcceptRoute(item.name) && $router.push(formatAcceptRoute(item.name).path)">
                         <div class="menu-item__content">
-                            <sc-iconify :icon="XEUtils.get(dataMockRoute(item.name), 'meta.icon')" size="26"></sc-iconify>
+                            <sc-iconify :icon="XEUtils.get(formatAcceptRoute(item.name), 'meta.icon')" size="26"></sc-iconify>
                             <div class="menu-item__tooltip"><scTooltip :content="item.name"></scTooltip></div>
                         </div>
-                        <div v-if="item.beginTime" class="diagnosis">{{ TOOL.dateFormat(item.beginTime, "YY.M.D") }}<span>-{{ item.endTime && TOOL.dateFormat(item.endTime, "YY.M.D") || "至今" }}</span></div>
+                        <div v-if="item.beginTime" class="diagnosis">{{ $TOOL.dateFormat(item.beginTime, "YY.M.D") }}<span>-{{ item.endTime && $TOOL.dateFormat(item.endTime, "YY.M.D") || "至今" }}</span></div>
                     </div>
                     <div class="tags-bottom">
                         <!-- 新增标签 -->
                         <el-scrollbar height="36px">
-                            <el-tag v-for="(tagItem, tagIndex) in XEUtils.get(diagnosisDic, `${XEUtils.get(dataMockRoute(item.name), 'name')}`, [])" :key="tagIndex" :type="tagItem.type" effect="plain" round>{{ tagItem.name }}</el-tag>
+                            <el-tag v-for="(tagItem, tagIndex) in XEUtils.get(diagnosisDic, `${XEUtils.get(formatAcceptRoute(item.name), 'name')}`, [])" :key="tagIndex" :type="tagItem.type" effect="plain" round>{{ tagItem.name }}</el-tag>
+                        </el-scrollbar>
+                    </div>
+                </el-col>
+            </el-row>
+
+            <!-- 全部项目展示dataMock路由 -->
+            <el-row v-else :gutter="40">
+                <el-col :md="6" :xs="24" v-for="item in filterDataMock" :key="item.id">
+                    <div class="menu-item" @click="$router.push(item.path)">
+                        <div class="menu-item__content">
+                            <sc-iconify :icon="XEUtils.get(item, 'meta.icon')" size="26"></sc-iconify>
+                            <div class="menu-item__tooltip"><scTooltip :content="item.title"></scTooltip></div>
+                        </div>
+                    </div>
+                    <div class="tags-bottom">
+                        <!-- 新增标签 -->
+                        <el-scrollbar height="36px">
+                            <el-tag v-for="(tagItem, tagIndex) in XEUtils.get(diagnosisDic, item.name, [])" :key="tagIndex" :type="tagItem.type" effect="plain" round>{{ tagItem.name }}</el-tag>
                         </el-scrollbar>
                     </div>
                 </el-col>
@@ -59,54 +80,58 @@
 <script setup>
 import XEUtils from "xe-utils";
 import API from "@/api";
+import STORE from "@/store";
 import TOOL from "@/utils/tool";
 import userRoutes from "@/config/route";
 import { diagnosisDic } from "./main"; // 诊断对象
-import { nextTick } from "vue";
 
 const day = ref("");
 const time = ref("");
 const week = ref("");
 
-const projects = ref(TOOL.data.get("PROJECT"));
-const formData = ref({
-    title: "",
-    projectId: TOOL.data.get("PROJECT_ID")
-});
-
 // 数据管理与模拟路由
+const queryName = ref("");
+const loading = ref(false);
 const acceptItem = ref([]);
-const dataMockRoute = name => XEUtils.get(XEUtils.findTree(userRoutes, item => XEUtils.last(XEUtils.get(item, "meta.title").split("-")) == name), "item")
-const filterAcceptItem = computed(() => XEUtils.filter(acceptItem.value, item => item.name.includes(formData.value.title)));
+const dataMockRoute = ref(XEUtils.map(XEUtils.get(userRoutes.find(item => item.name == "dataMock"), "children", []), item => ({ ...item, title: XEUtils.last(XEUtils.get(item, "meta.title", "").split("-")) })));
+const formatAcceptRoute = name => XEUtils.find(dataMockRoute.value, item => item.title == name);
+const filterAcceptItem = computed(() => XEUtils.filter(acceptItem.value, item => item.name.includes(queryName.value)));
+const filterDataMock = computed(() => XEUtils.filter(dataMockRoute.value, item => item.title.includes(queryName.value)));
+
+
 
 // 设备预警 --> 路由
 const equipmentItem = ref(XEUtils.map(XEUtils.get(userRoutes.find(item => item.name == "equipment"), "children", []), item => ({ ...item, title: XEUtils.last(XEUtils.get(item, "meta.title", "").split("-")) })));
 
-onMounted(() => {
-    showTime();
-    setInterval(() => showTime(), 1000);
-    window.addEventListener("setItemEvent", ({ key, newValue }) => {
-        if (key == "PROJECT" && newValue) projects.value = JSON.parse(newValue).content;
-    });
-    projectChange();
-});
-onUnmounted(() => window.removeEventListener("setItemEvent", () => {}));
-
 const showTime = () => {
     day.value = TOOL.dateFormat(new Date(), "YYYY年MM月DD日");
     time.value = TOOL.dateFormat(new Date(), "HH:mm:ss");
     week.value = TOOL.dateFormat(new Date(), "dddd");
 }
 
-const projectChange = async projectId => {
-    projectId && TOOL.data.set("PROJECT_ID", projectId);
+const projectChange = projectId => {
+    projectId && getItems(projectId);
+    if (!projectId) {
+        acceptItem.value = [];
+    }
+}
 
+const getItems = projectId => {
     // 先查询当前项目的验收清单
-    const infoRes = await API.system.project.bindItem.get({ projectId: formData.value.projectId });
-    const sortArr = XEUtils.orderBy(XEUtils.get(infoRes, "data", []), [["item.itemCategory", "asc"], ["item.createTime", "asc"]]);
-    acceptItem.value = XEUtils.map(sortArr, item => ({ ...item, name: XEUtils.get(item, "item.acceptItem", "") }));
+    loading.value = true;
+    API.system.project.bindItem.get({ projectId }).then(res => {
+        loading.value = false;
+        const sortArr = XEUtils.orderBy(XEUtils.get(res, "data", []), [["item.itemCategory", "asc"], ["item.createTime", "asc"]]);
+        acceptItem.value = XEUtils.map(sortArr, item => ({ ...item, name: XEUtils.get(item, "item.acceptItem", "") }));
+    }).catch(() => loading.value = false);
     // 再查询每一项验收清单的诊断结果
 }
+
+onMounted(() => {
+    showTime();
+    setInterval(() => showTime(), 1000);
+});
+watch(() => STORE.state.project.projectId, value => projectChange(value), { immediate: true });
 </script>
 
 <style lang="scss" scoped>

+ 1 - 0
src/views/login/index.vue

@@ -111,6 +111,7 @@ export default {
         this.$TOOL.data.remove("PASSQRCODE_GATE");
         this.$TOOL.data.remove("TOWER_GATE");
         
+        this.$store.commit("clearProject");
         this.$store.commit("clearViewTags");
         this.$store.commit("clearKeepLive");
         this.$store.commit("clearIframeList");