Pārlūkot izejas kodu

Merge branch 'all-v4-bxy' into all-v4-algoConfig

bxy 1 gadu atpakaļ
vecāks
revīzija
2a1c74aa19

+ 27 - 8
src/api/camera/camera-config.ts

@@ -6,13 +6,14 @@ export interface QueryCameraPageByAlgoParams {
   queryParam: {
     workspaceIdList?: string[]; // 工位id列表
     algoId?: number; // 算法id
-    isAlgoDisabled?: boolean; // 算法是否开启
-    isRenderDisabled?: boolean; // 渲染是否开启
+    isAlgoDisabled?: boolean; // 算法是否开启 false-开启, true-关闭
+    isRenderDisabled?: boolean; // 渲染是否开启 false-开启, true-关闭
     cameraName?: string; // 相机名称
     cameraCode?: string; // 相机code
   };
 }
 export interface QueryCameraPageByAlgoRes {
+  index: number; // 索引
   cameraId: number; // 相机id
   cameraName: string; // 相机名称
   cameraCode: string; // 相机code
@@ -37,30 +38,48 @@ export function getCameraListByAlgo(data: QueryCameraPageByAlgoParams) {
   });
 }
 
+export interface AlgoStruct {
+  algoId: number; // 算法id
+  algoName: string; // 算法名称
+}
+export interface QueryAlgoStatusByCameraIdRes {
+  enabledAlgoBingjiList: Array<AlgoStruct>; // 已开启算法列表(已添加)
+  notEnabledAlgoBingjiList: Array<AlgoStruct>; // 未开启算法列表(已添加)
+  savedAlgoBingjiList: Array<AlgoStruct>; // 已添加算法列表(全部)
+  savedAlgoJiaojiList: Array<AlgoStruct>; // 已添加算法列表(共同算法)
+}
+
 // 根据相机id查询算法状态
