Преглед на файлове

Merge branch 'all-v4-algoParamSet' into 'all-v4'

feat: 完成算法默认参数修改功能

See merge request skyeye/skyeye_frontend/skyeye-admin!400
楼航飞 преди 1 година
родител
ревизия
52add418e5

+ 2 - 0
package.json

@@ -27,6 +27,7 @@
   },
   "dependencies": {
     "@element-plus/icons-vue": "2.0.9",
+    "@originjs/vite-plugin-commonjs": "^1.0.3",
     "@types/fabric": "5.3.6",
     "@vicons/antd": "0.12.0",
     "@vicons/ionicons5": "0.12.0",
@@ -47,6 +48,7 @@
     "element-resize-detector": "1.2.4",
     "form-data": "^4.0.0",
     "html2canvas": "1.0.0",
+    "json-editor-vue3": "^1.1.1",
     "konva": "9.3.0",
     "lodash-es": "4.17.21",
     "mockjs": "1.1.0",

+ 8 - 25
src/api/algo/algo.ts

@@ -4,14 +4,6 @@ export interface algoQueryModule {
   pageNumber: number;
   pageSize: number;
   queryParam?: { name: string };
-  // updatedAt: string,
-}
-
-export interface algoUpdateModule {
-  id: number;
-  pushStatement: string;
-  pushLinkPrompt: string;
-  // updatedAt: string,
 }
 
 export interface AlgoDetail {
@@ -72,25 +64,16 @@ export function queryAlgoInfoAllByCameraId(cameraId) {
   });
 }
 
