zhuangyunsheng hace 3 meses
padre
commit
07f756afe5
Se han modificado 43 ficheros con 824 adiciones y 591 borrados
  1. 1 2
      .env.development
  2. 1 9
      src/api/model/carwash.js
  3. 16 0
      src/api/model/common.js
  4. 29 5
      src/api/model/ugliAi.js
  5. 13 13
      src/components/scPageHeader/index.vue
  6. 2 2
      src/components/scTable/renderer/cell-tag.vue
  7. 4 3
      src/components/scTable/renderer/form-radio.vue
  8. 9 8
      src/components/scTable/renderer/form-select.vue
  9. 7 4
      src/components/scUpload/fileViewer.vue
  10. 25 40
      src/components/scUpload/index.vue
  11. 5 0
      src/components/scUpload/main.js
  12. 1 4
      src/components/scUpload/multiple.vue
  13. 1 0
      src/components/scVideo/index.vue
  14. 7 5
      src/config/select.js
  15. 2 2
      src/config/table.js
  16. 6 6
      src/layout/components/NavMenu.vue
  17. 4 4
      src/layout/components/password.vue
  18. 4 4
      src/layout/index.vue
  19. 3 5
      src/router/index.js
  20. 3 1
      src/style/app.scss
  21. 33 16
      src/utils/basicDic.js
  22. 13 0
      src/utils/shortcuts.js
  23. 0 7
      src/views/dataMock/carwash/components/info/detail.vue
  24. 0 3
      src/views/dataMock/carwash/components/info/index.vue
  25. 2 2
      src/views/dataMock/carwash/components/record/detail.vue
  26. 10 9
      src/views/dataMock/carwash/components/record/index.vue
  27. 3 15
      src/views/dataMock/carwash/detail.vue
  28. 5 6
      src/views/dataMock/carwash/index.vue
  29. 0 9
      src/views/dataMock/carwash/main.js
  30. 40 12
      src/views/dataMock/carwash/components/monos.vue
  31. 34 0
      src/views/dataMock/tasks/tableExpand.vue
  32. 8 3
      src/views/dataMock/ugliAi/components/index.js
  33. 0 116
      src/views/dataMock/ugliAi/components/monos.vue
  34. 0 103
      src/views/dataMock/ugliAi/components/record.vue
  35. 170 0
      src/views/dataMock/ugliAi/components/record/detail.vue
  36. 182 0
      src/views/dataMock/ugliAi/components/record/index.vue
  37. 0 7
      src/views/dataMock/ugliAi/components/template.vue
  38. 12 0
      src/views/dataMock/ugliAi/components/template/index.vue
  39. 143 76
      src/views/dataMock/ugliAi/detail.vue
  40. 21 10
      src/views/dataMock/ugliAi/index.vue
  41. 5 0
      src/views/dataMock/ugliAi/main.js
  42. 0 47
      src/views/dataMock/ugliAi/mono/index.vue
  43. 0 33
      src/views/dataMock/ugliAi/mono/tableExpand.vue

+ 1 - 2
.env.development

@@ -7,10 +7,9 @@ VUE_APP_TITLE = EasyDo运营中心
 # 接口地址
 # VUE_APP_API_BASEURL = http://www.qdeasydo.com/api
 # VUE_APP_OPS_BASEURL = http://www.qdeasydo.com/ops
-# VUE_APP_ZEROAPI_BASEURL = http://www.qdeasydo.com
+VUE_APP_ZEROAPI_BASEURL = http://www.qdeasydo.com
 VUE_APP_API_BASEURL  = http://192.168.101.93:8802
 VUE_APP_OPS_BASEURL = http://192.168.101.93:8804
-VUE_APP_ZEROAPI_BASEURL  = http://192.168.101.93:8802
 
 # 本地端口
 VUE_APP_PORT = 3200

+ 1 - 9
src/api/model/carwash.js

@@ -2,14 +2,6 @@ import config from "@/config"
 import http from "@/utils/request"
 
 export default {
-    gate: {
-        name: "安装点查询",
-        url: `${config.API_URL}/api/carRinseTemp/getMountedPage`,
-        get: async function (data = {}) {
-            return await http.post(this.url, data);
-        }
-    },
-
     carInfo: {
         name: "车辆信息",
         url: `${config.API_URL}/ops/carRinse`,
@@ -68,5 +60,5 @@ export default {
         add: async function (data = {}) {
             return await http.post(this.url, data);
         }
-    },
+    }
 }

+ 16 - 0
src/api/model/common.js

@@ -28,6 +28,22 @@ export default {
 		}
 	},
 
