lvkun 2 سال پیش
والد
کامیت
b16d864351

+ 48 - 18
src/controller/cvs/aiboxCloudController.ts

@@ -1,12 +1,19 @@
 
 // aibox云端
-import { addFlow, getFlow, getTask, updateFlow, delFlow, addTask, updateTask, delTask, startTask, stopTask, getPLan, addPlan, delPLan, getAig, getTime, calibrationTime, getNet, getMeta, getArg, updateArg, getSys } from '@/api/cvs/aiboxCloud'
+import {
+  addFlow, getFlow, getTask, updateFlow, delFlow, addTask, updateTask,
+  delTask, startTask, stopTask, getPLan, addPlan, delPLan, getAig, getTime,
+  calibrationTime, getNet, getMeta, getArg, updateArg, getSys,
+  addTaskArea, getTaskArea, getTaskImg
+} from '@/api/cvs/aiboxCloud'
 import { message } from 'ant-design-vue'
 
 export class AiboxCloudController {
-  static clientId = 'JL-ZTAUYTUUMDD6A493D'
+  static clientId = ''
 
   static async flow () {
+    console.log('clientId:', AiboxCloudController.clientId)
+    if (!AiboxCloudController.clientId) return
     const data: any = await getFlow(AiboxCloudController.clientId)
     return {
       ...data,
@@ -30,8 +37,11 @@ export class AiboxCloudController {
   }
 
   static async task () {
+    if (!AiboxCloudController.clientId) return
     const data: any = await getTask(AiboxCloudController.clientId)
+
     const _data = JSON.parse(data?.data).data
+
     return {
       ...data,
       data: _data
@@ -73,6 +83,7 @@ export class AiboxCloudController {
   }
 
   static async plan () {
+    if (!AiboxCloudController.clientId) return
     const data: any = await getPLan(AiboxCloudController.clientId)
 
     const _data = JSON.parse(data.data).data
@@ -94,13 +105,19 @@ export class AiboxCloudController {
   }
 
   static async aig () {
+    if (!AiboxCloudController.clientId) return
     const data: any = await getAig(AiboxCloudController.clientId)
     return JSON.parse(JSON.parse(data.data).data).data
   }
 
   static async arg () {
+    if (!AiboxCloudController.clientId) return
     const data: any = await getArg(AiboxCloudController.clientId)
-    return JSON.parse(JSON.parse(data.data).data).data
+    const _data = JSON.parse(data.data).data
+    return {
+      ...data,
+      data: _data ? _data.map(item => JSON.parse(item)) : []
+    }
   }
 
   static async updateArg (data: any) {
@@ -109,38 +126,51 @@ export class AiboxCloudController {
 
   static async time () {
     const data: any = await getTime(AiboxCloudController.clientId)
-    return JSON.parse(JSON.parse(data.data).data).data
+    return JSON.parse(data.data).timestamp
   }
 
   static async calibrationTime () {
     await calibrationTime(AiboxCloudController.clientId, String(Math.floor(new Date().getTime() / 1000)))
-    message.success('校成功')
+    message.success('校成功')
   }
 
   static async net () {
     const data: any = await getNet(AiboxCloudController.clientId)
-    const _data = JSON.parse(data.data).data
-    return {
-      ...data,
-      data: _data ? _data.map(item => JSON.parse(item)) : []
-    }
+    const _data = JSON.parse(data.data)
+    console.log('JSON.parse(data.data):', JSON.parse(data.data))
+    return _data
   }
 
   static async meta () {
     const data: any = await getMeta(AiboxCloudController.clientId)
-    const _data = JSON.parse(data.data).data
+    console.log(JSON.parse(data.data).system)
+    const system = JSON.parse(data.data).system
     return {
-      ...data,
-      data: _data ? _data.map(item => JSON.parse(item)) : []
+      ...system,
+      diskRate: (system?.freeDiskSpace_GB / system?.totalDiskSpace_GB).toFixed(2),
+      memoryRate: (system?.freeMemory_GB / system?.totalMemory_GB).toFixed(2)
     }
   }
 
   static async sys () {
     const data: any = await getSys(AiboxCloudController.clientId)
-    const _data = JSON.parse(data.data).data
-    return {
-      ...data,
-      data: _data ? _data.map(item => JSON.parse(item)) : []
-    }
+    console.log('JSON.parse(data.data):', JSON.parse(data.data))
+
+    return JSON.parse(data.data)
+  }
+
+  static async addTaskArea (params: any) {
+    const { code, msg } = await addTaskArea(AiboxCloudController.clientId, params)
+    code === 200 ? message.success('新增成功') : message.error(msg)
+  }
+
+  static async getTaskArea (num: string) {
+    const data: any = await getTaskArea(AiboxCloudController.clientId, num)
+    return JSON.parse(data.data)
+  }
+
+  static async getImg (num: any) {
+    const data: any = await getTaskImg(AiboxCloudController.clientId, num)
+    return JSON.parse(data.data)
   }
 }

+ 76 - 3
src/pages/cvs/edge/aiTask.vue

@@ -6,6 +6,14 @@
       ref="tableProDom"
       @add="openModal('add')"
     >
+    <template #search >
+        <a-space>
+          <a-select v-model:value="currentClientId"  style="width: 170px;" >
+            <a-select-option v-for="item in deviceList" :key="item.clientId" :value="item.clientId">{{item.name}}</a-select-option>
+          </a-select>
+          <a-button type="primary" @click="search" >搜索</a-button>
+        </a-space>
+      </template>
     <template #render="{ column, record }">
         <template v-if="column.key === 'taskAbility'">
           <a-tag :key="item.value" v-for="item in abilityList.filter( able => record.taskAbility.includes(able.value))"  >
@@ -22,7 +30,7 @@
             <a  @click="updateTaskStatus('start', record.taskNum)" >启动任务</a>
             <a  @click="updateTaskStatus('stop', record.taskNum)" >停止任务</a>
             <a @click="openModal('update', record)" >编辑</a>
-            <!-- <a @click="openDisposeModal(record)" >配置区域</a> -->
+            <a @click="openDisposeModal(record)" >配置区域</a>
             <a-popconfirm
               title="确定要删除这个任务吗"
               ok-text="Yes"
@@ -46,12 +54,21 @@
   >
     <add-form ref="addFormRef" :opra-state="state.opraState"  :item-data="state.taskState" />
   </RealView>
+
+  <config-area
+    :visible="configAreaState.visible"
+    :item-data="state.taskState"
+    @close="configAreaState.visible = false"
+    @opraed="opraed"
+/>
 </template>
 <script lang='ts'  setup >
-import { AiboxCloudController } from '@/controller'
-import { computed, onMounted, reactive, ref } from 'vue'
+import { AiboxCloudController, AiboxController } from '@/controller'
+import { watch, onMounted, reactive, ref, nextTick } from 'vue'
 import { RealView } from '@/components/RealView/index'
 import AddForm from './components/add.vue'
+import { useRoute } from 'vue-router'
+import { useSchedulerOnce } from 'flicker-vue-hooks'
 
 const columns = [
   {
@@ -95,6 +112,10 @@ const tableProDom = ref()
 
 const addFormRef = ref()
 
+const clientId = useRoute().query.clientId as string
+
+const currentClientId = ref('')
+
 const state = reactive<{
   loading: boolean,
   visible: boolean,
@@ -109,12 +130,44 @@ const state = reactive<{
   taskState: {}
 })
 
+const configAreaState = reactive({
+  visible: false
+})
+
+watch(
+  () => currentClientId.value,
+  () => {
+    AiboxCloudController.clientId = currentClientId.value
+    console.log('AiboxCloudController.clientId 12213123:', AiboxCloudController.clientId)
+  }
+)
+
+watch(
+  () => clientId,
+  () => {
+    if (clientId) AiboxCloudController.clientId = clientId
+  },
+  {
+    immediate: true
+  }
+)
+
+const opraed = () => {
+  configAreaState.visible = false
+}
+
 const openModal = (type: 'add' | 'update', record = {}) => {
   state.opraState = type
   state.visible = true
   state.taskState = record
 }
 
+const openDisposeModal = (record: any) => {
+  state.taskState = record
+
+  configAreaState.visible = true
+}
+
 const submit = () => {
   addFormRef.value.submit()
 }
@@ -129,6 +182,26 @@ const delTask = async (taskNum: number) => {
   tableProDom.value.reload()
 }
 
+const deviceList = ref([])
+
+const search = async () => tableProDom.value.reload()
+
+const getDevice = async () => {
+  const data = await AiboxController.list()
+  console.log('data:', data)
+  deviceList.value = data
+  if (clientId) currentClientId.value = clientId
+  else {
+    currentClientId.value = data[0].clientId
+    AiboxCloudController.clientId = currentClientId.value
+    useSchedulerOnce(() => nextTick(() => search()), 100)
+  }
+}
+
+onMounted(() => {
+  getDevice()
+})
+
 </script>
 <style lang='less' scoped >
 

+ 53 - 2
src/pages/cvs/edge/arg.vue

@@ -5,6 +5,14 @@
       :service="AiboxCloudController.arg"
       ref="tableProDom"
     >
+      <template #search >
+        <a-space>
+          <a-select v-model:value="currentClientId"  style="width: 170px;" >
+            <a-select-option v-for="item in deviceList" :key="item.clientId" :value="item.clientId">{{item.name}}</a-select-option>
+          </a-select>
+          <a-button type="primary" @click="search" >搜索</a-button>
+        </a-space>
+      </template>
       <template #render="{ column, record }">
         <template v-if="column.key === 'argName'">
           <a-space>
@@ -58,9 +66,12 @@
 
 </template>
 <script lang='ts'  setup >
-import { AiboxCloudController } from '@/controller'
+import { AiboxCloudController, AiboxController } from '@/controller'
 import { Form } from 'ant-design-vue'
-import { reactive, ref } from 'vue'
+import { nextTick, onMounted, reactive, ref, watch } from 'vue'
+
+import { useRoute } from 'vue-router'
+import { useSchedulerOnce } from 'flicker-vue-hooks'
 
 const columns = [
   {
@@ -92,6 +103,10 @@ const columns = [
 
 const tableProDom = ref()
 
+const clientId = useRoute().query.clientId as string
+
+const currentClientId = ref('')
+
 const state = reactive({
   visable: false,
   loading: false,
@@ -106,6 +121,23 @@ const preferenceState = reactive({
   argDescribe: ''
 })
 
+watch(
+  () => currentClientId.value,
+  () => {
+    AiboxCloudController.clientId = currentClientId.value
+  }
+)
+
+watch(
+  () => clientId,
+  () => {
+    if (clientId) AiboxCloudController.clientId = clientId
+  },
+  {
+    immediate: true
+  }
+)
+
 const useForm = Form.useForm
 
 const { resetFields, validate, validateInfos } = useForm(preferenceState, reactive({
@@ -127,6 +159,25 @@ const openModal = (record: any) => {
   resetFields(record)
 }
 
+const deviceList = ref([])
+
+const search = async () => tableProDom.value.reload()
+
+const getDevice = async () => {
+  const data = await AiboxController.list()
+  console.log('data:', data)
+  deviceList.value = data
+  if (clientId) currentClientId.value = clientId
+  else {
+    currentClientId.value = data[0].clientId
+    AiboxCloudController.clientId = currentClientId.value
+    useSchedulerOnce(() => nextTick(() => search()), 100)
+  }
+}
+
+onMounted(() => {
+  getDevice()
+})
 </script>
 <style lang='less' scoped >
 

+ 328 - 0
src/pages/cvs/edge/components/configArea.vue

@@ -0,0 +1,328 @@
+<template>
+  <modal-pro
+    label="配置区域"
+    :open="IProps.visible"
+    @cancel="closeModal"
+    @ok="submitArea"
+    width="1200px"
+    destroyOnClose
+  >
+  <a-spin :spinning="state.spinning" >
+    <a-row :gutter="[8, 8]" >
+      <a-col span="13" >
+        <a-card title="绘制区域" >
+          <a-space>
+            <a-button
+              :type="state.drawType === DrawTypeEnum.line ? 'primary' : 'dashed'"
+              @click="setDrawType(DrawTypeEnum.line)"
+            >
+              {{state.drawType === DrawTypeEnum.line ? '绘制线段中...' : '线段' }}
+            </a-button>
+            <a-button
+              :type="state.drawType === DrawTypeEnum.rect ? 'primary' : 'dashed'"
+              @click="setDrawType(DrawTypeEnum.rect)"
+            >
+            {{state.drawType === DrawTypeEnum.rect ? '绘制区域中...' : '区域' }}
+            </a-button>
+          </a-space>
+          <a-row style="width: 580px;height: 400px;" >
+            <img style="width: 100%;margin-top: 20px;" :src="'data:image/jpg;base64,' + state.taskImg " />
+            <canvas
+              ref="canvasRef"
+              id='canvas'
+              width="580"
+              height="400"
+              style="position: absolute; userSelect: none;"
+            >
+            </canvas>
+          </a-row>
+        </a-card>
+
+      </a-col>
+
+      <a-col span="10">
+        <a-card title="绘制记录" >
+          <a-table
+            :key="state.count"
+            :columns="columns"
+            :dataSource="drawRecords"
+          >
+          <template #bodyCell="{ column, record, index }">
+            <template v-if="column.key === 'AreaType'">
+              {{record.RuleType == 2 ? (record.AreaType == '1' ? '方向线' : '辅助线' ) : ( record.AreaType == '0' ? '兴趣区域' : ( record.AreaType == '1' ? '禁入区域' : '岗位区域' ))}}
+            </template>
+            <template v-if="column.key === 'action'">
+              <a-space>
+                <a @click="lineHeightDrawRecord(index)" >  {{record.isLineHeight ? '取消' : '高亮'}} </a>
+                <a-popconfirm
+                  title="确定要删除这个记录吗"
+                  ok-text="Yes"
+                  cancel-text="No"
+                  @confirm="delDrawRecord(record)"
+                >
+                  <a >删除</a>
+                </a-popconfirm>
+              </a-space>
+            </template>
+        </template>
+        </a-table>
+        </a-card>
+      </a-col>
+    </a-row>
+  </a-spin>
+
+  </modal-pro>
+
+  <modal-pro
+    label="保存"
+    :open="state.saveModalVisible"
+    @ok="saveDrawRecord"
+    :keyboard="false"
+    :cancelButtonProps="{hidden: true}"
+    :closable="false"
+  >
+    <a-form
+      :label-col="{ span: 6 }"
+      :wrapper-col="{ span: 14 }"
+    >
+      <a-form-item
+        label="标识"
+        v-bind="validateInfos.RuleId"
+      >
+        <a-input  v-model:value="drawRecordState.RuleId" />
+      </a-form-item>
+      <a-form-item
+        label="区域类别"
+        v-bind="validateInfos.AreaType"
+      >
+        <a-select
+          v-model:value="drawRecordState.AreaType"
+        >
+          <a-select-option
+            v-for="item in drawTypeMap.get(state.drawType)"
+            :key="item.value"
+            :value="item.value"
+          >
+            {{item.label}}
+          </a-select-option>
+        </a-select>
+      </a-form-item>
+      <a-form-item
+        label="关联算法"
+        v-bind="validateInfos.algorithm"
+      >
+        <a-select
+          v-model:value="drawRecordState.algorithm"
+        >
+          <a-select-option
+            v-for="item in taskAbility"
+            :key="item.value"
+            :value="item.value"
+          >
+            {{item.label}}
+          </a-select-option>
+        </a-select>
+      </a-form-item>
+  </a-form>
+  </modal-pro>
+
+</template>
+
+<script setup lang="ts" >
+import { AiboxCloudController } from '@/controller'
+import { computed, reactive, ref, watch, nextTick, getCurrentInstance, onMounted } from 'vue'
+import { DrawController, DrawTypeEnum } from '../drawController'
+import { message, Form } from 'ant-design-vue'
+
+const IProps = defineProps<{
+  visible: boolean,
+  itemData: any
+}>()
+
+const drawTypeMap = new Map([
+  [0, [{ value: '0', label: '兴趣区域' }]],
+  [2, [{ value: '0', label: '辅助线' }, { value: '1', label: '方向线' }]]
+])
+
+const emits = defineEmits(['close', 'opraed'])
+
+// 0 是矩形 2 是线段
+const ctxProperty = {
+  0: {
+    lineWidth: 6,
+    fillStyle: 'rgba(166, 210, 230, 0.46)',
+    strokeStyle: '#A6D2E6',
+    lineCap: 'round',
+    lineJoin: 'round'
+  },
+  2: {
+    lineWidth: 6,
+    fillStyle: 'rgba(166, 210, 230, 0.46)',
+    strokeStyle: '#A6D2E6',
+    lineCap: 'round',
+    lineJoin: 'round'
+  }
+}
+
+const columns = [
+  {
+    title: '标识',
+    dataIndex: 'RuleId',
+    key: 'RuleId'
+  },
+  {
+    title: '类型',
+    dataIndex: 'RuleTypeName',
+    key: 'RuleTypeName'
+  },
+  {
+    title: '关联算法',
+    dataIndex: 'algorithm',
+    key: 'algorithm'
+  },
+  {
+    title: '区域类型',
+    dataIndex: 'AreaType',
+    key: 'AreaType'
+  },
+  {
+    title: '操作',
+    dataIndex: 'action',
+    key: 'action'
+  }
+]
+
+const drawControllerRef = ref<DrawController>()
+
+const taskAbility = computed(() => aigList.value.filter(item => IProps.itemData.taskAbility.includes(item.value)))
+
+const drawRecords = computed(() => drawControllerRef.value?.getDrawRecord())
+
+const canvasRef = ref()
+
+const state = reactive({
+  spinning: false,
+  taskImg: '',
+  saveModalVisible: false,
+  drawType: 999,
+  count: 0
+})
+
+const drawRecordState = reactive({
+  RuleId: '',
+  AreaType: '',
+  algorithm: ''
+})
+
+const useForm = Form.useForm
+
+const { resetFields, validate, validateInfos } = useForm(drawRecordState, reactive({
+  RuleId: [{ required: true, message: '请输入区域标识' }],
+  AreaType: [{ required: true, message: '请选择区域类别' }],
+  algorithm: [{ required: true, message: '请选择关联算法' }]
+}))
+
+const closeModal = () => {
+  drawControllerRef.value?.cacelDraw()
+  state.drawType = 999
+
+  emits('close')
+}
+
+// 高亮 对应的热区
+const lineHeightDrawRecord = (index: number) => {
+  if (drawControllerRef.value?.getDrawRecord()[index].isLineHeight) {
+    drawControllerRef.value?.hiddenlLineHieght(index)
+  } else {
+    drawControllerRef.value?.showLineHieght(index)
+  }
+}
+
+const submitArea = async () => {
+  await AiboxCloudController.addTaskArea({
+    opration: 1,
+    taskNum: IProps.itemData.taskStream,
+    taskAreaData: JSON.stringify(drawControllerRef.value?.getDrawRecord())
+  })
+  emits('opraed')
+}
+
+const saveDrawRecord = () => {
+  validate().then(() => {
+    const $par = {
+      ...drawRecordState,
+      ...drawControllerRef.value!.getDrawRecordLast(),
+      RuleTypeName: state.drawType === 0 ? '区域' : '线段'
+    }
+
+    drawControllerRef.value!.drawRecord[drawControllerRef.value!.drawRecordsLen - 1] = $par
+
+    resetFields({
+      RuleId: '',
+      AreaType: '',
+      algorithm: ''
+    })
+    state.count++
+    state.drawType = 999
+    state.saveModalVisible = false
+  }).catch(() => {})
+}
+
+const setDrawType = (key: DrawTypeEnum) => {
+  drawControllerRef.value?.start()
+  drawControllerRef.value?.setDrawType(key)
+  state.drawType = key
+  drawControllerRef.value?.setProperty(ctxProperty[key])
+  message.info('开始绘画')
+}
+
+const delDrawRecord = (record: any) => {
+  drawControllerRef.value?.delLine(record.id)
+}
+
+const getTaskImg = async () => {
+  state.spinning = true
+  const data = await AiboxCloudController.getImg(IProps.itemData.taskStream)
+  state.spinning = false
+  state.taskImg = data
+}
+
+const getTaskArea = async () => {
+  const { data } = await AiboxCloudController.getTaskArea(IProps.itemData.taskNum)
+  console.log(data)
+  drawControllerRef.value?.setDrawRecord(JSON.parse(data))
+}
+
+// 绘制结束
+const drawEndCallBack = () => {
+  state.saveModalVisible = true
+}
+
+watch(
+  () => IProps.visible,
+  () => {
+    if (IProps.visible) {
+      nextTick(() => {
+        drawControllerRef.value = new DrawController(canvasRef)
+        drawControllerRef.value.drawEnd(drawEndCallBack)
+        getTaskImg()
+        getTaskArea()
+      })
+    }
+  }
+)
+
+const aigList = ref([])
+
+const getAig = async () => {
+  aigList.value = await AiboxCloudController.aig()
+}
+
+onMounted(() => {
+  getAig()
+})
+
+</script>
+
+<style lang="less" scoped >
+</style>

+ 278 - 0
src/pages/cvs/edge/drawController.ts

@@ -0,0 +1,278 @@
+import { useId } from '@/hooks'
+import { Ref } from 'vue'
+
+interface DrawData {
+  id?: string,
+  Points: {x: number, y: number}[],
+  ctxStyle?: any,
+  isLineHeight: boolean
+  name: string
+  hidden: boolean
+  [key: string]: any
+}
+
+interface CtxProperty {
+  lineWidth: number,
+  fillStyle: string,
+  strokeStyle: string,
+  lineCap: string,
+  lineJoin: string,
+}
+
+export enum DrawTypeEnum {
+  'rect' = 0,
+  'line' = 2
+}
+
+/**
+ * 绘制热区的步骤
+ * 1. 点击canvas之后,创建一个a点, 从a点到鼠标一直有一条唯一的线段,点会一直跟随鼠标
+ * 2. 再次点击创建一个b点, a点与b点之间连接一条线段, 然后b点有一条线跟随鼠标移动
+ * 3. 重复1 、2 步骤形成一个区域,然后双击结束绘制,同时弹出一个框,输入这个区域的名字,点击确定结束
+ *
+ *
+ * 支持对区域的 删除 高亮
+ * 绘画的drawRecords 从外部传入
+ */
+
+export class DrawController {
+  el: HTMLCanvasElement
+  ctx: CanvasRenderingContext2D
+  isDrawing: boolean // 是否正在绘画状态
+  isStarting = false // 是否开始绘画
+  drawRecord: DrawData[] = [] // 绘画的全部数据
+  drawType: DrawTypeEnum = DrawTypeEnum.rect // 绘制的图形有两种类型  0 是绘制区域, 2 是绘制线段
+  drawEndCallback: () => void = () => {}
+  ctxProperty: CtxProperty | null = null
+  bgUrl = null
+
+  private mouseupCount = 0
+
+  constructor (
+    el: Ref<HTMLCanvasElement>
+  ) {
+    console.log('初始化drawController')
+
+    this.el = el.value
+    this.ctx = el.value.getContext('2d')!
+    this.isDrawing = false
+    this.el.addEventListener('mousedown', (event: MouseEvent) => this.mousedown(event))
+    this.el.addEventListener('mousemove', (event: MouseEvent) => this.mousemove(event))
+    this.el.addEventListener('mouseup', (event: MouseEvent) => this.mouseup(event))
+    this.el.addEventListener('dblclick', (event: MouseEvent) => this.dblclick(event))
+  }
+
+  getDrawRecordLast () {
+    return this.drawRecord[this.drawRecord.length - 1]
+  }
+
+  getDrawRecord () {
+    return this.drawRecord
+  }
+
+  setDrawRecord (records) {
+    this.drawRecord = records
+    this.clearCanvar()
+    if (records.length !== 0) this.drawRectByDrawData(records)
+  }
+
+  setProperty (records: CtxProperty) {
+    this.ctxProperty = records
+  }
+
+  get drawRecordsLen () {
+    return this.getDrawRecord().length
+  }
+
+  start () {
+    this.isStarting = true
+  }
+
+  // 取消绘制
+  cacelDraw () {
+    this.isStarting = false
+    this.isDrawing = false
+    this.mouseupCount = 0
+  }
+
+  // 传入绘制结束的回调
+  drawEnd (cb : () => void) {
+    this.drawEndCallback = cb
+  }
+
+  // 设置每次绘制的类型 0 是区域 2 是线段
+  setDrawType (type: DrawTypeEnum) {
+    this.drawType = type
+  }
+
+  // 隐藏绘制的线段
+  hiddenDrawRecordsByIndex (index: number) {
+    this.clearCanvar()
+    this.drawRecord[index].hidden = true
+    this.drawRectByDrawData()
+  }
+
+  // 这里直接传入 index即可
+  showDrawRecordsByIndex (index: number) {
+    this.clearCanvar()
+    this.drawRecord[index].hidden = false
+    this.drawRectByDrawData()
+  }
+
+  // 高亮
+  showLineHieght (index: number) {
+    this.clearCanvar()
+    this.drawRecord[index].isLineHeight = true
+    this.drawRectByDrawData()
+  }
+
+  // 取消高亮
+  hiddenlLineHieght (index: number) {
+    this.clearCanvar()
+    this.drawRecord[index].isLineHeight = false
+    this.drawRectByDrawData()
+  }
+
+  // 删除
+  delLine (id: string) {
+    this.clearCanvar()
+    const index = this.drawRecord.findIndex(item => item.id === id)
+    this.drawRecord.splice(index, 1)
+    this.drawRectByDrawData()
+  }
+
+  // 缩放
+  scale (scaleX, scaleY) {
+    this.ctx.scale(scaleX, scaleY)
+  }
+
+  // 设置背景图片
+  setBgImage (url: CanvasImageSource) {
+    this.bgUrl = url as any
+  }
+
+  setDrawImage () {
+    if (this.bgUrl) {
+      this.ctx.drawImage(this.bgUrl as unknown as CanvasImageSource, 0, 0, this.el.width, this.el.height)
+    }
+  }
+
+  // 清空画布
+  clearCanvar () {
+    this.ctx.clearRect(0, 0, this.el.width, this.el.height)
+    this.setDrawImage()
+  }
+
+  // 设置ctx的一些绘制属性
+  private setCtxProperty (records: CtxProperty) {
+    for (const key in records) {
+      this.ctx[key] = records[key]
+    }
+  }
+
+  private mousedown (event: MouseEvent) {
+    if (!this.isStarting) return
+    const canvasRect = this.el.getBoundingClientRect()
+    const x = event.clientX - canvasRect.left
+    const y = event.clientY - canvasRect.top
+    this.createDraw(x, y)
+
+    this.isDrawing = true
+  }
+
+  private mousemove (event: MouseEvent) {
+    if (!this.isDrawing) return ''
+    const canvasRect = this.el.getBoundingClientRect()
+    const x = event.clientX - canvasRect.left
+    const y = event.clientY - canvasRect.top
+
+    const lastRecords = this.drawRecord[this.drawRecord.length - 1]
+
+    this.clearCanvar()
+
+    this.drawRectByDrawData(this.drawRecord.slice(0, this.drawRecord.length - 1))
+    this.setCtxProperty(this.ctxProperty!)
+    this.ctx.beginPath()
+
+    this.ctx.moveTo(lastRecords.Points[0].x, lastRecords.Points[0].y)
+
+    lastRecords.Points.slice(1).forEach(point => {
+      this.ctx.lineTo(point.x, point.y)
+    })
+
+    this.ctx.lineTo(x, y)
+
+    this.ctx.lineTo(lastRecords.Points[0].x, lastRecords.Points[0].y)
+
+    this.ctx.stroke()
+
+    this.ctx.fill()
+  }
+
+  private mouseup (event: MouseEvent) {
+    this.mouseupCount++
+    if (this.drawType === DrawTypeEnum.line && this.mouseupCount === 2) {
+      this.handleDrawed()
+    }
+  }
+
+  // 双击代表本次绘画结束
+  private dblclick (event: MouseEvent) {
+    if (this.drawType === DrawTypeEnum.rect && this.isStarting) {
+      this.handleDrawed()
+    }
+  }
+
+  // 绘制结束
+  private handleDrawed () {
+    this.isStarting = false
+    this.isDrawing = false
+    this.mouseupCount = 0
+    this.drawEndCallback()
+  }
+
+  // 创建每次绘制图形的基本的数据结构
+  private createDraw (x: number, y: number) {
+    if (this.isDrawing) {
+      this.drawRecord[this.drawRecord.length - 1].Points.push({ x, y })
+    } else {
+      const drawItem = {
+        id: useId(),
+        Points: [{ x, y }],
+        ctxStyle: this.ctxProperty,
+        isLineHeight: false,
+        name: '',
+        hidden: false
+      }
+      this.drawRecord.push(drawItem)
+    }
+  }
+
+  // 根据 传入的drawData来绘制
+  drawRectByDrawData (records?: DrawData[]) {
+    const _records = records || this.drawRecord
+    _records.forEach(item => {
+      if (item.hidden) return
+      this.setCtxProperty(item.ctxStyle)
+      if (item.isLineHeight) {
+        this.ctx.strokeStyle = 'rgba(255, 215, 0, 1)'
+        this.ctx.setLineDash([10, 10])
+      } else {
+        this.ctx.setLineDash([])
+      }
+      this.ctx.beginPath()
+
+      this.ctx.moveTo(item.Points[0].x, item.Points[0].y) // 一个矩形的起始点
+      item.Points.slice(1).forEach(point => this.ctx.lineTo(point.x, point.y)) // 绘制中间路径
+      this.ctx.lineTo(item.Points[0].x, item.Points[0].y) // 最后一个点连接到起始点
+      this.ctx.fill()
+      // 根据绘制图形的中间的点, 与第一个y点,来确定绘制的图形的名字渲染的地方
+      const pointX = item.Points.map(point => point.x).sort((a, b) => a - b)
+      const pointY = item.Points.map(point => point.y).sort((a, b) => a - b)
+      const midX = (pointX[0] + pointX[pointX.length - 1]) / 2
+      this.ctx.font = '20px Arial'
+      this.ctx.fillText(item.name, midX, pointY[0] - 20)
+      this.ctx.stroke()
+    })
+  }
+}

+ 49 - 3
src/pages/cvs/edge/flow.vue

@@ -8,7 +8,10 @@
     >
       <template #search >
         <a-space>
-          <a-select></a-select>
+          <a-select v-model:value="currentClientId"  style="width: 170px;" >
+            <a-select-option v-for="item in deviceList" :key="item.clientId" :value="item.clientId">{{item.name}}</a-select-option>
+          </a-select>
+          <a-button type="primary" @click="search" >搜索</a-button>
         </a-space>
       </template>
       <template #render="{ column, record }">
@@ -99,9 +102,11 @@
 </modal-pro>
 </template>
 <script lang='ts'  setup >
-import { AiboxCloudController } from '@/controller'
-import { computed, onMounted, reactive, ref } from 'vue'
+import { AiboxCloudController, AiboxController } from '@/controller'
+import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
 import { Form } from 'ant-design-vue'
+import { useRoute } from 'vue-router'
+import { useSchedulerOnce } from 'flicker-vue-hooks'
 
 const columns = [
   {
@@ -143,6 +148,10 @@ const columns = [
 
 const tableProDom = ref()
 
+const clientId = useRoute().query.clientId as string
+
+const currentClientId = ref('')
+
 const modalTitle = computed(() => state.opraState === 'add' ? '新增视频流' : '编辑视频流')
 
 const state = reactive({
@@ -165,6 +174,23 @@ const videoDetailState = reactive<CVS.AiBox.Flow>({
   polling: '1'
 })
 
+watch(
+  () => currentClientId.value,
+  () => {
+    AiboxCloudController.clientId = currentClientId.value
+  }
+)
+
+watch(
+  () => clientId,
+  () => {
+    if (clientId) AiboxCloudController.clientId = clientId
+  },
+  {
+    immediate: true
+  }
+)
+
 const useForm = Form.useForm
 
 const { resetFields, validate, validateInfos } = useForm(videoDetailState, reactive({
@@ -195,6 +221,26 @@ const delFlow = async (streamNum: number) => {
   tableProDom.value.reload()
 }
 
+const deviceList = ref([])
+
+const search = async () => tableProDom.value.reload()
+
+const getDevice = async () => {
+  const data = await AiboxController.list()
+  console.log('data:', data)
+  deviceList.value = data
+  if (clientId) currentClientId.value = clientId
+  else {
+    currentClientId.value = data[0].clientId
+    AiboxCloudController.clientId = currentClientId.value
+    useSchedulerOnce(() => nextTick(() => search()), 100)
+  }
+}
+
+onMounted(() => {
+  getDevice()
+})
+
 </script>
 <style lang='less' scoped >
 

+ 162 - 20
src/pages/cvs/edge/list.vue

@@ -33,6 +33,28 @@
         <a-space>
           <a @click="getSys(record)">系统信息</a>
           <a @click="openlevelUpModal(record)">算子升级</a>
+          <a-dropdown>
+            <a class="ant-dropdown-link" @click.prevent>
+              属性
+              <DownOutlined />
+            </a>
+            <template #overlay>
+              <a-menu>
+                <a-menu-item>
+                  <a href="javascript:;" @click="deviceAttr('flow', record.clientId)" >视频</a>
+                </a-menu-item>
+                <a-menu-item>
+                  <a href="javascript:;" @click="deviceAttr('aiTask', record.clientId)" >任务</a>
+                </a-menu-item>
+                <a-menu-item>
+                  <a href="javascript:;" @click="deviceAttr('arg', record.clientId)">参数</a>
+                </a-menu-item>
+                <a-menu-item>
+                  <a href="javascript:;" @click="deviceAttr('plan', record.clientId)">计划</a>
+                </a-menu-item>
+              </a-menu>
+            </template>
+          </a-dropdown>
           <a-dropdown>
             <a class="ant-dropdown-link" @click.prevent>
               操作
@@ -41,7 +63,7 @@
             <template #overlay>
               <a-menu>
                 <a-menu-item>
-                  <a href="javascript:;" @click="getMeta" >运行参数</a>
+                  <a href="javascript:;" @click="openMetaModal" >运行参数</a>
                 </a-menu-item>
                 <a-menu-item>
                   <a href="javascript:;" @click="getTime" >运行时间</a>
@@ -50,7 +72,7 @@
                   <a href="javascript:;" @click="calibrationTime">校准时间</a>
                 </a-menu-item>
                 <a-menu-item>
-                  <a href="javascript:;" @click="getNet">网络信息</a>
+                  <a href="javascript:;" @click="openNetModal">网络信息</a>
                 </a-menu-item>
               </a-menu>
             </template>
@@ -61,6 +83,7 @@
     </template>
   </table-pro>
 
+  <!-- 系统信息 -->
   <modal-pro
     :labe="'客户端ID:' + sys?.clientId"
     :open="sysVisible"
@@ -79,6 +102,41 @@
   </a-form>
   </modal-pro>
 
+  <!-- 运行参数 -->
+  <modal-pro
+    width="800px"
+    title="运行参数"
+    :open="metaVisible"
+    @cancel="metaVisible = false"
+    @ok="metaVisible = true"
+  >
+  <a-descriptions title="" bordered :column="2" :contentStyle="{antDescriptionsItemLabel: '#fff'}" >
+        <a-descriptions-item label="cpu核数">{{metaState?.coreCount}}核</a-descriptions-item>
+          <a-descriptions-item label="cpu使用率">
+          <div class="process-container" >
+            <div class="process" :style="{width: metaState?.CPUUsage.toFixed(2) + '%'}" ></div>
+            <div class="count" >{{metaState?.CPUUsage.toFixed(2)}}%</div>
+          </div>
+        </a-descriptions-item>
+        <a-descriptions-item label="内存"> 已使用 / 总内存 : {{metaState?.freeDiskSpace_GB.toFixed(2)}}GB / {{metaState.systemdata?.totalDiskSpace_GB.toFixed(2)}}GB </a-descriptions-item>
+        <a-descriptions-item label="内存使用率">
+          <div class="process-container" >
+            <div class="process" :style="{width: metaState?.diskRate + '%'}"></div>
+            <div class="count" >{{metaState?.diskRate}}%</div>
+          </div>
+        </a-descriptions-item>
+        <a-descriptions-item label="存储">
+          已使用 / 总存储 : {{metaState?.freeMemory_GB.toFixed(2)}}GB / {{metaState?.totalMemory_GB.toFixed(2)}}GB
+        </a-descriptions-item>
+        <a-descriptions-item label="存储使用率">
+          <div class="process-container" >
+            <div class="process" :style="{width: metaState?.memoryRate + '%'}"></div>
+            <div class="count" >{{metaState?.memoryRate}}%</div>
+          </div>
+        </a-descriptions-item>
+      </a-descriptions>
+  </modal-pro>
+  <!-- 算子升级 -->
   <modal-pro
     style="width: 700px;"
     label="算子升级"
@@ -114,6 +172,41 @@
         </a-form-item>
     </a-form>
   </modal-pro>
+
+  <!-- 网络信息 -->
+  <!--     okText="修改" -->
+  <modal-pro
+    style="width: 700px;"
+    label="网络信息"
+    :open="netVisible"
+    @cancel="netVisible = false"
+    @ok="netVisible = false"
+  >
+    <a-form
+      :label-col="{ span: 6 }"
+      :wrapper-col="{ span: 14 }"
+    >
+      <a-form-item label="网卡名称" v-bind="validateInfos.networkIDName">
+        <a-input v-model:value="netState.networkIDName" />
+      </a-form-item>
+      <a-form-item label="网络地址" v-bind="validateInfos.networkAddr">
+        <a-input v-model:value="netState.networkAddr" />
+      </a-form-item>
+      <a-form-item label="子网掩码" v-bind="validateInfos.subnetMask">
+        <a-input v-model:value="netState.netMask" />
+      </a-form-item>
+      <a-form-item label="网关" v-bind="validateInfos.gatewayAddr" >
+        <a-input v-model:value="netState.gatewayAddr" />
+      </a-form-item>
+      <a-form-item label="物理地址" v-bind="validateInfos.physicalAddr" >
+        <a-input v-model:value="netState.physicalAddr" />
+      </a-form-item>
+      <a-form-item label="dns" v-bind="validateInfos.dnsAddr" >
+        <a-input v-model:value="netState.domainNameSys" />
+      </a-form-item>
+    </a-form>
+  </modal-pro>
+
 </a-card>
 </template>
 <script lang='ts' setup >
@@ -121,10 +214,10 @@ import { AiboxCloudController, AiboxController, OperatorController, SpaceControl
 import dayjs from 'dayjs'
 import { onMounted, reactive, ref } from 'vue'
 import { useRouter } from 'vue-router'
-import { Form } from 'ant-design-vue'
+import { Form, message } from 'ant-design-vue'
 import { CopyTsx, InputTsx, SelectTsx } from '@/components/MicroComponents'
 import UploadPro from '@/components/UploadPro'
-import { InboxOutlined } from '@ant-design/icons-vue'
+import { InboxOutlined, DownOutlined } from '@ant-design/icons-vue'
 import { format } from 'echarts'
 
 const router = useRouter()
@@ -176,6 +269,13 @@ const sys = ref<Partial<CVS.AiBox.Sys> & {clientId: string}>()
 
 const sysVisible = ref<boolean>(false)
 
+const metaVisible = ref(false)
+
+const metaState = ref()
+
+const netVisible = ref(false)
+const netState = ref()
+
 const levelUpVisble = ref(false)
 const aiboxState = reactive({
   clientId: '',
@@ -188,11 +288,17 @@ const aiboxState = reactive({
 const useForm = Form.useForm
 
 const { resetFields, validate, validateInfos } = useForm(aiboxState, {
-  file: [{ required: true, message: '请上传文件' }],
+
+  // file: [{ required: true, message: '请上传文件' }],
   version: [{ required: true, message: '请填写版本号' }],
   aiId: [{ required: true, message: '请选择升级算法' }]
 })
 
+const openMetaModal = async () => {
+  metaState.value = await AiboxCloudController.meta()
+  metaVisible.value = true
+}
+
 const uploadDone = (file) => {
   console.log(file)
   // aiboxState.file = url
@@ -211,38 +317,74 @@ const customRequest = (file) => {
   // return false
 }
 
+const openNetModal = async () => {
+  netState.value = await AiboxCloudController.net()
+  netVisible.value = true
+}
+
 const getSys = async (record: CVS.AiBox.AiBox) => {
   sys.value = Object.assign({}, { ...await AiboxCloudController.sys(), clientId: record.clientId })
   sysVisible.value = true
 }
 
-const getMeta = () => {
-  AiboxCloudController.meta()
+const getTime = async () => {
+  const timestamp = await AiboxCloudController.time()
+  message.success('当前设备运行时间:' + dayjs(timestamp * 1000).format('YYYY/MM/DD HH:mm:ss'))
 }
 
-const getTime = () => {
-  AiboxCloudController.time()
-}
-
-const calibrationTime = () => {
-  AiboxCloudController.calibrationTime()
-}
-
-const getNet = () => {
-  AiboxCloudController.net()
-}
+const calibrationTime = () => AiboxCloudController.calibrationTime()
 
 const levelUpOk = async () => {
   AiboxController.levelUp({ ...aiboxState, devId: aiboxState.clientId })
 }
 
 const openlevelUpModal = (record: CVS.AiBox.AiBox) => {
-  console.log(record)
-
   resetFields(record)
   levelUpVisble.value = true
 }
 
+const deviceAttr = (key: 'flow' | 'plan' | 'aiTask' | 'arg', clientId: string) => {
+  router.push({
+    path: '/cvs/edge/' + key,
+    query: { clientId }
+  })
+}
+
 </script>
 <style lang='less' scoped >
+
+.device-status {
+  display: flex;
+  align-items: center;
+  .dot {
+    width: 10px;
+    height: 10px;
+    border-radius: 50%;
+    scale: 0.8;
+    margin-right: 10px;
+  }
+}
+
+.process-container {
+  width: 200px;
+  height: 20px;
+  border-radius: 10px;
+  overflow: hidden;
+  background-color: #f1ebeb;
+  line-height: 10px;
+  position: relative;
+  .process {
+    width: 0%;
+    height: 100%;
+    background-color: #1677ff;
+  }
+  .count {
+    margin: 0 auto;
+    color: #000;
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+  }
+}
 </style>

+ 50 - 2
src/pages/cvs/edge/plan.vue

@@ -6,6 +6,14 @@
     ref="tableProDom"
     @add="openModal"
   >
+      <template #search >
+        <a-space>
+          <a-select v-model:value="currentClientId"  style="width: 170px;" >
+            <a-select-option v-for="item in deviceList" :key="item.clientId" :value="item.clientId">{{item.name}}</a-select-option>
+          </a-select>
+          <a-button type="primary" @click="search" >搜索</a-button>
+        </a-space>
+      </template>
     <template #render="{ column, record }">
         <template v-if="column.key === 'planUse'">
           <a-space>
@@ -100,9 +108,11 @@
 </modal-pro>
 </template>
 <script lang='ts'  setup >
-import { AiboxCloudController } from '@/controller'
+import { AiboxCloudController, AiboxController } from '@/controller'
 import { message } from 'ant-design-vue'
-import { ref, reactive, onMounted, watch, onUnmounted } from 'vue'
+import { useSchedulerOnce } from 'flicker-vue-hooks'
+import { ref, reactive, onMounted, watch, onUnmounted, nextTick } from 'vue'
+import { useRoute } from 'vue-router'
 
 const columns = [
   {
@@ -149,6 +159,10 @@ const calcLtTen = (time: number) => time < 10 ? `0${time}` : time
 
 const tableProDom = ref()
 
+const clientId = useRoute().query.clientId as string
+
+const currentClientId = ref('')
+
 const initSelectTime = [
   { week: '1', time: [], desc: [], value: '000000000000000000000000' },
   { week: '2', time: [], desc: [], value: '000000000000000000000000' },
@@ -173,6 +187,25 @@ const state = reactive({
   scheduleName: ''
 })
 
+watch(
+  () => currentClientId.value,
+  () => {
+    AiboxCloudController.clientId = currentClientId.value
+  }
+)
+
+watch(
+  () => clientId,
+  () => {
+    if (clientId) AiboxCloudController.clientId = clientId
+  },
+  {
+    immediate: true
+  }
+)
+
+const search = async () => tableProDom.value.reload()
+
 const delPlan = async (planNum: number) => {
   await AiboxCloudController.delPlan(planNum)
   tableProDom.value.reload()
@@ -305,11 +338,26 @@ const openModal = () => {
   state.visible = true
 }
 
+const deviceList = ref([])
+
+const getDevice = async () => {
+  const data = await AiboxController.list()
+  console.log('data:', data)
+  deviceList.value = data
+  if (clientId) currentClientId.value = clientId
+  else {
+    currentClientId.value = data[0].clientId
+    AiboxCloudController.clientId = currentClientId.value
+    useSchedulerOnce(() => nextTick(() => search()), 100)
+  }
+}
+
 onUnmounted(() => {
   document.removeEventListener('mouseup', () => {})
 })
 
 onMounted(async () => {
+  getDevice()
   document.addEventListener('mouseup', () => state.selectVisible = false)
 })
 

+ 6 - 6
src/router/index.ts

@@ -424,12 +424,12 @@ const cvs = {
           icon: '',
           component: () => import('@/pages/cvs/edge/aiTask.vue')
         },
-        {
-          path: '/cvs/edge/aig',
-          name: '设备算法',
-          icon: '',
-          component: () => import('@/pages/cvs/edge/aig.vue')
-        },
+        // {
+        //   path: '/cvs/edge/aig',
+        //   name: '设备算法',
+        //   icon: '',
+        //   component: () => import('@/pages/cvs/edge/aig.vue')
+        // },
         {
           path: '/cvs/edge/arg',
           name: '设备参数',