-// 旧接口
-export function algoInfoModify(algoId: number, pushLinkPrompt: string, pushStatement: string) {
-  return http.request({
-    url: '/cameraAlgo/saveAlgoState',
-    method: 'post',
-    data: {
-      algoId,
-      pushLinkPrompt,
-      pushStatement,
-    },
-  });
+// 修改算法默认参数
+export interface UpdateAlgoParamParams {
+  algoId?: number; // 算法id
+  algoParam?: string; // 算法参数(json格式)
+  remark?: string; // 算法描述
+  url?: string; // 展示视频或图片的地址
 }
-
-/**
- * v4: 编辑算法
- */
-export function updateAlgoInfo(data: algoUpdateModule) {
+export function updateAlgoParam(data: UpdateAlgoParamParams) {
   return http.request({
-    url: '/admin/algo/updateAlgoInfoById',
+    url: '/admin/algo/updateAlgoParam',
     method: 'post',
     data,
   });

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


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


+ 147 - 0
src/components/JsonFormat/JsonFormat.vue

@@ -0,0 +1,147 @@
+<template>
+  <div>
+    <template v-if="isEditing">
+      <el-input
+        v-model="editedJson"
+        :autosize="{ minRows: 20, maxRows: 40 }"
+        type="textarea"
+        placeholder="请输入算法参数"
+      />
+      <div class="btn-group">
+        <el-button @click="cancelEdit">取消</el-button>
+        <el-button type="primary" @click="saveEdit">提交</el-button>
+      </div>
+    </template>
+    <template v-else>
+      <pre v-html="preSetColor(parsedData())" id="preDom" @click="startEdit"></pre>
+    </template>
+  </div>
+</template>
+
+<script setup>
+  import { ref } from 'vue';
+  import { ElButton } from 'element-plus';
+
+  const props = defineProps({
+    jsonData: {
+      type: String,
+      default: '{}',
+    },
+    colors: {
+      type: Object,
+      default: () => ({}),
+    },
+    step: {
+      type: [Number, String],
+      default: 4,
+    },
+  });
+
+  const emit = defineEmits(['update:jsonData']);
+
+  const isEditing = ref(false);
+  // 存储编辑中的 JSON 数据
+  const editedJson = ref(props.jsonData);
+  // 存储原始 JSON 数据
+  const originalJson = ref(props.jsonData);
+
+  const colorMap = {
+    string: 'green',
+    number: '#FF8C00',
+    boolean: 'blue',
+    null: 'magenta',
+    key: 'red',
+  };
+
+  const addStyle = () => {
+    const obj = {
+      ...colorMap,
+      ...props.colors,
+    };
+    if (document.querySelector('#pre-id')) return;
+    const style = document.createElement('style');
+    style.id = 'pre-id';
+    style.innerText = `
+    .string{ color: ${obj['string']}; }
+    .number{ color: ${obj['number']}; }
+    .boolean{ color: ${obj['boolean']}; }
+    .null{ color: ${obj['null']}; }
+    .key{ color: ${obj['key']}; }
+  `;
+    document.head.appendChild(style);
+  };
+  addStyle();
+
+  const parsedData = () => {
+    if (typeof props.jsonData !== 'string') {
+      console.error('jsonData 不是有效的字符串类型');
+      return {};
+    }
+    try {
+      return JSON.parse(props.jsonData);
+    } catch (error) {
+      console.error('JSON 解析失败:', error);
+      return {};
+    }
+  };
+
+  function preSetColor(data) {
+    function syntaxHighlight(json) {
+      if (typeof json !== 'string') {
+        json = JSON.stringify(json, undefined, 2);
+      }
+      if (typeof json === 'string') {
+        json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
+        return json.replace(
+          /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
+          function (match) {
+            let cls = 'number';
+            if (/^"/.test(match)) {
+              if (/:$/.test(match)) {
+                cls = 'key';
+              } else {
+                cls = 'string';
+              }
+            } else if (/true|false/.test(match)) {
+              cls = 'boolean';
+            } else if (/null/.test(match)) {
+              cls = 'null';
+            }
+            return `<span class="${cls}">${match}</span>`;
+          },
+        );
+      }
+      return '';
+    }
+    const handler = JSON.stringify(data, null, +props.step);
+    return syntaxHighlight(handler);
+  }
+
+  const startEdit = () => {
+    isEditing.value = true;
+    editedJson.value = props.jsonData;
+    originalJson.value = props.jsonData;
+  };
+
+  const saveEdit = () => {
+    try {
+      JSON.parse(editedJson.value);
+      emit('update:jsonData', editedJson.value);
+      isEditing.value = false;
+    } catch (error) {
+      console.error('保存失败,JSON 格式错误:', error);
+    }
+  };
+
+  const cancelEdit = () => {
+    isEditing.value = false;
+    editedJson.value = originalJson.value;
+  };
+</script>
+
+<style scoped lang="scss">
+  .btn-group {
+    float: right;
+    margin-top: 10px;
+  }
+</style>

+ 6 - 0
src/types/permission/constants.ts

@@ -31,6 +31,12 @@ export enum PERM_ALGO {
   CONFIG_PARAM = 'algo_admin_module:config_param', // 算法参数配置
   CONFIG_ADD_GROUP = 'algo_admin_module:config_add_group', // 多相机算法参数配置——添加分组
   CONFIG_DELETE_GROUP = 'algo_admin_module:config_delete_group', // 多相机算法参数配置-—删除分组
+
+  /**
+   * 算法预览
+   */
+  PREVIEW_DESCRIPTION = 'algo_admin_module:preview_description', // 算法介绍设置
+  PREVIEW_PARAM_DEFAULT = 'algo_admin_module:preview_param_default', // 默认参数设置
 }
 
 /**

+ 61 - 307
src/views/cameras/algo-management/algoManagement.vue

@@ -3,20 +3,18 @@
     <div class="background">
       <div class="left">
         <el-input
-          v-model="searchKey"
+          class="search-input"
+          v-model="keyWord"
           placeholder="请输入算法信息搜索"
-          style="margin-top: 24px; height: 32px; margin-bottom: 16px"
           :suffix-icon="Search"
           @change="searchItem"
         />
         <div class="algo-table">
           <el-table
-            ref="singleTableRef"
             :data="algoList"
-            highlight-current-row
             stripe
-            style="width: 100%"
             height="100%"
+            highlight-current-row
             :row-style="{ height: '54px' }"
             :header-row-style="{ height: '54px' }"
             @row-click="handleRowClick"
@@ -26,338 +24,94 @@
             <el-table-column property="name" label="算法名称" />
           </el-table>
         </div>
-        <div style="display: flex; justify-content: flex-end">
-          <el-pagination
-            background
-            layout="prev, pager, next"
-            style="margin-top: 24px; height: 32px; margin-bottom: 34px"
-            :total="total"
-            v-model:current-page="curPage"
-            @current-change="changePage"
-            :page-size="pageSize"
-          />
-        </div>
+        <el-pagination
+          class="algo-pagination"
+          background
+          layout="prev, pager, next"
+          :total="total"
+          v-model:current-page="page"
+          :page-size="pageSize"
+          @current-change="getAlgoDatas"
+        />
       </div>
       <div class="right">
-        <div class="right_top">
-          <div class="top_left">
-            <el-scrollbar height="100%">
-              <div v-if="currentRow?.name == ''" class="details_title">请在左侧列表中选择算法</div>
-              <div v-else class="details_title">{{ currentRow?.name }}检测算法展示</div>
-              <div v-for="item in arrRemark" :key="item" class="textbox">
-                <div class="item_title">{{ item }}</div>
-                <!-- <div class="item_details">{{ currentRow?.remark }}</div> -->
-              </div>
-              <div class="textbox" style="justify-content: flex-start">
-                <div class="item_title" style="width: auto">设置参数:</div>
-                <ElTag v-for="tag in tags" :key="tag" class="mr-2">{{ labelNameMap[tag] }}</ElTag>
-              </div>
-            </el-scrollbar>
-          </div>
-          <div class="top_right">
-            <video :src="currentRow?.url" controls autoplay muted style="width: 100%; height: 100%"> </video>
-          </div>
-        </div>
-        <div class="right_bottom">
-          <div class="details_title" style="margin-bottom: 16px">报警推送编辑</div>
-          <el-form :model="alarmConfig" size="default" :rules="rules" ref="ruleFormRef">
-            <el-form-item label="语句编辑:" prop="pushStatement">
-              <div class="pushStatement">
-                <div class="remark">时间:(示例:2023.10.23 10:55:28)</div>
-                <div class="remark">地点:(示例:C919总装车间150A工位)</div>
-                <el-input
-                  v-model="alarmConfig.pushStatement"
-                  style="width: 100%; height: 74px; margin-top: 4px"
-                  placeholder="示例:【异常类:未穿反光背心违规】您好,经安全管控平台分析,在该区域发现员工未穿反光背心或穿戴不规范的情况,请及时提醒。"
-                />
-              </div>
-            </el-form-item>
-
-            <el-form-item label="链接提示:" prop="pushLinkPrompt">
-              <el-input
-                v-model="alarmConfig.pushLinkPrompt"
-                style="width: 100%"
-                placeholder="示例:请点击商飞大脑-天眼APP查看。"
-              />
-            </el-form-item>
-            <el-form-item>
-              <el-button type="primary" :loading="isSending" @click="onSubmit">保&nbsp;&nbsp;存</el-button>
-            </el-form-item>
-          </el-form>
+        <div class="no-algo-choose" v-if="!curRow">
+          <img src="@/assets/images/algo/no-algo-choose.png" alt="" />
+          <div>请在左侧列表中选择算法</div>
         </div>
+        <AlgoParamEdit v-else style="height: 100%" />
       </div>
-      <!-- <video :src="address" controls autoplay muted style="width: 100%; height: 100%"> </video> -->
     </div>
   </PageWrapper>
 </template>
+
 <script lang="ts" setup>
-  import { onMounted, ref } from 'vue';
-  import { PageWrapper } from '../../../components/Page';
-  import useAlgo from './useAlgoData';
+  import { onMounted } from 'vue';
+  import { storeToRefs } from 'pinia';
   import { Search } from '@element-plus/icons-vue';
-  import type { FormInstance, FormRules } from 'element-plus';
-  import { labelNameMap } from '@/modules/algo/algo-params-edit/types';
+  import { useAlgoDataStore } from './useAlgoData';
+  import { PageWrapper } from '../../../components/Page';
+  import AlgoParamEdit from './components/AlgoParamEdit.vue';
 
-  //调用后端数据
-  const algoDatas = useAlgo();
-  const {
-    algoList,
-    getAlgoDatas,
-    page,
-    total,
-    pageSize,
-    keyWord,
-    searchAlgoDatas,
-    algoId,
-    pushLinkPrompt,
-    pushStatement,
-    modifyAlgoDatas,
-  } = algoDatas;
-  //将后端拉到的数据存到algoListUse数组中进行使用
-  //刷新时从后端拉一次算法数组
-  onMounted(() => {
-    getAlgoDatas();
-  });
+  const algoDataStore = useAlgoDataStore();
+  const { keyWord, page, pageSize, total, algoList, curRow } = storeToRefs(algoDataStore);
+  const { getAlgoDatas } = algoDataStore;
 
-  const changePage = () => {
-    page.value = curPage.value;
+  const searchItem = () => {
+    page.value = 1;
     getAlgoDatas();
   };
 
-  const searchItem = () => {
-    keyWord.value = searchKey.value;
-    curPage.value = 1;
-    page.value = curPage.value;
-    searchAlgoDatas();
+  const handleRowClick = (row) => {
+    curRow.value = row;
   };
 
-  const curPage = ref(1);
-  const currentRow = ref({
-    algoCode: '',
-    name: '',
-    remark: '',
-    url: '',
-    id: 0,
-    pushLinkPrompt: '',
-    pushStatement: '',
-    extra: '',
+  onMounted(() => {
+    getAlgoDatas();
   });
+</script>
 
-  interface User {
-    algoCode: string;
-    name: string;
-    remark: string;
-    url: string;
-    id: number;
-    pushLinkPrompt: string;
-    pushStatement: string;
-    extra: string;
+<style lang="scss" scoped>
+  .background {
+    height: calc(100vh - 86px);
+    background-color: #ffffff;
+    display: flex;
   }
 
-  const arrRemark = ref<string[]>([]);
-  const tags = ref<string[]>([]);
+  .left {
+    height: 100%;
+    width: 31%;
+    padding-left: 1.66%;
+    padding-right: 1.33%;
+    border-right: 1px solid #dddddd;
 
-  const handleRowClick = (val: User | undefined) => {
-    tags.value = [];
-
-    // console.log('xxxxxxxxxx', currentRow.value);
-    currentRow.value = val || ({} as User);
-    arrRemark.value = currentRow.value.remark.split('\r\n');
-    // console.log('arrRemark', arrRemark.value);
-
-    alarmConfig.value.pushLinkPrompt = currentRow.value.pushLinkPrompt;
-    alarmConfig.value.pushStatement = currentRow.value.pushStatement;
-    const extra = currentRow.value.extra;
-    if (extra) {
-      const extraObj = JSON.parse(extra);
-      const params = extraObj?.inferParams;
-      if (params && params.length > 0) {
-        const metaObjs = params[0]?.metaObjs;
-        if (metaObjs && metaObjs.length > 0) {
-          tags.value = metaObjs.map((item) => item.label);
-        }
-      }
+    .search-input {
+      height: 32px;
+      margin: 24px 0 16px 0;
     }
-  };
 
-  const searchKey = ref('');
+    .algo-table {
+      width: 100%;
+      height: calc(100% - 132px);
+    }
 
-  interface SettingConfig {
-    pushLinkPrompt: string;
-    pushStatement: string;
+    .algo-pagination {
+      float: right;
+      margin-top: 15px;
+    }
   }
 
-  const alarmConfig = ref<SettingConfig>({
-    pushLinkPrompt: '',
-    pushStatement: '',
-  });
-  const rules = ref<FormRules>({
-    pushLinkPrompt: [{ required: true, message: '此处不可空缺' }],
-    pushStatement: [{ required: true, message: '此处不可空缺' }],
-  });
-
-  const ruleFormRef = ref<FormInstance>();
+  .right {
+    width: 69%;
+    height: 100%;
 
-  const isSending = ref(false);
-
-  const onSubmit = async () => {
-    if (!ruleFormRef.value) console.log('error submit!');
-    await ruleFormRef.value?.validate((valid, fields) => {
-      if (valid) {
-        isSending.value = true;
-        algoId.value = currentRow.value.id;
-        pushLinkPrompt.value = alarmConfig.value.pushLinkPrompt;
-        pushStatement.value = alarmConfig.value.pushStatement;
-        // console.log('algoId', algoId.value);
-        // console.log('pushStatement', pushStatement.value);
-        // console.log('pushLinkPrompt', pushLinkPrompt.value);
-        modifyAlgoDatas().finally(() => {
-          isSending.value = false;
-        });
-      } else {
-        console.log('error submit!', fields);
-      }
-    });
-  };
-</script>
-<style lang="scss" scoped>
-  .background {
-    position: relative;
-    height: calc(100vh - 64px - 12px);
-    background-color: #ffffff;
-    display: flex;
-    flex-direction: row;
-    .left {
-      position: relative;
-      height: 100%;
-      width: 31%;
-      padding-left: 1.66%;
-      padding-right: 1.33%;
-      // background-color: green;
-      border-right: #dddddd 1px solid;
-      display: flex;
-      flex-direction: column;
-      .algo-table {
-        position: relative;
-        height: calc(100% - 72px - 90px);
-        width: 100%;
-        // background-color: red;
-        // margin-top: 16px;
-      }
-    }
-    .right {
-      position: relative;
+    .no-algo-choose {
       height: 100%;
-      width: 69%;
       display: flex;
       flex-direction: column;
-      // background-color: yellow;
-      .right_top {
-        position: relative;
-        height: 364px;
-        width: 100%;
-        display: flex;
-        flex-direction: row;
-        border-bottom: #dddddd 1px solid;
-        // background-color: rebeccapurple;
-        .top_left {
-          position: relative;
-          margin-top: 90px;
-          margin-left: 4%;
-          height: 234px;
-          width: 42%;
-          display: flex;
-          flex-direction: column;
-          // background-color: rebeccapurple;
-          .details_title {
-            position: relative;
-            height: 22px;
-            width: 100%;
-            font-size: 14px;
-            font-weight: 600;
-            color: rgba(0, 0, 0, 0.85);
-            line-height: 22px;
-          }
-          .textbox {
-            position: relative;
-            margin-top: 14px;
-            width: 100%;
-            display: flex;
-            flex-direction: row;
-            .item_title {
-              position: relative;
-              width: 100%;
-              font-size: 14px;
-              font-weight: 500;
-              color: rgba(0, 0, 0, 0.85);
-              line-height: 22px;
-            }
-            .item_details {
-              position: relative;
-              width: 79.8%;
-              font-size: 14px;
-              font-weight: 400;
-              color: rgba(0, 0, 0, 0.85);
-              line-height: 22px;
-            }
-          }
-        }
-        .top_right {
-          position: relative;
-          margin-top: 90px;
-          margin-left: 4%;
-          height: 234px;
-          width: 45%;
-          display: flex;
-          flex-direction: row;
-          // background-color: rebeccapurple;
-        }
-      }
-      .right_bottom {
-        position: relative;
-        margin-left: 4%;
-        margin-top: 24px;
-        width: 91%;
-        display: flex;
-        flex-direction: column;
-        .details_title {
-          position: relative;
-          height: 22px;
-          width: 100%;
-          font-size: 14px;
-          font-weight: 600;
-          color: rgba(0, 0, 0, 0.85);
-          line-height: 22px;
-        }
-      }
-    }
-  }
-  .pushStatement {
-    position: relative;
-    display: flex;
-    flex-direction: column;
-    height: 155px;
-    width: 100%;
-    // background-color: red;
-    .remark {
-      position: relative;
-      height: 22px;
-      width: 100%;
-      margin-top: 4px;
-      margin-left: 10px;
-      margin-bottom: 12px;
-      font-size: 16px;
-      font-weight: 400;
-      color: rgba(0, 0, 0, 0.42);
-      line-height: 22px;
-    }
-  }
-  :deep() {
-    .el-input__wrapper {
-      align-items: flex-start !important;
-    }
-    .el-form-item__content {
-      justify-content: flex-end;
+      justify-content: center;
+      align-items: center;
+      font-weight: 600;
     }
   }
 </style>

+ 372 - 0
src/views/cameras/algo-management/components/AlgoParamEdit.vue

@@ -0,0 +1,372 @@
+<template>
+  <div>
+    <div class="algorithm-detail" v-loading="loading">
+      <div class="algo-name">
+        <div>【{{ curRow?.name }}】</div>
+        <div class="algo-set" @click="isParamSetVisible = true" v-if="hasSetParamPermission()">
+          <el-icon><Setting /></el-icon>
+          设置默认参数
+        </div>
+      </div>
+      <div class="algo-title">
+        <div class="title">算法介绍</div>
+        <el-icon class="edit-icon" :size="16" @click="changeEditStatus" v-if="hasAlgoEditPermission()"
+          ><EditPen
+        /></el-icon>
+      </div>
+      <div class="media-section">
+        <template v-if="editParam.url">
+          <el-image
+            v-if="isImage(editParam.url)"
+            class="media-img"
+            :src="editParam.url"
+            :preview-src-list="[editParam.url]"
+            :preview-teleported="true"
+            fit="cover"
+          />
+          <video class="media-video" v-else :src="editParam.url" controls />
+          <div class="media-set" v-if="isEditing">
+            <el-upload
+              :action="actionUrl"
+              :show-file-list="false"
+              :on-success="handleUpload"
+              :headers="getHeaders()"
+              :data="{ bizType: 'ALGO' }"
+            >
+              <el-icon class="icon-upload" :size="20"><Upload /></el-icon>
+            </el-upload>
+            <el-icon class="icon-delete" :size="20" @click="handleDelete"><Delete /></el-icon>
+          </div>
+        </template>
+        <template v-else>
+          <div class="empty-media">
+            <img src="@/assets/images/algo/no-algo-url.png" alt="" width="200" height="200" />
+            <el-upload
+              v-if="isEditing"
+              :action="actionUrl"
+              :show-file-list="false"
+              :on-success="handleUpload"
+              :headers="getHeaders()"
+              :data="{ bizType: 'ALGO' }"
+            >
+              <div class="empty-media-text">请点击上传算法介绍图片或视频</div>
+            </el-upload>
+          </div>
+        </template>
+      </div>
+      <el-card class="remark-card">
+        <template v-if="!isEditing">
+          <div v-for="(line, idx) in remarkLines" :key="idx">{{ line }}</div>
+        </template>
+        <template v-else>
+          <el-input
+            v-model="editParam.remark"
+            type="textarea"
+            :autosize="{ minRows: 4, maxRows: 8 }"
+            placeholder="请输入算法描述"
+          />
+        </template>
+      </el-card>
+      <div class="btn-group" v-if="isEditing">
+        <el-button @click="cancelEdit">取消</el-button>
+        <el-button type="primary" @click="saveChanges">保存</el-button>
+      </div>
+    </div>
+    <el-dialog
+      v-model="isParamSetVisible"
+      title="请输入代码修改算法参数,提交后生效"
+      width="60%"
+      align-center
+      @close="cancelAlgoParamsEdit"
+    >
+      <json-editor-vue
+        class="json-editor"
+        v-model="extraJson"
+        :language="'zh-CN'"
+        @update:modelValue="handleUpdateAlgoParam"
+        @validationError="editError"
+      />
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="cancelAlgoParamsEdit">取消</el-button>
+          <el-button type="primary" @click="saveAlgoParamsChanges"> 提交 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import urlJoin from 'url-join';
+  import { storeToRefs } from 'pinia';
+  import { ref, computed, watch, nextTick } from 'vue';
+  import { ElButton, ElIcon, ElMessage, ElUpload, ElDialog, ElInput, ElCard } from 'element-plus';
+  import { EditPen, Upload, Delete, Setting } from '@element-plus/icons-vue';
+  import { useGlobSetting } from '@/hooks/setting';
+  import { useUserStore } from '@/store/modules/user';
+  import { getHeaders } from '@/utils/http/axios';
+  import { useAlgoDataStore } from '../useAlgoData';
+  import { PERM_ALGO } from '@/types/permission/constants';
+  import { UpdateAlgoParamParams, updateAlgoParam } from '@/api/algo/algo';
+  import JsonEditorVue from 'json-editor-vue3';
+
+  const { urlPrefix } = useGlobSetting();
+  const userStore = useUserStore();
+
+  const algoDataStore = useAlgoDataStore();
+  const { curRow } = storeToRefs(algoDataStore);
+  const { updateCurRow } = algoDataStore;
+
+  const loading = ref(false);
+  const isParamSetVisible = ref(false);
+  const isEditing = ref(false);
+  const editParam = ref<UpdateAlgoParamParams>({});
+  const extraJson = ref({});
+
+  const remarkLines = computed(() => (curRow.value?.remark ? curRow.value?.remark.split(/\r?\n/) : []));
+
+  const actionUrl = computed(() => {
+    return urlJoin(urlPrefix!, `/admin/minio/uploadFile`);
+  });
+
+  function initEditParam() {
+    editParam.value.algoId = curRow.value?.id;
+    editParam.value.algoParam = curRow.value?.extra;
+    editParam.value.remark = curRow.value?.remark;
+    editParam.value.url = curRow.value?.url;
+    extraJson.value = JSON.parse(curRow.value?.extra || '{}');
+  }
+
+  function isImage(url: string) {
+    return /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(url);
+  }
+
+  watch(
+    curRow,
+    () => {
+      if (curRow.value) {
+        initEditParam();
+        loading.value = false;
+      }
+    },
+    { immediate: true, deep: true },
+  );
+
+  // 默认参数设置权限
+  const hasSetParamPermission = () => {
+    return userStore.checkPermission(PERM_ALGO.PREVIEW_PARAM_DEFAULT);
+  };
+  // 算法介绍设置权限
+  const hasAlgoEditPermission = () => {
+    return userStore.checkPermission(PERM_ALGO.PREVIEW_DESCRIPTION);
+  };
+
+  const handleUpload = (res) => {
+    editParam.value.url = res.data.url;
+  };
+
+  const handleDelete = () => {
+    editParam.value.url = '';
+  };
+
+  const changeEditStatus = () => {
+    isEditing.value = !isEditing.value;
+    initEditParam();
+  };
+
+  const saveChanges = () => {
+    if (!editParam.value.remark) {
+      ElMessage({
+        message: '算法描述不可为空',
+        type: 'warning',
+      });
+      return;
+    }
+    loading.value = true;
+    updateAlgoParam(editParam.value).then(() => {
+      updateCurRow(curRow.value!);
+      ElMessage({
+        message: '保存成功',
+        type: 'success',
+      });
+      nextTick(() => {
+        isEditing.value = false;
+        loading.value = false;
+      });
+    });
+  };
+
+  const cancelEdit = () => {
+    isEditing.value = false;
+    initEditParam();
+  };
+
+  const handleUpdateAlgoParam = (newJsonData) => {
+    editParam.value.algoParam = newJsonData;
+  };
+
+  const cancelAlgoParamsEdit = () => {
+    isParamSetVisible.value = false;
+    initEditParam();
+  };
+
+  const saveAlgoParamsChanges = () => {
+    updateAlgoParam(editParam.value).then(() => {
+      updateCurRow(curRow.value!);
+      ElMessage({
+        message: '算法参数设置成功',
+        type: 'success',
+      });
+      isParamSetVisible.value = false;
+    });
+  };
+
+  const editError = (_editor: any, errors: any[]) => {
+    if (errors.length > 0) {
+      ElMessage({
+        message: 'JSON 格式错误,请检查',
+        type: 'error',
+      });
+    }
+  };
+</script>
+
+<style scoped lang="scss">
+  .algorithm-detail {
+    height: 100%;
+    padding: 20px 8px;
+
+    .algo-name {
+      font-size: 20px;
+      font-weight: bold;
+      margin-bottom: 16px;
+      display: flex;
+      align-items: center;
+
+      .algo-set {
+        font-size: 14px;
+        color: #409eff;
+        cursor: pointer;
+        display: flex;
+        align-items: center;
+        margin-left: 10px;
+
+        .el-icon {
+          margin-right: 5px;
+        }
+      }
+    }
+
+    .algo-title {
+      height: 22px;
+      display: flex;
+      align-items: center;
+      margin: 16px 0 8px 12px;
+      font-weight: 600;
+
+      .title {
+        line-height: 22px;
+        margin-left: 10px;
+        position: relative;
+      }
+
+      .title::before {
+        content: '';
+        display: inline-block;
+        width: 3px;
+        height: 14px;
+        background-color: #409eff;
+        border-radius: 1px;
+        position: absolute;
+        left: -10px;
+        top: 50%;
+        transform: translateY(-50%);
+      }
+
+      .edit-icon {
+        margin-left: 8px;
+        color: #409eff;
+        cursor: pointer;
+      }
+    }
+  }
+
+  .media-section {
+    height: 40%;
+    margin: 20px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-direction: column;
+    position: relative;
+
+    .media-img,
+    .media-video {
+      height: 100%;
+    }
+
+    .media-img .img {
+      cursor: zoom-in;
+    }
+
+    .media-set {
+      padding: 10px 5px;
+      border-radius: 50px;
+      position: absolute;
+      top: 30%;
+      right: 0;
+      background: #fefefe;
+      box-shadow: #cdcdcda3 2px 2px 4px 2px;
+    }
+
+    .icon-upload,
+    .icon-delete {
+      margin: 5px 0;
+      cursor: pointer;
+    }
+
+    .icon-upload:hover,
+    .icon-delete:hover {
+      color: #409eff;
+    }
+
+    .icon-upload {
+      margin-bottom: 20px;
+    }
+  }
+
+  .empty-media {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+
+    .empty-media-text {
+      color: #409eff;
+      cursor: pointer;
+    }
+  }
+
+  .remark-card {
+    height: 40%;
+    margin: 0 10px 20px 10px;
+    margin-bottom: 24px;
+  }
+
+  .btn-group {
+    display: flex;
+    justify-content: flex-end;
+    gap: 12px;
+  }
+
+  :deep(.el-dialog__body) {
+    height: 500px;
+    overflow: auto;
+  }
+
+  .json-editor {
+    width: 100%;
+    height: 100%;
+  }
+</style>

+ 17 - 42
src/views/cameras/algo-management/useAlgoData.ts

@@ -1,65 +1,40 @@
-import { queryAlgoInfoPage, updateAlgoInfo, AlgoDetail } from '@/api/algo/algo';
 import { ref } from 'vue';
-import { ElMessage } from 'element-plus';
+import { defineStore } from 'pinia';
+import { queryAlgoInfoPage, AlgoDetail } from '@/api/algo/algo';
 
-export function useAlgo() {
-  const algoList = ref<AlgoDetail[]>();
+export const useAlgoDataStore = defineStore('algoData', () => {
   const page = ref(1);
-  const total = ref(0);
   const pageSize = ref(12);
+  const total = ref(0);
   const keyWord = ref('');
-
-  const algoId = ref(0);
-  const pushLinkPrompt = ref('');
-  const pushStatement = ref('');
+  const algoList = ref<AlgoDetail[]>();
+  // 当前选中行数据
+  const curRow = ref<AlgoDetail>();
 
   const getAlgoDatas = () => {
-    return queryAlgoInfoPage({ pageNumber: page.value, pageSize: pageSize.value }).then((res) => {
-      algoList.value = res.records;
-      total.value = res.totalRow;
-    });
-  };
-
-  const searchAlgoDatas = () => {
     return queryAlgoInfoPage({
       pageNumber: page.value,
       pageSize: pageSize.value,
       queryParam: { name: keyWord.value },
     }).then((res) => {
       algoList.value = res.records;
+      total.value = res.totalRow;
     });
   };
 
-  const modifyAlgoDatas = () => {
-    return updateAlgoInfo({
-      id: algoId.value,
-      pushStatement: pushLinkPrompt.value,
-      pushLinkPrompt: pushStatement.value,
-    })
-      .then(function () {
-        ElMessage({
-          message: '算法数据保存成功',
-          type: 'success',
-        });
-      })
-      .catch(function () {
-        ElMessage.error('算法数据保存失败');
-      });
+  const updateCurRow = async (row: AlgoDetail) => {
+    await getAlgoDatas();
+    curRow.value = algoList.value?.find((item) => item.id === row.id);
   };
 
   return {
-    algoList,
+    keyWord,
     page,
-    total,
     pageSize,
-    keyWord,
+    total,
+    curRow,
+    algoList,
     getAlgoDatas,
-    searchAlgoDatas,
-    algoId,
-    pushLinkPrompt,
-    pushStatement,
-    modifyAlgoDatas,
+    updateCurRow,
   };
-}
-
-export default useAlgo;
+});

+ 2 - 0
vite.config.ts

@@ -9,6 +9,7 @@ import { OUTPUT_DIR } from './build/constant';
 import devProxy from './utils/devProxy';
 import pkg from './package.json';
 import { formatToDateTime } from './src/utils/dateUtil';
+import { viteCommonjs } from '@originjs/vite-plugin-commonjs';
 
 const svg = createSvgIconsPlugin({
   // 要缓存的图标文件夹
@@ -59,6 +60,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
         bundler: 'vite',
         openIn: 'reuse',
       }),
+      viteCommonjs(),
     ],
     define: {
       __APP_INFO__: JSON.stringify(__APP_INFO__),

Файловите разлики са ограничени, защото са твърде много
+ 0 - 454
vite.config.ts.timestamp-1724323961663-8c4873e72d16e.mjs