Jelajahi Sumber

喷淋-塔机

zhuangyunsheng 5 bulan lalu
induk
melakukan
07f74a114d
36 mengubah file dengan 1743 tambahan dan 57 penghapusan
  1. 1 1
      .env.development
  2. 44 0
      src/api/model/spray.js
  3. 70 8
      src/api/model/tower.js
  4. 4 2
      src/components/scTable/index.vue
  5. 1 1
      src/components/scTable/renderer/form-select.vue
  6. 1 1
      src/components/scTable/renderer/pager-batch-del.vue
  7. 1 1
      src/components/scTable/renderer/table-search.vue
  8. 18 0
      src/config/route.js
  9. 4 4
      src/config/table.js
  10. 8 3
      src/layout/index.vue
  11. 2 2
      src/router/index.js
  12. 0 18
      src/utils/basicDic.js
  13. 1 1
      src/views/dataMock/carwash/components/template/index.vue
  14. 1 1
      src/views/dataMock/env/mono/tableExpand.vue
  15. 17 0
      src/views/dataMock/spray/components/index.js
  16. 113 0
      src/views/dataMock/spray/components/record/detail.vue
  17. 172 0
      src/views/dataMock/spray/components/record/index.vue
  18. 134 0
      src/views/dataMock/spray/detail.vue
  19. 46 0
      src/views/dataMock/spray/index.vue
  20. 9 0
      src/views/dataMock/spray/main.js
  21. 3 3
      src/views/dataMock/standard/components/record/detail.vue
  22. 7 6
      src/views/dataMock/standard/components/record/index.vue
  23. 1 1
      src/views/dataMock/standard/detail.vue
  24. 1 1
      src/views/dataMock/standard/index.vue
  25. 10 0
      src/views/dataMock/standard/main.js
  26. 7 2
      src/views/dataMock/tasks/monos.vue
  27. 128 0
      src/views/dataMock/tower/components/alarm.vue
  28. 139 0
      src/views/dataMock/tower/components/attendance.vue
  29. 17 0
      src/views/dataMock/tower/components/index.js
  30. 205 0
      src/views/dataMock/tower/components/record/detail.vue
  31. 171 0
      src/views/dataMock/tower/components/record/index.vue
  32. 1 1
      src/views/dataMock/ugliAi/components/template/index.vue
  33. 304 0
      src/views/dataMock/tower/detail.vue
  34. 58 0
      src/views/dataMock/tower/index.vue
  35. 32 0
      src/views/dataMock/tower/main.js
  36. 12 0
      src/views/dataMock/ugliAi/components/template.vue

+ 1 - 1
.env.development

@@ -15,4 +15,4 @@ VUE_APP_OPS_BASEURL = http://192.168.101.93:8804
 VUE_APP_PORT = 3200
 
 # 是否开启代理
-VUE_APP_PROXY = true
+VUE_APP_PROXY = true

+ 44 - 0
src/api/model/spray.js

@@ -0,0 +1,44 @@
+import config from "@/config"
+import http from "@/utils/request"
+
+export default {
+    mounted: {
+        url: `${config.API_URL}/ops/autospray/getMountedList`,
+        name: "安装点查询",
+        get: async function (data = {}) {
+            return await http.post(this.url, data);
+        }
+    },
+
+    record: {
+        name: "预警记录",
+        url: `${config.API_URL}/ops/autospray`,
+        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);
+        }
+    },
+
+    dataMock: {
+        url: `${config.API_URL}/ops/autospray`,
+        name: "数据模拟-参数/复制",
+        makeData: async function (data = {}) {
+            return await http.post(`${this.url}/makeData`, data);
+        }
+    }
+}

+ 70 - 8
src/api/model/tower.js

