sunhongyao341504 1 год назад
Родитель
Сommit
7571121120

+ 3 - 1
src/api/camera/camera-preview.ts

@@ -59,6 +59,7 @@ interface AlgoItem {
   status?: ALGO_ENABLED_STATUS;
   createdAt?: string;
   updatedAt?: string;
+  extra: string;
 }
 /** 查询所有的算法 */
 export const getAllAlgosApi = () => {
@@ -80,6 +81,7 @@ export interface CameraAlgoItem {
   electronicFence: number;
   status: ALGO_ENABLED_STATUS;
   algoInfo: AlgoItem;
+  extra: string;
 }
 
 /** 查询某个camera下的所有算法 */
@@ -108,7 +110,7 @@ interface CreateCameraAlgoParam {
 /** 保存相机的某个算法 */
 export const createCameraAlgoApi = (param: CreateCameraAlgoParam) => {
   return http.request({
-    url: '/cameraPreview/saveAlgo',
+    url: '/cameraPreview/saveAlgoWithConfig',
     data: param,
     method: 'post',
   });

+ 1 - 1
src/views/cameras/preview/CameraPreview.vue

@@ -191,7 +191,7 @@
   .cameraMain {
     display: flex;
     background: #fff;
-    height: calc(100vh - 90px);
+    // height: calc(100vh - 90px);
   }
   .cameraTree {
     width: 250px;

+ 333 - 0
src/views/cameras/preview/components/AlgorithmsSetting/AlgoParamsCard.vue

@@ -0,0 +1,333 @@
+<template>
+  <div
+    class="algo-params-card"
+    :class="{ 'algo-params-card-active': markedParamCardIds.includes(props.id) }"
+  >
+    <el-form
+      ref="ruleFormRef"
+      :model="algoParams"
+      :inline="true"
+      :rules="rules"
+      label-width="120px"
+    >
+      <div v-for="item in paramItems">
+        <el-form-item v-if="item.type === 'label'" label="检测对象:" prop="label">
+          <el-select v-model="algoParams.label" style="width: 186px" @change="handleLabelChange">
+            <el-option
+              v-for="item in getLabelList()"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+              :disabled="item.disabled"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          v-if="item.type === 'criticalCount'"
+          :label="labelNameMap[item.label] + '检测数量:'"
+          :prop="item.prop"
+          required
+        >
+          <ElInputNumber
+            v-model="algoParams[item.prop]"
+            controls-position="right"
+            :min="0"
+            :step="1"
+            style="width: 186px; margin-right: 5px"
+            :disabled="!algoParams.label"
+          />
+          <span>s</span>
+        </el-form-item>
+        <el-form-item
+          v-if="item.type === 'confidence'"
+          :label="labelNameMap[item.label] + '置信度:'"
+          :prop="item.prop"
+          required
+        >
+          <div style="display: flex">
+            <el-slider
+              v-model="algoParams[item.prop]"
+              :min="25"
+              :max="98"
+              :step="1"
+              style="width: 128px; margin-right: 6px"
+              :disabled="!algoParams.label"
+            />
+
+            <ElInputNumber
+              v-model="algoParams[item.prop]"
+              controls-position="right"
+              :min="0"
+              :step="1"
+              style="width: 88px; margin-right: 5px"
+              :disabled="!algoParams.label"
+            />
+            <span>%</span>
+          </div>
+        </el-form-item>
+        <el-form-item
+          v-if="item.type === 'minArea'"
+          :label="labelNameMap[item.label] + '最小检测面积:'"
+          required
+        >
+          <el-form-item :prop="item.label ? item.label + '.' + 'min-width' : 'min-width'">
+            <ElInputNumber
+              v-model="algoParams[item.label ? item.label + '.' + 'min-width' : 'min-width']"
+              controls-position="right"
+              :min="32"
+              :step="1"
+              style="width: 88px; margin-right: 5px"
+              :disabled="!algoParams.label"
+            />
+            <span>px</span>
+          </el-form-item>
+          <el-form-item
+            :prop="item.label ? item.label + '.' + 'min-height' : 'min-height'"
+            style="margin-left: 17px"
+          >
+            <ElInputNumber
+              v-model="algoParams[item.label ? item.label + '.' + 'min-height' : 'min-height']"
+              controls-position="right"
+              :min="32"
+              :step="1"
+              style="width: 88px; margin-right: 5px"
+              :disabled="!algoParams.label"
+            />
+            <span>px</span>
+          </el-form-item>
+        </el-form-item>
+      </div>
+    </el-form>
+    <div class="paramOptIcons">
+      <!-- <el-icon v-if="isEdit" size="16px" @click="handleSaveParam(ruleFormRef)">
+        <SaveOutline />
+      </el-icon>
+      <el-icon v-else size="16px" @click="isEdit = true"><Edit /></el-icon> -->
+      <el-icon size="16px" @click="deleteParam(props.id)"><Delete /></el-icon>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { computed, ref, watch } from 'vue';
+  import useCameraAlgoStore, { AlgoParamMetaItem } from '../../store/useCameraAlgoStore';
+  import { storeToRefs } from 'pinia';
+  import { ElInputNumber, ElMessage, FormInstance } from 'element-plus';
+  import { Delete, Edit } from '@element-plus/icons-vue';
+  import { SaveOutline } from '@vicons/ionicons5';
+  import { labelNameMap } from './types';
+
+  const props = defineProps<{
+    id: string;
+  }>();
+
+  const cameraAlgoStore = useCameraAlgoStore();
+  const { selectedAlgoDetail, metaObjList, markedParamCardIds } = storeToRefs(cameraAlgoStore);
+  const { deleteParam } = cameraAlgoStore;
+
+  const ruleFormRef = ref<FormInstance>();
+  //是否在编辑
+  const isEdit = ref(false);
+
+  const getLabelList = () => {
+    return metaObjList.value.map((item) => {
+      const entry = selectedAlgoDetail.value.metaValues.find(
+        (val) => val.label === item.label && item.label !== algoParams.value.label,
+      );
+      return {
+        label: labelNameMap[item.label],
+        value: item.label,
+        disabled: entry ? true : false,
+      };
+    });
+  };
+
+  const handleLabelChange = () => {
+    markedParamCardIds.value = selectedAlgoDetail.value.metaValues
+      .filter((item) => !item.label)
+      .map((item) => item.id);
+  };
+
+  const algoParams = computed(
+    () =>
+      selectedAlgoDetail.value.metaValues.find((item) => item.id === props.id) ||
+      ({} as AlgoParamMetaItem),
+  );
+
+  const paramItems = ref([
+    { label: '', type: 'label', prop: 'label' },
+    { label: '', type: 'criticalCount', prop: 'criticalCount' },
+    { label: '', type: 'confidence', prop: 'confidence' },
+    { label: '', type: 'minArea', prop: '' },
+  ]);
+
+  watch(
+    () => algoParams.value.label,
+    (val) => {
+      if (!val) return;
+      const meta = metaObjList.value.find((item) => item.label === val);
+      algoParams.value.criticalCount = 0;
+      algoParams.value.confidence = meta.confidence * 100;
+      algoParams.value['min-width'] = meta['min-width'];
+      algoParams.value['min-height'] = meta['min-height'];
+
+      const nexts = meta.nextObjs;
+      if (nexts) {
+        for (let i = 0; i < nexts.length; i++) {
+          const item = nexts[i];
+          paramItems.value.push({
+            label: item.label,
+            type: 'confidence',
+            prop: `${item.label}.confidence`,
+          });
+          algoParams.value[`${item.label}.confidence`] = item.confidence * 100;
+          paramItems.value.push({
+            label: item.label,
+            type: 'minArea',
+            prop: '',
+          });
+          algoParams.value[item.label + '.' + 'min-width'] = item['min-width'];
+          algoParams.value[item.label + '.' + 'min-height'] = item['min-height'];
+        }
+      }
+    },
+    {
+      immediate: true,
+    },
+  );
+
+  const rules = computed(() => {
+    const rule = {} as any;
+    paramItems.value.forEach((param) => {
+      if (param.type === 'label') {
+        rule.label = [
+          {
+            required: true,
+            message: '请选择检测对象',
+            trigger: 'blur',
+          },
+        ];
+      }
+      if (param.type === 'criticalCount') {
+        rule[`${param.label}.criticalCount`] = [
+          {
+            required: true,
+            message: '请输入检测数量',
+            trigger: 'blur',
+          },
+          { validator: integerJudge, trigger: 'blur' },
+        ];
+      }
+      if (param.type === 'confidence') {
+        rule[`${param.label}.confidence`] = [
+          {
+            required: true,
+            message: '请输入检测置信度',
+            trigger: 'blur',
+          },
+          { validator: integerJudge, trigger: 'blur' },
+        ];
+      }
+      if (param.type === 'minArea') {
+        rule[param.label ? param.label + '.' + 'min-width' : 'min-width'] = [
+          {
+            required: true,
+            message: '请输入最小宽度',
+            trigger: 'blur',
+          },
+          { validator: integerJudge, trigger: 'blur' },
+        ];
+        rule[param.label ? param.label + '.' + 'min-height' : 'min-height'] = [
+          {
+            required: true,
+            message: '请输入最小高度',
+            trigger: 'blur',
+          },
+          { validator: integerJudge, trigger: 'blur' },
+        ];
+      }
+    });
+    return rule;
+  });
+
+  const integerJudge = (rule, value, callback) => {
+    // 整数校验逻辑
+    const pattern = /^-?\d+$/;
+    if (pattern.test(value)) {
+      callback(); // 校验通过
+    } else {
+      callback(new Error('请输入一个整数'));
+    }
+  };
+
+  const handleSaveParam = async (formEl: FormInstance | undefined) => {
+    if (!formEl) return;
+    await formEl.validate((valid, fields) => {
+      if (valid) {
+        isEdit.value = false;
+      } else {
+        ElMessage.error('保存失败,请检查填写');
+      }
+    });
+  };
+
+  const checkValid = async () => {
+    if (!ruleFormRef.value) return false;
+    let isValid = true;
+    await ruleFormRef.value.validate((valid, fields) => {
+      if (valid) {
+      } else {
+        isValid = false;
+      }
+    });
+    return isValid;
+  };
+
+  const setEditable = (value: boolean) => {
+    isEdit.value = value;
+  };
+
+  defineExpose({
+    checkValid,
+  });
+</script>
+
+<style scoped lang="scss">
+  .algo-params-card {
+    position: relative;
+    width: 400px;
+    background: #0000000a;
+    border-radius: 5px;
+    border: 1px solid #e8ecf2;
+    padding: 10px 0 0 10px;
+    margin: 0 5px 5px 5px;
+
+    &-active {
+      border: 1px solid #ff0000ab;
+    }
+  }
+
+  .input-box {
+    width: 80px;
+  }
+
+  .paramOptIcons {
+    position: absolute;
+    top: 6px;
+    right: 6px;
+    display: flex;
+    justify-content: space-between;
+    color: #00000026;
+    cursor: pointer;
+  }
+
+  :deep(.el-form--inline .el-form-item) {
+    margin-bottom: 10px;
+    margin-right: 10px;
+  }
+
+  :deep(.el-form-item__label) {
+    line-height: 16px;
+    font-size: 14px;
+  }
+</style>

+ 408 - 0
src/views/cameras/preview/components/AlgorithmsSetting/AlgoPeriodCard copy.vue

@@ -0,0 +1,408 @@
+<template>
+  <div class="periodCard">
+    <div class="dayRrange">
+      <el-select class="daySelect" v-model="startDay" placeholder="开始日期">
+        <el-option
+          v-for="item in dayOptions"
+          :key="item.value"
+          :label="item.label"
+          :value="item.value"
+          :disabled="endDay && item.value > endDay"
+        >
+        </el-option>
+      </el-select>
+      <div class="divider">-</div>
+      <el-select class="daySelect" v-model="endDay" placeholder="结束日期">
+        <el-option
+          v-for="item in dayOptions"
+          :key="item.value"
+          :label="item.label"
+          :value="item.value"
+          :disabled="startDay && item.value < startDay"
+        >
+        </el-option>
+      </el-select>
+    </div>
+    <div class="timeRange" v-for="(item, index) in timeRangeList">
+      <el-time-picker
+        v-model="item.startTime"
+        format="HH:mm"
+        value-format="HH:mm"
+        :teleported="false"
+        :editable="true"
+        placeholder="开始时间"
+        :disabled-hours="() => disabledHours(index, false)"
+        :disabled-minutes="(hour:number)=>disabledMinutes(hour, index, false)"
+      />
+      <div class="divider">-</div>
+      <el-time-picker
+        v-model="item.endTime"
+        format="HH:mm"
+        value-format="HH:mm"
+        :teleported="false"
+        :editable="true"
+        placeholder="结束时间"
+        :disabled-hours="() => disabledHours(index, true)"
+        :disabled-minutes="(hour:number)=>disabledMinutes(hour, index, true)"
+      />
+      <div v-if="index == timeRangeList.length - 1" class="timeAdd" @click="handleAddTimeRange">
+        <el-icon color="#d0d0d0"><Plus /></el-icon>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import { TimePeriodItem } from './types';
+  import { Plus } from '@element-plus/icons-vue';
+
+  const props = defineProps<{ periodData: TimePeriodItem }>();
+
+  const dayOptions = [
+    {
+      value: 1,
+      label: '周一',
+    },
+    {
+      value: 2,
+      label: '周二',
+    },
+    {
+      value: 3,
+      label: '周三',
+    },
+    {
+      value: 4,
+      label: '周四',
+    },
+    {
+      value: 5,
+      label: '周五',
+    },
+    {
+      value: 6,
+      label: '周六',
+    },
+    {
+      value: 7,
+      label: '周日',
+    },
+  ];
+
+  const makeRange = (start: number, end: number): number[] => {
+    let result: number[] = [];
+    for (let i = start; i <= end; i++) {
+      result.push(i);
+    }
+    return result;
+  };
+
+  const startDay = ref(props.periodData.startDay);
+  const endDay = ref(props.periodData.endDay);
+  const timeRangeList = ref<{ startTime: Date | string; endTime: Date | string }[]>(
+    props.periodData.timeRangeList,
+  );
+
+  const handleAddTimeRange = () => {
+    if (!timeRangeList.value.find((item) => item.startTime === '' || item.endTime === '')) {
+      timeRangeList.value.push({ startTime: '', endTime: '' });
+      console.log(timeRangeList.value);
+    }
+  };
+
+  const disabledHours = (index: number, isEnd: boolean) => {
+    const newList = timeRangeList.value.filter(
+      (item, i) => item.startTime && item.endTime && i !== index,
+    );
+    let result: number[] = [];
+    // 选开始时间,结束时间被约束
+    if (!isEnd && timeRangeList.value[index].endTime) {
+      const curEnd = timeRangeList.value[index].endTime;
+      const cureh = Number(String(curEnd).split(':')[0]);
+      const curem = Number(String(curEnd).split(':')[1]);
+      if (newList.length == 0) {
+        if (curem == 0) {
+          result = result.concat(makeRange(cureh, 23));
+        } else {
+          result = result.concat(makeRange(getH(cureh + 1), 23));
+        }
+      }
+      for (let i = 0; i < newList.length; i++) {
+        if (compareHM(newList[i].startTime, curEnd)) {
+          if (i == 0) {
+            if (curem == 0) {
+              result = result.concat(makeRange(cureh, 23));
+              break;
+            } else {
+              result = result.concat(makeRange(getH(cureh + 1), 23));
+              break;
+            }
+          } else {
+            let sh = 0;
+            const lastEnd = newList[i - 1].endTime;
+            const lasteh = Number(String(lastEnd).split(':')[0]);
+            const lastem = Number(String(lastEnd).split(':')[1]);
+            if (lastem == 59) {
+              sh = lasteh;
+            } else {
+              sh = getH(lasteh - 1);
+            }
+            let eh = 0;
+            if (curem == 0) {
+              eh = cureh;
+            } else {
+              eh = getH(cureh + 1);
+            }
+            result = result.concat(makeRange(0, sh)).concat(makeRange(eh, 23));
+            break;
+          }
+        }
+        if (i == newList.length - 1) {
+          let sh = 0;
+          const lastEnd = newList[i].endTime;
+          const lasteh = Number(String(lastEnd).split(':')[0]);
+          const lastem = Number(String(lastEnd).split(':')[1]);
+          if (lastem == 59) {
+            sh = lasteh;
+          } else {
+            sh = getH(lasteh - 1);
+          }
+          let eh = 0;
+          if (curem == 0) {
+            eh = cureh;
+          } else {
+            eh = getH(cureh + 1);
+          }
+          result = result.concat(makeRange(0, sh)).concat(makeRange(eh, 23));
+        }
+      }
+    }
+    // 选结束时间,开始时间被约束
+    if (isEnd && timeRangeList.value[index].startTime) {
+      const curStart = timeRangeList.value[index].startTime;
+      const cursh = Number(String(curStart).split(':')[0]);
+      const cursm = Number(String(curStart).split(':')[1]);
+      if (newList.length == 0) {
+        if (cursm == 59) {
+          result = result.concat(makeRange(0, cursh));
+        } else {
+          result = result.concat(makeRange(0, getH(cursh - 1)));
+        }
+      }
+      for (let i = 0; i < newList.length; i++) {
+        if (compareHM(curStart, newList[i].endTime)) {
+          if (i == newList.length - 1) {
+            if (cursm == 59) {
+              result = result.concat(makeRange(0, cursh));
+              break;
+            } else {
+              result = result.concat(makeRange(0, getH(cursh - 1)));
+              break;
+            }
+          } else {
+            let sh = 0;
+            if (cursm == 59) {
+              sh = cursh;
+            } else {
+              sh = getH(cursh - 1);
+            }
+            let eh = 0;
+            const nextStart = newList[i + 1].startTime;
+            const nextsh = Number(String(nextStart).split(':')[0]);
+            const nextsm = Number(String(nextStart).split(':')[1]);
+            if (nextsm == 0) {
+              eh = nextsh;
+            } else {
+              eh = getH(nextsh + 1);
+            }
+            result = result.concat(makeRange(0, sh)).concat(makeRange(eh, 23));
+            break;
+          }
+        }
+        if (i == newList.length - 1) {
+          let sh = 0;
+          if (cursm == 59) {
+            sh = cursh;
+          } else {
+            sh = getH(cursh - 1);
+          }
+          let eh = 0;
+          const nextStart = newList[0].startTime;
+          const nextsh = Number(String(nextStart).split(':')[0]);
+          const nextsm = Number(String(nextStart).split(':')[1]);
+          if (nextsm == 0) {
+            eh = nextsh;
+          } else {
+            eh = getH(nextsh + 1);
+          }
+          result = result.concat(makeRange(0, sh)).concat(makeRange(eh, 23));
+        }
+      }
+    }
+    // 另一个时间为空
+    if (
+      (!isEnd && !timeRangeList.value[index].endTime) ||
+      (isEnd && !timeRangeList.value[index].startTime)
+    ) {
+      for (let i = 0; i < newList.length; i++) {
+        const timeRange = newList[i];
+        const s = timeRange.startTime;
+        const e = timeRange.endTime;
+        const sh = Number(String(s).split(':')[0]);
+        const sm = Number(String(s).split(':')[1]);
+        const eh = Number(String(e).split(':')[0]);
+        const em = Number(String(e).split(':')[1]);
+        if (eh - sh > 1) {
+          result = result.concat(makeRange(getH(sh + 1), getH(eh - 1)));
+          if (sm == 0) result.push(sh);
+          if (em == 59) result.push(eh);
+        }
+        if (sm == 0 && em == 59) result.push(sh);
+      }
+    }
+
+    return result;
+  };
+
+  const disabledMinutes = (hour: number, index: number, isEnd: boolean) => {
+    const newList = timeRangeList.value.filter(
+      (item, i) => item.startTime && item.endTime && i !== index,
+    );
+    let result: number[] = [];
+    for (let i = 0; i < newList.length; i++) {
+      const timeRange = newList[i];
+      const s = timeRange.startTime;
+      const e = timeRange.endTime;
+      const sh = Number(String(s).split(':')[0]);
+      const sm = Number(String(s).split(':')[1]);
+      const eh = Number(String(e).split(':')[0]);
+      const em = Number(String(e).split(':')[1]);
+      if (hour == sh) {
+        if (sh == eh) {
+          result = result.concat(makeRange(sm, em));
+        } else {
+          result = result.concat(makeRange(sm, 59));
+        }
+        continue;
+      }
+      if (hour == eh) {
+        result = result.concat(makeRange(0, em));
+      }
+    }
+    // 选开始时间,结束时间被约束
+    if (!isEnd && timeRangeList.value[index].endTime) {
+      const curEnd = timeRangeList.value[index].endTime;
+      const cureh = Number(String(curEnd).split(':')[0]);
+      const curem = Number(String(curEnd).split(':')[1]);
+      if (hour === cureh) {
+        result = result.concat(makeRange(curem, 59));
+      }
+    }
+    // 选结束时间,开始时间被约束
+    if (isEnd && timeRangeList.value[index].startTime) {
+      const curStart = timeRangeList.value[index].startTime;
+      const cursh = Number(String(curStart).split(':')[0]);
+      const cursm = Number(String(curStart).split(':')[1]);
+      if (hour === cursh) {
+        result = result.concat(makeRange(0, cursm));
+      }
+    }
+
+    return result;
+  };
+
+  const getH = (h) => {
+    if (h < 0) {
+      return 0;
+    } else if (h > 23) {
+      return 23;
+    } else {
+      return h;
+    }
+  };
+
+  const getM = (m) => {
+    if (m < 0) {
+      return 0;
+    } else if (m > 59) {
+      return 23;
+    } else {
+      return m;
+    }
+  };
+
+  const compareHM = (s, e) => {
+    const sh = Number(String(s).split(':')[0]);
+    const sm = Number(String(s).split(':')[1]);
+    const eh = Number(String(e).split(':')[0]);
+    const em = Number(String(e).split(':')[1]);
+
+    if (sh === eh) {
+      return sm - em > 0 ? true : false;
+    } else if (sh > eh) {
+      return true;
+    } else {
+      return false;
+    }
+  };
+</script>
+
+<style scoped lang="scss">
+  .periodCard {
+    width: 294px;
+    max-height: 174px;
+    background: #0000000a;
+    border-radius: 5px;
+    border: 1px solid #e8ecf2;
+    padding: 3px;
+    margin: 0 5px 5px 5px;
+  }
+
+  .dayRrange {
+    display: flex;
+    align-items: center;
+  }
+
+  .timeRange {
+    display: flex;
+    align-items: center;
+  }
+
+  .divider {
+    color: #00000040;
+  }
+
+  .daySelect {
+    width: 112px;
+    margin: 5px;
+  }
+
+  .timeAdd {
+    width: 28px;
+    height: 28px;
+    border-radius: 50%;
+    background: #ebebeb;
+    border: 1px dashed #00000026;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+
+  :deep(.el-date-editor) {
+    width: 112px;
+    margin: 5px;
+
+    .el-input__prefix {
+      order: 1;
+    }
+
+    .el-input__icon {
+      margin: 0;
+    }
+  }
+
+  :deep(.el-time-panel__footer) {
+    display: none;
+  }
+</style>

+ 440 - 0
src/views/cameras/preview/components/AlgorithmsSetting/AlgoPeriodCard.vue

@@ -0,0 +1,440 @@
+<template>
+  <div class="periodCard" :class="{ 'periodCard-active': markedTimeRangeIds.includes(props.id) }">
+    <div class="dayRrange">
+      <el-select
+        class="daySelect"
+        v-model="timeItem.startDay"
+        placeholder="开始日期"
+        @change="changeDay"
+      >
+        <el-option
+          v-for="item in dayOptions"
+          :key="item.value"
+          :label="item.label"
+          :value="item.value"
+          :disabled="timeItem.endDay && item.value > timeItem.endDay"
+        >
+        </el-option>
+      </el-select>
+      <div class="divider">-</div>
+      <el-select
+        class="daySelect"
+        v-model="timeItem.endDay"
+        placeholder="结束日期"
+        @change="changeDay"
+      >
+        <el-option
+          v-for="item in dayOptions"
+          :key="item.value"
+          :label="item.label"
+          :value="item.value"
+          :disabled="timeItem.startDay && item.value < timeItem.startDay"
+        >
+        </el-option>
+      </el-select>
+    </div>
+    <div class="timeRange" v-for="item in timeItem.timeRangeList" :key="item.id">
+      <div class="time-border" :class="{ 'time-border-active': markedTimes.includes(item.id) }">
+        <el-time-picker
+          v-model="item.startTime"
+          format="HH:mm"
+          value-format="HH:mm"
+          :teleported="false"
+          :editable="false"
+          placeholder="开始时间"
+          :disabled-hours="() => disabledHours(item.id, 0)"
+          :disabled-minutes="(hour) => disabledMinutes(hour, item.id, 0)"
+          @focus="handleOpen(item.id, 0)"
+          @blur="handleBlur(item.id)"
+        />
+        <div class="divider">-</div>
+        <el-time-picker
+          v-model="item.endTime"
+          format="HH:mm"
+          value-format="HH:mm"
+          :teleported="false"
+          :editable="false"
+          placeholder="结束时间"
+          :disabled-hours="() => disabledHours(item.id, 1)"
+          :disabled-minutes="(hour) => disabledMinutes(hour, item.id, 1)"
+          @focus="handleOpen(item.id, 1)"
+          @blur="handleBlur(item.id)"
+        />
+      </div>
+      <el-icon
+        size="18px"
+        color="#d0d0d0"
+        @click="handleDeleteTimeRange(item.id)"
+        style="cursor: pointer"
+      >
+        <CircleClose />
+      </el-icon>
+    </div>
+    <div class="timeAdd" @click="handleAddTimeRange">
+      <el-icon color="#d0d0d0"><Plus /></el-icon>
+    </div>
+    <div class="paramOptIcons">
+      <el-icon size="16px" @click="deleteTimeRange(props.id)"><Delete /></el-icon>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { computed, ref } from 'vue';
+  import { TimePeriodItem } from './types';
+  import { Plus, Delete, CircleClose } from '@element-plus/icons-vue';
+  import useCameraAlgoStore from '../../store/useCameraAlgoStore';
+  import { storeToRefs } from 'pinia';
+  import { ElMessage } from 'element-plus';
+  import { uid } from 'uid';
+
+  const props = defineProps<{ id: string }>();
+
+  const cameraAlgoStore = useCameraAlgoStore();
+  const { selectedAlgoDetail, markedTimeRangeIds } = storeToRefs(cameraAlgoStore);
+  const { deleteTimeRange } = cameraAlgoStore;
+
+  const dayOptions = [
+    {
+      value: 1,
+      label: '周一',
+    },
+    {
+      value: 2,
+      label: '周二',
+    },
+    {
+      value: 3,
+      label: '周三',
+    },
+    {
+      value: 4,
+      label: '周四',
+    },
+    {
+      value: 5,
+      label: '周五',
+    },
+    {
+      value: 6,
+      label: '周六',
+    },
+    {
+      value: 7,
+      label: '周日',
+    },
+  ];
+
+  const markedTimes = ref<string[]>([]);
+  const openTimeId = ref<string>('');
+  const openTimeType = ref<number | null>();
+
+  const timeItem = computed(
+    () =>
+      selectedAlgoDetail.value.timeRangeArr.find((item) => item.id === props.id) ||
+      ({} as TimePeriodItem),
+  );
+
+  const changeDay = () => {
+    if (timeItem.value.startDay && timeItem.value.endDay) {
+      let isConflict = false;
+      //判断日期是否冲突
+      const otherDays = selectedAlgoDetail.value.timeRangeArr.filter(
+        (item) => item.id !== props.id,
+      );
+      for (let i = 0; i < otherDays.length; i++) {
+        const dayRange = otherDays[i];
+        if (
+          betweenTwoDays(timeItem.value.startDay, dayRange.startDay!, dayRange.endDay!) ||
+          betweenTwoDays(timeItem.value.endDay, dayRange.startDay!, dayRange.endDay!)
+        ) {
+          isConflict = true;
+          break;
+        }
+      }
+      if (isConflict) {
+        ElMessage.error('日期存在冲突');
+        markedTimeRangeIds.value.push(props.id);
+        return;
+      }
+      markedTimeRangeIds.value = markedTimeRangeIds.value.filter((id) => id !== props.id);
+      //如果不冲突,排序
+      selectedAlgoDetail.value.timeRangeArr.sort((a, b) => {
+        return a.startDay! - b.endDay!;
+      });
+    }
+  };
+
+  const handleBlur = (curId: string) => {
+    openTimeId.value = '';
+    openTimeType.value = null;
+    const curTimes = timeItem.value.timeRangeList.find((item) => item.id === curId);
+    if (curTimes!.startTime && curTimes!.endTime) {
+      // 检查时间是否冲突
+      let isConflict = false;
+      const otherTimes = timeItem.value.timeRangeList.filter((item) => item.id !== curId);
+      for (let i = 0; i < otherTimes.length; i++) {
+        const item = otherTimes[i];
+        if (
+          betweenTwoTimes(curTimes!.startTime, item.startTime, item.endTime) ||
+          betweenTwoTimes(curTimes!.endTime, item.startTime, item.endTime)
+        ) {
+          isConflict = true;
+          break;
+        }
+      }
+      if (isConflict) {
+        ElMessage.error('时间存在冲突');
+        markedTimes.value.push(curId);
+        return;
+      }
+      markedTimes.value = markedTimes.value.filter((id) => id !== curId);
+      //如果不冲突,排序
+      timeItem.value.timeRangeList.sort((a, b) => {
+        return hm2Minutes(a.startTime) - hm2Minutes(b.endTime);
+      });
+    }
+  };
+
+  const betweenTwoDays = (day: number, start: number, end: number) => {
+    if (day <= end && day >= start) return true;
+    return false;
+  };
+
+  const betweenTwoTimes = (time: string, start: string, end: string) => {
+    if (hm2Minutes(time) <= hm2Minutes(end) && hm2Minutes(time) >= hm2Minutes(start)) {
+      return true;
+    }
+    return false;
+  };
+
+  const handleOpen = (curId: string, type: number) => {
+    openTimeType.value = type;
+    openTimeId.value = curId;
+  };
+
+  const disabledHours = (curId: string, type: number) => {
+    if (openTimeId.value !== curId || openTimeType.value !== type) {
+      return [];
+    }
+    let ret = [] as number[];
+    const curTime = timeItem.value.timeRangeList.find((item) => item.id === curId);
+    if (type === 0) {
+      //选择开始时间
+      if (curTime!.endTime) {
+        const [h, m] = curTime!.endTime.split(':').map(Number);
+        if (m === 0) {
+          ret = ret.concat(makeRange(h, 23));
+        } else {
+          ret = ret.concat(makeRange(getH(h + 1), 23));
+        }
+      }
+    }
+    if (type == 1) {
+      //选择结束时间
+      if (curTime!.startTime) {
+        const [h, m] = curTime!.startTime.split(':').map(Number);
+        if (m === 59) {
+          ret = ret.concat(makeRange(0, h));
+        } else {
+          ret = ret.concat(makeRange(0, getH(h - 1)));
+        }
+      }
+    }
+    return ret;
+  };
+
+  const disabledMinutes = (hour: number, curId: string, type: number) => {
+    if (openTimeId.value !== curId || openTimeType.value !== type) {
+      return [];
+    }
+    let ret = [] as number[];
+    const curTime = timeItem.value.timeRangeList.find((item) => item.id === curId);
+    if (type === 0) {
+      //选择开始时间
+      if (curTime!.endTime) {
+        const [h, m] = curTime!.endTime.split(':').map(Number);
+        if (hour == h) {
+          ret = ret.concat(makeRange(m, 59));
+        }
+      }
+    }
+    if (type == 1) {
+      //选择结束时间
+      if (curTime!.startTime) {
+        const [h, m] = curTime!.startTime.split(':').map(Number);
+        if (hour == h) {
+          ret = ret.concat(makeRange(0, m));
+        }
+      }
+    }
+    return ret;
+  };
+
+  const makeRange = (start: number, end: number): number[] => {
+    let result: number[] = [];
+    for (let i = start; i <= end; i++) {
+      result.push(i);
+    }
+    return result;
+  };
+
+  const handleDeleteTimeRange = (curId: string) => {
+    timeItem.value.timeRangeList = timeItem.value.timeRangeList.filter((item) => item.id !== curId);
+    markedTimes.value = markedTimes.value.filter((id) => id !== curId);
+  };
+
+  const handleAddTimeRange = () => {
+    const emptyList = timeItem.value.timeRangeList.filter(
+      (item) => item.startTime === '' || item.endTime === '',
+    );
+    if (emptyList && emptyList.length > 0) {
+      emptyList.forEach((time) => markedTimes.value.push(time.id));
+      ElMessage.error('请填写完整时间段');
+      return;
+    }
+    if (markedTimes.value.length > 0) {
+      ElMessage.error('请先解决时间段冲突');
+      return;
+    }
+    timeItem.value.timeRangeList.push({ id: uid(), startTime: '', endTime: '' });
+  };
+
+  const getD = (d) => {
+    if (d < 1) {
+      return 1;
+    } else if (d > 7) {
+      return 7;
+    } else {
+      return d;
+    }
+  };
+
+  const getH = (h) => {
+    if (h < 0) {
+      return 0;
+    } else if (h > 23) {
+      return 23;
+    } else {
+      return h;
+    }
+  };
+
+  const getM = (m) => {
+    if (m < 0) {
+      return 0;
+    } else if (m > 59) {
+      return 23;
+    } else {
+      return m;
+    }
+  };
+
+  const hm2Minutes = (hm: string) => {
+    const [h, m] = hm.split(':').map(Number);
+    return h * 60 + m;
+  };
+
+  const checkValid = () => {
+    if (markedTimes.value.length > 0) {
+      return 0; // 存在冲突时间段
+    }
+    const emptyTimes = timeItem.value.timeRangeList.filter(
+      (item) => !item.startTime || !item.endTime,
+    );
+    if (emptyTimes && emptyTimes.length > 0) {
+      emptyTimes.forEach((val) => markedTimes.value.push(val.id));
+      return 1; // 存在缺失时间段
+    }
+    if (timeItem.value.timeRangeList.length === 0) {
+      return 2; // 至少有一个时间段
+    }
+    return 3; // 检验成功
+  };
+
+  defineExpose({ checkValid });
+</script>
+
+<style scoped lang="scss">
+  .periodCard {
+    position: relative;
+    width: 294px;
+    background: #0000000a;
+    border-radius: 5px;
+    border: 1px solid #e8ecf2;
+    padding: 3px;
+    margin: 0 5px 5px 5px;
+
+    &-active {
+      border: 1px solid #ff0000ab;
+    }
+  }
+
+  .paramOptIcons {
+    position: absolute;
+    top: 6px;
+    right: 6px;
+    display: flex;
+    justify-content: space-between;
+    color: #00000026;
+    cursor: pointer;
+  }
+
+  .dayRrange {
+    display: flex;
+    align-items: center;
+  }
+
+  .timeRange {
+    display: flex;
+    align-items: center;
+  }
+
+  .time-border {
+    display: flex;
+    align-items: center;
+    border: 1px solid transparent;
+
+    &-active {
+      border: 1px solid #ff0000ab;
+    }
+  }
+
+  .divider {
+    color: #00000040;
+  }
+
+  .daySelect {
+    width: 112px;
+    margin: 5px;
+  }
+
+  .timeAdd {
+    width: 240px;
+    height: 26px;
+    margin: 5px;
+    border-radius: 4px;
+    background: #ebebeb;
+    border: 1px dashed #00000026;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+
+  :deep(.el-date-editor) {
+    width: 112px;
+    margin: 5px;
+
+    .el-input__prefix {
+      order: 1;
+    }
+
+    .el-input__icon {
+      margin: 0;
+    }
+  }
+
+  :deep(.el-time-panel__footer) {
+    display: none;
+  }
+</style>

+ 244 - 51
src/views/cameras/preview/components/AlgorithmsSetting/AlgoSettingCard.vue

@@ -10,62 +10,96 @@
     </div>
     <div class="algoCardMain">
       <div class="algoRow">
-        <div class="algoLabel">电子围栏:</div>
+        <div class="algoLabel">绘制电子围栏:</div>
         <div>
-          <div>
+          <div style="display: flex; align-items: center">
             <ElSwitch v-model="selectedAlgoDetail.electronicFenceBool" size="small" />
-            <span style="font-size: 10px; margin-left: 20px; color: #262626"
-              >备注:请绘制电子围栏</span
+            <el-tooltip
+              class="box-item"
+              effect="dark"
+              content="打开开关,绘制电子围栏。"
+              placement="top"
             >
+              <el-icon color="#e2e2e2" class="tipIcon"><InfoCircleOutlined /></el-icon>
+            </el-tooltip>
+            <!-- <span style="font-size: 10px; margin-left: 20px; color: #262626"
+              >备注:请绘制电子围栏</span
+            > -->
+            <el-radio-group v-model="selectedAlgoDetail.regionJudge" class="ml-4">
+              <el-radio :label="0">内</el-radio>
+              <el-radio :label="1">外</el-radio>
+            </el-radio-group>
           </div>
           <div class="presetList"> 预置位1 预置位2 预置位3 </div>
         </div>
       </div>
       <div class="algoRow">
-        <div class="algoLabel">检测时间:</div>
-        <div>
-          <div v-for="x in selectedAlgoDetail.timeRangeArr" :key="x.id">
-            <el-time-picker
-              v-model="x.value"
-              is-range
-              range-separator="-"
-              start-placeholder="Start time"
-              end-placeholder="End time"
-              :clearable="false"
-              size="small"
-              style="width: 180px; margin-bottom: 10px"
+        <div class="algoLabel">检测时间段:</div>
+        <div class="algoTimeContent">
+          <div class="timeAdd" @click="handleAddTimePeriod">
+            <el-icon color="#d0d0d0"><Plus /></el-icon>
+          </div>
+          <div class="timeList">
+            <PeriodCard
+              v-for="(x, index) in selectedAlgoDetail.timeRangeArr"
+              :key="x.id"
+              :ref="(el) => (periodCardRefs[index] = el)"
+              :id="x.id"
             />
-            <span @click="removeTime(x.id)" v-if="selectedAlgoDetail.timeRangeArr.length > 1">
-              <el-icon class="removeIcon"><CircleCloseFilled /></el-icon
-            ></span>
           </div>
-          <div @click="handleAddTimeRange" class="addTimeIcon">
-            <img :src="addTimeIcon" width="28" height="28" />
+        </div>
+      </div>
+      <div class="algoRow">
+        <div class="algoLabel">检测元数据:</div>
+        <div class="algoTimeContent">
+          <div class="timeAdd" @click="handleAddMetaObj">
+            <el-icon color="#d0d0d0"><Plus /></el-icon>
+          </div>
+          <div class="timeList">
+            <ParamCard
+              v-for="(v, index) in selectedAlgoDetail.metaValues"
+              :key="v.id"
+              :ref="(el) => (paramCardRefs[index] = el)"
+              :id="v.id"
+            />
           </div>
         </div>
       </div>
+      <div class="algoRow" style="align-items: center">
+        <div class="algoLabel">检测数量范围:</div>
+        <el-radio-group v-model="selectedAlgoDetail.judge" class="ml-4">
+          <el-radio :label="0">小于</el-radio>
+          <el-radio :label="1">大于</el-radio>
+          <el-radio :label="2">等于</el-radio>
+        </el-radio-group>
+      </div>
       <div class="algoRow" style="align-items: center">
         <div class="algoLabel">检测频率:</div>
         <ElInputNumber
-          v-model="selectedAlgoDetail.detectionJSON.detectionNum"
+          v-model="selectedAlgoDetail.detectionFrequency"
           controls-position="right"
-          :min="0"
+          :min="1"
+          :step="1"
           size="small"
-          style="width: 80px"
+          style="width: 186px"
+          @blur="checkFrequencyValid"
         />
-        <ElSelect
+        <span style="font-size: 12px; margin-left: 5px">S/次</span>
+      </div>
+      <div
+        v-if="selectedAlgoDetail.timeWindow !== undefined"
+        class="algoRow"
+        style="align-items: center"
+      >
+        <div class="algoLabel">检测窗口时长:</div>
+        <ElInputNumber
+          v-model="selectedAlgoDetail.timeWindow"
+          controls-position="right"
+          :min="1"
           size="small"
-          style="width: 60px; margin-left: 10px"
-          v-model="selectedAlgoDetail.detectionJSON.detectionUnit"
-        >
-          <ElOption
-            v-for="x in frequencyOptions"
-            :key="x.value"
-            :value="x.value"
-            :label="x.label"
-          />
-        </ElSelect>
-        <span style="font-size: 12px; margin-left: 5px">/次</span>
+          style="width: 186px"
+        />
+        <span style="font-size: 12px; margin-left: 5px">S</span>
       </div>
       <div style="display: flex; justify-content: flex-end">
         <ElButton size="small" @click="handleRemoveAlgo" :disabled="!selectedAlgoId">删除</ElButton>
@@ -77,18 +111,36 @@
   </div>
 </template>
 <script lang="ts" setup>
-  import { ElSelect, ElOption, ElSwitch, ElInputNumber, ElTimePicker } from 'element-plus';
-  import { CircleCloseFilled } from '@element-plus/icons-vue';
+  import {
+    ElSelect,
+    ElOption,
+    ElSwitch,
+    ElInputNumber,
+    ElTimePicker,
+    ElMessage,
+  } from 'element-plus';
+  import { CircleCloseFilled, Plus } from '@element-plus/icons-vue';
   import { storeToRefs } from 'pinia';
   import dayjs, { Dayjs } from 'dayjs';
   import addTimeIcon from '@/assets/icons/addTimeIcon.png';
-  import useCameraAlgoStore from '../../store/useCameraAlgoStore';
-  import { createDefaultTime, frequencyOptions } from './utils';
+  import useCameraAlgoStore, { AlgoParamMetaItem } from '../../store/useCameraAlgoStore';
+  import { createDefaultTime, frequencyOptions, getTimeCompletion } from './utils';
+  import { InfoCircleOutlined } from '@vicons/antd';
   import { ALGO_ENABLED_STATUS, FENCE_ENBALED_STATUS } from '@/api/camera/camera-preview';
+  import PeriodCard from './AlgoPeriodCard.vue';
+  import ParamCard from './AlgoParamsCard.vue';
+  import { uid } from 'uid';
+  import { ref } from 'vue';
 
   // const { data: algoList, loading } = useAllAlgos();
   const cameraAlgoStore = useCameraAlgoStore();
-  const { selectedAlgoId, selectedAlgoDetail } = storeToRefs(cameraAlgoStore);
+  const {
+    selectedAlgoId,
+    selectedAlgoDetail,
+    metaObjList,
+    markedTimeRangeIds,
+    markedParamCardIds,
+  } = storeToRefs(cameraAlgoStore);
 
   interface Param {
     /** 算法id */
@@ -108,10 +160,37 @@
     (e: 'onRemove', algoId: number): Promise<unknown>;
   }>();
 
-  const handleAddTimeRange = () => {
+  const periodCardRefs = ref<any>([]);
+  const paramCardRefs = ref<any>([]);
+
+  const handleAddTimePeriod = () => {
+    const emptyList = selectedAlgoDetail.value.timeRangeArr.filter(
+      (item) => !getTimeCompletion(item),
+    );
+    if (emptyList && emptyList.length > 0) {
+      emptyList.forEach((item) => markedTimeRangeIds.value.push(item.id));
+      ElMessage.error('请先完善检测时间段数据');
+      return;
+    }
     selectedAlgoDetail.value.timeRangeArr.push(createDefaultTime());
   };
 
+  const handleAddMetaObj = () => {
+    const unemptyList = selectedAlgoDetail.value.metaValues.filter((item) => item.label);
+    if (unemptyList.length < selectedAlgoDetail.value.metaValues.length) {
+      selectedAlgoDetail.value.metaValues
+        .filter((item) => !item.label)
+        .forEach((val) => markedParamCardIds.value.push(val.id));
+      ElMessage.error('存在未完善的检测元数据');
+      return;
+    }
+    if (unemptyList.length == metaObjList.value.length) {
+      ElMessage.warning('暂无更多检测对象');
+      return;
+    }
+    selectedAlgoDetail.value.metaValues.push({ id: uid() } as AlgoParamMetaItem);
+  };
+
   const removeTime = (id: string) => {
     const timeRangeArr = selectedAlgoDetail.value.timeRangeArr;
     selectedAlgoDetail.value.timeRangeArr = timeRangeArr.filter((x) => x.id !== id);
@@ -128,23 +207,113 @@
     handleSave();
   };
 
-  const handleSave = () => {
+  const checkFrequencyValid = () => {
+    selectedAlgoDetail.value.detectionFrequency = Math.ceil(
+      selectedAlgoDetail.value.detectionFrequency,
+    );
+  };
+
+  const handleSave = async () => {
+    //判断时间段是否合格
+    if (markedTimeRangeIds.value.length > 0) {
+      ElMessage.error('请正确填写检测时间段');
+      return;
+    }
+    const emptyTimes = selectedAlgoDetail.value.timeRangeArr.filter(
+      (item) => !item.startDay || !item.endDay,
+    );
+    if (emptyTimes && emptyTimes.length > 0) {
+      emptyTimes.forEach((item) => {
+        markedTimeRangeIds.value.push(item.id);
+      });
+      ElMessage.error('请完善检测时间段');
+      return;
+    }
+    for (let i = 0; i < periodCardRefs.value.length; i++) {
+      const item = periodCardRefs.value[i];
+      switch (item.checkValid()) {
+        case 0:
+          ElMessage.error('请正确填写检测时间段');
+          return;
+        case 1:
+          ElMessage.error('请完善检测时间段');
+          return;
+        case 2:
+          ElMessage.error('至少有一个时间段');
+          return;
+        case 3:
+          break;
+      }
+    }
+
+    //判断元数据是否合格
+    for (let i = 0; i < paramCardRefs.value.length; i++) {
+      const item = paramCardRefs.value[i];
+      await item.checkValid().then((res) => {
+        if (res) {
+        } else {
+          ElMessage.error('请正确填写检测元数据');
+          return;
+        }
+      });
+    }
+
+    //判断时间段和元数据都有值
     const detail = selectedAlgoDetail.value;
     if (!detail) return;
+    const timeRanges = detail.timeRangeArr;
+    if (timeRanges.length == 0) {
+      ElMessage.error('至少添加一个检测时间段');
+      return;
+    }
+    const metaValues = detail.metaValues;
+    if (metaValues.length == 0) {
+      ElMessage.error('至少添加一个检测元数据');
+      return;
+    }
+
+    //数据处理
+    const metaObjs = metaValues.map((meta) => {
+      const obj = metaObjList.value.find((item) => item.label === meta.label);
+      const val = {
+        label: meta.label,
+        confidence: meta.confidence / 100,
+        'min-width': meta['min-width'],
+        'min-height': meta['min-height'],
+      } as any;
+      const nextValues = [] as any[];
+      obj.nextObjs.forEach((next) => {
+        if (meta[`${next.label}.confidence`]) {
+          nextValues.push({
+            label: next.label,
+            confidence: meta[`${next.label}.confidence`] / 100,
+            'min-width': meta[next.label + '.' + 'min-width'],
+            'min-height': meta[next.label + '.' + 'min-height'],
+            nextObjs: [],
+          });
+        }
+      });
+      val.nextObjs = nextValues;
+      return val;
+    });
     const param = {
       id: detail.id,
+      inferCode: detail.inferCode,
       algoId: detail.algoId,
-      detectionFrequency: detail.detectionJSON.detectionNum * detail.detectionJSON.detectionUnit,
-      detectionTime: detail.timeRangeArr
-        .map((x) => {
-          return getTimeStrs(x.value);
-        })
-        .join(';'),
+      detectionFrequency: detail.detectionFrequency,
+      regionJudge: detail.regionJudge,
+      detectionTime: JSON.stringify(detail.timeRangeArr),
+      metaObjs,
+      criticalCounts: metaValues.map((item) => item.criticalCount),
+      judge: detail.judge,
       electronicFence: detail.electronicFenceBool
         ? FENCE_ENBALED_STATUS.enabled
         : FENCE_ENBALED_STATUS.disabled,
       status: detail.enableCardBool ? ALGO_ENABLED_STATUS.enabled : ALGO_ENABLED_STATUS.disabled,
-    };
+    } as any;
+    if (detail.timeWindow) {
+      param.timeWindow = detail.timeWindow;
+    }
     emits('onSubmit', param);
     console.log('param', param);
   };
@@ -159,7 +328,7 @@
     border: 1px solid #ccc;
     border-radius: 4px;
     /* padding: 10px; */
-    width: 400px;
+    width: 770px;
   }
 
   .algoRow {
@@ -170,6 +339,9 @@
   .algoLabel {
     margin-right: 10px;
   }
+  .algoTimeContent {
+    display: flex;
+  }
   .presetList {
     font-size: 12px;
     display: none;
@@ -195,4 +367,25 @@
   .algoCardMain {
     padding: 10px 20px;
   }
+
+  .timeAdd {
+    width: 28px;
+    height: 90px;
+    background: #ebebeb;
+    border-radius: 4px;
+    border: 1px dashed #00000026;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-right: 5px;
+  }
+  .timeList {
+    width: 608px;
+    display: flex;
+    flex-wrap: wrap;
+    align-items: flex-start;
+  }
+  .tipIcon {
+    margin-left: 10px;
+  }
 </style>

+ 36 - 5
src/views/cameras/preview/components/AlgorithmsSetting/AlgorithmsSetting.vue

@@ -40,7 +40,15 @@
   import useCameraDetailStore from '../../store/useCameraDetailStore';
   import usePresetListStore from '../../store/usePresetListStore';
   import AddAlgoDialog from './AddAlgoDialog.vue';
-  import { createDefaultTime, getDetectionJSON, getDetectionTimeJSON } from './utils';
+  import {
+    createDefaultTime,
+    getDetectionJSON,
+    getDetectionTimeJSON,
+    getInferCode,
+    getMetaValues,
+    getExtraCommonInfo,
+    getDetectionTime,
+  } from './utils';
   import { ALGO_ENABLED_STATUS } from '@/api/camera/camera-preview';
   import { watchEffect } from 'vue';
 
@@ -63,31 +71,53 @@
     if (!algoId) return;
     const detail = getAlgoDetail(algoId);
     if (!detail) return;
-    console.log('detail change', detail);
-    const detectionJSON = getDetectionJSON(detail?.detectionFrequency);
+    // console.log('detail change', detail);
+    // const detectionJSON = getDetectionJSON(detail?.detectionFrequency);
     const enableCard = detail?.status === ALGO_ENABLED_STATUS.enabled ? true : false;
     const electronicFenceBool =
       detail?.electronicFence === FENCE_ENBALED_STATUS.enabled ? true : false;
 
-    const timeRangeArr = getDetectionTimeJSON(detail?.detectionTime) || [createDefaultTime()];
+    // const timeRangeArr = getDetectionTimeJSON(detail?.detectionTime) || [];
+    const timeRangeArr = getDetectionTime(detail?.detectionTime) || [];
+    const metaValues = getMetaValues(detail?.extra) || [];
+
+    const commonInfo = getExtraCommonInfo(detail);
 
     selectedAlgoDetail.value = {
       ...detail,
-      detectionJSON,
+      inferCode: getInferCode(detail?.extra),
+      // detectionJSON,
       enableCardBool: enableCard,
       electronicFenceBool,
       timeRangeArr,
+      metaValues,
+      regionJudge: commonInfo.regionJudge || 0,
+      judge: commonInfo.judge || commonInfo.judge == 0 ? commonInfo.judge : 1,
     };
     fenceStore.getFence({
       algoId: algoId,
       cameraId: cameraDetailStore.cameraId,
       presetToken: presetStore.currentPresetToken,
     });
+
+    //是否有窗口时间
+    if (commonInfo.timeWindow || commonInfo.timeWindow == 0) {
+      selectedAlgoDetail.value.timeWindow = commonInfo.timeWindow;
+    }
   });
 
   const handleSubmit = (param) => {
     console.log('submitParam', param);
     const cameraId = cameraDetailStore.cameraId;
+    const inferParams = {} as any;
+    inferParams.metaObjs = param.metaObjs;
+    inferParams.regionJudge = param.regionJudge;
+    inferParams.criticalCounts = param.criticalCounts;
+    inferParams.judge = param.judge;
+    const extraValue = {
+      inferCode: param.inferCode,
+      inferParams: [inferParams],
+    };
     const newParam = {
       cameraId: cameraId,
       electronicFence: param.electronicFence,
@@ -95,6 +125,7 @@
       detectionFrequency: param.detectionFrequency,
       detectionTime: param.detectionTime,
       status: param.status,
+      extra: JSON.stringify(extraValue),
     };
     if (param.id) {
       updateCameraAlgoApi({ ...newParam, id: param.id }).then(() => {

+ 25 - 0
src/views/cameras/preview/components/AlgorithmsSetting/types.ts

@@ -4,3 +4,28 @@ export interface TimeRangeItem {
   id: string;
   value: [Dayjs, Dayjs];
 }
+
+export interface TimePeriodItem {
+  id: string;
+  startDay: number | null;
+  endDay: number | null;
+  timeRangeList: { id: string; startTime: string; endTime: string }[];
+}
+
+export const labelNameMap = {
+  '': '',
+  person: '人',
+  withHelmet: '安全帽',
+  bird: '鸟',
+  cat: '猫',
+  dog: '狗',
+  snake: '蛇',
+  mouse: '鼠',
+  vest: '反光背心',
+  car: '普通车',
+  fire: '火焰',
+  sleep: '睡觉',
+  phone: '手机',
+  smoke: '香烟',
+  airplane: '飞机',
+};

+ 103 - 3
src/views/cameras/preview/components/AlgorithmsSetting/utils.ts

@@ -1,11 +1,22 @@
 import dayjs, { Dayjs } from 'dayjs';
 import { uid } from 'uid';
 import NP from 'number-precision';
+import { isEqual } from 'lodash-es';
 
-import { TimeRangeItem } from './types';
+import { TimeRangeItem, TimePeriodItem } from './types';
+import { CameraAlgoItem } from '@/api/camera/camera-preview';
 
-export const createDefaultTime = (): TimeRangeItem => {
-  return { id: uid(), value: [dayjs(), dayjs().add(1, 'hour')] as [Dayjs, Dayjs] };
+// export const createDefaultTime = (): TimeRangeItem => {
+//   return { id: uid(), value: [dayjs(), dayjs().add(1, 'hour')] as [Dayjs, Dayjs] };
+// };
+
+export const createDefaultTime = (): TimePeriodItem => {
+  return {
+    id: uid(),
+    startDay: null,
+    endDay: null,
+    timeRangeList: [{ id: uid(), startTime: '', endTime: '' }],
+  };
 };
 
 export enum FrequencyEnum {
@@ -51,3 +62,92 @@ export const getDetectionTimeJSON = (time?: string): TimeRangeItem[] | null => {
     });
   return timeStrArr;
 };
+
+export const getMetaValues = (extra: string | undefined | null) => {
+  if (!extra) return [];
+  const extraObj = JSON.parse(extra);
+  const params = extraObj?.inferParams;
+  if (!params || (params && params.length == 0)) return [];
+  const metaObjs = params[0]?.metaObjs;
+  if (!metaObjs || (metaObjs && metaObjs.length == 0)) return [];
+  const metaArr = metaObjs.map((item: any) => {
+    const val = {
+      id: uid(),
+      label: item.label,
+      confidence: item.confidence * 100,
+      'min-width': item['min-width'],
+      'min-height': item['min-height'],
+    } as any;
+    item.nextObjs.forEach((next) => {
+      val[`${next.label}.confidence`] = next.confidence * 100;
+      val[next.label + '.' + 'min-width'] = next['min-width'];
+      val[next.label + '.' + 'min-height'] = next['min-height'];
+    });
+    return val;
+  });
+
+  return metaArr;
+};
+
+export const getDetectionTime = (time: string | undefined | null) => {
+  if (!time) return [];
+  const timeList = JSON.parse(time);
+  if (!timeList || (timeList && timeList.length === 0)) {
+    return [];
+  }
+  return timeList;
+};
+
+export const getInferCode = (extra: string | undefined | null) => {
+  if (!extra) return '';
+  const extraObj = JSON.parse(extra);
+  return extraObj?.inferCode || '';
+};
+
+export const getExtraCommonInfo = (detail: CameraAlgoItem | undefined | null) => {
+  if (!detail) return {};
+  let extraValue = getCommonInfo(detail.extra);
+  if (isEqual(extraValue, {})) {
+    extraValue = getCommonInfo(detail.algoInfo?.extra);
+  }
+  return extraValue;
+};
+
+interface CommonInfo {
+  regionJudge?: number;
+  judge?: number;
+  timeWindow?: number;
+}
+
+const getCommonInfo = (extra: string | undefined | null): CommonInfo => {
+  if (!extra) return {};
+  const extraObj = JSON.parse(extra);
+  const params = extraObj?.inferParams;
+  if (!params || (params && params.length == 0)) return {};
+  const regionJudge = params[0]?.regionJudge;
+  const judge = params[0]?.judge;
+  const timeWindow = params[0]?.timeWindow;
+  const ret = {} as CommonInfo;
+  if (regionJudge || regionJudge == 0) {
+    ret.regionJudge = regionJudge;
+  }
+  if (judge || judge == 0) {
+    ret.judge = judge;
+  }
+  if (timeWindow || timeWindow == 0) {
+    ret.timeWindow = timeWindow;
+  }
+  return ret;
+};
+
+export const getTimeCompletion = (time: TimePeriodItem) => {
+  if (!time.startDay || !time.endDay) {
+    return false;
+  }
+  time.timeRangeList.forEach((item) => {
+    if (!item.startTime || !item.endTime) {
+      return false;
+    }
+  });
+  return true;
+};

+ 59 - 5
src/views/cameras/preview/store/useCameraAlgoStore.ts

@@ -1,18 +1,37 @@
 import { getAllAlgosApi, getCameraAlgoListApi, CameraAlgoItem } from '@/api/camera/camera-preview';
 import { defineStore } from 'pinia';
-import { ref } from 'vue';
+import { computed, ref } from 'vue';
 import { useRequest } from 'vue-hooks-plus';
-import { TimeRangeItem } from '../components/AlgorithmsSetting/types';
+import { TimePeriodItem } from '../components/AlgorithmsSetting/types';
 import { DetectionJSON } from '../components/AlgorithmsSetting/utils';
 
 interface CameraAlgoItemInCard extends CameraAlgoItem {
-  detectionJSON: DetectionJSON;
+  // detectionJSON: DetectionJSON;
+  inferCode: string;
   enableCardBool: boolean;
   electronicFenceBool: boolean;
-  timeRangeArr: TimeRangeItem[];
+  regionJudge: number;
+  timeRangeArr: TimePeriodItem[];
+  metaValues: AlgoParamMetaItem[];
+  judge: number; // 0-小于 1-大于 2-等于
+  detectionFrequency: number;
+  timeWindow?: number;
 }
 
-const defaultSelectedAlgoDetail = { detectionJSON: { detectionNum: 0, detectionUnit: 1 } };
+export interface AlgoParamMetaItem {
+  id: string;
+  label: string;
+  criticalCount: number;
+  confidence: number;
+  'min-width': number;
+  'max-width': number;
+  nextObjs: AlgoParamMetaItem[];
+}
+
+const defaultSelectedAlgoDetail = {
+  // detectionJSON: { detectionNum: 0, detectionUnit: 1 },
+  regionJudge: 0,
+};
 
 const useCameraAlgoStore = defineStore('cameraAlgo', () => {
   const {
@@ -34,6 +53,12 @@ const useCameraAlgoStore = defineStore('cameraAlgo', () => {
     manual: true,
   });
 
+  // 标记的paramCard集合
+  const markedParamCardIds = ref<string[]>([]);
+
+  // 标记的timeRange集合
+  const markedTimeRangeIds = ref<string[]>([]);
+
   /** 所有算法列表中选定的算法id */
   const selectedAlgoId = ref<number | null | undefined>();
 
@@ -41,6 +66,18 @@ const useCameraAlgoStore = defineStore('cameraAlgo', () => {
     ...defaultSelectedAlgoDetail,
   } as CameraAlgoItemInCard);
 
+  //计算原始模板数据
+  const metaObjList = computed(() => {
+    const extra = selectedAlgoDetail.value.algoInfo?.extra;
+    if (!extra) return [];
+    const extraObj = JSON.parse(extra);
+    const params = extraObj?.inferParams;
+    if (!params || (params && params.length == 0)) return [];
+    const metaObjs = params[0]?.metaObjs;
+
+    return metaObjs ? metaObjs : [];
+  });
+
   const getAlgoDetail = (algoId: number): null | CameraAlgoItem => {
     const detail = cameraAlgoList.value?.find((x) => x.algoId === algoId);
     if (!detail) return null;
@@ -52,6 +89,18 @@ const useCameraAlgoStore = defineStore('cameraAlgo', () => {
     return cameraAlgoList.value?.find((x) => x.algoId === algoId);
   };
 
+  const deleteParam = (id: string) => {
+    selectedAlgoDetail.value.metaValues = selectedAlgoDetail.value.metaValues.filter(
+      (x) => x.id !== id,
+    );
+  };
+
+  const deleteTimeRange = (id: string) => {
+    selectedAlgoDetail.value.timeRangeArr = selectedAlgoDetail.value.timeRangeArr.filter(
+      (item) => item.id !== id,
+    );
+  };
+
   const clear = () => {
     mutateCameraAlgoList();
     mutateAllAlgoList();
@@ -62,12 +111,17 @@ const useCameraAlgoStore = defineStore('cameraAlgo', () => {
     cameraAlgoList,
     getCameraAlgoList,
     selectedAlgoId,
+    metaObjList,
     allAlgoList,
+    markedParamCardIds,
+    markedTimeRangeIds,
     getAllAlgoList,
     getAlgoDetail,
     selectedAlgoDetail,
     isAlgoBind,
     clear,
+    deleteParam,
+    deleteTimeRange,
   };
 });