-export function getAlgoStatusByCameraIds(params: { cameraIdList: number[] }) {
+export function getAlgoStatusByCameraIds(data: { cameraIdList: number[] }) {
   return http.request({
     url: '/admin/algo/queryAlgoStatusByCameraId',
     method: 'post',
-    params,
+    data,
   });
 }
 
 // 相机算法批量添加
-export function addAlgosByBatch(params: { cameraIdList: number[]; algoIdList: number[] }) {
+export function addAlgosByBatch(data: { cameraIdList: number[]; algoIdList: number[] }) {
   return http.request({
     url: '/admin/algo/batchSaveCameraAlgoRel',
     method: 'post',
-    params,
+    data,
   });
 }
 
+export interface BatchUpdateCameraAlgoRelParams {
+  cameraIdList?: number[]; //相机id列表
+  algoId?: number; //算法id
+  extra?: string; //扩展数据
+  detectionTime?: string; //检测时间段
+  detectionFrequency?: number; //检测频率
+}
 // 相机算法参数批量设置
-export function updateAlgosByBatch(params: { cameraIdList: number[]; algoId: number; algoParam: string }) {
+export function updateAlgosByBatch(data: BatchUpdateCameraAlgoRelParams) {
   return http.request({
     url: '/admin/algo/batchUpdateCameraAlgoRelParam',
     method: 'post',
-    params,
+    data,
   });
 }
 

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

@@ -453,7 +453,7 @@ export const saveCameraParamsApi = (data: SaveCameraParams) => {
 };
 
 interface RenderPara {
-  render: number | string;
+  render: number | string | null;
   cameraId: number;
 }
 

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 21 - 0
src/assets/icons/filter.svg


BIN
src/assets/images/no-algo.png


+ 1 - 1
src/modules/algo/algo-params-edit/AlgoParamsCard.vue

@@ -166,7 +166,7 @@
 
       //先定form字段集合,如果有nextobj,添加子字段
       const meta = metaObjList.value.find((item) => item.label === val);
-      const nexts = meta.nextObjs;
+      const nexts = meta?.nextObjs;
       if (nexts) {
         for (let i = 0; i < nexts.length; i++) {
           const item = nexts[i];

+ 3 - 3
src/modules/algo/algo-params-edit/index.vue

@@ -11,7 +11,7 @@
             <div
               class="timeAdd"
               :class="{ addDisable: unEmptyLabels.length >= metaObjList.length }"
-              @click="handleAddMetaObj"
+              @click="unEmptyLabels.length >= metaObjList.length ? null : handleAddMetaObj"
             >
               <el-icon color="#d0d0d0"><Plus /></el-icon>
             </div>
@@ -182,7 +182,7 @@
   }
 
   const props = defineProps<{
-    isChanged: boolean;
+    isChanged?: boolean;
     algoDetail: CameraAlgoItemInCard;
   }>();
 
@@ -322,7 +322,7 @@
         min_height: meta['min_height'],
       } as any;
       const nextValues = [] as any[];
-      obj.nextObjs.forEach((next) => {
+      obj?.nextObjs?.forEach((next) => {
         if (meta[`${next.label}.confidence`]) {
           nextValues.push({
             label: next.label,

+ 2 - 2
src/modules/algo/algo-params-edit/utils.ts

@@ -220,11 +220,9 @@ export const createAlgoSubmitParams = (param, initialAlgoDetail) => {
   } as any;
 
   const newParam = {
-    electronicFence: initialAlgoDetail.electronicFence,
     algoId: initialAlgoDetail.algoId,
     detectionFrequency: param.detectionFrequency,
     detectionTime: param.detectionTime,
-    status: initialAlgoDetail.status,
     extra: JSON.stringify(extraValue),
   };
   return newParam;
@@ -243,6 +241,8 @@ export const algoDetailToJSON = (detail) => {
 
   return {
     ...detail,
+    algoId: detail?.id, // 兼容算法默认参数
+    algoInfo: detail, // 兼容算法默认参数
     inferCode: getInferCode(detail?.extra),
     // detectionJSON,
     enableCardBool: enableCard,

+ 1 - 1
src/views/cameras/algo-params-setting/components/RenderSwitch/RenderSwitch.vue

@@ -28,7 +28,7 @@
   const selectedIds = ref<number | string>('');
 
   const changeRender = (val) => {
-    const trueRender = val === RenderTypes.none ? '' : val;
+    const trueRender = val === RenderTypes.none ? null : val;
     const renderData = {
       render: trueRender,
       cameraId: cameraDetailStore.cameraId,

+ 258 - 0
src/views/cameras/preview/components/CameraConfigSingle/BatchOperationDialog.vue

@@ -0,0 +1,258 @@
+<template>
+  <el-dialog v-model="batchDialogVisible" width="50%" align-center :close-on-click-modal="false" @close="handleClose">
+    <template #header>
+      <div class="header-text">请选择需要{{ getOperationNameByType }}的算法:</div>
+    </template>
+    <div class="tips" v-if="type === 'set' && hasAlgosOfType">
+      <el-icon :size="15"><InfoFilled /></el-icon>
+      <div class="tip">以下为所选相机已添加的共同算法</div>
+    </div>
+    <div class="algos-list" v-if="hasAlgosOfType">
+      <div
+        v-for="(item, index) in algosList"
+        :key="index"
+        class="algo-item"
+        :class="{
+          'active-item': chooseAlgos.find((obj) => obj.algoId === item.algoId),
+          'added-item':
+            type === 'add' && algoStatusList?.savedAlgoJiaojiList?.find((obj) => obj.algoId === item.algoId),
+        }"
+        @click="
+          type === 'add' && algoStatusList?.savedAlgoJiaojiList?.find((obj) => obj.algoId === item.algoId)
+            ? null
+            : handleChangeChooseAlgos(item)
+        "
+      >
+        {{ item.algoName }}
+      </div>
+    </div>
+    <div v-else class="no-algo">
+      <img src="@/assets/images/no-algo.png" alt="" />
+      <div class="no-algo-tip">{{ getNoAlgoTip }}</div>
+    </div>
+    <template #footer v-if="hasAlgosOfType">
+      <div class="dialog-footer">
+        <el-button v-if="type === 'open' || type === 'close' || type === 'delete'" @click="handleChooseAllAlgos"
+          >全选</el-button
+        >
+        <el-button @click="resetChooseAlgos">重置</el-button>
+        <el-button type="primary" :disabled="chooseAlgos.length === 0" @click="handleConfirmOperation">
+          确认
+          <span v-if="chooseAlgos.length > 0">({{ chooseAlgos.length }}个算法)</span>
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup>
+  import { computed, onMounted, ref, watch } from 'vue';
+  import { ElDialog, ElButton, ElIcon } from 'element-plus';
+  import { InfoFilled } from '@element-plus/icons-vue';
+  import {
+    AlgoStruct,
+    QueryAlgoInfoRes,
+    QueryAlgoStatusByCameraIdRes,
+    getAlgoStatusByCameraIds,
+  } from '@/api/camera/camera-config';
+
+  const props = defineProps<{
+    type: string;
+    cameraIds: number[];
+    allAlgoList: QueryAlgoInfoRes[];
+  }>();
+  const emits = defineEmits(['onConfirm', 'onClose']);
+
+  const batchDialogVisible = ref(true);
+  const algoStatusList = ref<QueryAlgoStatusByCameraIdRes>(); // 当前选择的相机组的算法状态列表
+  const algosList = ref<AlgoStruct[] | undefined>([]); // 可选的算法列表
+  const chooseAlgos = ref<AlgoStruct[]>([]); // 选中的算法列表
+
+  const getOperationNameByType = computed(() => {
+    switch (props.type) {
+      case 'add':
+        return '添加';
+      case 'set':
+        return '设置参数';
+      case 'open':
+        return '开启';
+      case 'close':
+        return '关闭';
+      case 'delete':
+        return '删除';
+      default:
+        return '';
+    }
+  });
+
+  const hasAlgosOfType = computed(() => {
+    switch (props.type) {
+      case 'set':
+        if (algoStatusList.value) return algoStatusList.value?.savedAlgoJiaojiList?.length > 0;
+      case 'open':
+        if (algoStatusList.value) return algoStatusList.value?.notEnabledAlgoBingjiList?.length > 0;
+      case 'close':
+        if (algoStatusList.value) return algoStatusList.value?.enabledAlgoBingjiList?.length > 0;
+      case 'delete':
+        if (algoStatusList.value) return algoStatusList.value?.savedAlgoBingjiList?.length > 0;
+      default:
+        return true;
+    }
+  });
+
+  const getNoAlgoTip = computed(() => {
+    switch (props.type) {
+      case 'set':
+        return '所选中的相机不存在共同算法,无法批量设置算法参数';
+      case 'open':
+        return '所选中的相机不存在未开启的算法';
+      case 'close':
+        return '所选中的相机不存在已开启的算法';
+      case 'delete':
+        return '所选中的相机无已添加的算法';
+      default:
+        return '';
+    }
+  });
+
+  const getAlgoListByType = () => {
+    if (props.type === 'set') {
+      algosList.value = algoStatusList.value?.savedAlgoJiaojiList.map((item) => ({
+        algoName: item.algoName,
+        algoId: item.algoId,
+      }));
+    }
+    if (props.type === 'open') {
+      algosList.value = algoStatusList.value?.notEnabledAlgoBingjiList.map((item) => ({
+        algoName: item.algoName,
+        algoId: item.algoId,
+      }));
+    }
+    if (props.type === 'close') {
+      algosList.value = algoStatusList.value?.enabledAlgoBingjiList.map((item) => ({
+        algoName: item.algoName,
+        algoId: item.algoId,
+      }));
+    }
+    if (props.type === 'delete') {
+      algosList.value = algoStatusList.value?.savedAlgoBingjiList.map((item) => ({
+        algoName: item.algoName,
+        algoId: item.algoId,
+      }));
+    }
+    if (props.type === 'add') {
+      algosList.value = props.allAlgoList.map((item) => ({ algoName: item.name, algoId: item.id }));
+    }
+  };
+
+  watch(
+    [() => algoStatusList.value, () => props.allAlgoList],
+    () => {
+      getAlgoListByType();
+    },
+    { immediate: true },
+  );
+
+  // 切换选中状态
+  const handleChangeChooseAlgos = (algo: AlgoStruct) => {
+    const index = chooseAlgos.value.findIndex((item) => item.algoId === algo.algoId);
+    if (index === -1) {
+      if (props.type === 'set') {
+        chooseAlgos.value = [];
+      }
+      chooseAlgos.value.push(algo);
+    } else {
+      chooseAlgos.value.splice(index, 1);
+    }
+  };
+
+  // 全选
+  const handleChooseAllAlgos = () => {
+    chooseAlgos.value = algosList.value || [];
+  };
+  // 重置
+  const resetChooseAlgos = () => {
+    chooseAlgos.value = [];
+  };
+  // 确认
+  const handleConfirmOperation = () => {
+    const algoIdList = chooseAlgos.value.map((item) => item.algoId);
+    emits('onConfirm', algoIdList);
+  };
+
+  const handleClose = () => {
+    emits('onClose');
+  };
+
+  onMounted(() => {
+    getAlgoStatusByCameraIds({ cameraIdList: props.cameraIds }).then((res) => {
+      algoStatusList.value = res;
+    });
+  });
+</script>
+
+<style scoped lang="less">
+  .header-text {
+    font-size: 18px;
+  }
+
+  .tips {
+    height: 21px;
+    margin-bottom: 10px;
+    display: flex;
+    align-items: center;
+    color: #909399;
+
+    .tip {
+      margin-left: 4px;
+    }
+  }
+
+  .algos-list {
+    height: 280px;
+    overflow: auto;
+    display: flex;
+    flex-wrap: wrap;
+    align-content: flex-start;
+    margin-top: 10px;
+
+    .algo-item {
+      margin: 0 20px 15px 0;
+      padding: 4px 16px;
+      cursor: pointer;
+      font-size: 16px;
+    }
+
+    .algo-item:hover {
+      background-color: #f5f7fa;
+    }
+
+    .active-item {
+      color: #409eff;
+      background-color: #e6f7ff;
+    }
+
+    .added-item {
+      color: #88bcff;
+      cursor: not-allowed;
+    }
+
+    .added-item:hover {
+      background-color: transparent;
+    }
+  }
+
+  .no-algo {
+    height: 340px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding-top: 35px;
+
+    .no-algo-tip {
+      margin-top: 20px;
+      font-size: 14px;
+      color: #606266;
+    }
+  }
+</style>

+ 336 - 217
src/views/cameras/preview/components/CameraConfigSingle/CameraConfigSingle.vue

@@ -1,118 +1,117 @@
 <template>
-  <div class="query-box">
-    <div class="query-param">
-      <el-input
-        v-model="tableQueryTypeContent"
-        style="width: 300px; height: 32px; margin-right: 40px; margin-bottom: 10px"
-        :placeholder="tableQueryType ? '请输入' + tableQueryType : '请输入查找内容'"
-        clearable
-        :disabled="!tableQueryType"
-        @input="handleTableQueryTypeContentChange"
+  <div>
+    <QueryForm :algos-info="algosInfo" @on-search="handleQueryTableData" @on-reset="handleResetTableData" />
+    <div v-if="showActionBar" class="action-bar">
+      <span class="num-text">已选{{ chooseNum }}项</span>
+      <el-button :class="isAlgoAdd ? 'btn-active' : 'btn-normal'" @click="handleAlgosAdd" v-if="hasAddPermission()"
+        >算法添加</el-button
       >
-        <template #prepend>
-          <el-select
-            v-model="tableQueryType"
-            placeholder="选择类型"
-            style="width: 110px"
-            @change="handleTableQueryTypeChange"
-            clearable
-          >
-            <el-option v-for="item in tableQueryTypeOptions" :key="item.value" :label="item.name" :value="item.value" />
-          </el-select>
-        </template>
-      </el-input>
-      <div class="locations-query">
-        <div>地点:</div>
-        <el-cascader
-          v-model="workLocation"
-          :options="locationOptions"
-          :props="locationProp"
-          clearable
-          collapse-tags
-          :show-all-levels="false"
-          placeholder="请选择地点"
-          popper-class="special-cascader"
-          @change="handleCascaderChange"
-        />
-      </div>
-      <div class="algo-query">
-        <div>算法:</div>
-        <el-select v-model="tableQueryParams.queryParam.algoId" placeholder="请选择算法" clearable style="width: 200px">
-          <el-option v-for="item in algoOptions" :key="item.value" :label="item.name" :value="item.value" />
-        </el-select>
-      </div>
-    </div>
-    <div class="query-btn">
-      <el-button type="primary" @click="submitTableQuery">查询</el-button>
-      <el-button @click="resetTable">重置</el-button>
+      <el-button
+        :class="isParamsSet ? 'btn-active' : 'btn-normal'"
+        @click="handleParamsSet"
+        v-if="hasParamSetPermission()"
+        >参数设置</el-button
+      >
+      <el-button :class="isAlgoOpen ? 'btn-active' : 'btn-normal'" @click="handleAlgosOpen">算法开启</el-button>
+      <el-button :class="isAlgoClose ? 'btn-active' : 'btn-normal'" @click="handleAlgosClose">算法关闭</el-button>
+      <el-button
+        :class="isAlgoDelete ? 'btn-active' : 'btn-normal'"
+        @click="handleAlgosDel"
+        v-if="hasDeletePermission()"
+        >算法删除</el-button
+      >
+      <span class="close-btn" @click="handleSelectNone"></span>
     </div>
+    <SingleCameraTable
+      ref="singleCameraTableRef"
+      class="table-bar"
+      :table-data="tableData"
+      @on-filter="handleFilterTable"
+      @on-reset="handleResetTable"
+      @on-refresh="getTableData"
+      @update:selection="handlePop"
+    />
+    <Pagination
+      v-model:page="tableQueryParams.pageNumber"
+      v-model:size="tableQueryParams.pageSize"
+      :total="total"
+      @update:page="handlePageChange"
+      @update:size="handleSizeChange"
+      style="margin-bottom: 0"
+    />
+    <BatchOperationDialog
+      v-if="batchOperationVisible"
+      :type="batchOperationType"
+      :camera-ids="chooseId"
+      :all-algo-list="algosInfo"
+      @on-confirm="handleConfirmBatchOperation"
+      @on-close="handleCloseBatchOperation"
+    />
+    <el-dialog
+      v-model="isBatchParamSetVisible"
+      width="1022"
+      align-center
+      :close-on-click-modal="false"
+      @close="handleCancel"
+    >
+      <template #header>
+        <div class="batch-param-set-header">
+          <div class="algo-title">{{ selectedAlgoDetail?.name }}</div>
+          <div class="set-tips">
+            <el-icon :size="15"><InfoFilled /></el-icon>
+            <div class="tip"
+              >当前显示默认参数值,请选择具体参数进行修改,提交后新参数值将对所选全部相机生效,覆盖原有参数值</div
+            >
+          </div>
+        </div>
+      </template>
+      <AlgoParamsSetting
+        v-if="selectedAlgoDetail"
+        :algo-detail="selectedAlgoDetail"
+        @on-submit="handleSubmit"
+        @on-cancel="handleCancel"
+      />
+    </el-dialog>
   </div>
-  <div class="table-box">
-    <el-table ref="multipleTableRef" :data="tableData" style="width: 100%" height="100%">
-      <el-table-column type="selection" width="30" />
-      <el-table-column label="相机名称" prop="cameraName" width="180" />
-      <el-table-column label="设备ID" prop="cameraCode" width="180" />
-      <el-table-column label="地点" prop="location" width="280">
-        <template #default="{ row }">
-          {{ row.workshopName + ' - ' + row.workspaceName }}
-        </template>
-      </el-table-column>
-      <el-table-column label="算法" prop="algoStatusList" min-width="200">
-        <template #default="{ row }">
-          <div v-for="item in row.algoStatusList" :class="item.isDisabled ? 'close-algo' : ''">{{ item.algoName }}</div>
-        </template>
-      </el-table-column>
-      <el-table-column label="渲染" prop="isRenderDisabled" width="100">
-        <template #default="{ row }">
-          <el-switch :model-value="!row.isRenderDisabled" @change="handleChangeRenderStatus(row)" @click.stop />
-        </template>
-      </el-table-column>
-      <el-table-column label="算法操作" fixed="right" width="300">
-        <template #default="{ row }">
-          <el-button type="primary" text @click="handleSettingConfig(row)">配置</el-button>
-          <el-button type="primary" text @click="batchOpenAlgos(row)">一键开启</el-button>
-          <el-button type="primary" text @click="batchCloseAlgos(row)">一键关闭</el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-  </div>
-  <Pagination
-    v-model:page="tableQueryParams.pageNumber"
-    v-model:size="tableQueryParams.pageSize"
-    :total="total"
-    @update:page="handlePageChange"
-    @update:size="handleSizeChange"
-    style="margin-bottom: 0"
-  />
 </template>
 
 <script lang="ts" setup>
   import { onMounted, ref } from 'vue';
-  import {
-    ElSelect,
-    ElOption,
-    ElInput,
-    ElCascader,
-    ElButton,
-    ElTable,
-    ElTableColumn,
-    ElSwitch,
-    ElMessageBox,
-  } from 'element-plus';
-  import { useWorkLocation } from '@/views/datamanager/alertformdata/hooks/useWorkLocation';
+  import { ElButton, ElIcon, ElMessage, ElDialog } from 'element-plus';
+  import { InfoFilled } from '@element-plus/icons-vue';
   import {
     QueryCameraPageByAlgoParams,
     QueryCameraPageByAlgoRes,
     getCameraListByAlgo,
-    updateAlgosStatusByCameraId,
-    updateAlgosStatusByBatch,
+    QueryAlgoInfoRes,
     getAlgosInfo,
+    addAlgosByBatch,
+    deleteAlgosByBatch,
+    updateAlgosStatusByBatch,
+    updateAlgosByBatch,
   } from '@/api/camera/camera-config';
-  import { renderCamera } from '@/api/camera/camera-preview';
+  import QueryForm from './QueryForm.vue';
+  import SingleCameraTable from './SingleCameraTable.vue';
   import Pagination from '@/components/Pagination/Pagination.vue';
-  import { mockQueryCameraPageByAlgoRes } from './mockData';
+  import BatchOperationDialog from './BatchOperationDialog.vue';
+  import AlgoParamsSetting from '@/modules/algo/algo-params-edit/index.vue';
+  import { CameraAlgoItemInCard } from '@/modules/algo/algo-params-edit/types';
+  import { createAlgoSubmitParams, algoDetailToJSON } from '@/modules/algo/algo-params-edit/utils';
+  import { useUserStore } from '@/store/modules/user';
+  import { PERM_ALGO } from '@/types/permission/constants';
+
+  const userStore = useUserStore();
 
-  const { locationOptions, getLocationOptions } = useWorkLocation();
+  // 权限设置
+  const hasAddPermission = () => {
+    return userStore.checkPermission(PERM_ALGO.CONFIG_ADD);
+  };
+  const hasDeletePermission = () => {
+    return userStore.checkPermission(PERM_ALGO.CONFIG_DELETE);
+  };
+  const hasParamSetPermission = () => {
+    return userStore.checkPermission(PERM_ALGO.CONFIG_PARAM);
+  };
 
   const tableQueryParams = ref<QueryCameraPageByAlgoParams>({
     pageNumber: 1,
@@ -120,63 +119,194 @@
     queryParam: {},
   });
 
+  const algosInfo = ref<QueryAlgoInfoRes[]>([]);
+  const singleCameraTableRef = ref<typeof SingleCameraTable>();
   const tableData = ref<QueryCameraPageByAlgoRes[]>([]);
-  // const tableData = ref(mockQueryCameraPageByAlgoRes); // Mock data for testing
+  const total = ref(0);
+  // 多选相机 批量操作
+  const showActionBar = ref(false);
+  const chooseNum = ref(0);
+  const chooseRow = ref<QueryCameraPageByAlgoRes[]>([]); // 被选中的数据行
+  const chooseId = ref<number[]>([]);
+  // 按钮点击激活状态
+  const isAlgoAdd = ref(false);
+  const isParamsSet = ref(false);
+  const isAlgoClose = ref(false);
+  const isAlgoOpen = ref(false);
+  const isAlgoDelete = ref(false);
+  const batchOperationType = ref('');
+  const batchOperationVisible = ref(false);
+  // 批量参数设置
+  const isBatchParamSetVisible = ref(false);
+  const selectedAlgoDetail = ref<CameraAlgoItemInCard>(); // 批量参数设置组件的参数
 
-  const tableQueryTypeOptions = [
-    { name: '相机名称', value: '相机名称' },
-    { name: '相机ID', value: '相机ID' },
-  ];
-  const algoOptions = ref<{ name: string; value: number }[]>([]); // 算法选择器选项
-  const tableQueryType = ref<string>('');
-  const tableQueryTypeContent = ref<string>('');
-  const workLocation = ref([]); // 级联选择器,为二维数组(提取workspaceId)
-  const locationProp = { multiple: true, expandTrigger: 'hover' as const }; // 级联选择器(打开多选)
-  // 分页
-  const total = ref(0); // 总条数
+  // 查询
+  const handleQueryTableData = (queryParams) => {
+    tableQueryParams.value.queryParam.cameraName = queryParams.queryParam.cameraName;
+    tableQueryParams.value.queryParam.cameraCode = queryParams.queryParam.cameraCode;
+    tableQueryParams.value.queryParam.workspaceIdList = queryParams.queryParam.workspaceIdList;
+    tableQueryParams.value.queryParam.algoId = queryParams.queryParam.algoId;
+    tableQueryParams.value.pageNumber = 1;
+    tableQueryParams.value.pageSize = 10;
+    getTableData();
+  };
+  // 重置 (清空查询条件, 不清空筛选条件)
+  const handleResetTableData = () => {
+    Reflect.deleteProperty(tableQueryParams.value.queryParam, 'cameraName');
+    Reflect.deleteProperty(tableQueryParams.value.queryParam, 'cameraCode');
+    Reflect.deleteProperty(tableQueryParams.value.queryParam, 'workspaceIdList');
+    Reflect.deleteProperty(tableQueryParams.value.queryParam, 'algoId');
+    tableQueryParams.value.pageNumber = 1;
+    tableQueryParams.value.pageSize = 10;
+    getTableData();
+  };
 
-  const handleTableQueryTypeChange = () => {
-    if (tableQueryType.value === '相机名称') {
-      delete tableQueryParams.value.queryParam.cameraCode;
-    } else if (tableQueryType.value === '相机ID') {
-      delete tableQueryParams.value.queryParam.cameraName;
-    } else {
-      delete tableQueryParams.value.queryParam.cameraCode;
-      delete tableQueryParams.value.queryParam.cameraName;
+  // 筛选
+  const handleFilterTable = (type: string, opened: boolean, closed: boolean) => {
+    // 为了在筛选时 保留 查询条件,将筛选处理放在此处
+    if (type === 'algo') {
+      if (opened && !closed) {
+        tableQueryParams.value.queryParam.isAlgoDisabled = false;
+      } else if (!opened && closed) {
+        tableQueryParams.value.queryParam.isAlgoDisabled = true;
+      } else {
+        Reflect.deleteProperty(tableQueryParams.value.queryParam, 'isAlgoDisabled');
+      }
+    }
+    if (type === 'render') {
+      if (opened && !closed) {
+        tableQueryParams.value.queryParam.isRenderDisabled = false;
+      } else if (!opened && closed) {
+        tableQueryParams.value.queryParam.isRenderDisabled = true;
+      } else {
+        Reflect.deleteProperty(tableQueryParams.value.queryParam, 'isRenderDisabled');
+      }
     }
-    tableQueryTypeContent.value = '';
+    tableQueryParams.value.pageNumber = 1;
+    tableQueryParams.value.pageSize = 10;
+    getTableData();
   };
-  const handleTableQueryTypeContentChange = () => {
-    if (tableQueryType.value === '相机名称') {
-      tableQueryParams.value.queryParam.cameraName = tableQueryTypeContent.value;
-    } else if (tableQueryType.value === '相机ID') {
-      tableQueryParams.value.queryParam.cameraCode = tableQueryTypeContent.value;
+  // 清空筛选 (不清空查询条件)
+  const handleResetTable = (type: string) => {
+    if (type === 'algo') {
+      Reflect.deleteProperty(tableQueryParams.value.queryParam, 'isAlgoDisabled');
+    }
+    if (type === 'render') {
+      Reflect.deleteProperty(tableQueryParams.value.queryParam, 'isRenderDisabled');
     }
+    tableQueryParams.value.pageNumber = 1;
+    tableQueryParams.value.pageSize = 10;
+    getTableData();
+  };
+
+  // 多选
+  const handlePop = (selection) => {
+    chooseRow.value = selection;
+    chooseId.value = [];
+    selection.forEach((item) => {
+      if (chooseId.value.indexOf(item.cameraId) === -1) chooseId.value.push(item.cameraId);
+    });
+    chooseNum.value = selection.length;
+    showActionBar.value = chooseNum.value > 0 ? true : false;
+  };
+  // 取消多选
+  const handleSelectNone = () => {
+    chooseId.value = [];
+    chooseNum.value = 0;
+    singleCameraTableRef.value?.clearAll();
+    showActionBar.value = false;
+    handleResetBtnStatus();
+  };
+  // 重置按钮点击状态
+  function handleResetBtnStatus() {
+    isAlgoAdd.value = false;
+    isParamsSet.value = false;
+    isAlgoClose.value = false;
+    isAlgoOpen.value = false;
+    isAlgoDelete.value = false;
+  }
+
+  // 批量操作
+  const handleAlgosAdd = () => {
+    isAlgoAdd.value = true;
+    batchOperationType.value = 'add';
+    batchOperationVisible.value = true;
+  };
+  const handleParamsSet = () => {
+    isParamsSet.value = true;
+    batchOperationType.value = 'set';
+    batchOperationVisible.value = true;
+  };
+  const handleAlgosOpen = () => {
+    isAlgoOpen.value = true;
+    batchOperationType.value = 'open';
+    batchOperationVisible.value = true;
   };
-  const handleCascaderChange = () => {
-    if (workLocation.value.length !== 0) {
-      const arr = [];
-      workLocation.value.forEach((item) => {
-        arr.push(item[1]);
+  const handleAlgosClose = () => {
+    isAlgoClose.value = true;
+    batchOperationType.value = 'close';
+    batchOperationVisible.value = true;
+  };
+  const handleAlgosDel = () => {
+    isAlgoDelete.value = true;
+    batchOperationType.value = 'delete';
+    batchOperationVisible.value = true;
+  };
+  // 批量操作弹窗
+  const handleConfirmBatchOperation = (algoIds) => {
+    if (batchOperationType.value === 'add') {
+      addAlgosByBatch({ cameraIdList: chooseId.value, algoIdList: algoIds }).then(() => {
+        ElMessage({ type: 'success', message: '算法添加成功' });
+        handleCloseBatchOperation();
+        getTableData();
+      });
+    }
+    if (batchOperationType.value === 'set') {
+      isBatchParamSetVisible.value = true;
+      const algoDetail = algosInfo.value.find((item) => item.id === algoIds[0]);
+      selectedAlgoDetail.value = algoDetailToJSON(algoDetail);
+    }
+    if (batchOperationType.value === 'open') {
+      updateAlgosStatusByBatch({ cameraIdList: chooseId.value, algoIdList: algoIds, isEnabled: true }).then(() => {
+        ElMessage({ type: 'success', message: '算法开启成功' });
+        handleCloseBatchOperation();
+        getTableData();
+      });
+    }
+    if (batchOperationType.value === 'close') {
+      updateAlgosStatusByBatch({ cameraIdList: chooseId.value, algoIdList: algoIds, isEnabled: false }).then(() => {
+        ElMessage({ type: 'success', message: '算法关闭成功' });
+        handleCloseBatchOperation();
+        getTableData();
+      });
+    }
+    if (batchOperationType.value === 'delete') {
+      deleteAlgosByBatch({ cameraIdList: chooseId.value, algoIdList: algoIds }).then(() => {
+        ElMessage({ type: 'success', message: '算法删除成功' });
+        handleCloseBatchOperation();
+        getTableData();
       });
-      tableQueryParams.value.queryParam.workspaceIdList = arr;
-    } else {
-      Reflect.deleteProperty(tableQueryParams.value.queryParam, 'workspaceIdList');
     }
   };
-
-  const submitTableQuery = () => {
-    getTableData();
+  const handleCloseBatchOperation = () => {
+    batchOperationType.value = '';
+    batchOperationVisible.value = false;
+    handleResetBtnStatus();
   };
 
-  const resetTable = () => {
-    tableQueryType.value = '';
-    tableQueryTypeContent.value = '';
-    workLocation.value = [];
-    tableQueryParams.value.pageNumber = 1;
-    tableQueryParams.value.pageSize = 10;
-    tableQueryParams.value.queryParam = {};
-    getTableData();
+  // 批量参数设置提交
+  const handleSubmit = (param) => {
+    const newParam = createAlgoSubmitParams(param, selectedAlgoDetail.value);
+    updateAlgosByBatch({
+      cameraIdList: chooseId.value,
+      ...newParam,
+    }).then(() => {
+      ElMessage({ type: 'success', message: '参数设置成功' });
+      isBatchParamSetVisible.value = false;
+    });
+  };
+  const handleCancel = () => {
+    isBatchParamSetVisible.value = false;
   };
 
   // 换页,重新获取表格
@@ -189,103 +319,92 @@
     getTableData();
   };
 
-  // 切换渲染状态
-  const handleChangeRenderStatus = (row) => {
-    const tempRenderStatus = row.isRenderDisabled ? 'demo' : 'null';
-    console.log('切换渲染状态', tempRenderStatus);
-    renderCamera({
-      render: tempRenderStatus,
-      cameraId: row.cameraId,
-    })
-      .then(() => {
-        console.log('渲染开启成功');
-        getTableData();
-      })
-      .catch(() => {
-        console.log('渲染开启失败');
-        ElMessageBox.alert('开启数量达到上限,请关闭其他相机渲染后再开启。', '渲染开启失败', {
-          type: 'warning',
-        });
-      });
-  };
-
-  // 配置算法(单相机配置)
-  const handleSettingConfig = (row) => {
-    console.log('配置算法,进入算法设置详情页', row);
-    // TODO: 跳转到算法设置详情页
-  };
-  // 一键开启算法(单相机一键开启)
-  const batchOpenAlgos = (row) => {
-    console.log('开启算法', row);
-    updateAlgosStatusByCameraId({ cameraId: row.cameraId, isDisabled: false }).then(() => {
-      console.log('开启算法成功');
-      getTableData();
-    });
-  };
-  // 一键关闭算法(单相机一键关闭)
-  const batchCloseAlgos = (row) => {
-    console.log('关闭算法', row);
-    updateAlgosStatusByCameraId({ cameraId: row.cameraId, isDisabled: true }).then(() => {
-      console.log('关闭算法成功');
-      getTableData();
-    });
-  };
-
   const getTableData = async () => {
-    console.log('Query params:', tableQueryParams.value);
     await getCameraListByAlgo(tableQueryParams.value).then((res) => {
-      console.log('Camera data:', res);
       tableData.value = res.records;
       total.value = res.totalRow;
     });
   };
 
-  const getAllAlgosInfo = async () => {
-    await getAlgosInfo().then((res) => {
-      algoOptions.value = res.map((item) => ({ name: item.name, value: item.id }));
-    });
-  };
-
   onMounted(() => {
     getTableData();
-    getLocationOptions();
-    getAllAlgosInfo();
+    getAlgosInfo().then((res) => {
+      algosInfo.value = res;
+    });
   });
 </script>
 
 <style lang="scss" scoped>
-  .query-box {
-    margin-bottom: 10px;
+  .action-bar {
     display: flex;
+    align-items: center;
+    position: absolute;
+    min-width: calc(100vw - 266px);
+    height: 50px;
+    border-radius: 4px 4px 0px 0px;
+    background-color: #ddefff;
+    z-index: 10;
 
-    .query-param {
-      display: flex;
-      flex-wrap: wrap;
-      flex-direction: row;
-      align-items: center;
-      flex: 1;
+    .num-text {
+      margin: 0 34px 0 25px;
+      color: rgba(0, 0, 0, 0.85);
+      font-weight: 500;
     }
 
-    .locations-query,
-    .algo-query {
-      display: flex;
-      align-items: center;
-      margin-right: 40px;
-      margin-bottom: 10px;
+    .btn-normal {
+      color: #1890ff;
+      background: transparent;
+      border: 1px solid #1890ff;
+      border-radius: 2px;
     }
 
-    .query-btn {
+    .btn-active,
+    .btn-normal:hover {
+      color: #ffffff;
+      background-color: #1890ff;
+    }
+
+    .close-btn {
       margin-left: auto;
-      margin-bottom: 10px;
+      margin-right: 20px;
     }
+
+    .close-btn:before {
+      content: '\2716';
+      color: #000;
+      cursor: pointer;
+    }
+  }
+
+  .table-bar {
+    position: relative;
   }
 
-  .table-box {
-    height: calc(100vh - 280px);
+  .batch-param-set-header {
+    width: 100%;
+    height: 80px;
+    border-bottom: 1px solid #e4e7ec;
+    position: relative;
+
+    .algo-title {
+      font-size: 20px;
+      font-weight: 600;
+      color: #000000;
+    }
+
+    .set-tips {
+      display: flex;
+      align-items: center;
+      margin-top: 10px;
+      color: #66abeb;
 
-    .close-algo {
-      color: #cccccc;
-      text-decoration: line-through;
+      .tip {
+        margin-left: 2px;
+      }
     }
   }
+
+  :deep(.el-dialog__header) {
+    padding-right: 0px;
+  }
 </style>

+ 176 - 0
src/views/cameras/preview/components/CameraConfigSingle/QueryForm.vue

@@ -0,0 +1,176 @@
+<template>
+  <div class="query-box">
+    <div class="query-param">
+      <el-input
+        v-model="tableQueryTypeContent"
+        style="width: 300px; height: 32px; margin-right: 40px; margin-bottom: 10px"
+        :placeholder="tableQueryType ? '请输入' + tableQueryType : '请输入查找内容'"
+        clearable
+        :disabled="!tableQueryType"
+        @input="handleTableQueryTypeContentChange"
+      >
+        <template #prepend>
+          <el-select
+            v-model="tableQueryType"
+            placeholder="选择类型"
+            style="width: 110px"
+            @change="handleTableQueryTypeChange"
+            clearable
+          >
+            <el-option v-for="item in tableQueryTypeOptions" :key="item.value" :label="item.name" :value="item.value" />
+          </el-select>
+        </template>
+      </el-input>
+      <div class="locations-query">
+        <div>地点:</div>
+        <el-cascader
+          v-model="workLocation"
+          :options="locationOptions"
+          :props="locationProp"
+          clearable
+          collapse-tags
+          :show-all-levels="false"
+          placeholder="请选择地点"
+          popper-class="special-cascader"
+          @change="handleCascaderChange"
+        />
+      </div>
+      <div class="algo-query">
+        <div>算法:</div>
+        <el-select v-model="queryParams.queryParam.algoId" placeholder="请选择算法" clearable style="width: 200px">
+          <el-option v-for="item in algoOptions" :key="item.value" :label="item.name" :value="item.value" />
+        </el-select>
+      </div>
+    </div>
+    <div class="query-btn">
+      <el-button type="primary" @click="submitTableQuery">查询</el-button>
+      <el-button @click="resetTable">重置</el-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import { onMounted, reactive, ref, watch } from 'vue';
+  import { ElSelect, ElOption, ElInput, ElCascader, ElButton } from 'element-plus';
+  import { useWorkLocation } from '@/views/datamanager/alertformdata/hooks/useWorkLocation';
+  import { QueryCameraPageByAlgoParams, QueryAlgoInfoRes } from '@/api/camera/camera-config';
+
+  const { locationOptions, getLocationOptions } = useWorkLocation();
+
+  const props = defineProps<{
+    algosInfo: QueryAlgoInfoRes[];
+  }>();
+  const emits = defineEmits(['onSearch', 'onReset']);
+
+  const queryParams = reactive<QueryCameraPageByAlgoParams>({
+    pageNumber: 1,
+    pageSize: 10,
+    queryParam: {},
+  });
+
+  // 查询参数选项
+  const tableQueryTypeOptions = [
+    { name: '相机名称', value: '相机名称' },
+    { name: '相机ID', value: '相机ID' },
+  ];
+  // 算法选择器选项
+  const algoOptions = ref<{ name: string; value: number }[]>([]);
+
+  const tableQueryType = ref<string>('');
+  const tableQueryTypeContent = ref<string>('');
+  const workLocation = ref([]);
+  const locationProp = { multiple: true, expandTrigger: 'hover' as const };
+
+  const handleTableQueryTypeChange = () => {
+    if (tableQueryType.value === '相机名称') {
+      delete queryParams.queryParam.cameraCode;
+    } else if (tableQueryType.value === '相机ID') {
+      delete queryParams.queryParam.cameraName;
+    } else {
+      delete queryParams.queryParam.cameraCode;
+      delete queryParams.queryParam.cameraName;
+    }
+    tableQueryTypeContent.value = '';
+  };
+
+  const handleTableQueryTypeContentChange = () => {
+    if (tableQueryType.value === '相机名称') {
+      queryParams.queryParam.cameraName = tableQueryTypeContent.value;
+    } else if (tableQueryType.value === '相机ID') {
+      queryParams.queryParam.cameraCode = tableQueryTypeContent.value;
+    }
+  };
+
+  const handleCascaderChange = () => {
+    if (workLocation.value.length !== 0) {
+      const arr = [];
+      workLocation.value.forEach((item) => {
+        arr.push(item[1]);
+      });
+      queryParams.queryParam.workspaceIdList = arr;
+    } else {
+      Reflect.deleteProperty(queryParams.queryParam, 'workspaceIdList');
+    }
+  };
+
+  const submitTableQuery = () => {
+    emits('onSearch', queryParams);
+  };
+
+  const resetTable = () => {
+    tableQueryType.value = '';
+    tableQueryTypeContent.value = '';
+    workLocation.value = [];
+    queryParams.pageNumber = 1;
+    queryParams.pageSize = 10;
+    queryParams.queryParam = {};
+    emits('onReset');
+  };
+
+  // 获取所有算法信息
+  const getAllAlgosInfo = () => {
+    algoOptions.value = props.algosInfo.map((item) => ({ name: item.name, value: item.id }));
+  };
+
+  watch(
+    () => props.algosInfo,
+    (newVal) => {
+      if (newVal.length > 0) {
+        getAllAlgosInfo();
+      }
+    },
+    { immediate: true },
+  );
+
+  onMounted(() => {
+    getLocationOptions();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .query-box {
+    margin-bottom: 10px;
+    display: flex;
+
+    .query-param {
+      display: flex;
+      flex-wrap: wrap;
+      flex-direction: row;
+      align-items: center;
+      flex: 1;
+    }
+
+    .locations-query,
+    .algo-query {
+      display: flex;
+      align-items: center;
+      margin-right: 40px;
+      margin-bottom: 10px;
+    }
+
+    .query-btn {
+      margin-left: auto;
+      margin-bottom: 10px;
+    }
+  }
+</style>

+ 389 - 0
src/views/cameras/preview/components/CameraConfigSingle/SingleCameraTable.vue

@@ -0,0 +1,389 @@
+<template>
+  <div class="table-box">
+    <el-table
+      ref="multipleTableRef"
+      :data="tableData"
+      style="width: 100%"
+      height="100%"
+      :row-class-name="colorOfRow"
+      @select="handleShiftSelect"
+      @selection-change="handleSelectionChange"
+      @row-click="handleRowClick"
+    >
+      <el-table-column type="selection" width="30" />
+      <el-table-column label="相机名称" prop="cameraName" width="150">
+        <template #default="{ row }">
+          <el-popover placement="right" :width="300" trigger="hover" effect="dark" popper-style="padding: 0;">
+            <template #reference>
+              <span
+                :class="[row.integrationState ? 'notconnect-camera' : '', row.networkingState ? 'offline-camera' : '']"
+                >{{ row.cameraName }}</span
+              >
+            </template>
+            <img v-if="row.cameraImgUrl" :src="row.cameraImgUrl" alt="" />
+            <div v-else style="display: flex; flex-direction: column; align-items: center">
+              <img src="@/assets/icons/no-content.png" alt="" width="200" />
+              <div>暂无相机预览图</div>
+            </div>
+          </el-popover>
+        </template>
+      </el-table-column>
+      <el-table-column label="设备ID" prop="cameraCode" width="150" />
+      <el-table-column label="地点" prop="location" width="300">
+        <template #default="{ row }">
+          {{ row.workshopName + ' - ' + row.workspaceName }}
+        </template>
+      </el-table-column>
+      <el-table-column label="算法" prop="algoStatusList" min-width="200">
+        <template #header>
+          <span>算法</span>
+          <el-popover
+            placement="bottom-start"
+            :width="100"
+            trigger="click"
+            popper-class="filter-popper"
+            :popper-style="{ minWidth: '100px' }"
+          >
+            <template #reference>
+              <SvgIcon class="filter-btn" icon-name="filter" :color="algoOpened || algoClosed ? '#409EFF' : ''" />
+            </template>
+            <el-checkbox v-model="algoOpened" label="开启" />
+            <el-checkbox v-model="algoClosed" label="未开启" />
+            <div class="filter-btns">
+              <span class="filter-apply" @click="applyFilter('algo')">应用</span>
+              <span class="filter-clear" @click="clearFilter('algo')">清空</span>
+            </div>
+          </el-popover>
+        </template>
+        <template #default="{ row }">
+          <el-tag v-if="row.algoStatusList.length === 0" type="info" effect="dark">未添加</el-tag>
+          <div v-for="item in row.algoStatusList" :class="item.isDisabled ? 'close-algo' : ''">{{ item.algoName }}</div>
+        </template>
+      </el-table-column>
+      <el-table-column label="渲染" prop="isRenderDisabled" width="100">
+        <template #header>
+          <span>渲染</span>
+          <el-popover
+            placement="bottom-start"
+            :width="100"
+            trigger="click"
+            popper-class="filter-popper"
+            :popper-style="{ minWidth: '100px' }"
+          >
+            <template #reference>
+              <SvgIcon class="filter-btn" icon-name="filter" :color="renderOpened || renderClosed ? '#409EFF' : ''" />
+            </template>
+            <el-checkbox v-model="renderOpened" label="开启" />
+            <el-checkbox v-model="renderClosed" label="未开启" />
+            <div class="filter-btns">
+              <span class="filter-apply" @click="applyFilter('render')">应用</span>
+              <span class="filter-clear" @click="clearFilter('render')">清空</span>
+            </div>
+          </el-popover>
+        </template>
+        <template #default="{ row }">
+          <el-switch :model-value="!row.isRenderDisabled" @change="handleChangeRenderStatus(row)" @click.stop />
+        </template>
+      </el-table-column>
+      <el-table-column label="算法操作" fixed="right" width="280">
+        <template #default="{ row }">
+          <el-button
+            type="primary"
+            text
+            @click="
+              handleSettingConfig(row);
+              $event.stopPropagation();
+            "
+            >配置</el-button
+          >
+          <el-button
+            v-if="row.algoStatusList.find((item) => item.isDisabled === true)"
+            type="primary"
+            text
+            @click="
+              batchOpenAlgos(row);
+              $event.stopPropagation();
+            "
+            >一键开启</el-button
+          >
+          <el-button
+            v-if="row.algoStatusList.find((item) => item.isDisabled === false)"
+            type="primary"
+            text
+            @click="
+              batchCloseAlgos(row);
+              $event.stopPropagation();
+            "
+            >一键关闭</el-button
+          >
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import { onMounted, ref } from 'vue';
+  import router from '@/router';
+  import {
+    ElButton,
+    ElTable,
+    ElTableColumn,
+    ElPopover,
+    ElTag,
+    ElSwitch,
+    ElMessage,
+    ElMessageBox,
+    ElCheckbox,
+  } from 'element-plus';
+  import { QueryCameraPageByAlgoRes, updateAlgosStatusByCameraId } from '@/api/camera/camera-config';
+  import { renderCamera } from '@/api/camera/camera-preview';
+  import { SvgIcon } from '@/components/SvgIcon';
+
+  const props = defineProps<{ tableData: QueryCameraPageByAlgoRes[] }>();
+  const emits = defineEmits(['onFilter', 'onReset', 'onRefresh', 'update:selection']);
+
+  const multipleTableRef = ref<InstanceType<typeof ElTable>>();
+  // shift多选操作
+  const startPoint = ref<number | undefined>();
+  const endPoint = ref<number | undefined>();
+  const shiftKeyBoard = ref(false); // shift按钮按住/松开
+  const selections = ref<QueryCameraPageByAlgoRes[]>([]);
+  const rowClickSelections = ref<QueryCameraPageByAlgoRes[]>([]);
+
+  const algoOpened = ref(false); // 算法开启 勾选状态
+  const algoClosed = ref(false); // 算法未开启 勾选状态
+  const renderOpened = ref(false); // 渲染开启 勾选状态
+  const renderClosed = ref(false); // 渲染未开启 勾选状态
+
+  // 渲染/算法 开启/未开启状态筛选
+  const applyFilter = (type) => {
+    if (type === 'algo') {
+      emits('onFilter', type, algoOpened.value, algoClosed.value);
+    } else if (type === 'render') {
+      emits('onFilter', type, renderOpened.value, renderClosed.value);
+    }
+  };
+  const clearFilter = (type) => {
+    if (type === 'algo') {
+      algoOpened.value = false;
+      algoClosed.value = false;
+    }
+    if (type === 'render') {
+      renderOpened.value = false;
+      renderClosed.value = false;
+    }
+    emits('onReset', type);
+  };
+
+  // 切换渲染状态
+  const handleChangeRenderStatus = (row) => {
+    const tempRenderStatus = row.isRenderDisabled ? 'demo' : null;
+    renderCamera({
+      render: tempRenderStatus,
+      cameraId: row.cameraId,
+    })
+      .then(() => {
+        ElMessage({
+          message: '渲染状态切换成功',
+          type: 'success',
+        });
+        emits('onRefresh');
+      })
+      .catch(() => {
+        ElMessage({
+          message: '开启数量已达上限,可关闭其他相机渲染后再开启',
+          type: 'error',
+        });
+      });
+  };
+
+  // 配置算法(单相机配置)
+  const handleSettingConfig = (row) => {
+    router.push({ path: '/algorithm/params', query: { cameraId: row.cameraId } });
+  };
+  // 一键开启算法(单相机一键开启)
+  const batchOpenAlgos = (row) => {
+    ElMessageBox.confirm('开启后,所选相机已配置的算法将全部生效。', '确认开启算法吗?', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+    })
+      .then(() => {
+        updateAlgosStatusByCameraId({ cameraId: row.cameraId, isDisabled: false }).then(() => {
+          ElMessage({
+            type: 'success',
+            message: '算法开启成功',
+          });
+          emits('onRefresh');
+        });
+      })
+      .catch(() => {
+        ElMessage({
+          type: 'info',
+          message: '取消开启',
+        });
+      });
+  };
+  // 一键关闭算法(单相机一键关闭)
+  const batchCloseAlgos = (row) => {
+    ElMessageBox.confirm('关闭后,所选相机已配置的算法将不再生效。', '确认关闭算法吗?', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+    })
+      .then(() => {
+        updateAlgosStatusByCameraId({ cameraId: row.cameraId, isDisabled: true }).then(() => {
+          ElMessage({
+            type: 'success',
+            message: '算法关闭成功',
+          });
+          emits('onRefresh');
+        });
+      })
+      .catch(() => {
+        ElMessage({
+          type: 'info',
+          message: '取消关闭',
+        });
+      });
+  };
+
+  // 选中行
+  const colorOfRow = ({ row }) => {
+    if (selections.value.includes(row)) {
+      return 'selected-row';
+    }
+    return '';
+  };
+
+  const handleSelectionChange = (selection: any[]) => {
+    selections.value = selection;
+    emits('update:selection', selections.value);
+  };
+
+  const handleShiftSelect = (_, row) => {
+    props.tableData.forEach((item, index) => {
+      item.index = index;
+    });
+    // 按住shift
+    if (shiftKeyBoard.value) {
+      // 有起点startPoint
+      if (startPoint.value !== undefined) {
+        endPoint.value = row.index as number;
+        if (startPoint.value > endPoint.value) {
+          let temp = startPoint.value;
+          startPoint.value = endPoint.value;
+          endPoint.value = temp;
+        }
+        setTimeout(() => {
+          for (let i = startPoint.value as number; i < (endPoint.value as number); i++) {
+            multipleTableRef.value!.toggleRowSelection(props.tableData[i], true);
+          }
+        }, 100);
+      }
+      // 没有起点startPoint
+      else {
+        startPoint.value = row.index;
+        endPoint.value = undefined;
+      }
+    }
+    // 没按住shift
+    else {
+      startPoint.value = row.index;
+      endPoint.value = undefined;
+    }
+  };
+
+  const handleRowClick = (row) => {
+    if (rowClickSelections.value.find((item) => item.cameraId === row.cameraId)) {
+      let index = rowClickSelections.value.findIndex((item) => item.cameraId === row.cameraId);
+      rowClickSelections.value.splice(index, 1);
+      multipleTableRef.value!.toggleRowSelection(row, false);
+    } else {
+      rowClickSelections.value.push(row);
+      multipleTableRef.value!.toggleRowSelection(row, true);
+    }
+  };
+
+  const clearAll = () => {
+    multipleTableRef.value!.clearSelection();
+  };
+
+  defineExpose({ clearAll });
+
+  onMounted(() => {
+    window.addEventListener('keydown', (code) => {
+      if (code.shiftKey) {
+        shiftKeyBoard.value = true;
+      }
+    });
+    window.addEventListener('keyup', (code) => {
+      if (!code.shiftKey) {
+        shiftKeyBoard.value = false;
+        startPoint.value = -1;
+        endPoint.value = -1;
+      }
+    });
+  });
+</script>
+
+<style lang="scss" scoped>
+  .table-box {
+    height: calc(100vh - 280px);
+
+    .filter-btn {
+      display: inline-block;
+      margin-left: 3px;
+      cursor: pointer;
+    }
+
+    .notconnect-camera {
+      color: #cccccc;
+    }
+    .offline-camera::before {
+      content: '!';
+      color: red;
+      font-weight: bold;
+      margin-right: 4px;
+    }
+
+    .close-algo {
+      color: #cccccc;
+    }
+  }
+
+  :deep(.el-table) {
+    ::before {
+      height: 0px;
+    }
+
+    .selected-row {
+      background: #f3f8ff;
+    }
+  }
+</style>
+<style lang="scss">
+  .el-checkbox {
+    margin-right: 0;
+  }
+
+  .el-popover.filter-popper {
+    display: flex;
+    flex-direction: column;
+
+    .filter-btns {
+      margin-top: 20px;
+
+      .filter-apply {
+        color: #409eff;
+        margin-right: 16px;
+        cursor: pointer;
+      }
+
+      .filter-clear {
+        cursor: pointer;
+      }
+    }
+  }
+</style>

+ 0 - 410
src/views/cameras/preview/components/CameraConfigSingle/mockData.ts

@@ -1,410 +0,0 @@
-export const mockQueryCameraPageByAlgoRes = [
-  {
-    cameraName: 'Camera-1',
-    cameraCode: 'CAM-1',
-    networkingState: 0,
-    integrationState: 1,
-    cameraImgUrl: 'https://example.com/camera1.jpg',
-    workshopName: 'Workshop A',
-    workspaceName: 'Workspace 1',
-    isRenderDisabled: false,
-    algoStatusList: [
-      {
-        algoName: 'Algo Alpha',
-        isDisabled: false,
-      },
-      {
-        algoName: 'Algo Beta',
-        isDisabled: true,
-      },
-      {
-        algoName: 'Algo Alpha2',
-        isDisabled: false,
-      },
-      {
-        algoName: 'Algo Beta2',
-        isDisabled: false,
-      },
-    ],
-  },
-  {
-    cameraName: 'Camera-2',
-    cameraCode: 'CAM-2',
-    networkingState: 1,
-    integrationState: 0,
-    cameraImgUrl: 'https://example.com/camera2.jpg',
-    workshopName: 'Workshop B',
-    workspaceName: 'Workspace 2',
-    isRenderDisabled: true,
-    algoStatusList: [
-      {
-        algoName: 'Algo Gamma',
-        isDisabled: true,
-      },
-      {
-        algoName: 'Algo Delta',
-        isDisabled: false,
-      },
-    ],
-  },
-  {
-    cameraName: 'Camera-3',
-    cameraCode: 'CAM-3',
-    networkingState: 0,
-    integrationState: 1,
-    cameraImgUrl: 'https://example.com/camera3.jpg',
-    workshopName: 'Workshop A',
-    workspaceName: 'Workspace 3',
-    isRenderDisabled: false,
-    algoStatusList: [
-      {
-        algoName: 'Algo Alpha',
-        isDisabled: false,
-      },
-      {
-        algoName: 'Algo Epsilon',
-        isDisabled: true,
-      },
-    ],
-  },
-  {
-    cameraName: 'Camera-4',
-    cameraCode: 'CAM-4',
-    networkingState: 1,
-    integrationState: 0,
-    cameraImgUrl: 'https://example.com/camera4.jpg',
-    workshopName: 'Workshop B',
-    workspaceName: 'Workspace 4',
-    isRenderDisabled: true,
-    algoStatusList: [
-      {
-        algoName: 'Algo Gamma',
-        isDisabled: true,
-      },
-      {
-        algoName: 'Algo Zeta',
-        isDisabled: false,
-      },
-    ],
-  },
-  {
-    cameraName: 'Camera-5',
-    cameraCode: 'CAM-5',
-    networkingState: 0,
-    integrationState: 1,
-    cameraImgUrl: 'https://example.com/camera5.jpg',
-    workshopName: 'Workshop A',
-    workspaceName: 'Workspace 5',
-    isRenderDisabled: false,
-    algoStatusList: [
-      {
-        algoName: 'Algo Alpha',
-        isDisabled: false,
-      },
-      {
-        algoName: 'Algo Eta',
-        isDisabled: true,
-      },
-    ],
-  },
-  // {
-  //   cameraName: 'Camera-6',
-  //   cameraCode: 'CAM-6',
-  //   networkingState: 1,
-  //   integrationState: 0,
-  //   cameraImgUrl: 'https://example.com/camera6.jpg',
-  //   workshopName: 'Workshop B',
-  //   workspaceName: 'Workspace 6',
-  //   isRenderDisabled: true,
-  //   algoStatusList: [
-  //     {
-  //       algoName: 'Algo Gamma',
-  //       isDisabled: true,
-  //     },
-  //     {
-  //       algoName: 'Algo Theta',
-  //       isDisabled: false,
-  //     },
-  //   ],
-  // },
-  // {
-  //   cameraName: 'Camera-7',
-  //   cameraCode: 'CAM-7',
-  //   networkingState: 0,
-  //   integrationState: 1,
-  //   cameraImgUrl: 'https://example.com/camera7.jpg',
-  //   workshopName: 'Workshop A',
-  //   workspaceName: 'Workspace 7',
-  //   isRenderDisabled: false,
-  //   algoStatusList: [
-  //     {
-  //       algoName: 'Algo Alpha',
-  //       isDisabled: false,
-  //     },
-  //     {
-  //       algoName: 'Algo Iota',
-  //       isDisabled: true,
-  //     },
-  //   ],
-  // },
-  // {
-  //   cameraName: 'Camera-8',
-  //   cameraCode: 'CAM-8',
-  //   networkingState: 1,
-  //   integrationState: 0,
-  //   cameraImgUrl: 'https://example.com/camera8.jpg',
-  //   workshopName: 'Workshop B',
-  //   workspaceName: 'Workspace 8',
-  //   isRenderDisabled: true,
-  //   algoStatusList: [
-  //     {
-  //       algoName: 'Algo Gamma',
-  //       isDisabled: true,
-  //     },
-  //     {
-  //       algoName: 'Algo Kappa',
-  //       isDisabled: false,
-  //     },
-  //   ],
-  // },
-  // {
-  //   cameraName: 'Camera-9',
-  //   cameraCode: 'CAM-9',
-  //   networkingState: 0,
-  //   integrationState: 1,
-  //   cameraImgUrl: 'https://example.com/camera9.jpg',
-  //   workshopName: 'Workshop A',
-  //   workspaceName: 'Workspace 9',
-  //   isRenderDisabled: false,
-  //   algoStatusList: [
-  //     {
-  //       algoName: 'Algo Alpha',
-  //       isDisabled: false,
-  //     },
-  //     {
-  //       algoName: 'Algo Lambda',
-  //       isDisabled: true,
-  //     },
-  //   ],
-  // },
-  // {
-  //   cameraName: 'Camera-10',
-  //   cameraCode: 'CAM-10',
-  //   networkingState: 1,
-  //   integrationState: 0,
-  //   cameraImgUrl: 'https://example.com/camera10.jpg',
-  //   workshopName: 'Workshop B',
-  //   workspaceName: 'Workspace 10',
-  //   isRenderDisabled: true,
-  //   algoStatusList: [
-  //     {
-  //       algoName: 'Algo Gamma',
-  //       isDisabled: true,
-  //     },
-  //     {
-  //       algoName: 'Algo Mu',
-  //       isDisabled: false,
-  //     },
-  //   ],
-  // },
-  // {
-  //   cameraName: 'Camera-11',
-  //   cameraCode: 'CAM-11',
-  //   networkingState: 0,
-  //   integrationState: 1,
-  //   cameraImgUrl: 'https://example.com/camera11.jpg',
-  //   workshopName: 'Workshop A',
-  //   workspaceName: 'Workspace 11',
-  //   isRenderDisabled: false,
-  //   algoStatusList: [
-  //     {
-  //       algoName: 'Algo Alpha',
-  //       isDisabled: false,
-  //     },
-  //     {
-  //       algoName: 'Algo Nu',
-  //       isDisabled: true,
-  //     },
-  //   ],
-  // },
-  // {
-  //   cameraName: 'Camera-12',
-  //   cameraCode: 'CAM-12',
-  //   networkingState: 1,
-  //   integrationState: 0,
-  //   cameraImgUrl: 'https://example.com/camera12.jpg',
-  //   workshopName: 'Workshop B',
-  //   workspaceName: 'Workspace 12',
-  //   isRenderDisabled: true,
-  //   algoStatusList: [
-  //     {
-  //       algoName: 'Algo Gamma',
-  //       isDisabled: true,
-  //     },
-  //     {
-  //       algoName: 'Algo Xi',
-  //       isDisabled: false,
-  //     },
-  //   ],
-  // },
-  // {
-  //   cameraName: 'Camera-13',
-  //   cameraCode: 'CAM-13',
-  //   networkingState: 0,
-  //   integrationState: 1,
-  //   cameraImgUrl: 'https://example.com/camera13.jpg',
-  //   workshopName: 'Workshop A',
-  //   workspaceName: 'Workspace 13',
-  //   isRenderDisabled: false,
-  //   algoStatusList: [
-  //     {
-  //       algoName: 'Algo Alpha',
-  //       isDisabled: false,
-  //     },
-  //     {
-  //       algoName: 'Algo Omicron',
-  //       isDisabled: true,
-  //     },
-  //   ],
-  // },
-  // {
-  //   cameraName: 'Camera-14',
-  //   cameraCode: 'CAM-14',
-  //   networkingState: 1,
-  //   integrationState: 0,
-  //   cameraImgUrl: 'https://example.com/camera14.jpg',
-  //   workshopName: 'Workshop B',
-  //   workspaceName: 'Workspace 14',
-  //   isRenderDisabled: true,
-  //   algoStatusList: [
-  //     {
-  //       algoName: 'Algo Gamma',
-  //       isDisabled: true,
-  //     },
-  //     {
-  //       algoName: 'Algo Pi',
-  //       isDisabled: false,
-  //     },
-  //   ],
-  // },
-  // {
-  //   cameraName: 'Camera-15',
-  //   cameraCode: 'CAM-15',
-  //   networkingState: 0,
-  //   integrationState: 1,
-  //   cameraImgUrl: 'https://example.com/camera15.jpg',
-  //   workshopName: 'Workshop A',
-  //   workspaceName: 'Workspace 15',
-  //   isRenderDisabled: false,
-  //   algoStatusList: [
-  //     {
-  //       algoName: 'Algo Alpha',
-  //       isDisabled: false,
-  //     },
-  //     {
-  //       algoName: 'Algo Rho',
-  //       isDisabled: true,
-  //     },
-  //   ],
-  // },
-  // {
-  //   cameraName: 'Camera-16',
-  //   cameraCode: 'CAM-16',
-  //   networkingState: 1,
-  //   integrationState: 0,
-  //   cameraImgUrl: 'https://example.com/camera16.jpg',
-  //   workshopName: 'Workshop B',
-  //   workspaceName: 'Workspace 16',
-  //   isRenderDisabled: true,
-  //   algoStatusList: [
-  //     {
-  //       algoName: 'Algo Gamma',
-  //       isDisabled: true,
-  //     },
-  //     {
-  //       algoName: 'Algo Sigma',
-  //       isDisabled: false,
-  //     },
-  //   ],
-  // },
-  // {
-  //   cameraName: 'Camera-17',
-  //   cameraCode: 'CAM-17',
-  //   networkingState: 0,
-  //   integrationState: 1,
-  //   cameraImgUrl: 'https://example.com/camera17.jpg',
-  //   workshopName: 'Workshop A',
-  //   workspaceName: 'Workspace 17',
-  //   isRenderDisabled: false,
-  //   algoStatusList: [
-  //     {
-  //       algoName: 'Algo Alpha',
-  //       isDisabled: false,
-  //     },
-  //     {
-  //       algoName: 'Algo Tau',
-  //       isDisabled: true,
-  //     },
-  //   ],
-  // },
-  // {
-  //   cameraName: 'Camera-18',
-  //   cameraCode: 'CAM-18',
-  //   networkingState: 1,
-  //   integrationState: 0,
-  //   cameraImgUrl: 'https://example.com/camera18.jpg',
-  //   workshopName: 'Workshop B',
-  //   workspaceName: 'Workspace 18',
-  //   isRenderDisabled: true,
-  //   algoStatusList: [
-  //     {
-  //       algoName: 'Algo Gamma',
-  //       isDisabled: true,
-  //     },
-  //     {
-  //       algoName: 'Algo Upsilon',
-  //       isDisabled: false,
-  //     },
-  //   ],
-  // },
-  // {
-  //   cameraName: 'Camera-19',
-  //   cameraCode: 'CAM-19',
-  //   networkingState: 0,
-  //   integrationState: 1,
-  //   cameraImgUrl: 'https://example.com/camera19.jpg',
-  //   workshopName: 'Workshop A',
-  //   workspaceName: 'Workspace 19',
-  //   isRenderDisabled: false,
-  //   algoStatusList: [
-  //     {
-  //       algoName: 'Algo Alpha',
-  //       isDisabled: false,
-  //     },
-  //     {
-  //       algoName: 'Algo Phi',
-  //       isDisabled: true,
-  //     },
-  //   ],
-  // },
-  // {
-  //   cameraName: 'Camera-20',
-  //   cameraCode: 'CAM-20',
-  //   networkingState: 1,
-  //   integrationState: 0,
-  //   cameraImgUrl: 'https://example.com/camera20.jpg',
-  //   workshopName: 'Workshop B',
-  //   workspaceName: 'Workspace 20',
-  //   isRenderDisabled: true,
-  //   algoStatusList: [
-  //     {
-  //       algoName: 'Algo Gamma',
-  //       isDisabled: true,
-  //     },
-  //     {
-  //       algoName: 'Algo Chi',
-  //       isDisabled: false,
-  //     },
-  //   ],
-  // },
-];