@@ -2,27 +2,89 @@ import config from "@/config"
 import http from "@/utils/request"
 
 export default {
-    gate: {
-        url: `${config.API_URL}/api/tcm/mounted/fetch`,
+    mounted: {
+        url: `${config.API_URL}/ops/tcm/getMountedList`,
         name: "安装点查询",
         get: async function (data = {}) {
             return await http.post(this.url, data);
         }
     },
 
-    records: {
-        url: `${config.API_URL}/api/iotTcm/getPage`,
+    record: {
         name: "监测记录",
+        url: `${config.API_URL}/ops/tcm`,
         get: async function (data = {}) {
-            return await http.post(this.url, 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);
         }
     },
 
-    warnings: {
-        url: `${config.API_URL}/api/tcm/warnings/fetch`,
+    warning: {
         name: "告警记录",
+        url: `${config.API_URL}/ops/tcm`,
         get: async function (data = {}) {
-            return await http.post(this.url, data);
+            return await http.post(`${this.url}/getWarningPage`, data);
+        },
+
+        del: async function (data = {}) {
+            return await http.post(`${this.url}/removeWarning`, data);
+        },
+
+        batchDel: async function (data = {}) {
+            return await http.post(`${this.url}/batchRemoveWarning`, data);
+        }
+    },
+
+    attendance: {
+        name: "打卡记录",
+        url: `${config.API_URL}/ops/tcm`,
+        get: async function (data = {}) {
+            return await http.post(`${this.url}/getPersonPage`, data);
+        },
+
+        del: async function (data = {}) {
+            return await http.post(`${this.url}/removePersonRecord`, data);
+        },
+
+        batchDel: async function (data = {}) {
+            return await http.post(`${this.url}/batchRemovePerson`, data);
+        }
+    },
+
+    dataMock: {
+        copyData: {
+            url: `${config.API_URL}/ops/tcm`,
+            name: "数据模拟-复制",
+            normal: async function (data = {}) {
+                return await http.post(`${this.url}/copyData`, data);
+            }
+        },
+
+        makeData: {
+            url: `${config.API_URL}/ops/tcm`,
+            name: "数据模拟-参数",
+            attendance: async function (data = {}) {
+                return await http.post(`${this.url}/makePersonData`, data);
+            },
+
+            alarm: async function (data = {}) {
+                return await http.post(`${this.url}/makeWarningData`, data);
+            }
         }
     }
 }

+ 4 - 2
src/components/scTable/index.vue

@@ -206,7 +206,9 @@ const getData = () => {
     nextTick(() => {
         gridOptions.value.loading = true;
         const reqData = config.framework[props.framework].queryData(gridOptions.value, props.paramsColums);
-        props.apiObj[props.apiKey](reqData).then(res => {
+        const query = props.apiObj.url == "/ops/projectInfo" ? XEUtils.omit(reqData, "orderBy") : reqData;
+
+        props.apiObj[props.apiKey](query).then(res => {
             const response = config.framework[props.framework].parseData(res);
             gridOptions.value.data = response.data || [];
             gridOptions.value.pagerConfig.total = response.total || 0;
@@ -297,5 +299,5 @@ defineExpose({
 </script>
 
 <style scoped>
-    .el-main {padding: 0 12px 12px;background: var(--el-bg-color);}
+.el-main {padding: 0 12px 12px;background: var(--el-bg-color);}
 </style>

+ 1 - 1
src/components/scTable/renderer/form-select.vue

@@ -1,7 +1,7 @@
 <template>
     <el-select v-model="modelValue" :loading="loading" v-bind="renderOpts.props" @change="compChange">
         <template #label="{ label }">
-            <span v-if="renderOpts.slot">{{ XEUtils.first(label.split(" ")) }}</span>
+            <span v-if="renderOpts.slot && XEUtils.isString(label)">{{ 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 })">

+ 1 - 1
src/components/scTable/renderer/pager-batch-del.vue

@@ -16,5 +16,5 @@ watch(() => props.params.$grid?.getCheckboxRecords(), val => ids.value = XEUtils
 </script>
 
 <style scoped>
-    .el-button {position: absolute;left: 0;}
+.el-button {position: absolute;left: 0;}
 </style>

+ 1 - 1
src/components/scTable/renderer/table-search.vue

@@ -47,5 +47,5 @@ const searchInTable = () => {
 </script>
 
 <style scoped>
-    .el-input {width: 180px;}
+.el-input {width: 180px;}
 </style>

+ 18 - 0
src/config/route.js

@@ -100,6 +100,18 @@ const routes = [
                 meta: { title: "数据管理与模拟-AI视频危险源识别", icon: "hugeicons:ai-brain-02" },
                 component: "dataMock/ugliAi"
             },
+            {
+                name: "towerMock",
+                path: "/dataMock/tower",
+                meta: { title: "数据管理与模拟-塔机监测", icon: "mingcute:tower-crane-line" },
+                component: "dataMock/tower"
+            },
+            {
+                name: "elevatorMock",
+                path: "/dataMock/elevator",
+                meta: { title: "数据管理与模拟-施工升降电梯监测", icon: "icon-park-outline:elevator" },
+                component: "dataMock/elevator"
+            },
             {
                 name: "standardMock",
                 path: "/dataMock/standard",
@@ -112,6 +124,12 @@ const routes = [
                 meta: { title: "数据管理与模拟-环境监测", icon: "fluent:earth-leaf-16-regular" },
                 component: "dataMock/env"
             },
+            {
+                name: "sprayMock",
+                path: "/dataMock/spray",
+                meta: { title: "数据管理与模拟-自动喷淋系统", icon: "covid:vaccine-protection-sanitizer-spray" },
+                component: "dataMock/spray"
+            },
             {
                 name: "carwashMock",
                 path: "/dataMock/carwash",

+ 4 - 4
src/config/table.js

@@ -2,9 +2,9 @@
 const XEUtils = require("xe-utils");
 
 export default {
-	pageSize: 20,													                                            //表格每一页条数
-	pageSizes: [5, 10, 15, 20, 50, 100, 200, 500, 1000],                                                        //表格可设置的一页条数
-	layouts: ["PrevJump", "PrevPage", "Jump", "PageCount", "NextPage", "NextJump", "Sizes", "Total"],	        //表格分页布局
+	pageSize: 20,													                                            // 表格每一页条数
+	pageSizes: [5, 10, 15, 20, 50, 100, 200, 500, 1000],                                                        // 表格可设置的一页条数
+	layouts: ["PrevJump", "PrevPage", "Jump", "PageCount", "NextPage", "NextJump", "Sizes", "Total"],	        // 表格分页布局
     exportExcludeFields: ["checkbox", "radio"],
     
     framework: {
@@ -12,7 +12,7 @@ export default {
             queryData: function ({ formConfig: { data }, pagerConfig: { queryType, currentPage, pageSize } }, paramsColumns) {
                 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)))
+                XEUtils.arrayEach(XEUtils.filter(paramsColumns, item => !valueIsNull(data, item.field || item.column)), item => query[item.column] = XEUtils.get(data, item.field || item.column))
 
                 return XEUtils.omit(query, val => XEUtils.isEmpty(val) && !XEUtils.isNumber(val))
             },

+ 8 - 3
src/layout/index.vue

@@ -46,7 +46,7 @@
             <div class="aminui-main" id="aminui-main">
                 <router-view v-slot="{ Component }">
                     <keep-alive :include="this.$store.state.keepAlive.keepLiveRoute">
-                        <component :is="Component" :key="$route.fullPath" v-if="$store.state.keepAlive.routeShow"/>
+                        <component :is="Component" :key="`${$route.fullPath}.${componentKey}`" v-if="$store.state.keepAlive.routeShow"/>
                     </keep-alive>
                 </router-view>
             </div>
@@ -71,7 +71,9 @@ export default {
         userbar
     },
     data() {
-        return {}
+        return {
+            componentKey: 0
+        }
     },
     computed: {
         ismobile() {
@@ -107,7 +109,10 @@ export default {
         },
 
         storageChange({ key, newValue }) {
-            if (key == "PROJECT") this.$store.commit("SET_projects", XEUtils.toStringJSON(newValue).content);
+            if (key == "PROJECT") {
+                this.$store.commit("SET_projects", XEUtils.toStringJSON(newValue).content);
+                this.componentKey++;
+            }
         },
 
         // 点击显示

+ 2 - 2
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"].findIndex(key => to.fullPath.includes(key));
-    if (to.name && index !== -1) router.getGates(["facerec", "passqrcode", "tower", "env"][index])
+    const index = ["facerec", "passqrcode", "env"].findIndex(key => to.fullPath.includes(key));
+    if (to.name && index !== -1) router.getGates(["facerec", "passqrcode", "env"][index])
     
     // 加载动态/静态路由
 	if (!isGetRouter) {

+ 0 - 18
src/utils/basicDic.js

@@ -12,14 +12,6 @@ export const deviceStateDic = {
     offline: "离线"
 }
 
-export const towerWarningDic = {
-    WARNING_TILT: "倾斜告警",
-    WARNING_WEIGHT: "重量告警",
-    WARNING_POWER: "力矩告警",
-    WARNING_WIND_SPEED: "风速告警",
-    WARNING_XIANWEI: "限位告警"
-}
-
 /* ************************************************************************* */ 
 export const taskDic = {
     option: {
@@ -56,16 +48,6 @@ export const aiTypeDic = {
     AIHAZARD_REC_VEHICLE_NOT_CLEANED: "车辆未清洗"
 }
 
-export const sccTypeDic = {
-    SCC_RECORD_VTYPE_C: "温度",
-    SCC_RECORD_VTYPE_RH: "湿度"
-}
-
-export const sccTypeUnitDic = {
-    SCC_RECORD_VTYPE_C: "℃",
-    SCC_RECORD_VTYPE_RH: "%rh"
-}
-
 export function objectToArray(obj) {
     return XEUtils.map(XEUtils.keys(obj), value => ({ label: XEUtils.get(obj, value), value }))
 }

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

@@ -3,7 +3,7 @@
 </template>
 
 <script setup>
-import dataTable from "../record";
+import dataTable from "./record";
 
 const tableRef = ref();
 defineExpose({

+ 1 - 1
src/views/dataMock/env/mono/tableExpand.vue

@@ -29,5 +29,5 @@ const options = reactive({
 </script>
 
 <style scoped>
-    .el-main {padding: 0;}
+.el-main {padding: 0;}
 </style>

+ 17 - 0
src/views/dataMock/spray/components/index.js

@@ -0,0 +1,17 @@
+import XEUtils from "xe-utils"
+
+const resultComps = {}
+let requireComponent = require.context(
+    "./", // 在当前目录下查找
+    true, // 遍历子文件夹
+    /\.vue$/ // 正则匹配 以 .vue结尾的文件
+)
+requireComponent.keys().forEach(fileName => {
+    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

+ 113 - 0
src/views/dataMock/spray/components/record/detail.vue

@@ -0,0 +1,113 @@
+<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="dataType">
+                <el-select v-model="form.dataType" placeholder="请选择开关状态">
+                    <el-option v-for="(label, key) in typeDic" :key="key" :label="label" :value="key"></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>
+            <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 { typeDic } from "@/views/dataMock/spray/main";
+
+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,
+    dataType: null,
+    createTime: null
+});
+const rules = reactive({
+    projectId: [{ required: true, message: "请选择所属项目" }],
+    mountedId: [{ required: true, message: "请选择设备安装点" }],
+    dataType: [{ required: true, message: "请选择开关状态" }],
+    createTime: [{ required: true, message: "请选择喷淋时间" }]
+})
+
+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.spray.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) => XEUtils.set(form.value, key, XEUtils.get(data, key)));
+}
+
+const formRef = ref();
+const submit = () => {
+    formRef.value.validate(valid => {
+        if (valid) {
+            isSaving.value = true;
+            API.spray.record[mode.value](form.value).then(() => {
+                isSaving.value = false;
+                ElMessage.success("操作成功");
+                visible.value = false;
+                $emit("success", mode.value);
+            }).catch(() => isSaving.value = false);
+        } else {
+            return false;
+        }
+    });
+}
+
+defineExpose({
+    open,
+    setData
+})
+</script>
+
+<style lang="scss" scoped>
+.el-form {
+    padding-right: calc(var(--el-dialog-padding-primary) + var(--el-message-close-size, 16px));
+}
+</style>

+ 172 - 0
src/views/dataMock/spray/components/record/index.vue

@@ -0,0 +1,172 @@
+<template>
+    <scTable ref="xGridTable" batchDel :apiObj="$API.spray.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="dialog = false"></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, objectToArray } from "@/utils/basicDic";
+import { typeDic } from "@/views/dataMock/spray/main";
+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({
+    visible,
+    span: 5,
+    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: "spray.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(typeDic),
+    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("dataType", "开关状态", selectConfig)
+    ]
+})
+
+const paramsColums = reactive([
+    { column: "projectId", field: visible.value ? "" : "projectIdNot" },
+    visible.value ? { column: "projectIdNot" } : {},
+    { column: "mountedId" },
+    { column: "dataType" },
+    { 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: "dataType", title: "开关状态", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(typeDic, cellValue, cellValue) },
+    { type: "html", field: "createTime", title: "喷淋时间", minWidth: 160, sortable: true },
+    { 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 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.spray.record.del({ id }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    });
+}
+
+defineExpose({
+    table_add,
+    refreshTable,
+    getTableTotal
+})
+</script>

+ 134 - 0
src/views/dataMock/spray/detail.vue

@@ -0,0 +1,134 @@
+<template>
+    <el-dialog v-model="visible" title="数据模拟" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-tabs v-model="apiKey">
+            <el-tab-pane label="参数配置" name="makeData"></el-tab-pane>
+            <el-tab-pane label="数据复制" name="copyData" disabled></el-tab-pane>
+        </el-tabs>
+
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="126">
+            <el-row>
+                <el-col :md="12" :xs="24">
+                    <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 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="targetTime">
+                        <el-date-picker v-model="form.targetTime" type="daterange" :clearable="false" value-format="YYYY-MM-DD 00:00:00" :shortcuts="shortcuts" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
+                    </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-row>
+        </el-form>
+
+        <template #footer>
+            <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 XEUtils from "xe-utils";
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { rangeShortcuts } from "@/utils/shortcuts";
+
+const route = useRoute();
+const $emit = defineEmits(["success", "closed"]);
+const apiKey = ref("makeData");
+const visible = ref(false);
+const isSaving = ref(false);
+
+const shortcuts = rangeShortcuts("YYYY-MM-DD 00:00:00");
+const form = ref({
+    targetProjectId: TOOL.data.get("PROJECT_ID"),
+    targetMountedId: null,
+    targetTime: [],
+    isCover: false
+});
+
+const rules = reactive({
+    targetProjectId: [{ required: true, message: "请选择模拟项目" }],
+    targetMountedId: [{ required: true, message: "请选择模拟项目安装点" }],
+    targetTime: [{ required: true, message: "请选择模拟时间范围" }],
+    isCover: [{ required: true }]
+});
+
+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 mounteds = ref([]);
+const filterTargetM = computed(() => form.value.targetProjectId ? XEUtils.filter(mounteds.value, item => item.projectId == form.value.targetProjectId) : []);
+const fetchMounted = async () => {
+    const res = await API.spray.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) {
+            const data = XEUtils.omit(form.value, "targetTime");
+            XEUtils.set(data, "targetBeginTime", XEUtils.first(form.value.targetTime));
+            XEUtils.set(data, "targetEndTime", XEUtils.last(form.value.targetTime));
+            
+            isSaving.value = true;
+            API.spray.dataMock[apiKey.value](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 {margin-top: 5px;padding-right: var(--el-message-close-size, 16px);}
+.el-form-item .el-radio-group {flex-wrap: nowrap;}
+</style>

+ 46 - 0
src/views/dataMock/spray/index.vue

@@ -0,0 +1,46 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header addText="数据模拟" @add="mock_add">
+            <template #extra-right>
+                <el-button v-if="activeName == 'record'" 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 ref="componentRef" :is="allcomp[activeName]" taskType="autospray" />
+	</el-container>
+
+    <mock-detail v-if="dialog" ref="mockRef" @success="refreshState" @closed="dialog = false"></mock-detail>
+</template>
+
+<script setup>
+import { workerStates } from "./main";
+import comp from "./components";
+import monos from "@/views/dataMock/tasks/monos";
+import mockDetail from "./detail";
+
+const allcomp = { ...comp, monos };
+const activeName = ref("record");
+
+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 refreshState = () => {
+    if (activeName.value == "monos") setTimeout(() => componentRef.value.refreshTable(), 2000);
+}
+</script>
+
+<style lang="scss" scoped>
+.el-tabs {padding: 0 12px;background: #fff;}
+</style>

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

@@ -0,0 +1,9 @@
+export const workerStates = {
+    record: "设备监控",
+    monos: "任务中心"
+}
+
+export const typeDic = {
+    AUTOSPRAY_ON: "开启",
+    AUTOSPRAY_OFF: "关闭"
+}

+ 3 - 3
src/views/dataMock/standard/components/record/detail.vue

@@ -15,12 +15,12 @@
             </template>
             <el-form-item label="监测类型" prop="vtype">
                 <el-select v-model="form.vtype" placeholder="请选择监测类型" @change="form.value = null">
-                    <el-option v-for="(label, key) in sccTypeDic" :key="key" :label="label" :value="key"></el-option>
+                    <el-option v-for="(label, key) in typeDic" :key="key" :label="label" :value="key"></el-option>
                 </el-select>
             </el-form-item>
             <el-form-item label="监测数值" prop="value">
                 <el-input-number v-model="form.value" :readonly="!form.vtype" :precision="2" :controls="false" placeholder="请输入监测数值">
-                    <template v-if="form.vtype" #suffix>{{ XEUtils.get(sccTypeUnitDic, form.vtype) }}</template>
+                    <template v-if="form.vtype" #suffix>{{ XEUtils.get(unitDic, form.vtype) }}</template>
                 </el-input-number>
             </el-form-item>
             <el-form-item label="监测时间" prop="createTime">
@@ -39,7 +39,7 @@
 import XEUtils from "xe-utils";
 import API from "@/api";
 import TOOL from "@/utils/tool";
-import { sccTypeDic, sccTypeUnitDic } from "@/utils/basicDic";
+import { typeDic, unitDic } from "@/views/dataMock/standard/main";
 
 const $emit = defineEmits(["success", "closed"]);
 const props = defineProps({

+ 7 - 6
src/views/dataMock/standard/components/record/index.vue

@@ -25,7 +25,8 @@ import XEUtils from "xe-utils";
 import API from "@/api";
 import TOOL from "@/utils/tool";
 import { mapFormItemSelect, mapFormItemDatePicker } from "@/components/scTable/helper";
-import { dataSource, sccTypeDic, sccTypeUnitDic, objectToArray } from "@/utils/basicDic";
+import { dataSource, objectToArray } from "@/utils/basicDic";
+import { typeDic, unitDic } from "@/views/dataMock/standard/main";
 import recordDetail from "./detail";
 
 const props = defineProps({
@@ -74,7 +75,7 @@ const mountedConfig = reactive({
 })
 
 const selectConfig = reactive({
-    options: objectToArray(sccTypeDic),
+    options: objectToArray(typeDic),
     events: {
         change: data => XEUtils.merge(formConfig.data, data)
     }
@@ -82,7 +83,7 @@ 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("day").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")],
     props: {
         type: "datetimerange",
         startPlaceholder: "开始时间",
@@ -101,7 +102,7 @@ const formConfig = reactive({
         projectId: TOOL.data.get("PROJECT_ID"),
         projectIdNot: 1,
         isWarning: 0,
-        createTime: [moment().startOf("month").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")]
+        createTime: [moment().startOf("day").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")]
     },
     items: [
         mapFormItemSelect("isWarning", "数据类型", radioConfig),
@@ -128,8 +129,8 @@ const columns = reactive([
     { 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: "vtype", title: "监测类型", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(sccTypeDic, cellValue, cellValue) },
-    { type: "html", field: "taskValue", title: "监测数值", minWidth: 100, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.divide(XEUtils.get(row, "value"), 100) + XEUtils.get(sccTypeUnitDic, XEUtils.get(row, "vtype")) },
+    { type: "html", field: "vtype", title: "监测类型", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(typeDic, cellValue, cellValue) },
+    { type: "html", field: "taskValue", title: "监测数值", minWidth: 100, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.divide(XEUtils.get(row, "value"), 100) + XEUtils.get(unitDic, XEUtils.get(row, "vtype")) },
     { type: "html", field: "createTime", title: "监测时间", minWidth: 160, sortable: true },
     { 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" } }

+ 1 - 1
src/views/dataMock/standard/detail.vue

@@ -109,7 +109,7 @@ const shortcuts = rangeShortcuts();
 const form = ref({
     targetProjectId: TOOL.data.get("PROJECT_ID"),
     targetMountedId: null,
-    targetTime: ["2025-08-28 00:00:00", "2025-08-28 00:59:59"],
+    targetTime: [],
     dataSouce: "normal",
     isCover: false,
     timeStepSec: 3,

+ 1 - 1
src/views/dataMock/standard/index.vue

@@ -2,7 +2,7 @@
 	<el-container class="is-vertical">
         <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>
+                <el-button v-if="activeName == 'record'" type="primary" @click="table_add">数据录入</el-button>
             </template>
         </sc-page-header>
 

+ 10 - 0
src/views/dataMock/standard/main.js

@@ -2,4 +2,14 @@ export const workerStates = {
     record: "设备监控",
     monos: "任务中心",
     threshold: "阈值设置"
+}
+
+export const typeDic = {
+    SCC_RECORD_VTYPE_C: "温度",
+    SCC_RECORD_VTYPE_RH: "湿度"
+}
+
+export const unitDic = {
+    SCC_RECORD_VTYPE_C: "℃",
+    SCC_RECORD_VTYPE_RH: "%rh"
 }

+ 7 - 2
src/views/dataMock/tasks/monos.vue

@@ -21,7 +21,12 @@ import { mapFormItemSelect, mapFormItemRadio, mapFormItemDatePicker } from "@/co
 import { taskDic, objectToArray } from "@/utils/basicDic";
 import tableExpand from "./tableExpand";
 
+const props = defineProps({
+    taskType: { type: String, default: "" }
+});
+
 const radioConfig = reactive({
+    visibleMethod: ({ data }) => ["car_rinse", "aihazard", "tcm"].includes(data.taskType),
     span: 5,
     resetValue: false,
     options: [{ label: "是", value: true }, { label: "否", value: false }],
@@ -44,7 +49,7 @@ const proConfig = reactive({
 })
 
 const mountedConfig = reactive({
-    visibleMethod: ({ data }) => !(data.isTemp == 1 || useAttrs().taskType == "car_rinse"),
+    visibleMethod: ({ data }) => !(data.isTemp == 1 || data.taskType == "car_rinse"),
     api: {
         key: "ugliAi.mounted",
         query: {
@@ -100,7 +105,7 @@ const formConfig = reactive({
         parentId: "0",
         projectId: TOOL.data.get("PROJECT_ID"),
         projectIdNot: 1,
-        taskType: useAttrs().taskType,
+        taskType: computed(() => props.taskType),
         dateRange: [],
         createTime: [moment().startOf("month").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")]
     },

+ 128 - 0
src/views/dataMock/tower/components/alarm.vue

@@ -0,0 +1,128 @@
+<template>
+    <scTable ref="xGridTable" batchDel :apiObj="$API.tower.warning" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns">
+        <template #action="{ row }">
+            <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 { mapFormItemSelect, mapFormItemDatePicker } from "@/components/scTable/helper";
+import { dataSource, objectToArray } from "@/utils/basicDic";
+import { warningTypeDic } from "@/views/dataMock/tower/main";
+
+const proConfig = reactive({
+    span: 5,
+    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({
+    api: {
+        key: "tower.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(XEUtils.omit(warningTypeDic, "WARNING_MANY")),
+    events: {
+        change: data => XEUtils.merge(formConfig.data, data)
+    }
+})
+
+const datetimerangeConfig = reactive({
+    span: 7,
+    resetValue: () => [moment().startOf("day").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: {
+        orderBy: "tw.createTime_desc",
+        projectId: TOOL.data.get("PROJECT_ID"),
+        projectIdNot: 1,
+        createTime: [moment().startOf("day").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("warningType", "告警类型", selectConfig)
+    ]
+})
+
+const paramsColums = reactive([
+    { column: "orderBy" },
+    { column: "projectId" },
+    { column: "projectIdNot" },
+    { column: "mountedId" },
+    { column: "warningType" },
+    { column: "tr.createTimeBegin", field: "createTime[0]" },
+    { column: "tr.createTimeEnd", field: "createTime[1]" }
+])
+
+const columns = reactive([
+    { type: "checkbox", fixed: "left", width: 40 },
+    { type: "seq", fixed: "left", 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: "groundName", title: "工地场区", minWidth: 160, sortable: true },
+    { type: "html", field: "mountedName", title: "设备安装点", minWidth: 160, sortable: true },
+    { type: "html", field: "createTime", title: "告警时间", minWidth: 160, sortable: true },
+    { type: "html", field: "warningType", title: "告警类型", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(warningTypeDic, cellValue, cellValue) },
+    { type: "html", field: "dataSource", title: "数据来源", fixed: "right", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(dataSource, cellValue, cellValue) },
+    { title: "操作", fixed: "right", width: 120, align: "center", slots: { default: "action" } }
+])
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const toggleFormEnabled = () => xGridTable.value.toggleFormEnabled();
+
+const refreshTable = () => {
+    xGridTable.value.reloadColumn(columns);
+    xGridTable.value.searchData();
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该告警记录?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.tower.warning.del({ id }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    });
+}
+</script>

+ 139 - 0
src/views/dataMock/tower/components/attendance.vue

@@ -0,0 +1,139 @@
+<template>
+    <scTable ref="xGridTable" batchDel :apiObj="$API.tower.attendance" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns">
+        <template #default_imgText="{ row, column }">
+            <template v-if="formatCertificate(row, column)">
+                <el-button type="primary" link @click="handlePreview(row, column)">{{ XEUtils.get(formatCertificate(row, column), "certificateNo") }}</el-button>
+            </template>
+        </template>
+        
+        <template #action="{ row }">
+            <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>
+
+    <file-viewer v-if="showViewer" ref="viewerRef" @closed="showViewer = false"></file-viewer>
+</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, objectToArray } from "@/utils/basicDic";
+import { folderKeyDic } from "@/views/dataMock/tower/main";
+
+const formatCertificate = (row, { field }) => XEUtils.find(XEUtils.get(XEUtils.toStringJSON(XEUtils.get(row, "person.features")), "certificate", []), item => item.type == XEUtils.get(folderKeyDic, field));
+
+const proConfig = reactive({
+    span: 5,
+    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({
+    api: {
+        key: "tower.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 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,
+        recordType: "TCM_RECORD_ENTER",
+        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)
+    ]
+})
+
+const paramsColums = reactive([
+    { column: "projectId" },
+    { column: "projectIdNot" },
+    { column: "mountedId" },
+    { column: "recordType" },
+    { column: "createTimeBegin", field: "createTime[0]" },
+    { column: "createTimeEnd", field: "createTime[1]" }
+])
+
+const columns = reactive([
+    { type: "checkbox", fixed: "left", width: 40 },
+    { type: "seq", fixed: "left", width: 60 },
+    { type: "html", field: "person.name", title: "人员姓名", minWidth: 100, sortable: true },
+    { type: "html", field: "person.idCard", title: "身份证号", minWidth: 160, sortable: true },
+    { type: "html", field: "person.phoneNumber", title: "手机号", minWidth: 160, sortable: true },
+    { type: "html", field: "createTime", title: "打卡时间", minWidth: 160, sortable: true },
+    { 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: "groundName", title: "工地场区", minWidth: 160, sortable: true },
+    { type: "html", field: "mountedName", title: "设备安装点", minWidth: 160, sortable: true },
+    { field: "facerec/towerCrane_zs", title: "特种作业证", minWidth: 140, align: "center", slots: { default: "default_imgText" } },
+    { type: "html", field: "dataSource", title: "数据来源", fixed: "right", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(dataSource, cellValue, cellValue) },
+    { title: "操作", fixed: "right", width: 120, align: "center", slots: { default: "action" } }
+])
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const toggleFormEnabled = () => xGridTable.value.toggleFormEnabled();
+
+const refreshTable = () => {
+    xGridTable.value.reloadColumn(columns);
+    xGridTable.value.searchData();
+}
+
+const showViewer = ref(false);
+const viewerRef = ref();
+const handlePreview = (row, { field }) => {
+    showViewer.value = true;
+    nextTick(() => viewerRef.value.init(XEUtils.get(row, `person.folders.${field}.entities[0]`)));
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该考勤记录?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.tower.attendance.del({ id }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    });
+}
+</script>

+ 17 - 0
src/views/dataMock/tower/components/index.js

@@ -0,0 +1,17 @@
+import XEUtils from "xe-utils"
+
+const resultComps = {}
+let requireComponent = require.context(
+    "./", // 在当前目录下查找
+    true, // 遍历子文件夹
+    /\.vue$/ // 正则匹配 以 .vue结尾的文件
+)
+requireComponent.keys().forEach(fileName => {
+    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

+ 205 - 0
src/views/dataMock/tower/components/record/detail.vue

@@ -0,0 +1,205 @@
+<template>
+    <el-dialog v-model="visible" :title="titleMap[mode]" :width="860" :close-on-click-modal="false" @closed="$emit('closed', isDel)">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-row v-if="props.projectId != 1">
+                <el-col :md="12" :xs="24">
+                    <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-col>
+                <el-col :md="12" :xs="24">
+                    <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>
+                </el-col>
+            </el-row>
+
+            <el-row>
+                <el-col :md="12" :xs="24">
+                    <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-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="起重量" prop="features.weight">
+                        <el-input-number v-model="form.features.weight" :controls="false" placeholder="请输入起重量">
+                            <template #suffix>kg</template>
+                        </el-input-number>
+                    </el-form-item>
+                </el-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="载重百分比" prop="features.weightRate">
+                        <el-input-number v-model="form.features.weightRate" :precision="2" :controls="false" placeholder="请输入载重百分比">
+                            <template #suffix>%</template>
+                        </el-input-number>
+                    </el-form-item>
+                </el-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="高度" prop="features.height">
+                        <el-input-number v-model="form.features.height" :precision="1" :controls="false" placeholder="请输入高度">
+                            <template #suffix>m</template>
+                        </el-input-number>
+                    </el-form-item>
+                </el-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="力矩百分比" prop="features.powerRate">
+                        <el-input-number v-model="form.features.powerRate" :precision="2" :controls="false" placeholder="请输入力矩百分比">
+                            <template #suffix>%</template>
+                        </el-input-number>
+                    </el-form-item>
+                </el-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="回转角度" prop="features.rotationAngle">
+                        <el-input-number v-model="form.features.rotationAngle" :precision="1" :controls="false" placeholder="请输入回转角度">
+                            <template #suffix>°</template>
+                        </el-input-number>
+                    </el-form-item>
+                </el-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="倾角" prop="features.tiltAngle">
+                        <el-input-number v-model="form.features.tiltAngle" :precision="2" :controls="false" placeholder="请输入回转角度">
+                            <template #suffix>°</template>
+                        </el-input-number>
+                    </el-form-item>
+                </el-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="风力" prop="features.windSpeed">
+                        <el-input-number v-model="form.features.windSpeed" :precision="1" :controls="false" placeholder="请输入风力">
+                            <template #suffix>级</template>
+                        </el-input-number>
+                    </el-form-item>
+                </el-col>
+                <el-col :md="12" :xs="24">
+                    <el-form-item label="幅度" prop="features.workAngle">
+                        <el-input-number v-model="form.features.workAngle" :precision="1" :controls="false" placeholder="请输入幅度">
+                            <template #suffix>m</template>
+                        </el-input-number>
+                    </el-form-item>
+                </el-col>
+            </el-row>
+        </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 { valueFormatDic } from "@/views/dataMock/tower/main";
+
+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,
+    createTime: null,
+    features: {
+        weight: null,
+        weightRate: null,
+        height: null,
+        powerRate: null,
+        rotationAngle: null,
+        tiltAngle: null,
+        windSpeed: null,
+        workAngle: null
+    }
+});
+const rules = reactive({
+    projectId: [{ required: true, message: "请选择所属项目" }],
+    mountedId: [{ required: true, message: "请选择设备安装点" }],
+    createTime: [{ required: true, message: "请选择监测时间" }],
+    "features.weight": [{ required: true, message: "请输入起重量" }],
+    "features.weightRate": [{ required: true, message: "请输入载重百分比" }],
+    "features.height": [{ required: true, message: "请输入高度" }],
+    "features.powerRate": [{ required: true, message: "请输入力矩百分比" }],
+    "features.rotationAngle": [{ required: true, message: "请输入回转角度" }],
+    "features.tiltAngle": [{ required: true, message: "请输入倾角" }],
+    "features.windSpeed": [{ required: true, message: "请输入风力" }],
+    "features.workAngle": [{ required: true, message: "请输入幅度" }]
+})
+
+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.tower.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));
+            XEUtils.objectEach(valueFormatDic, (item, digits) => XEUtils.arrayEach(item, feaKey => XEUtils.set(features, feaKey, XEUtils.divide(XEUtils.get(features, feaKey), digits))));
+            XEUtils.merge(form.value.features, features);
+        } 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");
+
+            const features = XEUtils.clone(form.value.features, true);
+            XEUtils.objectEach(valueFormatDic, (item, digits) => XEUtils.arrayEach(item, feaKey => XEUtils.set(features, feaKey, XEUtils.multiply(XEUtils.get(features, feaKey), digits))));
+            XEUtils.set(data, "features", XEUtils.toJSONString(features));
+            
+            isSaving.value = true;
+            API.tower.record[mode.value](data).then(() => {
+                isSaving.value = false;
+                ElMessage.success("操作成功");
+                visible.value = false;
+                $emit("success", mode.value);
+            }).catch(() => isSaving.value = false);
+        } else {
+            return false;
+        }
+    });
+}
+
+defineExpose({
+    open,
+    setData
+})
+</script>
+
+<style lang="scss" scoped>
+.el-form {
+    padding-right: calc(var(--el-dialog-padding-primary) + var(--el-message-close-size, 16px));
+
+    .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;}
+}
+</style>

+ 171 - 0
src/views/dataMock/tower/components/record/index.vue

@@ -0,0 +1,171 @@
+<template>
+    <scTable ref="xGridTable" batchDel :apiObj="$API.tower.record" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns" v-bind="props.options">
+        <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="dialog = false"></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, objectToArray } from "@/utils/basicDic";
+import recordDetail from "./detail";
+
+const formatTilt = row => {
+    const feaTilt = XEUtils.divide(XEUtils.get(XEUtils.toStringJSON(XEUtils.get(row, "features", "{}")), "tiltAngle"), 100);
+    const tilt = feaTilt > 0 ? feaTilt : XEUtils.add(360, feaTilt);
+    return tilt >= 180 ? XEUtils.subtract(360, tilt) : tilt;
+};
+
+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: "tower.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 datetimerangeConfig = reactive({
+    span: 7,
+    resetValue: () => [moment().startOf("day").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("day").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)
+    ]
+})
+
+const paramsColums = reactive([
+    { column: "projectId", field: visible.value ? "" : "projectIdNot" },
+    visible.value ? { column: "projectIdNot" } : {},
+    { column: "mountedId" },
+    { 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: "weight", title: "起重量(kg)", minWidth: 100, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.toStringJSON(XEUtils.get(row, "features", "{}")), "weight") },
+    { type: "html", field: "weightRate", title: "载重百分比", minWidth: 100, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.toStringJSON(XEUtils.get(row, "features", "{}")), "weightRate") },
+    { type: "html", field: "height", title: "高度(m)", minWidth: 100, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.divide(XEUtils.get(XEUtils.toStringJSON(XEUtils.get(row, "features", "{}")), "height"), 10) },
+    { type: "html", field: "powerRate", title: "力矩百分比", minWidth: 100, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.toStringJSON(XEUtils.get(row, "features", "{}")), "powerRate") },
+    { type: "html", field: "rotationAngle", title: "回转角度(°)", minWidth: 110, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.divide(XEUtils.get(XEUtils.toStringJSON(XEUtils.get(row, "features", "{}")), "rotationAngle"), 10) },
+    { type: "html", field: "tiltAngle", title: "倾角(°)", minWidth: 100, sortable: true, formatter: ({ cellValue, row }) => cellValue || formatTilt(row) },
+    { type: "html", field: "tiltAngleRate", title: "倾角百分比", minWidth: 100, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.toInteger(XEUtils.multiply(XEUtils.divide(formatTilt(row), XEUtils.divide(XEUtils.get(XEUtils.toStringJSON(XEUtils.get(row, "mountedFeatures", "{}")), "maxTilt"), 10)), 100)) },
+    { type: "html", field: "windSpeed", title: "风力(级)", minWidth: 100, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.divide(XEUtils.get(XEUtils.toStringJSON(XEUtils.get(row, "features", "{}")), "windSpeed"), 10) },
+    { type: "html", field: "windSpeedRate", title: "风力百分比", minWidth: 100, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.toInteger(XEUtils.multiply(XEUtils.divide(XEUtils.get(XEUtils.toStringJSON(XEUtils.get(row, "features", "{}")), "windSpeed"), XEUtils.get(XEUtils.toStringJSON(XEUtils.get(row, "mountedFeatures", "{}")), "maxWindSpeed")), 100)) },
+    { type: "html", field: "workAngle", title: "幅度(m)", minWidth: 100, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.divide(XEUtils.get(XEUtils.toStringJSON(XEUtils.get(row, "features", "{}")), "workAngle"), 10) },
+    { 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 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.tower.record.del({ id }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    });
+}
+
+defineExpose({
+    table_add,
+    refreshTable,
+    getTableTotal
+})
+</script>

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

@@ -3,7 +3,7 @@
 </template>
 
 <script setup>
-import dataTable from "../record";
+import dataTable from "./record";
 
 const tableRef = ref();
 defineExpose({

+ 304 - 0
src/views/dataMock/tower/detail.vue

@@ -0,0 +1,304 @@
+<template>
+    <el-dialog v-model="visible" title="数据模拟" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-tabs v-model="dataSouce" @tab-change="apiKey = dataSouce == 'normal' ? 'copyData' : 'makeData'">
+            <el-tab-pane label="标准数据" name="normal"></el-tab-pane>
+            <el-tab-pane label="报警数据" name="alarm"></el-tab-pane>
+            <el-tab-pane label="考勤数据" name="attendance"></el-tab-pane>
+        </el-tabs>
+
+        <el-tabs v-model="apiKey">
+            <el-tab-pane label="参数配置" name="makeData" :disabled="dataSouce == 'normal'"></el-tab-pane>
+            <el-tab-pane label="数据复制" name="copyData" :disabled="dataSouce == 'alarm' || dataSouce == 'attendance'"></el-tab-pane>
+        </el-tabs>
+
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="126">
+            <el-row>
+                <el-col :md="12" :xs="24">
+                    <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 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>
+
+                <template v-if="dataSouce == 'normal'">
+                    <el-col :md="12" :xs="24">
+                        <el-form-item class="range-item" label="模拟时间范围" prop="targetBeginTime">
+                            <el-date-picker v-model="form.targetBeginTime" type="datetime" :clearable="false" value-format="YYYY-MM-DD HH:mm:ss" :default-time="new Date(2000, 1, 1, 23, 59, 59)" placeholder="请选择模拟数据开始时间" @change="dateChange"></el-date-picker>
+                            <span>至</span>
+                            <el-input v-model="form.targetEndTime" readonly placeholder="模拟数据结束时间">
+                                <template #prefix><sc-iconify icon="ep:clock"></sc-iconify></template>
+                            </el-input>
+                        </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" @change="refreshTable">
+                                <el-radio value="template">模版项目</el-radio>
+                                <el-radio value="other">其他项目</el-radio>
+                            </el-radio-group>
+                        </el-form-item>
+                    </el-col>
+                </template>
+
+                <template v-if="dataSouce == 'alarm'">
+                    <el-col :md="12" :xs="24">
+                        <el-form-item label="模拟时间范围" prop="targetTime">
+                            <el-date-picker v-model="form.targetTime" 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)]" :shortcuts="shortcuts" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间"></el-date-picker>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :md="12" :xs="24">
+                        <el-form-item label="报警条数" prop="warnNum">
+                            <el-input-number v-model="form.warnNum" :min="0" :controls="false" placeholder="请输入报警条数"></el-input-number>
+                        </el-form-item>
+                    </el-col>
+                </template>
+
+                <template v-if="dataSouce == 'attendance'">
+                    <el-col :md="12" :xs="24">
+                        <el-form-item label="模拟时间范围" prop="targetTime">
+                            <el-date-picker v-model="form.targetTime" type="daterange" :clearable="false" value-format="YYYY-MM-DD 00:00:00" :shortcuts="shortcuts" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :md="12" :xs="24">
+                        <el-form-item label="人员身份证号" prop="personIdCard">
+                            <el-input v-model="form.personIdCard" placeholder="请输入人员身份证号"></el-input>
+                        </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>
+                </template>
+            </el-row>
+
+            <template v-if="dataSouce == 'normal'">
+                <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="dateChange(), refreshTable()"></el-date-picker>
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+
+                <data-table ref="tableRef" :isTemp="form.source == 'template'" hideHandler :options="tableOptions"></data-table>
+            </template>
+        </el-form>
+
+        <template #footer>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit()">提交</el-button>
+            <el-button v-if="dataSouce == 'normal' && 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 { rangeShortcuts } from "@/utils/shortcuts";
+import { verifyIdCard } from "@/utils/verificate";
+import dataTable from "./components/record";
+
+const route = useRoute();
+const $emit = defineEmits(["success", "closed"]);
+const dataSouce = ref("normal");
+const apiKey = ref("copyData");
+
+const visible = ref(false);
+const isSaving = ref(false);
+
+const shortcuts = rangeShortcuts();
+const form = ref({
+    targetProjectId: TOOL.data.get("PROJECT_ID"),
+    targetMountedId: null,
+    targetBeginTime: null,
+    targetEndTime: null,
+    targetTime: [],
+    isCover: false,
+    source: "other",
+    warnNum: null,
+    personIdCard: null,
+    sourceProjectId: null,
+    sourceProjectIdNot: 1,
+    sourceMountedId: null,
+    sourceTime: [moment().startOf("day").format("YYYY-MM-DD HH:mm:ss"), moment().format("YYYY-MM-DD HH:mm:ss")]
+});
+
+const rules = reactive({
+    targetProjectId: [{ required: true, message: "请选择模拟项目" }],
+    targetMountedId: [{ required: true, message: "请选择模拟项目安装点" }],
+    targetBeginTime: [{ required: true, message: "请选择模拟数据开始时间" }],
+    targetTime: [
+        { required: true, message: "请选择模拟时间范围" },
+        { validator: (rule, value, callback) => {
+            if (dataSouce.value == "alarm" && moment(XEUtils.last(value)).diff(XEUtils.first(value), "minute") < 5) {
+                callback(new Error("模拟时间范围至少为5分钟"));
+            } else callback();
+        }}
+    ],
+    isCover: [{ required: true }],
+    source: [{ required: true }],
+    warnNum: [{ required: true, message: "请输入报警条数" }],
+    personIdCard: [
+        { required: true, message: "请输入人员身份证号" },
+        { validator: (rule, value, callback) => verifyIdCard(rule, value, callback) }
+    ],
+    sourceProjectId: [{ required: true, message: "请选择数据源项目" }],
+    sourceMountedId: [{ required: true, message: "请选择数据源安装点" }],
+    sourceTime: [{ required: true, message: "请选择数据源抓拍时间" }]
+})
+
+const tableRef = ref();
+const tableOptions = reactive({
+    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 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 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.tower.mounted.get();
+    mounteds.value = res || [];
+}
+
+const open = () => {
+    visible.value = true;
+    TOOL.data.get("PROJECT_ID") && dataTimeRange();
+    fetchMounted();
+}
+
+const dateChange = () => {
+    if (form.value.sourceTime && form.value.sourceTime.length && form.value.targetBeginTime) form.value.targetEndTime = moment(form.value.targetBeginTime).add(moment(XEUtils.last(form.value.sourceTime)).diff(XEUtils.first(form.value.sourceTime), "millisecond"), "millisecond").format("YYYY-MM-DD HH:mm:ss")
+    else form.value.targetEndTime = null;
+}
+
+const formRef = ref();
+const submit = key => {
+    formRef.value.validate(valid => {
+        if (valid) {
+            if (tableRef.value?.getTableTotal() == 0) return ElMessage.warning("暂无相关数据,请调整条件后重试。");
+            
+            let data = XEUtils.omit(form.value, "sourceProjectId", "sourceProjectIdNot", "targetTime", "source", "warnNum", "sourceTime");
+            if (dataSouce.value == "normal") {
+                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"));
+                }
+            }
+            if (dataSouce.value == "alarm") {
+                data = XEUtils.pick(form.value, "targetProjectId", "targetMountedId", "warnNum");
+                XEUtils.set(data, "targetBeginTime", XEUtils.first(form.value.targetTime));
+                XEUtils.set(data, "targetEndTime", XEUtils.last(form.value.targetTime));
+            }
+
+            if (dataSouce.value == "attendance") {
+                data = XEUtils.pick(form.value, "targetProjectId", "targetMountedId", "personIdCard", "isCover");
+                XEUtils.set(data, "targetBeginTime", XEUtils.first(form.value.targetTime));
+                XEUtils.set(data, "targetEndTime", XEUtils.last(form.value.targetTime));
+            }
+            
+            isSaving.value = true;
+            API.tower.dataMock[apiKey.value][dataSouce.value](data).then(() => {
+                isSaving.value = false;
+                ElMessage.success("操作成功");
+                visible.value = false;
+                $emit("success", dataSouce.value);
+            }).catch(() => isSaving.value = false);
+        } else {
+            return false;
+        }
+    });
+}
+
+defineExpose({
+    open
+})
+</script>
+
+<style lang="scss" scoped>
+.el-form {margin-top: 5px;padding-right: var(--el-message-close-size, 16px);}
+.el-form-item .el-radio-group {flex-wrap: nowrap;}
+.el-form .range-item :deep(.el-form-item__content) {
+    .el-date-editor, .el-input {flex: 1;}
+    .el-date-editor + span {padding: 0 10px;}
+}
+
+.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 :deep(.el-main) {padding-right: 0;padding-bottom: 0;}
+</style>

+ 58 - 0
src/views/dataMock/tower/index.vue

@@ -0,0 +1,58 @@
+<template>
+	<el-container class="is-vertical">
+        <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 workerStates" :key="key" :label="label" :name="key"></el-tab-pane>
+        </el-tabs>
+
+        <el-tabs v-if="activeName == 'monos'" v-model="taskType" @tab-change="refreshMono">
+            <el-tab-pane label="标准数据" name="tcm"></el-tab-pane>
+            <el-tab-pane label="报警数据" name="tcm_warning"></el-tab-pane>
+            <el-tab-pane label="考勤数据" name="tcm_person"></el-tab-pane>
+        </el-tabs>
+
+        <component ref="componentRef" :is="allcomp[activeName]" :taskType="taskType" />
+	</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, monoType } from "./main";
+import comp from "./components";
+import monos from "@/views/dataMock/tasks/monos";
+import mockDetail from "./detail";
+
+const allcomp = { ...comp, monos };
+const activeName = ref("record");
+const taskType = ref("tcm");
+
+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 refreshMono = () => componentRef.value.refreshTable();
+const refreshState = mode => {
+    if (activeName.value == "monos") {
+        taskType.value = XEUtils.get(monoType, mode, "tcm");
+        setTimeout(() => refreshMono(), 2000);
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+.el-tabs {padding: 0 12px;background: #fff;}
+</style>

+ 32 - 0
src/views/dataMock/tower/main.js

@@ -0,0 +1,32 @@
+export const workerStates = {
+    record: "设备监控",
+    alarm: "监控告警",
+    attendance: "考勤记录",
+    monos: "任务中心",
+    template: "模版项目"
+}
+
+export const valueFormatDic = {
+    10: ["height", "rotationAngle", "windSpeed", "workAngle"],
+    100: ["tiltAngle"]
+}
+
+export const warningTypeDic = {
+    WARNING_MANY: "/",
+    WARNING_TILT: "倾斜告警",
+    WARNING_WEIGHT: "重量告警",
+    WARNING_POWER: "力矩告警",
+    WARNING_WIND_SPEED: "风速告警",
+    WARNING_XIANWEI: "限位告警"
+}
+
+export const folderKeyDic = {
+    "facerec/towerCrane_zs": "tower",
+    "facerec/elevator_zs": "elevator"
+}
+
+export const monoType = {
+    normal: "tcm",
+    alarm: "tcm_warning",
+    attendance: "tcm_person"
+}

+ 12 - 0
src/views/dataMock/ugliAi/components/template.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>