+    minio: {
+		url: `${config.API_URL}/ops/minio`,
+		name: "文件上传",
+		up: async function (data, config = {}) {
+			return await http.post(`${this.url}/uploadAihazard`, data, config);
+		},
+        
+		rm: async function (fileName) {
+			return await http.post(`${this.url}/remove`, { fileName });
+		},
+
+        download: async function (entityID) {
+			return await http.get(`${this.url.replace("/ops", "")}${entityID}`, {}, { responseType: "blob" });
+		}
+	},
+
     opsTask: {
         name: "任务列表",
         url: `${config.API_URL}/ops/opsTask`,

+ 29 - 5
src/api/model/ugliAi.js

@@ -2,18 +2,42 @@ import config from "@/config"
 import http from "@/utils/request"
 
 export default {
-    gate: {
-        url: `${config.API_URL}/api/aihazard/mounted/fetch`,
+    mounted: {
+        url: `${config.API_URL}/ops/aihazard/getMountedList`,
         name: "安装点查询",
         get: async function (data = {}) {
             return await http.post(this.url, data);
         }
     },
 
-    records: {
-        url: `${config.API_URL}/api/aihazard/records/fetch`,
-        name: "安装点查询",
+    record: {
+        name: "预警记录",
+        url: `${config.API_URL}/ops/aihazard`,
         get: async function (data = {}) {
+            return await http.post(`${this.url}/getPage`, data);
+        },
+
+        add: async function (data = {}) {
+            return await http.post(`${this.url}/save`, data);
+        },
+
+        edit: async function (data = {}) {
+            return await http.post(`${this.url}/update`, data);
+        },
+
+        del: async function (data = {}) {
+            return await http.post(`${this.url}/remove`, data);
+        },
+
+        batchDel: async function (data = {}) {
+            return await http.post(`${this.url}/batchRemove`, data);
+        }
+    },
+
+    copyData: {
+        url: `${config.API_URL}/ops/aihazard/copyData`,
+        name: "数据模拟-复制",
+        add: async function (data = {}) {
             return await http.post(this.url, data);
         }
     }

+ 13 - 13
src/components/scPageHeader/index.vue

@@ -38,19 +38,19 @@ const pageTitle = computed(() => useRoute().meta.title);
 </script>
 
 <style lang="scss" scoped>
-    .scPageHeader {padding: 20px 24px;background: #fff;}
-    :deep(.el-page-header__header) .el-page-header__left {display: none;}
-    :deep(.el-page-header__header) .el-page-header__extra {flex: 1;display: flex;justify-content: space-between;}
-    .el-page-header :deep(.el-page-header__main) {border: none;}
+.scPageHeader {padding: 20px 24px;background: #fff;}
+:deep(.el-page-header__header) .el-page-header__left {display: none;}
+:deep(.el-page-header__header) .el-page-header__extra {flex: 1;display: flex;justify-content: space-between;}
+.el-page-header :deep(.el-page-header__main) {border: none;}
 
-    :deep(.el-page-header__header) .el-page-header__extra .page-header-extra__left {display: flex;}
-    :deep(.el-page-header__header) .el-page-header__extra .page-header-extra__right {display: flex;}
-    :deep(.el-page-header__header) .el-page-header__extra .extra-title {display: flex;max-width: 100%;line-height: 32px;font-size: 20px;font-weight: 600;color: rgba(0, 0, 0, 0.88);}
-    :deep(.el-page-header__header) .el-page-header__extra .el-button.is-text {font-weight: 400;color: inherit;}
+:deep(.el-page-header__header) .el-page-header__extra .page-header-extra__left {display: flex;}
+:deep(.el-page-header__header) .el-page-header__extra .page-header-extra__right {display: flex;}
+:deep(.el-page-header__header) .el-page-header__extra .extra-title {display: flex;max-width: 100%;line-height: 32px;font-size: 20px;font-weight: 600;color: rgba(0, 0, 0, 0.88);}
+:deep(.el-page-header__header) .el-page-header__extra .el-button.is-text {font-weight: 400;color: inherit;}
 
-    @media (max-width: 992px) {
-        :deep(.el-page-header__header) {display: block;}
-        :deep(.el-page-header__header) .el-page-header__extra {display: block;}
-        :deep(.el-page-header__header) .el-page-header__extra .page-header-extra__left {margin-bottom: 10px;}
-    }
+@media (max-width: 992px) {
+    :deep(.el-page-header__header) {display: block;}
+    :deep(.el-page-header__header) .el-page-header__extra {display: block;}
+    :deep(.el-page-header__header) .el-page-header__extra .page-header-extra__left {margin-bottom: 10px;}
+}
 </style>

+ 2 - 2
src/components/scTable/renderer/cell-tag.vue

@@ -27,6 +27,6 @@ const props = defineProps({
     params: { type: Object, default: () => {} }
 })
 
-const modelValue = reactive(XEUtils.get(props, "renderOpts.defaultValue", null));
-const tagType = ref(XEUtils.get(colorDic, modelValue, ""));
+const modelValue = computed(() => XEUtils.get(props, "renderOpts.defaultValue", null));
+const tagType = computed(() => XEUtils.get(colorDic, modelValue.value, ""));
 </script>

+ 4 - 3
src/components/scTable/renderer/form-radio.vue

@@ -13,13 +13,14 @@ const props = defineProps({
     params: { type: Object, default: () => {} }
 })
 
-const modelValue = ref(XEUtils.get(props.params.data, props.params.field));
-const optionProps = reactive(props.renderOpts.optionProps || config.props);
+const modelValue = ref(null);
+watch(() => props.params, val => modelValue.value = XEUtils.get(val.data, val.field), { deep: true, immediate: true });
 
+const optionProps = reactive(props.renderOpts.optionProps || config.props);
 const formatOptions = (key, { item, index }) => {
     if (XEUtils.isFunction(optionProps[key])) return optionProps[key]({ data: item, index });
     return XEUtils.get(item, optionProps[key]);
-};
+}
 
 const compChange = () => props.renderOpts.events.change({ [props.params.field]: modelValue.value });
 </script>

+ 9 - 8
src/components/scTable/renderer/form-select.vue

@@ -1,7 +1,8 @@
 <template>
     <el-select v-model="modelValue" :loading="loading" v-bind="renderOpts.props" @change="compChange">
         <template #label="{ label }">
-            <span>{{ label.split(" ")[0] }}</span>
+            <span v-if="renderOpts.slot">{{ XEUtils.first(label.split(" ")) }}</span>
+            <span v-else>{{ label }}</span>
         </template>
         <el-option v-for="(item, index) in options" :key="index" :label="formatOptions('label', { item, index }) + ' ' + formatOptions('slot', { item, index })" :value="formatOptions('value', { item, index })">
             <span style="float: left;">{{ formatOptions('label', { item, index }) }}</span>
@@ -23,6 +24,7 @@ const props = defineProps({
 watch(props.params.data, e => {
     if (modelValue.value != XEUtils.get(e, props.params.field)) modelValue.value = XEUtils.get(e, props.params.field)
 }, { deep: true })
+watch(props.renderOpts.api.query, () => getRemoteData())
 
 const loading = ref(false);
 const modelValue = ref(XEUtils.get(props, "renderOpts.defaultValue", null));
@@ -31,7 +33,7 @@ const optionProps = reactive(props.renderOpts.optionProps);
 
 const getRemoteData = async () => {
 	loading.value = true;
-    options.value = await XEUtils.get(config.queryData, XEUtils.get(props.renderOpts, "api.framework", "common"))(props.renderOpts.api);    
+    options.value = await XEUtils.get(config.queryData, XEUtils.get(props.renderOpts, "api.framework", "common"))(props.renderOpts.api);   
     loading.value = false;
 }
 
@@ -44,10 +46,9 @@ const compChange = () => props.renderOpts.events.change({ [props.params.field]:
 
 !options.value.length && !props.renderOpts.storageKey && getRemoteData();
 
-onMounted(() => {
-    window.addEventListener("setItemEvent", ({ key, newValue }) => {
-        if (props.renderOpts.storageKey && key === props.renderOpts.storageKey && newValue) options.value = JSON.parse(newValue)?.content;
-    });
-});
-onUnmounted(() => window.removeEventListener("setItemEvent", () => {}));
+const storageChange = ({ key, newValue }) => {
+    if (key === props.renderOpts.storageKey) options.value = XEUtils.toStringJSON(newValue).content;
+}
+window.addEventListener("setItemEvent", storageChange);
+onUnmounted(() => window.removeEventListener("setItemEvent", storageChange));
 </script>

+ 7 - 4
src/components/scUpload/fileViewer.vue

@@ -19,12 +19,12 @@
             </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 { VxeUI } from "vxe-pc-ui";
 import { fileTypes, officeOptions } from "./main";
 
 import vue_office_docx from "@vue-office/docx";
@@ -54,7 +54,6 @@ export default {
             filePath: null,
             options: {},
 
-            showViewer: false,
             showVideoViewer: false
         }
     },
@@ -68,8 +67,12 @@ export default {
                 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;
+                if (this.fileType == "image") {
+                    VxeUI.previewImage({
+                        showDownloadButton: true,
+                        urlList: ["/api/folder/" + uploadFile.path]
+                    })
+                } else if (this.fileType == "video") this.showVideoViewer = true;
                 else {
                     this.loading = true;
                     this.visible = true;

+ 25 - 40
src/components/scUpload/index.vue

@@ -7,18 +7,11 @@
 			<el-image class="image" :src="file.tempFile" fit="cover"></el-image>
 		</div>
 		<div v-if="file && file.status=='success'" class="sc-upload__img">
-			<el-image v-if="isImage(file.mineType)" class="image" :src="'/api/folder/' + file.path" :preview-src-list="['/api/folder/' + file.path]" fit="cover" preview-teleported :z-index="9999">
-				<template #placeholder>
-					<div class="sc-upload__img-slot">Loading...</div>
-				</template>
-			</el-image>
-			<sc-video v-if="isVideo(file.mineType)" :src="'/api/folder/' + file.path" showMask @play="videoPlay"></sc-video>
+            <vxe-image v-if="isImage(file.mineType)" class="image" :src="urlPrefix[apiKey] + file.path" :toolbar-config="imageToolbar"></vxe-image>
+			<sc-video v-if="isVideo(file.mineType)" :src="urlPrefix[apiKey] + file.path" showMask @play="videoPlay"></sc-video>
 			
 			<div class="sc-upload__img-actions" v-if="!disabled">
-                <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">
+                <el-button type="danger" @click="handleRemove">
                     <sc-iconify icon="ant-design:delete-outlined"></sc-iconify>
                 </el-button>
 			</div>
@@ -59,11 +52,12 @@
 </template>
 
 <script>
-import { fileTypes } from "./main";
+import { urlPrefix, fileTypes } from "./main";
 
 export default {
     props: {
         modelValue: { type: Object, default: () => {} },
+        apiKey: { type: String, default: () => "folder" },
         width: { type: Number, default: 148 },
         height: { type: Number, default: 148 },
         title: { type: String, default: "" },
@@ -80,6 +74,12 @@ export default {
 
     data() {
         return {
+            urlPrefix,
+            imageToolbar: {
+                print: false,
+                download: true
+            },
+
             value: "{}",
             file: null,
             style: {
@@ -89,7 +89,6 @@ export default {
             cropperDialogVisible: false,
             cropperFile: null,
 
-            loading: false,
             showViewer: false
         }
     },
@@ -114,7 +113,7 @@ export default {
     
     methods: {
         isImage(type) {
-            return fileTypes[type] == "image"
+            return type == "jpg" || fileTypes[type] == "image"
         },
 
         isVideo(type) {
@@ -150,11 +149,18 @@ export default {
                 type: "warning",
                 confirmButtonText: "移除"
             }).then(() => {
-                if (file.id) {
-                    this.$API.common.folder.rm(file.id).then(res => {
-                        if (res.code == 200) this.clearFiles();
+                if (this.apiKey == "folder") {
+                    if (file.id) {
+                        this.$API.common[this.apiKey].rm(file.id).then(res => {
+                            if (res.code == 200) this.clearFiles();
+                        }).catch(() => {});
+                    } else this.clearFiles();
+                } else {
+                    this.$API.common[this.apiKey].rm(this.file.id || this.file.path).then(res => {
+                        this.clearFiles();
+                        this.$emit("removeSuccess");
                     }).catch(() => {});
-                } else this.clearFiles();
+                }
             }).catch(() => {});
         },
 
@@ -227,7 +233,7 @@ export default {
             const data = new FormData();
             data.append(param.filename, param.file);
 
-            this.$API.common.folder.up(data, {
+            this.$API.common[this.apiKey].up(data, {
                 onUploadProgress: e => {
                     const percent = parseInt(((e.loaded / e.total) * 100) | 0, 10);
                     param.onProgress({ percent });
@@ -241,18 +247,6 @@ export default {
         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);
         }
     }
 }
@@ -289,6 +283,7 @@ export default {
   width: 100%;
   height: 100%;
   border: 1px solid var(--el-border-color);
+  cursor: pointer;
 }
 .sc-upload__img-actions {
   z-index: 120;
@@ -309,16 +304,6 @@ export default {
 .sc-upload__img:hover .sc-upload__img-actions {
   display: flex;
 }
-.sc-upload__img-slot {
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  width: 100%;
-  height: 100%;
-  font-size: 12px;
-  background-color: var(--el-fill-color-lighter);
-}
-
 .sc-upload__uploading {
   width: 100%;
   height: 100%;

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

@@ -1,3 +1,8 @@
+export const urlPrefix = {
+    folder: "/api/folder/",
+    minio: "/minio"
+}
+
 export const fileTypes = {
     "image/gif": "image",
     "image/jpeg": "image",

+ 1 - 4
src/components/scUpload/multiple.vue

@@ -32,7 +32,7 @@
 						<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)">
+                        <el-button type="danger" @click="handleRemove(file)">
                             <sc-iconify icon="ant-design:delete-outlined"></sc-iconify>
                         </el-button>
 					</div>
@@ -254,9 +254,6 @@ export default {
 .sc-upload__item-actions span i {
   font-size: 12px;
 }
-.sc-upload__item-actions .del {
-  background: #f56c6c;
-}
 .sc-upload__item-progress {
   position: absolute;
   width: 100%;

+ 1 - 0
src/components/scVideo/index.vue

@@ -53,6 +53,7 @@
 					fluid: true, // 播放器宽度跟随父元素的宽度大小变化
 					videoInit: true, // 初始化显示视频首帧
 					lang: 'zh-cn',
+                    download: true, //显示下载按钮
 					...this.options
 				});
 			},

+ 7 - 5
src/config/select.js

@@ -6,11 +6,13 @@ import XEUtils from "xe-utils";
 export default {
     queryData: {
         common: function ({ key, query }) {
-            return [];
-            // if (XEUtils.isEmpty(XEUtils.get(API, key))) return [];
-            // return new Promise(resolve => {
-            //     XEUtils.get(API, key).get(query).then(res => resolve(res.records));
-            // });
+            if (XEUtils.isEmpty(XEUtils.get(API, key))) return [];
+            return new Promise(resolve => {
+                XEUtils.get(API, key).get(XEUtils.omit(query, val => XEUtils.isEmpty(val) && !XEUtils.isNumber(val))).then(res => {
+                    const response = XEUtils.get(config, "framework.common.parseData")(res)
+                    resolve(response.data)
+                });
+            });
         },
 
         zeroLiteOld: function ({ key, query, expands }) {

+ 2 - 2
src/config/table.js

@@ -10,7 +10,7 @@ export default {
     framework: {
         common: {
             queryData: function ({ formConfig: { data }, pagerConfig: { queryType, currentPage, pageSize } }, paramsColumns) {
-                const query = queryType == "limit" ? { current: currentPage, size: pageSize } : {}
+                const query = queryType == "limit" ? { current: currentPage, size: pageSize, orderBy: "createTime_desc" } : { orderBy: "createTime_desc" }
 
                 XEUtils.arrayEach(XEUtils.filter(paramsColumns, item => !valueIsNull(data, item.field || item.column)), item => XEUtils.set(query, item.column, XEUtils.get(data, item.field || item.column)))
 
@@ -19,7 +19,7 @@ export default {
             parseData: function (res) {
                 return {
                     data: res.records || res,			    // 分析数据字段结构
-                    total: res.total	            // 分析总数字段结构
+                    total: res.total	                    // 分析总数字段结构
                 }
             }
         },

+ 6 - 6
src/layout/components/NavMenu.vue

@@ -44,10 +44,10 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-    .menu-collapse-popper {
-        display: flex;
-        align-items: center;
-        margin: 1px 0;
-        line-height: 22px;
-    }
+.menu-collapse-popper {
+    display: flex;
+    align-items: center;
+    margin: 1px 0;
+    line-height: 22px;
+}
 </style>

+ 4 - 4
src/layout/components/password.vue

@@ -83,9 +83,9 @@ defineExpose({
 </script>
 
 <style lang="scss" scoped>
-    .el-form {padding-right: 40px;}
+.el-form {padding-right: 40px;}
 
-    @media (max-width: 992px) {
-        .el-form {padding-right: 0;}
-    }
+@media (max-width: 992px) {
+    .el-form {padding-right: 0;}
+}
 </style>

+ 4 - 4
src/layout/index.vue

@@ -120,8 +120,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-    .aminui-header .aminui-header-left {
-        .no-m-r {margin-right: 0;}
-        .m-l-20 {margin-left: 20px;}
-    }
+.aminui-header .aminui-header-left {
+    .no-m-r {margin-right: 0;}
+    .m-l-20 {margin-left: 20px;}
+}
 </style>

+ 3 - 5
src/router/index.js

@@ -67,8 +67,8 @@ router.beforeEach(async (to, from, next) => {
 	if (to.meta.fullpage) to.matched = [to.matched[to.matched.length - 1]];
     
     // 所有闸口/安装点
-    const index = ["facerec", "passqrcode", "tower", "env", "carwash", "ugliAi"].findIndex(key => to.fullPath.includes(key));
-    if (to.name && index !== -1) router.getGates(["facerec", "passqrcode", "tower", "env", "carwash", "ugliAi"][index])
+    const index = ["facerec", "passqrcode", "tower", "env"].findIndex(key => to.fullPath.includes(key));
+    if (to.name && index !== -1) router.getGates(["facerec", "passqrcode", "tower", "env"][index])
     
     // 加载动态/静态路由
 	if (!isGetRouter) {
@@ -133,9 +133,7 @@ router.getProject = async 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 = []) => {
-    const path = storagePath == "carwash" ? "ugliAi" : storagePath;
-
+router.getGates = async (path, start = 0, gates = []) => {
     if (!tool.data.get(`${path.toUpperCase()}_GATE`) || !tool.data.get(`${path.toUpperCase()}_GATE`).length) {
         const res = await fetchGates(path, start);
         gates = gates.concat(XEUtils.get(res, "datas", []));

+ 3 - 1
src/style/app.scss

@@ -116,4 +116,6 @@ a,button,input,textarea{-webkit-tap-highlight-color:rgba(0,0,0,0);box-sizing: bo
 .vxe-table-slot--top__form .el-form-item {margin-bottom: 0;padding-bottom: .6em;}
 .vxe-table-slot--top__form .el-form-item__label, .vxe-table-slot--top__form .el-form-item__content {font-size: var(--el-font-size-medium);color: #000;}
 
-.el-dialog .vxe-grid.size--mini .vxe-table-query {background-color: unset;}
+.el-dialog .vxe-grid.size--mini .vxe-table-query {background-color: unset;}
+
+.vxe-modal--wrapper.vxe-image-preview-popup-wrapper.is--visible.is--active {z-index: 9999 !important;}

+ 33 - 16
src/utils/basicDic.js

@@ -25,23 +25,40 @@ export const sccRecordTypeDic = {
     SCC_RECORD_VTYPE_RH: { label: "湿度", unit: "%rh" }
 }
 
+/* ************************************************************************* */ 
+export const taskDic = {
+    option: {
+        d: "删除数据",
+        i: "新增数据"
+    },
+    state: {
+        inactive: "等待执行",
+        active: "执行中",
+        success: "已完成",
+        cancel: "已取消",
+        fail: "执行失败"
+    }
+}
+
+export const dataSource = ["现存数据", "iot数据", "第三方数据"];
+
 export const aiTypeDic = {
-    AIHAZARD_REC_NO_HELMET: '未带安全帽',
-    AIHAZARD_REC_NO_CLOTHES: '未穿工服',
-    AIHAZARD_REC_ILLEGAL_CALL: '打电话检测',
-    AIHAZARD_REC_SMOKE_WARN: '吸烟告警',
-    AIHAZARD_REC_LEAVE_POS: '离岗检测',
-    AIHAZARD_REC_MATTER_SHOVED: '物料乱堆乱放',
-    AIHAZARD_REC_ILLEGAL_ENTRY: '区域入侵',
-    AIHAZARD_REC_ILLEGAL_PARKING: '车辆违停占用',
-    AIHAZARD_REC_NO_REFLECT_VEST: '反光衣/带检测',
-    AIHAZARD_REC_NO_MASK: '口罩检测',
-    AIHAZARD_REC_FALL_WARN: '跌倒检测',
-    AIHAZARD_REC_FIRE_WARN: '明火告警',
-    AIHAZARD_REC_SMOG_WARN: '烟雾检测',
-    AIHAZARD_REC_NO_SAFETY_BELT: '安全背带检测',
-    AIHAZARD_REC_BARE_SOIL_WARN: '裸土未覆盖',
-    AIHAZARD_REC_VEHICLE_NOT_CLEANED: '车辆未清洗'
+    AIHAZARD_REC_NO_HELMET: "未带安全帽",
+    AIHAZARD_REC_NO_CLOTHES: "未穿工服",
+    AIHAZARD_REC_ILLEGAL_CALL: "打电话检测",
+    AIHAZARD_REC_SMOKE_WARN: "吸烟告警",
+    AIHAZARD_REC_LEAVE_POS: "离岗检测",
+    AIHAZARD_REC_MATTER_SHOVED: "物料乱堆乱放",
+    AIHAZARD_REC_ILLEGAL_ENTRY: "区域入侵",
+    AIHAZARD_REC_ILLEGAL_PARKING: "车辆违停占用",
+    AIHAZARD_REC_NO_REFLECT_VEST: "反光衣/带检测",
+    AIHAZARD_REC_NO_MASK: "口罩检测",
+    AIHAZARD_REC_FALL_WARN: "跌倒检测",
+    AIHAZARD_REC_FIRE_WARN: "明火告警",
+    AIHAZARD_REC_SMOG_WARN: "烟雾检测",
+    AIHAZARD_REC_NO_SAFETY_BELT: "安全背带检测",
+    AIHAZARD_REC_BARE_SOIL_WARN: "裸土未覆盖",
+    AIHAZARD_REC_VEHICLE_NOT_CLEANED: "车辆未清洗"
 }
 
 export function objectToArray(obj) {

+ 13 - 0
src/utils/shortcuts.js

@@ -0,0 +1,13 @@
+import moment from "moment"
+
+export function rangeShortcuts(format = "YYYY-MM-DD HH:mm:ss") {
+    return [
+        { text: "今天", value: () => [moment().startOf("day").format(format), moment().format(format)] },
+        { text: "本月", value: () => [moment().startOf("month").format(format), moment().format(format)] },
+        { text: "本季度", value: () => [moment().startOf("quarter").format(format), moment().format(format)] },
+        { text: "今年", value: () => [moment().startOf("year").format(format), moment().format(format)] },
+        { text: "近7天", value: () => [moment().subtract(1, "week").format(format), moment().format(format)] },
+        { text: "近1月", value: () => [moment().subtract(1, "month").format(format), moment().format(format)] },
+        { text: "近1年", value: () => [moment().subtract(1, "year").format(format), moment().format(format)] },
+    ]
+}

+ 0 - 7
src/views/dataMock/carwash/components/info/detail.vue

@@ -19,12 +19,6 @@
                     <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-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>
@@ -61,7 +55,6 @@ const form = ref({
     vehicleType: null,
     vehicleColor: null,
     isSupervise: null,
-    dataSources: null,
     createTime: moment().format("YYYY-MM-DD HH:mm:ss")
 });
 const rules = reactive({

+ 0 - 3
src/views/dataMock/carwash/components/info/index.vue

@@ -44,7 +44,6 @@ const toolbarConfig = reactive({
 
 const formConfig = reactive({
     data: {
-        orderBy: "createTime_desc",
         projectId: TOOL.data.get("PROJECT_ID"),
         projectIdNot: 1
     },
@@ -58,7 +57,6 @@ const formConfig = reactive({
 })
 
 const paramsColums = reactive([
-    { column: "orderBy" },
     { column: "projectId" },
     { column: "projectIdNot" },
     { column: "plateNumberLike" },
@@ -74,7 +72,6 @@ const columns = reactive([
     { 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: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(carWashDic.dataSources, cellValue, "/") },
     { type: "html", field: "createTime", title: "创建时间", minWidth: 160, sortable: true },
     { title: "操作", width: 140, fixed: "right", slots: { default: "action" } }
 ])

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

@@ -137,9 +137,9 @@ const setData = data => {
     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 })))
+                XEUtils.set(form.value, `${key}.${folder_key}.entities`, XEUtils.map(XEUtils.get(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));
+        } else XEUtils.set(form.value, key, XEUtils.get(data, key));
     });
 }
 

+ 10 - 9
src/views/dataMock/carwash/components/record/index.vue

@@ -2,7 +2,7 @@
     <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 }">
             <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>
+                <vxe-image style="cursor: pointer;" :src="'/api/folder/' + XEUtils.get(row, `folders.${column.field}.entities[0].path`)" width="40" height="40" :toolbar-config="imageToolbar"></vxe-image>
             </template>
         </template>
         
@@ -16,7 +16,7 @@
         </template>
     </scTable>
 
-    <record-detail v-if="dialog" ref="recordRef" :projectId="props.isTemp ? 1 : TOOL.data.get('PROJECT_ID')" @success="refreshTable" @closed="dialog = false"></record-detail>
+    <record-detail v-if="dialog" ref="recordRef" :projectId="props.isTemp ? 1 : TOOL.data.get('PROJECT_ID')" @success="refreshTable" @closed="dialog.value = false"></record-detail>
 </template>
 
 <script setup>
@@ -25,6 +25,7 @@ import XEUtils from "xe-utils";
 import API from "@/api";
 import TOOL from "@/utils/tool";
 import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker } from "@/components/scTable/helper";
+import { dataSource } from "@/utils/basicDic";
 import { carWashDic } from "@/views/dataMock/carwash/main";
 import recordDetail from "./detail";
 
@@ -33,9 +34,10 @@ const props = defineProps({
     isTemp: { type: Boolean, default: false },
     hideHandler: { type: Boolean, default: false }
 })
+const visible = computed(() => !props.isTemp);
 
 const proConfig = reactive({
-    visible: !props.isTemp,
+    visible,
     storageKey: "PROJECT",
     resetValue: TOOL.data.get("PROJECT_ID"),
     optionProps: { label: "projectName", value: "fpiId" },
@@ -69,7 +71,6 @@ const toolbarConfig = reactive({
 
 const formConfig = reactive({
     data: {
-        orderBy: "createTime_desc",
         projectId: TOOL.data.get("PROJECT_ID"),
         fpiIdNot: 1,
         createTime: [moment().startOf("month").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")]
@@ -84,9 +85,8 @@ const formConfig = reactive({
 })
 
 const paramsColums = reactive([
-    { column: "orderBy" },
-    { column: "fpiId", field: props.isTemp ? "fpiIdNot" : "projectId" },
-    !props.isTemp && { column: "fpiIdNot" },
+    { column: "fpiId", field: visible.value ? "projectId" : "fpiIdNot" },
+    visible.value ? { column: "fpiIdNot" } : {},
     { column: "licensePlate" },
     { column: "carType" },
     { column: "alarmType" },
@@ -95,9 +95,9 @@ const paramsColums = reactive([
 ])
 
 const columns = reactive([
-    { visible: !props.hideHandler, type: "checkbox", fixed: "left", width: 40 },
+    { visible: !props.hideHandler, type: "checkbox", fixed: "left", width: 40, align: "center" },
     { type: "seq", fixed: "left", width: 60 },
-    { type: "html", field: "projectName", title: "项目名称", minWidth: 160, sortable: true, formatter: ({ cellValue, row }) => cellValue || row.fpiId == 1 ? "模版项目" : XEUtils.get(XEUtils.find(TOOL.data.get("PROJECT"), item => item.fpiId == row.fpiId), "projectName") },
+    { visible, 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.fpiId), "projectName") },
     { 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 },
@@ -107,6 +107,7 @@ 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" } },
+    { visible, type: "html", field: "dataSource", title: "数据来源", fixed: "right", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(dataSource, cellValue, cellValue) },
     { visible: !props.hideHandler, title: "操作", fixed: "right", width: 140, align: "center", slots: { default: "action" } }
 ])
 

+ 3 - 15
src/views/dataMock/carwash/detail.vue

@@ -63,7 +63,7 @@
                 </el-col>
             </el-row>
 
-            <data-table ref="tableRef" hideHandler :options="tableOptions"></data-table>
+            <data-table ref="tableRef" :isTemp="form.source == 'template'" hideHandler :options="tableOptions"></data-table>
         </el-form>
 
         <template #footer>
@@ -112,8 +112,8 @@ const tableOptions = reactive({
     toolbarConfig: { enabled: true, print: false, zoom: false },
     formConfig: { enabled: false, data: form },
     paramsColums: computed(() => [
-        { column: "fpiId", field: form.value.source == "other" ? "sourceProjectId" : "sourceProjectIdNot" },
-        form.value.source == "other" && { column: "fpiIdNot", field: "sourceProjectIdNot" },
+        { column: "fpiId", field: form.value.source == "template" ? "sourceProjectIdNot" : "sourceProjectId"  },
+        form.value.source == "template" ? {} : { column: "fpiIdNot", field: "sourceProjectIdNot" },
         { column: "captureTimeBegin", field: "sourceTime[0]" },
         { column: "captureTimeEnd", field: "sourceTime[1]" }
     ])
@@ -168,18 +168,6 @@ defineExpose({
 
 <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>

+ 5 - 6
src/views/dataMock/carwash/index.vue

@@ -11,20 +11,19 @@
             <el-tab-pane v-for="(label, key) in workerStates" :key="key" :label="label" :name="key"></el-tab-pane>
         </el-tabs>
 
-        <component ref="componentRef" :is="allcomp[activeName]" />
+        <component ref="componentRef" :is="allcomp[activeName]" taskType="car_rinse" />
 	</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 comp from "./components";
+import monos from "@/views/dataMock/tasks/monos";
 import mockDetail from "./detail";
 
+const allcomp = { ...comp, monos };
 const activeName = ref("info");
 
 const componentRef = ref();
@@ -39,7 +38,7 @@ const mock_add = () => {
 }
 
 const refreshState = () => {
-    if (activeName.value == "monos") componentRef.value.refreshTable();
+    if (activeName.value == "monos") setTimeout(() => componentRef.value.refreshTable(), 2000);
     activeName.value = "monos";
 }
 </script>

+ 0 - 9
src/views/dataMock/carwash/main.js

@@ -9,15 +9,6 @@ export const carWashDic = {
     carType: ["未知", "大型客车", "货车", "轿车", "面包车", "小货车", "行人", "二轮车", "三轮车", "SUV/MPV", "中型客车", "机动车", "非机动车", "小型轿车", "微型轿车", "皮卡车"],
     carColor: ["未知", "白色", "银色", "灰色", "黑色", "红色", "深蓝色", "蓝色", "黄色", "绿色", "棕色", "粉色", "紫色", "深灰色", "青色", "橙色"],
     plateColor: [null, "蓝色", "黄色", "黑色", "白色", "绿色"],
-    dataSources: ["三方系统推送", "其他"],
     isSupervise: [{ label: "使用中", value: true }, { label: "已拆除", value: false }],
     alarmType: [null, "未冲洗", "冲洗不足", "冲洗完成", "绕道未冲洗", "其他"]
-}
-
-export const taskStateDic = {
-    inactive: "等待执行",
-    active: "执行中",
-    success: "已完成",
-    cancel: "已取消",
-    fail: "执行失败"
 }

+ 40 - 12
src/views/dataMock/carwash/components/monos.vue

@@ -1,5 +1,9 @@
 <template>
     <scTable ref="xGridTable" :apiObj="$API.common.opsTask" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns">
+        <template #expand_content="{ row }">
+            <table-expand :rowData="row.taskChildrenList"></table-expand>
+        </template>
+
         <template #action="{ row }">
             <el-button v-if="row.taskStatus == 'active'" type="primary" link @click="table_stop(row)">
                 <template #icon><sc-iconify icon="mdi:stop-pause-outline" size="13"></sc-iconify></template>终止
@@ -14,14 +18,18 @@ import XEUtils from "xe-utils";
 import API from "@/api";
 import TOOL from "@/utils/tool";
 import { mapFormItemSelect, mapFormItemRadio, mapFormItemDatePicker } from "@/components/scTable/helper";
-import { objectToArray } from "@/utils/basicDic";
-import { taskStateDic } from "../main";
+import { taskDic, objectToArray } from "@/utils/basicDic";
+import tableExpand from "./tableExpand";
 
 const radioConfig = reactive({
     span: 5,
+    resetValue: false,
     options: [{ label: "是", value: true }, { label: "否", value: false }],
     events: {
-        change: data => XEUtils.merge(formConfig.data, data)
+        change: data => {
+            XEUtils.merge(formConfig.data, data);
+            refreshTable();
+        }
     }
 })
 
@@ -35,8 +43,26 @@ const proConfig = reactive({
     }
 })
 
+const mountedConfig = reactive({
+    visibleMethod: ({ data }) => !(data.isTemp == 1 || useAttrs().taskType == "car_rinse"),
+    api: {
+        key: "ugliAi.mounted",
+        query: {
+            projectId: computed(() => formConfig.data.projectId),
+            projectIdNot: 1
+        }
+    },
+    slot: {
+        style: { float: "right", paddingLeft: "6px", color: "#8492a6" }
+    },
+    optionProps: { label: "mountedName", value: "id", slot: ({ data }) => XEUtils.get(XEUtils.find(TOOL.data.get("PROJECT"), item => item.fpiId === data.projectId), "projectName") },
+    events: {
+        change: data => XEUtils.assign(formConfig.data, data)
+    }
+})
+
 const selectConfig = reactive({
-    options: objectToArray(taskStateDic),
+    options: objectToArray(taskDic.state),
     events: {
         change: data => XEUtils.merge(formConfig.data, data)
     }
@@ -71,17 +97,17 @@ const formConfig = reactive({
     data: {
         isTemp: false,
 
-        orderBy: "createTime_desc",
         parentId: "0",
         projectId: TOOL.data.get("PROJECT_ID"),
         projectIdNot: 1,
-        taskType: "car_rinse",
+        taskType: useAttrs().taskType,
         dateRange: [],
         createTime: [moment().startOf("month").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")]
     },
     items: [
         mapFormItemRadio("isTemp", "是否模版项目", radioConfig),
         mapFormItemSelect("projectId", "所属项目", proConfig),
+        mapFormItemSelect("mountedId", "所属安装点", mountedConfig),
         mapFormItemSelect("taskStatus", "任务状态", selectConfig),
         mapFormItemDatePicker("dateRange", "时间范围", daterangeConfig),
         mapFormItemDatePicker("createTime", "创建时间", datetimerangeConfig)
@@ -89,11 +115,11 @@ const formConfig = reactive({
 })
 
 const paramsColums = computed(() => [
-    { column: "orderBy" },
     { column: "parentId" },
     { column: "taskType" },
     { column: "projectId", field: formConfig.data.isTemp ? "projectIdNot" : "projectId" },
-    !formConfig.data.isTemp && { column: "projectIdNot" },
+    formConfig.data.isTemp ? {} : { column: "projectIdNot" },
+    { column: "mountedId" },
     { column: "taskStatus" },
     { column: "planBeginTimeBegin", field: "dateRange[0]" },
     { column: "planEndTimeEnd", field: "dateRange[1]" },
@@ -102,15 +128,17 @@ const paramsColums = computed(() => [
 ])
 
 const columns = reactive([
-    { type: "seq", width: 60 },
-    { type: "html", field: "projectName", title: "项目名称", minWidth: 160, sortable: true, formatter: ({ cellValue, row }) => cellValue || row.projectId == 1 ? "模版项目" : XEUtils.get(XEUtils.find(TOOL.data.get("PROJECT"), item => item.fpiId == row.projectId), "projectName") },
+    { type: "expand", fixed: "left", width: 40, align: "center", slots: { content: "expand_content" } },
+    { type: "seq", fixed: "left", width: 60 },
+    { visible: computed(() => !formConfig.data.isTemp), 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") },
+    { visible: computed(() => !(formConfig.data.isTemp || formConfig.data.taskType == "car_rinse")), type: "html", field: "mountedName", title: "设备安装点", minWidth: 160, sortable: true },
     { type: "html", field: "planNumber", title: "任务总数", minWidth: 100, sortable: true },
     { type: "html", field: "finishNumber", title: "已完成数量", minWidth: 120, sortable: true },
-    { type: "html", field: "taskStatus", title: "任务状态", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(taskStateDic, cellValue, cellValue) },
+    { field: "taskStatus", title: "任务状态", minWidth: 100, align: "center", editRender: { name: "$cell-tag" }, formatter: ({ cellValue }) => XEUtils.get(taskDic.state, cellValue, cellValue) },
     { 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 },
+    { field: "message", title: "失败原因", minWidth: 250, editRender: { name: "$cell-tag", props: { effect: "dark", type: "custom", color: "#f50" } }, formatter: ({ cellValue, row }) => row.taskStatus == "fail" ? cellValue : "" },
     { title: "操作", fixed: "right", minWidth: 100, align: "center", slots: { default: "action" } }
 ])
 

+ 34 - 0
src/views/dataMock/tasks/tableExpand.vue

@@ -0,0 +1,34 @@
+<template>
+    <scTable v-bind="tableOptions" :options="options"></scTable>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+import TOOL from "@/utils/tool";
+import { taskDic } from "@/utils/basicDic";
+
+const tableOptions = reactive({
+    minHeight: 108,
+    maxHeight: 192,
+    formConfig: { enabled: false },
+    pagerConfig: { enabled: false },
+    columns: [
+        { field: "executeType", title: "任务类型", minWidth: 120, formatter: ({ cellValue }) => XEUtils.get(taskDic.option, cellValue, cellValue) },
+        { field: "taskStatus", title: "任务状态", minWidth: 100, align: "center", editRender: { name: "$cell-tag" }, formatter: ({ cellValue }) => XEUtils.get(taskDic.state, cellValue, cellValue) },
+        { field: "planNumber", title: "任务总数", minWidth: 100 },
+        { field: "finishNumber", title: "已完成数量", minWidth: 120 },
+        { field: "dateRange", title: "时间范围", minWidth: 190, formatter: ({ cellValue, row }) => cellValue || TOOL.dateFormat(row.planBeginTime, "YYYY-MM-DD") + " 至 " + TOOL.dateFormat(row.planEndTime, "YYYY-MM-DD") },
+        { field: "createTime", title: "创建时间", minWidth: 160 },
+        { field: "finishTime", title: "任务终止/完成时间", minWidth: 160 },
+        { field: "message", title: "失败原因", minWidth: 250, editRender: { name: "$cell-tag", props: { effect: "dark", type: "custom", color: "#f50" } } },
+    ]
+});
+
+const options = reactive({
+    data: useAttrs().rowData
+})
+</script>
+
+<style scoped>
+.el-main {padding: 0;}
+</style>

+ 8 - 3
src/views/dataMock/ugliAi/components/index.js

@@ -1,12 +1,17 @@
+import XEUtils from "xe-utils"
+
 const resultComps = {}
 let requireComponent = require.context(
     "./", // 在当前目录下查找
-    false, // 不遍历子文件夹
+    true, // 遍历子文件夹
     /\.vue$/ // 正则匹配 以 .vue结尾的文件
 )
 requireComponent.keys().forEach(fileName => {
-    let comp = requireComponent(fileName)
-    resultComps[fileName.replace(/^\.\/(.*)\.\w+$/, "$1")] = comp.default
+    const compName = fileName.replace(/^\.\/(.*)\.\w+$/, "$1")
+    const comp = requireComponent(fileName)
+    if (compName.includes("/")) {
+        if (XEUtils.last(compName.split("/")) == "index") resultComps[XEUtils.first(compName.split("/"))] = comp.default
+    } else resultComps[compName] = comp.default
 })
 
 export default resultComps

+ 0 - 116
src/views/dataMock/ugliAi/components/monos.vue

@@ -1,116 +0,0 @@
-<template>
-    <el-main>
-        <el-card>
-            <template #header>Group任务池配置<el-button type="primary" @click="mono_detail">任务列表</el-button></template>
-
-            <el-form>
-                <el-row :gutter="15">
-                    <el-col :span="8">
-                        <el-form-item label="最大组任务数:">{{ XEUtils.get(defaultGroups, "configs.maxGroupQueues") }}</el-form-item>
-                    </el-col>
-                    <el-col :span="8">
-                        <el-form-item label="队列最大任务数:">{{ XEUtils.get(defaultGroups, "configs.maxQueueLimit") }}</el-form-item>
-                    </el-col>
-                    <el-col :span="8">
-                        <el-form-item label="最大并发队列数:">{{ XEUtils.get(defaultGroups, "configs.maxQueues") }}</el-form-item>
-                    </el-col>
-                    <el-col :span="8">
-                        <el-form-item label="队列任务间隔(秒):">{{ XEUtils.get(defaultGroups, "configs.taskIntervalSeconds") }}</el-form-item>
-                    </el-col>
-                    <el-col :span="8">
-                        <el-form-item label="重试任务间隔(秒):">{{ XEUtils.get(defaultGroups, "configs.taskRetryInterval") }}</el-form-item>
-                    </el-col>
-                    <el-col :span="8">
-                        <el-form-item label="任务最大重试次数(秒):">{{ XEUtils.get(defaultGroups, "configs.taskRetryTimes") }}</el-form-item>
-                    </el-col>
-                    <el-col :span="8">
-                        <el-form-item label="任务最大等待时间(秒):">{{ XEUtils.get(defaultGroups, "configs.taskWaitSeconds") }}</el-form-item>
-                    </el-col>
-                </el-row>
-            </el-form>
-        </el-card>
-
-        <el-card>
-            <el-form>
-                <el-form-item label="workers忙碌数量:">0</el-form-item>
-                <el-form-item label="workers空闲数量:">50</el-form-item>
-                <el-form-item label="Groups忙碌数量:">0</el-form-item>
-                <el-form-item label="Groups队列数量:">0</el-form-item>
-                <el-form-item label="mono总数:">0</el-form-item>
-            </el-form>
-        </el-card>
-        <div class="echart-bar">
-            <sc-echarts :option="option" @chartClick="chart_click"></sc-echarts>
-        </div>
-    </el-main>
-
-    <mono-detail v-if="dialog" ref="monoRef" @closed="dialog = false"></mono-detail>
-</template>
-
-<script setup>
-import XEUtils from "xe-utils"
-import { defaultGroups } from "../../main";
-import monoDetail from "../mono/index"
-
-const $emit = defineEmits(["closed"]);
-
-const option = ref({
-    grid: { left: "5%", right: "10%", bottom: 60 },
-    dataZoom: [
-        { type: "inside", endValue: 15, zoomLock: true }, { 
-        type: "slider",
-        showDetail: false,
-        moveHandleIcon: "none", // 移动手柄
-        moveHandleStyle: { opacity: 0 }
-    }],
-    xAxis: {
-        name: "workers",
-        nameTextStyle: { fontSize: 14, color: "#606266" },
-        axisLabel: { color: '#5d5d5d', rotate: -30 },
-        data: XEUtils.keys(XEUtils.get(defaultGroups, "workers"))
-    },
-    yAxis: {
-        name: "mono",
-        type: "value",
-        min: 0,
-        max: XEUtils.max([8, 5]),
-        nameTextStyle: { fontSize: 14, color: "#606266" },
-        minInterval: 1,
-        axisLabel: { color: '#5d5d5d' },
-        axisLine: { show: true, lineStyle: { color: "#d3d2d3" } },
-        splitLine: { show: false }
-    },
-    series: [{
-        type: "bar",
-        data: [8, 5],
-        animationDuration: 2500,
-        animationDurationUpdate: 2500
-    }]
-})
-
-const dialog = ref(false);
-const monoRef = ref();
-const chart_click = ({ name }) => {
-    dialog.value = true;
-    nextTick(() => monoRef.value.open(XEUtils.get(defaultGroups, `groups.${name}.monos`)));
-}
-
-const mono_detail = () => {
-    dialog.value = true;
-    nextTick(() => monoRef.value.open());
-}
-</script>
-
-<style lang="scss" scoped>
-.el-main {padding: 0 12px 12px;background: #fff;}
-.el-card :deep(.el-card__header) {display: flex;justify-content: space-between;align-items: center;padding-left: 20px;background: #f9fdff;font-weight: 700;}
-.el-card :deep(.el-card__body) {padding: 10px var(--el-card-padding);}
-.el-card + .el-card {margin-top: 10px;}
-
-.el-form .el-form-item {margin-bottom: 0;}
-.el-form :deep(.el-form-item__content) {font: 22px calculator-all;color: #1890ff;letter-spacing: 2px;}
-
-.el-card + .el-card .el-form {display: flex;justify-content: space-evenly;}
-
-.echart-bar {width: 100%;height: 400px;}
-</style>

+ 0 - 103
src/views/dataMock/ugliAi/components/record.vue

@@ -1,103 +0,0 @@
-<template>
-    <scTable ref="xGridTable" :apiObj="$API.ugliAi.records" framework="zeroLiteOld" :toolbarConfig="toolbarConfig" :formConfig="formConfig" :paramsColums="paramsColums" :columns="columns" :options="props.options">
-        <template #default_imgUrl="{ row }">
-            <vxe-image v-if="XEUtils.get(row, 'features.bigImage.image')" :src="'/minio' + XEUtils.get(row, 'features.bigImage.image')" width="40" height="40"></vxe-image>
-            <vxe-image v-else-if="XEUtils.get(row, 'features.bigImage.testImage')" :src="XEUtils.get(row, 'features.bigImage.testImage')" 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_edit(row)">
-                <template #icon><sc-iconify icon="material-symbols:step-over"></sc-iconify></template>覆盖
-            </el-button>
-        </template>
-    </scTable>
-</template>
-
-<script setup>
-import moment from "moment";
-import XEUtils from "xe-utils";
-import TOOL from "@/utils/tool";
-import { aiTypeDic, objectToArray } from "@/utils/basicDic";
-import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker } from "@/components/scTable/helper";
-
-const formatAIGate = row => XEUtils.find(TOOL.data.get("UGLIAI_GATE"), item => XEUtils.findIndexOf(item.devices, d => d.device_num === XEUtils.get(row, "deviceSn", XEUtils.get(row, "deviceNum"))) !== -1);
-
-const props = defineProps({
-    checked: { type: Boolean, default: false },
-    hideProject: { type: Boolean, default: false },
-    options: { type: Object, default: () => {} }
-})
-
-const route = useRoute()
-
-const proConfig = reactive({
-    visibleMethod: () => !props.hideProject,
-    storageKey: "PROJECT",
-    resetValue: props.hideProject ? 234 : XEUtils.toNumber(XEUtils.get(route, "query.projectId", 234)),
-    props: { clearable: false },
-    optionProps: { label: "projectName", value: "fpiId" },
-    events: {
-        change: data => XEUtils.assign(formConfig.data, data)
-    }
-})
-
-const typeConfig = reactive({
-    props: { multiple: true, collapseTags: true, collapseTagsTooltip: true },
-    options: objectToArray(aiTypeDic),
-    events: {
-        change: data => XEUtils.assign(formConfig.data, data)
-    }
-})
-
-const daterangeConfig = reactive({
-    span: 9,
-    props: {
-        type: "datetimerange",
-        startPlaceholder: "开始时间",
-        endPlaceholder: "结束时间",
-        format: "YYYY-MM-DD HH:mm"
-    }
-})
-
-const toolbarConfig = reactive({
-    enabled: true,
-    print: false
-})
-
-const formConfig = reactive({
-    data: {
-        projectID: props.hideProject ? 234 : XEUtils.toNumber(XEUtils.get(route, "query.projectId", 234)),
-        recordType: XEUtils.keys(aiTypeDic)
-    },
-    items: [
-        mapFormItemSelect("projectID", "所属项目", proConfig),
-        mapFormItemSelect("recordType", "识别类型", typeConfig),
-        mapFormItemDatePicker("createTime", "监测时间", daterangeConfig)
-    ]
-})
-
-const paramsColums = reactive([
-    { type: "relation", column: "recordType", symbol: "or" },
-    { type: "relation", column: "createTime", symbol: "eqgt", field: "createTime[0]", formatValue: value => moment(value).format("YYYY-MM-DDTHH:mm:ss[Z]") },
-    { type: "relation", column: "createTime", symbol: "eqlt", field: "createTime[1]", formatValue: value => moment(value).format("YYYY-MM-DDTHH:mm:ss[Z]") },
-    { type: "orderby", column: "createTime", symbol: "desc" },
-    { type: "expands", column: "projectID", symbol: "eq" }
-]);
-
-const columns = reactive([
-    { visible: props.checked, type: "checkbox", fixed: "left", title: "", width: 40 },
-    { type: "seq", fixed: "left", width: 60 },
-    { type: "html", field: "features.serialNumber", title: "设备唯一标识", fixed: "left", minWidth: 150, sortable: true },
-    { type: "html", field: "projectName", title: "所属项目", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(TOOL.data.get("PROJECT"), item => item.fpiId === formConfig.data.projectID), "projectName") },
-    { type: "html", field: "mounted.ground.groundName", title: "工地场区", minWidth: 150, sortable: true },
-    { type: "html", field: "mounted.mountedName", title: "安装点名称", minWidth: 150, sortable: true },
-    { type: "html", field: "taskTime", title: "监测时间", minWidth: 160, sortable: true, formatter: ({ cellValue, row }) => cellValue || TOOL.dateFormat(row.createTime) },
-    { type: "html", field: "recordType", title: "识别类型", minWidth: 110, sortable: true, formatter: ({ cellValue }) => XEUtils.get(aiTypeDic, cellValue, cellValue) },
-    { title: "抓拍图片", minWidth: 110, align: "center", slots: { default: "default_imgUrl" } },
-    { visible: props.checked, title: "操作", fixed: "right", minWidth: 140, slots: { default: "action" } }
-])
-</script>

+ 170 - 0
src/views/dataMock/ugliAi/components/record/detail.vue

@@ -0,0 +1,170 @@
+<template>
+    <el-dialog v-model="visible" :title="titleMap[mode]" :width="480" :close-on-click-modal="false" @closed="$emit('closed', isDel)">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <template v-if="props.projectId != 1">
+                <el-form-item label="所属项目" prop="projectId">
+                    <el-select v-model="form.projectId" filterable placeholder="请选择所属项目" @change="form.mountedId = null">
+                        <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-form-item label="设备安装点" prop="mountedId">
+                    <el-select v-model="form.mountedId" filterable placeholder="请选择设备安装点">
+                        <el-option v-for="item in filterMounteds" :key="item.id" :label="item.mountedName" :value="item.id"></el-option>
+                    </el-select>
+                </el-form-item>
+            </template>
+            <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-item label="识别结果" prop="recordType">
+                <el-select v-model="form.recordType" filterable placeholder="请选择识别结果">
+                    <el-option v-for="(label, key) in aiTypeDic" :key="key" :label="label" :value="key"></el-option>
+                </el-select>
+            </el-form-item>
+            <el-form-item label="抓拍图片" prop="features.bigImage.file">
+                <sc-upload v-model="form.features.bigImage.file" apiKey="minio" :width="140" :height="180" accept="image/jpeg" @removeSuccess="removeSuccess"></sc-upload>
+            </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 { aiTypeDic } from "@/utils/basicDic";
+import scUpload from "@/components/scUpload/index";
+
+const $emit = defineEmits(["success", "closed"]);
+const props = defineProps({
+    projectId: { type: Number, default: TOOL.data.get("PROJECT_ID") }
+});
+
+const visible = ref(false);
+const isSaving = ref(false);
+const isDel = ref(false);
+
+const mode = ref("add");
+const titleMap = reactive({
+    add: "数据录入",
+    edit: "修改"
+});
+
+const form = ref({
+    id: null,
+    projectId: props.projectId,
+    mountedId: null,
+    recordType: null,
+    createTime: null,
+    features: {
+        bigImage: {
+            file: {}
+        }
+    }
+});
+const rules = reactive({
+    projectId: [{ required: true, message: "请选择所属项目" }],
+    mountedId: [{ required: true, message: "请选择设备安装点" }],
+    createTime: [{ required: true, message: "请选择抓拍时间" }],
+    recordType: [{ required: true, message: "请选择识别结果" }],
+    "features.bigImage.file": [{ required: true, validator: (rule, value, callback) => {
+        if (XEUtils.isEmpty(value)) return callback(new Error("请上传抓拍图片"));
+        callback();
+    }}]
+})
+
+const mounteds = ref([]);
+const filterMounteds = computed(() => form.value.projectId ? XEUtils.filter(mounteds.value, item => item.projectId == form.value.projectId) : []);
+const fetchMounted = async () => {
+    const res = await API.ugliAi.mounted.get();
+    mounteds.value = res || [];
+    if (props.projectId == 1) form.value.mountedId = XEUtils.get(XEUtils.find(res, item => item.projectId == 1), "id");
+}
+
+const open = () => {
+    visible.value = true;
+    fetchMounted();
+}
+const setData = data => {
+    open();
+    mode.value = "edit";
+    XEUtils.objectEach(form.value, (_, key) => {
+        if (key == "features") {
+            const features = XEUtils.toStringJSON(XEUtils.get(data, key));
+            const path = XEUtils.get(features, "bigImage.image", "");
+
+            XEUtils.set(form.value, key, {
+                ...XEUtils.omit(features, "bigImage"),
+                bigImage: {
+                    ...XEUtils.get(features, "bigImage"),
+                    file: path ? {
+                        name: `/minio${path}`.replaceAll("/", "_"),
+                        mineType: "image/jpeg",
+                        path
+                    } : {}
+                }
+            })
+        } else XEUtils.set(form.value, key, XEUtils.get(data, key));
+    });
+}
+
+const formRef = ref();
+const submit = () => {
+    formRef.value.validate(valid => {
+        if (valid) {
+            const data = {
+                ...XEUtils.omit(form.value, "features"),
+                features: XEUtils.toJSONString({
+                    ...form.value.features,
+                    bigImage: {
+                        ...XEUtils.omit(form.value.features.bigImage, "file"),
+                        image: form.value.features.bigImage.file.path
+                    }
+                })
+            }
+
+            isSaving.value = true;
+            API.ugliAi.record[mode.value](data).then(() => {
+                isSaving.value = false;
+                ElMessage.success("操作成功");
+                visible.value = false;
+                $emit("success", mode.value);
+            }).catch(() => isSaving.value = false);
+        } else {
+            return false;
+        }
+    });
+}
+
+const removeSuccess = () => {
+    const data = {
+        id: form.value.id,
+        features: JSON.stringify({
+            ...form.value.features,
+            bigImage: {
+                ...XEUtils.omit(form.value.features.bigImage, "file"),
+                image: ""
+            }
+        })
+    }
+    
+    isDel.value = true;
+    API.ugliAi.record.edit(data);
+}
+
+defineExpose({
+    open,
+    setData
+})
+</script>
+
+<style scoped>
+.el-form {
+    padding-right: calc(var(--el-dialog-padding-primary) + var(--el-message-close-size, 16px));
+}
+</style>

+ 182 - 0
src/views/dataMock/ugliAi/components/record/index.vue

@@ -0,0 +1,182 @@
+<template>
+    <scTable ref="xGridTable" batchDel :apiObj="$API.ugliAi.record" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns" v-bind="props.options">
+        <template #default_imgUrl="{ row }">
+            <template v-if="XEUtils.get(XEUtils.toStringJSON(row.features), 'bigImage.image')">
+                <vxe-image style="cursor: pointer;" :src="'/minio' + XEUtils.get(XEUtils.toStringJSON(row.features), 'bigImage.image')" width="40" height="40" :toolbar-config="imageToolbar"></vxe-image>
+            </template>
+        </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>
+
+    <record-detail v-if="dialog" ref="recordRef" :projectId="props.isTemp ? 1 : TOOL.data.get('PROJECT_ID')" @success="refreshTable" @closed="dialogClose"></record-detail>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { mapFormItemSelect, mapFormItemDatePicker } from "@/components/scTable/helper";
+import { dataSource, aiTypeDic, objectToArray } from "@/utils/basicDic";
+import recordDetail from "./detail";
+
+const props = defineProps({
+    options: { type: Object, default: () => {} },
+    isTemp: { type: Boolean, default: false },
+    hideHandler: { type: Boolean, default: false }
+})
+const visible = computed(() => !props.isTemp);
+
+const proConfig = reactive({
+    span: 5,
+    visible,
+    storageKey: "PROJECT",
+    resetValue: TOOL.data.get("PROJECT_ID"),
+    optionProps: { label: "projectName", value: "fpiId" },
+    events: {
+        change: data => XEUtils.assign(formConfig.data, { ...data, mountedId: null })
+    }
+})
+
+const mountedConfig = reactive({
+    visible,
+    api: {
+        key: "ugliAi.mounted",
+        query: {
+            projectId: computed(() => formConfig.data.projectId),
+            projectIdNot: 1
+        }
+    },
+    slot: {
+        style: { float: "right", paddingLeft: "6px", color: "#8492a6" }
+    },
+    optionProps: { label: "mountedName", value: "id", slot: ({ data }) => XEUtils.get(XEUtils.find(TOOL.data.get("PROJECT"), item => item.fpiId === data.projectId), "projectName") },
+    events: {
+        change: data => XEUtils.assign(formConfig.data, data)
+    }
+})
+
+const selectConfig = reactive({
+    options: objectToArray(aiTypeDic),
+    props: {
+        multiple: true,
+        collapseTags: true,
+        collapseTagsTooltip: true
+    },
+    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: {
+        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"),
+        projectIdNot: 1,
+        createTime: [moment().startOf("month").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")]
+    },
+    items: [
+        mapFormItemSelect("projectId", "所属项目", proConfig),
+        mapFormItemSelect("mountedId", "设备安装点", mountedConfig),
+        mapFormItemDatePicker("createTime", "抓拍时间", datetimerangeConfig),
+        mapFormItemSelect("recordType", "识别结果", selectConfig)
+    ]
+})
+
+const paramsColums = reactive([
+    { column: "projectId", field: visible.value ? "" : "projectIdNot" },
+    visible.value ? { column: "projectIdNot" } : {},
+    { column: "mountedId" },
+    { column: "recordTypeIn", field: "recordType" },
+    { column: "createTimeBegin", field: "createTime[0]" },
+    { column: "createTimeEnd", field: "createTime[1]" }
+])
+
+const columns = reactive([
+    { visible: !props.hideHandler, type: "checkbox", fixed: "left", width: 40 },
+    { type: "seq", fixed: "left", width: 60 },
+    { visible, 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") },
+    { visible, type: "html", field: "groundName", title: "工地场区", minWidth: 160, sortable: true },
+    { visible, type: "html", field: "mountedName", title: "设备安装点", minWidth: 160, sortable: true },
+    { type: "html", field: "createTime", title: "抓拍时间", minWidth: 160, sortable: true },
+    { type: "html", field: "recordType", title: "识别结果", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(aiTypeDic, cellValue, cellValue) },
+    { field: "bigImage", title: "抓拍图片", minWidth: 110, align: "center", slots: { default: "default_imgUrl" } },
+    { visible, type: "html", field: "dataSource", title: "数据来源", fixed: "right", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(dataSource, cellValue, cellValue) },
+    { 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();
+const getTableTotal = () => xGridTable.value.getTableData().tableData.length;
+
+const refreshTable = () => {
+    xGridTable.value.reloadColumn(columns);
+    xGridTable.value.searchData();
+}
+
+const recordRef = ref();
+const dialog = ref(false);
+
+const dialogClose = isDel => {
+    dialog.value = false;
+    isDel && refreshTable();
+}
+
+const table_add = () => {
+    dialog.value = true;
+    nextTick(() => recordRef.value?.open());
+}
+
+const table_edit = row => {
+    dialog.value = true;
+    nextTick(() => recordRef.value?.setData(row));
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该监控记录?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.ugliAi.record.del({ id }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    });
+}
+
+defineExpose({
+    table_add,
+    refreshTable,
+    getTableTotal
+})
+</script>

+ 0 - 7
src/views/dataMock/ugliAi/components/template.vue

@@ -1,7 +0,0 @@
-<template>
-    <data-table hideProject></data-table>
-</template>
-
-<script setup>
-import dataTable from "./record.vue";
-</script>

+ 12 - 0
src/views/dataMock/ugliAi/components/template/index.vue

@@ -0,0 +1,12 @@
+<template>
+    <data-table ref="tableRef" isTemp></data-table>
+</template>
+
+<script setup>
+import dataTable from "../record";
+
+const tableRef = ref();
+defineExpose({
+    table_add: () => tableRef.value.table_add()
+})
+</script>

+ 143 - 76
src/views/dataMock/ugliAi/detail.vue

@@ -1,115 +1,194 @@
 <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="数据模拟" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="126">
             <el-row>
                 <el-col :md="12" :xs="24">
-                    <el-form-item label="时间范围" prop="dateRange">
-                        <el-date-picker v-model="form.dateRange" type="daterange" :clearable="false" :shortcuts="shortcuts" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
+                    <el-form-item label="模拟项目" prop="targetProjectId">
+                        <el-select v-model="form.targetProjectId" filterable placeholder="请选择模拟项目" @change="form.targetMountedId = null, 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 class="step-item" label="时间步长" prop="timeStep">
-                        <el-input-number v-model="form.timeStep" :min="0"
-                            :max="['second', 'minute'].includes(form.timeStepType) ? 60 : form.timeStepType == 'hour' ? 12 : Infinity" 
-                            :controls="false" placeholder="时间步长">
-                        </el-input-number>
-                        <el-form-item>
-                            <el-select v-model="form.timeStepType">
-                                <el-option label="秒" value="second"></el-option>
-                                <el-option label="分钟" value="minute"></el-option>
-                                <el-option label="小时" value="hour"></el-option>
-                                <el-option label="天" value="day"></el-option>
-                            </el-select>
-                        </el-form-item>
+                    <el-form-item label="模拟项目安装点" prop="targetMountedId">
+                        <el-select v-model="form.targetMountedId" filterable placeholder="请选择模拟项目安装点">
+                            <el-option v-for="item in filterTargetM" :key="item.id" :label="item.mountedName" :value="item.id"></el-option>
+                        </el-select>
                     </el-form-item>
                 </el-col>
                 <el-col :md="12" :xs="24">
-                    <el-form-item label="精度偏差" prop="precision">
-                        <el-input-number v-model="form.precision" :min="0" :max="2" :precision="2" :controls="false" placeholder="请输入精度偏差">
-                            <template #prefix>±</template>
-                        </el-input-number>
+                    <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="数据处理" prop="handler">
-                        <el-radio-group v-model="form.handler">
-                            <el-radio value="copy">重复新增</el-radio>
-                            <el-radio value="cover">数据覆盖</el-radio>
-                            <el-radio value="partly">部分覆盖</el-radio>
+                    <el-form-item label="模拟月份">
+                        <el-select v-model="form.targetMonth" filterable clearable 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 label="数据模式" prop="checkList">
-                        <el-checkbox-group v-model="form.checkList">
-                            <el-checkbox label="模版项目" value="template" />
-                            <el-checkbox label="选择项目" value="select" />
-                        </el-checkbox-group>
+                    <el-form-item style="margin-bottom: 0;" label="数据来源" prop="source">
+                        <el-radio-group v-model="form.source" @change="refreshTable">
+                            <el-radio value="template">模版项目</el-radio>
+                            <el-radio value="other">其他项目</el-radio>
+                        </el-radio-group>
                     </el-form-item>
                 </el-col>
             </el-row>
 
-            <template v-if="form.checkList.includes('select')">
-                <data-table ref="tableRef" checked :options="tableOptions"></data-table>
-            </template>
+            <el-divider />
+
+            <el-row>
+                <template v-if="form.source == 'other'">
+                    <el-col :md="12" :xs="24">
+                        <el-form-item label="数据源项目" prop="sourceProjectId">
+                            <el-select v-model="form.sourceProjectId" filterable placeholder="请选择数据源项目" @change="form.sourceMountedId = null, 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="sourceMountedId">
+                            <el-select v-model="form.sourceMountedId" filterable placeholder="请选择数据源安装点" @change="refreshTable">
+                                <el-option v-for="item in filterSourceM" :key="item.id" :label="item.mountedName" :value="item.id"></el-option>
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
+                </template>
+
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="抓拍时间" prop="sourceTime">
+                        <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="结束时间" @change="refreshTable"></el-date-picker>
+                    </el-form-item>
+                </el-col>
+            </el-row>
+
+            <data-table ref="tableRef" :isTemp="form.source == 'template'" hideHandler :options="tableOptions"></data-table>
         </el-form>
 
         <template #footer>
-            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit()">提交</el-button>
+            <el-button v-if="form.source == 'other'" :loading="isSaving" type="primary" auto-insert-space @click="submit('template')">保存为模版</el-button>
             <el-button auto-insert-space @click="visible = false">取消</el-button>
         </template>
     </el-dialog>
 </template>
 
 <script setup>
-import moment from "moment";
 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 shortcuts = [
-    { text: "上周", value: () => [moment().subtract(1, "week"), moment()] },
-    { text: "上月", value: () => [moment().subtract(1, "month"), moment()] },
-    { text: "去年", value: () => [moment().subtract(1, "year"), moment()] }
-]
-
 const form = ref({
-    id: null,
-    dateRange: null,
-    timeStepType: "minute",
-    timeStep: 3,
-    precision: null,
-    handler: "copy",
-    checkList: ["select"]
+    targetProjectId: TOOL.data.get("PROJECT_ID"),
+    targetMountedId: null,
+    targetYear: null,
+    targetMonth: null,
+    isCover: false,
+    source: "other",
+    sourceProjectId: null,
+    sourceProjectIdNot: 1,
+    sourceMountedId: null,
+    sourceTime: []
 });
 
 const rules = reactive({
-    dateRange: [{ required: true, message: "请选择时间范围" }],
-    timeStep: [{ required: true, message: "请输入时间步长" }],
-    precision: [{ required: true, message: "请输入精度偏差" }],
-    checkList: [{ required: true, message: "请选择生成模式" }]
+    targetProjectId: [{ required: true, message: "请选择模拟项目" }],
+    targetMountedId: [{ required: true, message: "请选择模拟项目安装点" }],
+    targetYear: [{ required: true, message: "请选择模拟年份" }],
+    isCover: [{ required: true }],
+    source: [{ required: true }],
+    sourceProjectId: [{ required: true, message: "请选择数据源项目" }],
+    sourceMountedId: [{ required: true, message: "请选择数据源安装点" }],
+    sourceTime: [{ required: true, message: "请选择数据源抓拍时间" }]
 })
 
-const open = () => visible.value = true;
-const formRef = ref();
 const tableRef = ref();
-
 const tableOptions = reactive({
-    height: 392,
-    checkboxConfig: {
-        highlight: true,
-        labelField: ""
-    }
+    batchDel: false,
+    maxHeight: 1048,
+    toolbarConfig: { enabled: true, print: false, zoom: false },
+    formConfig: { enabled: false, data: form },
+    paramsColums: computed(() => [
+        { column: "projectId", field: form.value.source == "template" ? "sourceProjectIdNot" : "sourceProjectId"  },
+        form.value.source == "template" ? {} : { column: "projectIdNot", field: "sourceProjectIdNot" },
+        { column: "createTimeBegin", field: "sourceTime[0]" },
+        { column: "createTimeEnd", field: "sourceTime[1]" }
+    ])
 })
+const refreshTable = () => tableRef.value.refreshTable();
+
 
-const refreshTable = e => tableRef.value.refreshTable(e);
+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 submit = () => {
+const mounteds = ref([]);
+const filterTargetM = computed(() => form.value.targetProjectId ? XEUtils.filter(mounteds.value, item => item.projectId == form.value.targetProjectId) : []);
+const filterSourceM = computed(() => form.value.sourceProjectId ? XEUtils.filter(mounteds.value, item => item.projectId == form.value.sourceProjectId) : []);
+const fetchMounted = async () => {
+    const res = await API.ugliAi.mounted.get();
+    mounteds.value = res || [];
+}
+
+const open = () => {
+    visible.value = true;
+    TOOL.data.get("PROJECT_ID") && dataTimeRange();
+    fetchMounted();
+}
+
+const formRef = ref();
+const submit = key => {
     formRef.value.validate(valid => {
         if (valid) {
+            if (tableRef.value.getTableTotal() == 0) return ElMessage.warning("暂无相关数据,请调整条件后重试。");
+            
+            const data = XEUtils.omit(form.value, "sourceProjectId", "sourceProjectIdNot", "source", "sourceTime");
+            XEUtils.set(data, "sourceBeginTime", XEUtils.first(form.value.sourceTime));
+            XEUtils.set(data, "sourceEndTime", XEUtils.last(form.value.sourceTime));
+            
+            form.value.source == "template" && XEUtils.set(data, "sourceMountedId", XEUtils.get(XEUtils.find(mounteds.value, item => item.projectId == 1), "id"));
+            if (key == "template") {
+                XEUtils.set(data, "targetProjectId", 1);
+                XEUtils.set(data, "targetMountedId", XEUtils.get(XEUtils.find(mounteds.value, item => item.projectId == 1), "id"));
+            }
+
+            isSaving.value = true;
+            API.ugliAi.copyData.add(data).then(() => {
+                isSaving.value = false;
+                ElMessage.success("操作成功");
+                visible.value = false;
+                $emit("success");
+            }).catch(() => isSaving.value = false);
         } else {
             return false;
         }
@@ -122,19 +201,7 @@ defineExpose({
 </script>
 
 <style lang="scss" scoped>
-.el-form {padding-right: calc(var(--el-dialog-padding-primary) + 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 {padding-right: var(--el-message-close-size, 16px);}
 .el-form-item .el-radio-group {flex-wrap: nowrap;}
-
 .el-form :deep(.el-main) {padding-right: 0;padding-bottom: 0;}
 </style>

+ 21 - 10
src/views/dataMock/ugliAi/index.vue

@@ -1,34 +1,45 @@
 <template>
 	<el-container class="is-vertical">
-        <sc-page-header @add="dock_add"></sc-page-header>
+        <sc-page-header addText="数据模拟" @add="mock_add">
+            <template #extra-right>
+                <el-button v-if="activeName == 'record' || activeName == 'template'" type="primary" @click="table_add">数据录入</el-button>
+            </template>
+        </sc-page-header>
 
         <el-tabs v-model="activeName">
-            <el-tab-pane v-for="(label, key) in XEUtils.omit(workerStates, ['threshold', 'calendar'])" :key="key" :label="label" :name="key"></el-tab-pane>
-            <el-tab-pane label="模版项目" name="template"></el-tab-pane>
+            <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]" taskType="aihazard" />
 	</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 { workerStates } from "../main";
-import allcomp from "./components";
+import { workerStates } from "./main";
+import comp from "./components";
+import monos from "@/views/dataMock/tasks/monos";
 import mockDetail from "./detail";
 
-const activeName = ref("monos");
-const refreshState = () => {}
+const allcomp = { ...comp, monos };
+const activeName = ref("record");
 
+const componentRef = ref();
 const mockRef = ref();
 const dialog = ref(false);
 
-const dock_add = () => {
+const table_add = () => componentRef.value.table_add();
+
+const mock_add = () => {
     dialog.value = true;
     nextTick(() => mockRef.value?.open());
 }
+
+const refreshState = () => {
+    if (activeName.value == "monos") setTimeout(() => componentRef.value.refreshTable(), 2000);
+    activeName.value = "monos";
+}
 </script>
 
 <style lang="scss" scoped>

+ 5 - 0
src/views/dataMock/ugliAi/main.js

@@ -0,0 +1,5 @@
+export const workerStates = {
+    record: "设备监控",
+    monos: "任务中心",
+    template: "模版项目"
+}

+ 0 - 47
src/views/dataMock/ugliAi/mono/index.vue

@@ -1,47 +0,0 @@
-<template>
-    <el-dialog v-model="visible" title="任务详情" width="80%" @closed="$emit('closed')">
-        <!-- <scTable :apiObj="formConfig.data.deviceCode && $API.passqrcode.online" min-height="108" max-height="600" framework="zeroLite" :formConfig="formConfig" :paramsColums="paramsColums" :columns="columns"></scTable> -->
-        <scTable :maxHeight="600" :options="options">
-            <template #expand_content="{ row }">
-                <table-expand :rowData="row.monos"></table-expand>
-            </template>
-            <template #progress_content="{ row }">
-                <el-progress text-inside :stroke-width="16" :status="row.status == '已完成' ? 'success' : row.status == '执行失败' ? 'exception' : ''" :percentage="row.progress" />
-            </template>
-        </scTable>
-    </el-dialog>
-</template>
-
-<script setup>
-import XEUtils from "xe-utils";
-import TOOL from "@/utils/tool";
-import { defaultGroups } from "../../main";
-
-import tableExpand from "./tableExpand";
-
-const visible = ref(false);
-const open = data => {
-    visible.value = true;
-    options.data = data ? [XEUtils.first(XEUtils.get(defaultGroups, "groups.000.monos"))] : XEUtils.get(defaultGroups, "groups.000.monos")
-}
-
-const options = reactive({
-    minHeight: 108,
-    data: [],
-    formConfig: { enabled: false },
-    pagerConfig: { enabled: false },
-    columns: [
-        { type: "expand", fixed: "left", width: 50, align: "center", slots: { content: "expand_content" } },
-        { field: "option", title: "任务类型", minWidth: 100 },
-        { field: "createTime", title: "触发时间", minWidth: 160, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue) },
-        { field: "status", title: "任务状态", minWidth: 100 },
-        { field: "executeTimes", title: "执行次数", minWidth: 80 },
-        { field: "progress", title: "执行进度", minWidth: 250, slots: { default: 'progress_content' } },
-        { field: "reason", title: "失败原因", minWidth: 160 }
-    ]
-})
-
-defineExpose({
-    open
-})
-</script>

+ 0 - 33
src/views/dataMock/ugliAi/mono/tableExpand.vue

@@ -1,33 +0,0 @@
-<template>
-    <scTable :maxHeight="192" :options="options">
-        <template #progress_content="{ row }">
-            <el-progress text-inside :stroke-width="16" :status="row.status == '已完成' ? 'success' : row.status == '执行失败' ? 'exception' : ''" :percentage="row.progress" />
-        </template>
-    </scTable>
-</template>
-
-<script setup>
-import TOOL from "@/utils/tool";
-
-const options = reactive({
-    minHeight: 108,
-    data: useAttrs().rowData,
-    formConfig: { enabled: false },
-    pagerConfig: { enabled: false },
-    columns: [
-        { field: "dateRange", title: "时间范围", minWidth: 160 },
-        { field: "timeStep", title: "时间步长", minWidth: 100 },
-        { field: "precision", title: "精度偏差", minWidth: 100 },
-        { field: "handler", title: "数据处理", minWidth: 80 },
-        { field: "createTime", title: "触发时间", minWidth: 160, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue) },
-        { field: "status", title: "任务状态", minWidth: 100 },
-        { field: "executeTimes", title: "执行次数", minWidth: 80 },
-        { field: "progress", title: "执行进度", minWidth: 250, slots: { default: 'progress_content' } },
-        { field: "reason", title: "失败原因", minWidth: 160 }
-    ]
-})
-</script>
-
-<style scoped>
-    .el-main {padding: 0;}
-</style>