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

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

dao qin преди 1 година
родител
ревизия
4ee667e78b
променени са 30 файла, в които са добавени 1614 реда и са изтрити 202 реда
  1. 80 0
      src/api/camera/camera-preview-group.ts
  2. 2 2
      src/api/comments/comments.ts
  3. BIN
      src/assets/icons/deleteTip.png
  4. BIN
      src/assets/icons/deleted.png
  5. 48 0
      src/directives/preview.ts
  6. 3 1
      src/layout/components/Sider/Sider.vue
  7. 0 2
      src/layout/index.vue
  8. 167 0
      src/modules/camera-tree/CameraTree.vue
  9. 6 0
      src/modules/camera-tree/types/constant.ts
  10. 103 0
      src/types/camera/camera-preview.ts
  11. 9 9
      src/types/comments/constant.ts
  12. 4 3
      src/types/comments/type.ts
  13. 6 2
      src/types/permission/constants.ts
  14. 284 3
      src/views/cameras/preview/components/CameraConfigGroup/CameraConfigGroup.vue
  15. 53 0
      src/views/cameras/preview/components/CameraConfigGroup/components/CameraTreeList.vue
  16. 442 0
      src/views/cameras/preview/components/CameraConfigGroup/components/SettingCamera.vue
  17. 51 0
      src/views/cameras/preview/components/CameraConfigGroup/hooks/useCameraGroupQuery.ts
  18. 8 2
      src/views/datamanager/playback/components/NvrCameraView.vue
  19. 4 24
      src/views/datamanager/playback/components/NvrTimeSelect.vue
  20. 23 3
      src/views/map-config/mini-map/MiniMapConfig.vue
  21. 1 1
      src/views/message/question-notifications/QuestionNotifications.vue
  22. 61 1
      src/views/message/question-notifications/components/contentPanel.vue
  23. 17 2
      src/views/page-config/ConfigEdit.vue
  24. 5 15
      src/views/system-config/scene-manage/SceneManage.vue
  25. 11 10
      src/views/system/comments/PageCommentsManage.vue
  26. 200 100
      src/views/system/comments/component/SingleComment.vue
  27. 10 6
      src/views/system/comments/use-comments.ts
  28. 2 0
      types/config.d.ts
  29. 3 2
      utils/devProxy/local/app.config.js
  30. 11 14
      utils/devProxy/shangfei/app.config.js

+ 80 - 0
src/api/camera/camera-preview-group.ts

@@ -0,0 +1,80 @@
+import { http } from "@/utils/http/axios";
+import { CameraGroupResponse, CameraPreviewRequest, CameraTreeRequest, GropAlgoProps, saveDetectionGroupRequest, updateGroupStatusRequest } from "@/types/camera/camera-preview";
+
+/**
+  *@description: V4: 查询联合检测相机分组列表
+*/
+export function queryDetectionGroupList(params: CameraPreviewRequest) {
+  return http.request<CameraGroupResponse>({
+    url: '/admin/cameraDetectionGroup/queryDetectionGroupList',
+    method: 'GET',
+    params
+  })
+}
+
+/**
+  *@description: V4: 删除联合相机分组
+*/
+export function deleteDetectionGroup(data: number[]) {
+  return http.request({
+    url: '/admin/cameraDetectionGroup/deleteDetectionGroup',
+    method: 'DELETE',
+    data
+  })
+}
+
+/**
+  *@description: V4: 开启-关闭联合相机分组
+*/
+export function updateGroupStatus(data: updateGroupStatusRequest) {
+  return http.request({
+    url: '/admin/cameraDetectionGroup/updateDetectionGroupStatus',
+    method: 'POST',
+    data
+  })
+}
+
+/**
+  *@description: V4: 条件查询相机树
+*/
+export function queryCameraTreeByCondition(data: CameraTreeRequest) {
+  return http.request({
+    url: 'admin/cameraPreview/queryCameraTreeByCondition',
+    method: 'POST',
+    data
+  })
+}
+
+/**
+  *@description: V4: 查询联合相机可选算法
+*/
+export function getDetectionGroupLAlgoList() {
+  return http.request<GropAlgoProps[]>({
+    url: '/admin/algo/getDetectionGroupLAlgoList',
+    method: 'GET',
+  })
+}
+
+/**
+  *@description: V4: 新建多相机联合分组
+*/
+export function saveDetectionGroup(data: saveDetectionGroupRequest) {
+  return http.request({
+    url: '/admin/cameraDetectionGroup/saveDetectionGroup',
+    method: 'POST',
+    data
+  })
+}
+
+/**
+  *@description: V4: 根据联合相机分组id查询相机树
+*/
+export function queryCameraTreeByGroupId(params: {groupId: number}) {
+  return http.request({
+    url: `/admin/cameraDetectionGroup/queryCameraTreeByGroupId?groupId=${params.groupId}`,
+    method: 'GET',
+  })
+}
+
+
+

+ 2 - 2
src/api/comments/comments.ts

@@ -1,5 +1,5 @@
 import { http } from '@/utils/http/axios';
-import { COMMENTSTATUS } from '@/types/comments/constant';
+import { COMMENT_STATUS } from '@/types/comments/constant';
 import { CommentsQuery, ListType } from '@/types/comments/type';
 
 //查询留言列表
@@ -12,7 +12,7 @@ export const getCommentsList = (data: CommentsQuery) => {
 };
 
 //更新评论状态
-export const undateCommentStatus = (data: { id: number; status: COMMENTSTATUS }) => {
+export const undateCommentStatus = (data: { id: number; status: COMMENT_STATUS }) => {
   return http.request({
     url: `/admin/comment/updateCommentStatus`,
     method: 'PUT',

BIN
src/assets/icons/deleteTip.png


BIN
src/assets/icons/deleted.png


+ 48 - 0
src/directives/preview.ts

@@ -0,0 +1,48 @@
+import type { Directive } from 'vue'
+
+interface PreviewElement extends HTMLElement {
+  _previewHandler?: EventListener
+  _previewImage?: HTMLImageElement
+}
+
+const vPreview: Directive<PreviewElement> = {
+  mounted(el, binding) {
+    const img = new Image()
+    img.src = binding.value
+    img.style.position = 'fixed'
+    img.style.zIndex = '9999'
+    img.style.pointerEvents = 'none'
+    img.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)'
+    img.style.maxWidth = '300px'
+    img.style.maxHeight = '200px'
+    img.style.display = 'none'
+    document.body.appendChild(img)
+
+    const showPreview = (e: MouseEvent) => {
+      img.style.display = 'block'
+      img.style.left = `${e.clientX + 15}px`
+      img.style.top = `${e.clientY + 15}px`
+    }
+
+    const hidePreview = () => {
+      img.style.display = 'none'
+    }
+
+    el._previewHandler = showPreview
+    el._previewImage = img
+
+    el.addEventListener('mouseenter', showPreview)
+    el.addEventListener('mousemove', showPreview)
+    el.addEventListener('mouseleave', hidePreview)
+  },
+  unmounted(el) {
+    if (el._previewImage) {
+      document.body.removeChild(el._previewImage)
+    }
+    el.removeEventListener('mouseenter', el._previewHandler!)
+    el.removeEventListener('mousemove', el._previewHandler!)
+    el.removeEventListener('mouseleave', el._previewHandler!)
+  }
+}
+
+export default vPreview

+ 3 - 1
src/layout/components/Sider/Sider.vue

@@ -69,7 +69,7 @@
 
   // 获取当前打开的子菜单
   const matched = currentRoute.matched;
-  const activeMenu = currentRoute.meta?.activeMenu ?? null;
+  const activeMenu = currentRoute.meta?.activeMenu || null; // activeMenu undefined,null 或 空字符串,统一变为 null。
   const selectedKeys = ref<string>((activeMenu ?? currentRoute.name) as string);
   const openKeys = ref(matched && matched.length ? matched.map((item) => item.name) : []);
 
@@ -86,6 +86,8 @@
 
   const getSelectedKeys = computed(() => {
     let location = props.location;
+    console.log('location', location);
+    console.log('selectedKeys', selectedKeys.value);
     return location === 'left' || (location === 'header' && unref(navMode) === 'horizontal')
       ? unref(selectedKeys)
       : unref(headerMenuSelectKey);

+ 0 - 2
src/layout/index.vue

@@ -240,8 +240,6 @@
     z-index: auto;
     transition: box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
       background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-    &-header {
-    }
     &-shade {
       position: fixed;
       top: 0;

+ 167 - 0
src/modules/camera-tree/CameraTree.vue

@@ -0,0 +1,167 @@
+<template>
+  <el-tree
+    ref="treeRef"
+    node-key="tempCode"
+    :data="treeData"
+    :props="defaultProps"
+    default-expand-all
+    :show-checkbox="isShowCheckbox"
+    @node-click="handleNodeClick"
+    @check="handleTreeCheck"
+  >
+    <template #default="{ node, data }">
+      <span class="flexCenter" :class="{ integrationState: data.integrationState === IntegrationState.DISABLED, nodeSelect: isSelect(data) }" >
+        <span v-if="data.nodeType === CameraTreeNodeType.camera" class="iconWrapper flexCenter" >
+          <span
+            class="cameraCommon"
+            :class="{
+              cameraSelect: isSelect(data),
+            }"
+          ></span>
+
+          <el-icon
+            class="cameraIcon"
+            :class="{
+              iconSelect: isSelect(data),
+            }"
+          >
+            <VideoCamera />
+          </el-icon>
+          <el-icon class="invalidCamera" v-if="isInvalid(data)"><WarningFilled /></el-icon>
+        </span>
+        <span 
+          v-if="data.nodeType === CameraTreeNodeType.camera && enablePreview "
+          v-preview="previewImage(data)">
+          {{ node.label }}
+          </span>
+        <span v-else>{{ node.label }}</span>
+      </span>
+    </template>
+  </el-tree>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import { CameraTree, CameraTreeNodeType } from '@/api/camera/camera-preview';
+  import { useRouteQuery } from '@vueuse/router';
+  import { VideoCamera, WarningFilled } from '@element-plus/icons-vue';
+  import { IntegrationState } from './types/constant';
+  import vPreview from '@/directives/preview'
+  // import { useGlobSetting } from '@/hooks/setting';
+  // import urlJoin from 'url-join';
+
+  // const { urlPrefix } = useGlobSetting();
+  const cameraId = useRouteQuery('cameraId');
+
+  interface CameraTreeTempType extends CameraTree {
+    tempCode?: string;
+  }
+
+  defineProps<{
+    treeData: CameraTreeTempType[];
+    isShowCheckbox: boolean;
+    enablePreview?: boolean; 
+  }>();
+
+  const defaultProps = {
+    children: 'children',
+    label: 'name',
+  };
+
+  // 事件定义
+  const emit = defineEmits(['node-click', 'check']);
+
+  // 事件处理
+  const handleNodeClick = (data: CameraTreeTempType) => {
+    emit('node-click', data);
+  };
+
+  const handleTreeCheck = (node: CameraTreeTempType, checked: { checkedNodes: CameraTreeTempType[] }) => {
+    emit('check', node, checked);
+  };
+
+  const isSelect = (data) => data.nodeType === CameraTreeNodeType.camera && data.id === Number(cameraId.value);
+
+  const isInvalid = (data) => {
+    return data.networkingState !== 0;
+  };
+
+  const previewImage = (data) => {
+    if (data.nodeType === CameraTreeNodeType.camera) {
+      return data.pushStreamDTO?.imageUrl ? data.pushStreamDTO?.imageUrl  : '';
+    }
+
+  }
+  // 暴露方法和引用
+  const treeRef = ref();
+  const setCheckedKeys = (keys: string[]) => treeRef.value?.setCheckedKeys(keys);
+  const getCheckedNodes = (e) => treeRef.value?.getNode(e);
+  const setChecked = (key: string, state: boolean, deep: boolean) => treeRef.value?.setChecked(key, state, deep);
+
+  defineExpose({
+    getCheckedNodes,
+    setChecked,
+    setCheckedKeys
+  });
+</script>
+
+<style scoped>
+  .cameraSelect {
+    width: 6px;
+    height: 6px;
+    background: #0052d9;
+    display: inline-block;
+    border-radius: 6px;
+    margin-right: 6px;
+  }
+  .cameraTreeTitle {
+    background: #f0f2f5;
+    padding: 12px;
+    display: flex;
+  }
+
+  .detail-num {
+    font-size: 10px;
+    margin-top: 4px;
+    margin-left: 6px;
+  }
+
+  .cameraTreeInputWrapper {
+    padding: 8px;
+  }
+  .filterTextInput {
+    margin: 8px 0;
+  }
+  .flexCenter {
+    display: flex;
+    align-items: center;
+  }
+
+  .integrationState {
+    cursor: not-allowed;
+    color: #ccc;
+  }
+  .cameraIcon {
+    margin-right: 5px;
+    font-size: 18px;
+  }
+  .iconSelect {
+    color: #0052d9;
+  }
+
+  .iconWrapper {
+    position: relative;
+  }
+
+  .invalidCamera {
+    color: #dd5869;
+    font-size: 12px;
+    position: absolute;
+    right: 2px;
+    top: -4px;
+  }
+
+  .nodeSelect {
+    color: #0052d9;
+  }
+</style>

+ 6 - 0
src/modules/camera-tree/types/constant.ts

@@ -0,0 +1,6 @@
+export enum IntegrationState {
+  /* 启用 */
+  ENABLE = 0,
+  /* 启用 */
+  DISABLED = 1,
+}

+ 103 - 0
src/types/camera/camera-preview.ts

@@ -0,0 +1,103 @@
+import { PaginationRequest, PaginationResponse } from "@/types/common/type";
+
+export enum CameraGroupStatus {
+  /*开启 */
+  OPEN = 0,
+  /*关闭 */
+  CLOSE = 1,
+}
+
+interface GroupDetailListItem {
+  /*相机id */
+  cameraId: number;
+  /*设备ID */
+  cameraCode: string;
+  /*位置 */
+  location: string;
+  /*联合检测算法id */
+  algoId: number;
+  /*是否为主相机 */
+  isMainCamera: number;
+}
+
+export type CameraGroupItem = {
+  cameraDetectionGroupId: number;
+  /*联合检测相机分组状态 */
+  status: number;
+  /*分组详情 */
+  groupDetailList: GroupDetailListItem[];
+}
+
+export type CameraPreviewRequest = PaginationRequest;
+
+export type CameraGroupResponse = PaginationResponse<CameraGroupItem>;
+
+export type CameraGroupTableItem = Pick<CameraGroupItem, 'cameraDetectionGroupId' | 'status'> & GroupDetailListItem
+
+export type CameraTreeRequest = {
+  queryString: string,
+  isEnableAlgo: boolean,
+  isEnableRender: boolean
+}
+
+export interface GropAlgoProps {
+  id: number;
+  /*算法名称 */
+  name: string;
+  /*算法提供编码 */
+  code: string;
+  /*前端显示名称 */
+  showName: string;
+  /*描述 */
+  remark: string;
+  /*展示视频的地址 */
+  url: string;
+  /*推送语句 */
+  pushStatement: string;
+  /*推送链接提示 */
+  pushLinkPrompt: string;
+  /*图标的地址 */
+  iconUrl: string;
+  /*状态: 0-启用, 1-禁用 */
+  status: number;
+  /*创建时间 */
+  createdAt: Record<string, unknown>;
+  /*更新时间 */
+  updatedAt: Record<string, unknown>;
+  /*0-未删除,大于0(时间戳)-已删除 */
+  isDeleted: number;
+  /*扩展数据 */
+  extra: string;
+  /*算法类型 0-单算法 1-组算法 */
+  type: number;
+}
+
+export interface saveDetectionGroupRequest {
+  cameraId?: number;
+  /*算法id */
+  algoId?: number;
+  /*是否为主相机 */
+  isMainCamera?: number;
+}
+
+export interface SelectOption {
+  code: number;
+  name: string;
+  isMainCamera: number;
+  algoName: string;
+  algoCode: string;
+  isActive: boolean;
+}
+
+export interface updateGroupStatusRequest {
+  cameraDetectionGroupId: number;
+  /*联合检测相机分组状态 */
+  status: number;
+}
+
+export enum IsMainCamera {
+  /*主相机 */
+  YES = 0,
+  /*副相机 */
+  NO = 1,
+}

+ 9 - 9
src/types/comments/constant.ts

@@ -1,12 +1,12 @@
-export enum REPLYSTATUS {
-  replied = 1,
-  unReplied = 0,
-  all = 2,
+export enum REPLY_STATUS {
+  REPLIED = 1,
+  UN_REPLIED = 0,
+  ALL = 2,
 }
 
-export enum COMMENTSTATUS {
-  unAuthed = 0,
-  rejected = 1,
-  passed = 2,
-  all = 3,
+export enum COMMENT_STATUS {
+  UN_AUTHED = 1,
+  REJECTED = 3,
+  PASSED = 2,
+  ALL = 0,
 }

+ 4 - 3
src/types/comments/type.ts

@@ -1,4 +1,4 @@
-import { REPLYSTATUS, COMMENTSTATUS } from '@/types/comments/constant';
+import { REPLY_STATUS, COMMENT_STATUS } from '@/types/comments/constant';
 
 export interface Records {
   id: number; //评论id
@@ -9,11 +9,12 @@ export interface Records {
   picUrl: string; //评论图片
   staffNo: string; //员工编号
   mobile: string; //手机号
+  realname: string; //真实姓名
   // problemImage: Array<File>;
   reply: string | null; //回复内容
-  isReplied: REPLYSTATUS; //是否已回复
+  isReplied: REPLY_STATUS; //是否已回复
   replyAt: string | null; //回复时间
-  status: COMMENTSTATUS; //审核状态
+  status: COMMENT_STATUS; //审核状态
   createdAt: string; //创建时间
   updatedAt: string; //更新时间
   isUserDeleted: number;

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

@@ -19,7 +19,6 @@ export enum PERM_DEVICE {
   NVR_DELETE = 'device_admin_module:nvr_delete', // NVR设备删除
 }
 
-
 /**
  * 算法管理模块
  */
@@ -48,6 +47,11 @@ export enum PERM_DATA {
    */
   VIOLATION_FAKE_ADD = 'data_admin_module:violation_fake_add', // 添加数据
   VIOLATION_FAKE_DELETE = 'data_admin_module:violation_fake_delete', // 删除数据
+
+  /**
+   * 历史视频(视频回看)
+   */
+  PLAYBACK_DOWNLOAD = 'data_admin_module:playback_download', // 视频下载
 }
 
 /**
@@ -57,7 +61,7 @@ export enum PERM_NOTICE {
   /**
    * 报表消息
    */
-  REPORT_ADD = 'notice_admin_module:report_add', 
+  REPORT_ADD = 'notice_admin_module:report_add',
   REPORT_EDIT = 'notice_admin_module:report_edit',
   REPORT_DELETE = 'notice_admin_module:report_delete',
   REPORT_ENABLE = 'notice_admin_module:report_enable',

+ 284 - 3
src/views/cameras/preview/components/CameraConfigGroup/CameraConfigGroup.vue

@@ -1,7 +1,288 @@
 <template>
-  <div> 组相机配置 </div>
+  <div class="cameraConfigGroup">
+     <el-card>
+      <template #header>
+        <el-button
+          type="primary"
+          @click="createGroupDialog = true"
+          style="margin-top: 24px; margin-bottom: 16px; width: 138px"
+        >
+          <img src="@/assets/images/create.png" style="margin-top: -1px; margin-right: 5px" />新建相机分组
+        </el-button>
+      </template>
+      <el-table
+        :data="cameraGroupList"
+        :span-method="objectSpanMethod"
+        height="calc(100vh - 380px)"
+        style="width: 100%; margin-top: 16px; --el-table-border-color: none"
+        v-loading="loading"
+      >
+        <el-table-column prop="id" label="组序号"  width="70"/>
+        <el-table-column prop="cameraName" label="相机名称" align="center">
+          <template #default="scope">
+            <div class="cameraName-text" v-if="scope.row.isMainCamera === IsMainCamera.YES">
+              <el-icon><Star color="Gold" /></el-icon>
+              {{ scope.row.cameraName }}
+            </div>
+          </template>  
+        </el-table-column>
+        <el-table-column prop="cameraCode" label="设备ID"/>
+        <el-table-column prop="location" label="地点"/>
+        <el-table-column prop="algoName" label="算法">
+          <template #default="scope">
+            <div class="algoId-text" @click="handleView(scope.row)">
+              {{ scope.row.algoName }}
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" fixed="right" width="120px">
+          <template #default="scope">
+            <div class="operation">
+              <el-tooltip effect="light" content="关闭/开启" placement="bottom">
+                <el-switch 
+                  :model-value="scope.row.status" 
+                  :inactive-value="CameraGroupStatus.CLOSE"
+                  :active-value="CameraGroupStatus.OPEN"
+                  @update:model-value="(val) => handleSwitch(val, scope.row)"/>
+              </el-tooltip>
+              
+              <el-tooltip effect="light" content="删除" placement="bottom">
+                <img src="@/views/message/alarmMessages/img/delete.png" @click="handleDelete(scope.row)" />
+              </el-tooltip>
+            </div>
+          </template>
+        </el-table-column>
+
+        <template #empty>
+          <div class="emptyDiv">
+            <img src="@/assets/images/empty.png" class="emptyImg" />
+            <span class="emptySpan">暂无数据</span>
+          </div>
+        </template>
+      </el-table>
+      <section class="mt-4 flex justify-end">
+        <el-pagination
+          background
+          layout="total, sizes, prev, pager, next"
+          :page-sizes="[10, 30, 50]"
+          :total="total"
+          v-model:page-size="requestParams.pageSize"
+          v-model:current-page="requestParams.pageNumber"
+          @change="queryCameraGroupPage"
+        />
+      </section>
+    </el-card>
+
+    <!--删除弹窗 -->
+    <el-dialog v-model="deleteDialog" width="424px" top="20%" class="deleteDialog">
+      <template #header="">
+        <div class="deleteDialogHeader">
+          <img src="@/assets/images/deleteTip.png" class="deleteTip" />
+          <span class="titleSpan">请确认删除该相机分组吗?</span>
+        </div>
+      </template>
+      <span style="margin-left: 37px">删除之后,该组多相机检测将失效!</span>
+      <div class="dialogBottom">
+        <el-button class="dialogBtn" @click="deleteDialog = false">取消</el-button>
+        <el-button class="dialogBtn" type="primary" @click="confirmDelete">确定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 新增相机分组弹窗 -->
+    <el-dialog v-model="createGroupDialog" width="70%" top="15%" left="25%" class="deleteDialog" title="设置相机组">
+    <SettingCamera ref="settingCameraRef"/>
+      <div class="dialogBottom">
+        <el-button class="dialogBtn" @click="handleCancle">取消</el-button>
+        <el-button class="dialogBtn" type="primary" @click="handleCreateGroup">确定</el-button>
+      </div>
+    </el-dialog>
+  </div>
 </template>
 
-<script lang="ts" setup></script>
+<script lang="ts" setup>
+import { onMounted, ref } from 'vue';
+import { useRouter } from 'vue-router';
+import { ElMessage } from 'element-plus';
+import { Star } from '@element-plus/icons-vue';
+import useCameraGroupQuery from './hooks/useCameraGroupQuery';
+import { CameraGroupTableItem, CameraGroupStatus, IsMainCamera } from '@/types/camera/camera-preview';
+import { deleteDetectionGroup, updateGroupStatus, saveDetectionGroup } from '@/api/camera/camera-preview-group';
+import SettingCamera from './components/SettingCamera.vue'
+
+const router = useRouter();
+const { requestParams, total, queryCameraGroupPage, cameraGroupList, loading } = useCameraGroupQuery();
+const createGroupDialog = ref(false);
+const deleteDialog = ref(false);
+const cameraDetectionGroupId = ref();
+
+
+const objectSpanMethod = ({
+  row,
+  rowIndex,
+  columnIndex,
+}) => {
+  // 合并组序列和操作列
+  if (columnIndex === 0 || columnIndex === 5) {
+    const list = cameraGroupList.value;
+    const groupId = row.cameraDetectionGroupId;
+    const firstIndex = list.findIndex(item => item.cameraDetectionGroupId === groupId);
+    
+    if (rowIndex !== firstIndex) {
+      return { rowspan: 0, colspan: 0 };
+    }
+    
+    let rowCount = 0;
+    for (let i = firstIndex; i < list.length; i++) {
+      if (list[i].cameraDetectionGroupId === groupId) rowCount++;
+      else break;
+    }
+    
+    return {
+      rowspan: rowCount,
+      colspan: 1
+    };
+  }
+};
+
+const handleDelete = (row: CameraGroupTableItem) => {
+  if (row.status === CameraGroupStatus.OPEN) {
+    ElMessage({
+      message: '开启状态的分组不可删除',
+      type: 'warning',
+      plain: true,
+    });
+  } else {
+    deleteDialog.value = true;
+    cameraDetectionGroupId.value = row.cameraDetectionGroupId;
+  }
+}
+
+const handleSwitch = (newStatus: number, row: CameraGroupTableItem) => {
+  const data = {
+    cameraDetectionGroupId: row.cameraDetectionGroupId,
+    status: newStatus,
+  }
+  updateGroupStatus(data).then(res => {
+    ElMessage.success('操作成功')
+    queryCameraGroupPage();
+  })
+}
+
+const confirmDelete = () => {
+  deleteDetectionGroup([Number(cameraDetectionGroupId.value)]).then(() => {
+    ElMessage({
+      message: '删除成功',
+      type: 'success',
+      plain: true,
+    });
+    deleteDialog.value = false;
+    queryCameraGroupPage();
+  })
+  
+}
+
+const settingCameraRef = ref<InstanceType<typeof SettingCamera>>();
+const handleCreateGroup = () => {
+  const { valid, data } = settingCameraRef.value?.isValidate();
+ 
+  if(valid) {
+    // 执行提交逻辑
+    const saveData = data.map(item => {
+      return {
+        cameraId: Number(item.code),
+        algoId: Number(item.algoCode),
+        isMainCamera: item.isMainCamera,
+      }
+    })
+    
+    saveDetectionGroup(saveData).then(() => {
+      ElMessage({
+        message: '相机组添加成功',
+        type:'success',
+        plain: true,
+      });
+      createGroupDialog.value = false;
+      settingCameraRef.value?.clearForm();
+      queryCameraGroupPage();
+    })
+  }
+}
+
+const handleCancle = () => {
+  createGroupDialog.value = false
+  settingCameraRef.value?.clearForm();
+}
+
+const handleView = (row: CameraGroupTableItem) => {
+  router.push({
+    path: '/algorithm/module-camera',
+    query: {
+      groupId: row.cameraDetectionGroupId,
+    }
+  })
+}
+
+onMounted(() => {
+  queryCameraGroupPage();
+})
+</script>
+
+<style lang="scss" scoped>
+  .emptyDiv {
+    margin-top: 78px;
+    margin: auto;
+    width: 396px;
+    .emptyImg {
+      height: 257px;
+    }
+    .emptySpan {
+      font-family: PingFangSC, PingFang SC;
+      font-weight: 400;
+      font-size: 18px;
+      color: rgba(0, 0, 0, 0.45);
+      text-align: left;
+      font-style: normal;
+    }
+  }
+
+  .algoId-text {
+    cursor: pointer;
+    color: #409eff;
+  }
+
+    .deleteDialog {
+    .deleteDialogHeader {
+      display: flex;
+      .deleteTip {
+        height: 24px;
+        width: 24px;
+      }
+      
+      .titleSpan {
+        height: 24px;
+        font-size: 16px;
+        color: rgba(0, 0, 0, 0.88);
+        line-height: 24px;
+        text-align: center;
+        margin-left: 12px;
+      }
+    }
+    .dialogBottom {
+      display: flex;
+      justify-content: flex-end;
+      margin-top: 12px;
+    }
+  }
+
+  .operation {
+    display: flex;
+    align-items: center;
+
+    img {
+      margin-left: 20px;
+    }
+    
+  }
+
 
-<style lang="scss" scoped></style>
+</style>

+ 53 - 0
src/views/cameras/preview/components/CameraConfigGroup/components/CameraTreeList.vue

@@ -0,0 +1,53 @@
+<template>
+  <div class="camera-tree-list">
+    <div class="camera-tree-list__title">
+      相机列表
+    </div>
+    <div class="camera-tree-list__content">
+      <CameraTreeCom ref="cameraTreeRef" :tree-data="treeData" :is-show-checkbox="false" @node-click="handleNodeClick" />
+    </div>
+  </div>  
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';  
+import { useRoute } from 'vue-router';
+import CameraTreeCom from '@/modules/camera-tree/CameraTree.vue';
+import { CameraTree, CameraTreeNodeType, CameraQueryForm } from '@/api/camera/camera-preview';
+import { queryCameraTreeByGroupId } from '@/api/camera/camera-preview-group';
+  
+interface CameraTreeTempType extends CameraTree {
+  tempCode?: string;
+}
+
+const route = useRoute();
+const cameraTreeRef = ref<InstanceType<typeof CameraTreeCom>>();
+const treeData = ref<CameraTreeTempType[]>([]);
+const handleNodeClick = () => {
+
+}
+
+onMounted(() => {
+  if (!route.query.groupId) return;
+  queryCameraTreeByGroupId(Number(route.query.groupId)).then(res => {
+    treeData.value = res;
+  })
+})
+  
+</script>
+
+<style scoped lang="scss">
+  .camera-tree-list{
+    min-width: 270px;
+    max-width: 600px;
+    flex-shrink: 0;
+    border: 1px solid #f0f2f5;
+    margin: 5px;
+
+    .camera-tree-list__title{
+      background: #f0f2f5;
+      padding: 12px;
+      display: flex;
+    }
+  } 
+</style>

+ 442 - 0
src/views/cameras/preview/components/CameraConfigGroup/components/SettingCamera.vue

@@ -0,0 +1,442 @@
+<template>
+  <div class="setting-camera-page">
+    <div class="camera-item">
+      <p class="camera-header">
+        <el-input
+          placeholder="请输入相机名称或设备ID"
+          :prefix-icon="Search"
+          v-model="queryForm.queryString"
+          @keyup.enter="getCameraData"
+          clearable
+          @clear="getCameraData"
+        />
+      </p>
+      <div class="camera-content">
+        <el-scrollbar class="tree-scroll">
+          <CameraTreeCom
+            ref="treeRef"
+            :treeData="cameraTreeTemp"
+            :isShowCheckbox="true"
+            :enablePreview="true"
+            @node-click="handleNodeClick"
+            @check="handleTreeCheck"
+          />
+        </el-scrollbar>
+      </div>
+    </div>
+    <div class="camera-item">
+      <p class="camera-header"> 已选相机 </p>
+      <div class="camera-content">
+        <div class="select-item" v-for="item in selectedCameraList" :key="item.code">
+          <div class="select-botton">
+            <el-icon @click="handleSelectIcon(item)"
+              ><Star :color="item.isMainCamera === IsMainCamera.YES ? 'Gold' : 'gray'"
+            /></el-icon>
+          </div>
+          <div class="select-content">
+            <div class="content-main" @click="handleSelect(item)">
+              <p class="name" :class="item.isActive ? 'active' : ''">{{ item.name }}</p>
+              <p class="message">{{ item.algoName }}</p>
+            </div>
+            <el-icon class="delete-icon" @click="handleDelete(item)"><Close color="#fff" /></el-icon>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="camera-item">
+      <p class="camera-header"> 联合算法列表</p>
+      <div class="camera-content">
+        <div class="mb-2 ml-4">
+          <el-radio-group :model-value="selectedAlgo" @change="handleRiadoChange">
+            <el-radio :value="item.code" size="large" v-for="item in groupAlgoData" :key="item.id">
+              {{ item.name }}</el-radio
+            >
+          </el-radio-group>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { onMounted, ref } from 'vue';
+  import { Search, Star, Close } from '@element-plus/icons-vue';
+  import { ElMessage } from 'element-plus';
+  import { uid } from 'uid';
+  import { CameraTree, CameraTreeNodeType, CameraQueryForm } from '@/api/camera/camera-preview';
+  import { queryCameraTreeByCondition, getDetectionGroupLAlgoList } from '@/api/camera/camera-preview-group';
+  import { GropAlgoProps, SelectOption, IsMainCamera } from '@/types/camera/camera-preview';
+  import CameraTreeCom from '@/modules/camera-tree/CameraTree.vue';
+  import { IntegrationState } from '@/modules/camera-tree/types/constant';
+
+  interface CameraTreeTempType extends CameraTree {
+    tempCode?: string;
+  }
+
+  const queryForm = ref<CameraQueryForm>({
+    isEnableAlgo: false,
+    isEnableRender: false,
+    queryString: '',
+  });
+
+  const cameraTreeTemp = ref<CameraTreeTempType[]>([]); // 保存修改name之后的树
+  const selectedCameraList = ref<SelectOption[]>([]); // 保存选中的算法列表
+  const handleNodeClick = (e: CameraTreeTempType) => {
+    selectedAlgo.value = '';
+    if (e.nodeType === CameraTreeNodeType.camera) {
+      if (selectedCameraList.value.some((item) => item.code === e.id)) {
+        ElMessage({
+          message: '该相机已被选中',
+          type: 'warning',
+          plain: true,
+        });
+        return;
+      }
+
+      if (e.integrationState === IntegrationState.DISABLED) {
+        ElMessage({
+          message: '该相机未开启',
+          type: 'warning',
+          plain: true,
+        });
+        return;
+      }
+
+      selectedCameraList.value.push({
+        code: e.id,
+        name: e.name,
+        isMainCamera: selectedCameraList.value.length === 0 ? IsMainCamera.YES : IsMainCamera.NO,
+        algoName: '',
+        algoCode: '',
+        isActive: false,
+      });
+
+      // 获取当前节点在树中的完整节点对象
+      const treeNode = treeRef.value?.getCheckedNodes(e.tempCode);
+      // 勾选当前节点
+      treeRef.value?.setChecked(treeNode, true, false);
+      // 递归勾选所有父节点
+      const checkParentNodes = (node: Node) => {
+        if (node.parent) {
+          treeRef.value?.setChecked(node.parent, true, false);
+          checkParentNodes(node.parent);
+        }
+      };
+
+      if (treeNode.parent) {
+        checkParentNodes(treeNode.parent);
+      }
+    }
+  };
+
+  const handleTreeCheck = (node: CameraTreeTempType, checked: { checkedNodes: CameraTreeTempType[] }) => {
+    // 获取当前所有选中节点中的相机节点
+    const currentCameraNodes = checked.checkedNodes.filter(
+      (n) => n.nodeType === CameraTreeNodeType.camera && n.integrationState !== IntegrationState.DISABLED,
+    );
+
+    // 检查并过滤禁用节点
+    const disabledNodes = checked.checkedNodes.filter(
+      (n) => n.nodeType === CameraTreeNodeType.camera && n.integrationState === IntegrationState.DISABLED,
+    );
+    disabledNodes.forEach((n) => {
+      // ElMessage.warning(`${n.name} 已禁用,不可选中`);
+      treeRef.value?.setChecked(n.tempCode as string, false, true);
+    });
+
+    // 生成当前选中的code集合
+    const currentCodes = new Set(currentCameraNodes.map((n) => n.code));
+
+    // 移除已取消选中的项
+    selectedCameraList.value = selectedCameraList.value.filter((item: SelectOption) =>
+      currentCodes.has(item.code + ''),
+    );
+    // 添加新选中的项
+    currentCameraNodes.forEach((camera) => {
+      if (!selectedCameraList.value.some((item) => item.code === camera.id)) {
+        selectedCameraList.value.push({
+          code: camera.id,
+          name: camera.name.replace(/ \[\w+\] $/, ''),
+          isMainCamera: selectedCameraList.value.length === 0 ? IsMainCamera.YES : IsMainCamera.NO,
+          algoName: '',
+          algoCode: '',
+          isActive: false,
+        });
+      }
+    });
+  };
+
+  const getCameraData = async () => {
+    await queryCameraTreeByCondition(queryForm.value).then((res) => {
+      cameraTreeTemp.value = getCameraNameCode(res);
+    });
+  };
+
+  // 把树节点中所有 nodeType = camera 的 name 替换成 name + code
+  function getCameraNameCode(data) {
+    const cameraNameCode = data;
+    for (let i = 0; i < data.length; i++) {
+      const node = cameraNameCode[i];
+      node.tempCode = uid(); // 为相机树节点创建唯一code
+      if (node.nodeType === 'camera') {
+        node.name = node.name + ` [${node.code}] `;
+      }
+      if (node.children && node.children.length > 0) {
+        getCameraNameCode(node.children);
+      }
+    }
+    return cameraNameCode;
+  }
+
+  const groupAlgoData = ref<GropAlgoProps[]>([]);
+  const getGroupLAlgoList = () => {
+    getDetectionGroupLAlgoList().then((res) => {
+      groupAlgoData.value = res;
+    });
+  };
+
+  const handleSelect = (row: SelectOption) => {
+    selectedCameraList.value.map((item) => {
+      if (item.code === row.code) {
+        item.isActive = true;
+      } else {
+        item.isActive = false;
+      }
+    });
+    selectedAlgo.value = '';
+  };
+
+  const handleSelectIcon = (row: SelectOption) => {
+    selectedCameraList.value.map((item) => {
+      if (item.code === row.code) {
+        item.isMainCamera = item.isMainCamera === IsMainCamera.YES ? IsMainCamera.NO : IsMainCamera.YES;
+      } else {
+        item.isMainCamera = IsMainCamera.NO;
+      }
+    });
+  };
+
+  const treeRef = ref<InstanceType<typeof CameraTreeCom>>();
+  const handleDelete = (row: SelectOption) => {
+    // 从已选列表删除
+    selectedCameraList.value = selectedCameraList.value.filter((item) => item.code !== row.code);
+
+    // 同步取消树节点的选中状态
+    const findNode = (nodes: CameraTreeTempType[]): CameraTreeTempType | undefined => {
+      for (const node of nodes) {
+        if (node.id === row.code && node.nodeType === CameraTreeNodeType.camera) return node;
+        if (node.children) {
+          const found = findNode(node.children);
+          if (found) return found;
+        }
+      }
+    };
+
+    const targetNode = findNode(cameraTreeTemp.value);
+    if (targetNode?.tempCode) {
+      treeRef.value?.setChecked(targetNode.tempCode, false, false);
+    }
+  };
+
+  const selectedAlgo = ref();
+  const handleRiadoChange = (value) => {
+    const selectedCameraItem = selectedCameraList.value.filter((item) => item.isActive);
+    if (selectedCameraItem.length === 0) {
+      ElMessage({
+        message: '请先选中的相机',
+        type: 'warning',
+        plain: true,
+      });
+      return;
+    }
+    const selectedAlgoItem = groupAlgoData.value.find((item) => item.code === value);
+
+    selectedCameraItem.forEach((item) => {
+      item.algoName = selectedAlgoItem?.name as unknown as string;
+      item.algoCode = selectedAlgoItem?.code as unknown as string;
+    });
+
+    selectedAlgo.value = value;
+  };
+
+  const isValidate = () => {
+    // 校验至少选择两个相机
+    if (selectedCameraList.value.length < 2) {
+      ElMessage.error('请至少选择两个相机');
+      return { valid: false, message: '相机数量不足' };
+    }
+
+    // 校验所有选中相机都已配置算法
+    const unsetAlgoCameras = selectedCameraList.value.filter((item) => !item.algoCode || item.algoCode === '');
+
+    if (unsetAlgoCameras.length > 0) {
+      ElMessage.error('存在未选择算法的相机');
+      return { valid: false, message: '算法未配置完整' };
+    }
+
+    return { valid: true, data: selectedCameraList.value };
+  };
+
+  // 重置设置相机组
+  const clearForm = () => {
+    selectedCameraList.value = [];
+    selectedAlgo.value = '';
+    queryForm.value = {
+      isEnableAlgo: false,
+      isEnableRender: false,
+      queryString: '',
+    };
+    //  清除树组件的所有选中状态
+    if (treeRef.value) {
+      treeRef.value.setCheckedKeys([]);
+    }
+  };
+
+  // 暴露校验方法给父组件
+  defineExpose({
+    isValidate,
+    clearForm,
+  });
+
+  onMounted(() => {
+    getCameraData();
+    getGroupLAlgoList();
+  });
+</script>
+
+<style scoped lang="scss">
+  .setting-camera-page {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    gap: 20px;
+
+    .camera-item {
+      flex: 1;
+
+      &:first-child {
+        .camera-header {
+          padding-left: 0;
+        }
+      }
+
+      .camera-header {
+        width: 100%;
+        height: 40px;
+        border-radius: 2px;
+        font-size: 15px;
+        line-height: 40px;
+        color: #333;
+        cursor: pointer;
+        background-color: #dcdfe6;
+        padding-left: 20px;
+        display: flex;
+        align-items: center;
+
+        :deep(.el-input--default) {
+          width: 95%;
+          margin: 0 auto;
+        }
+
+        :deep(.el-input__wrapper) {
+          border-radius: 50px;
+        }
+      }
+
+      .camera-content {
+        width: 100%;
+        height: 400px;
+        background-color: #fff;
+        overflow-y: auto;
+        border: 1px solid #dcdfe6;
+        padding: 10px 20px;
+
+        .select-item {
+          display: flex;
+          align-self: start;
+          justify-content: center;
+          margin-bottom: 10px;
+
+          &:last-child {
+            margin-bottom: 0;
+          }
+
+          .select-botton {
+            cursor: pointer;
+          }
+
+          .select-content {
+            position: relative; // 新增
+            margin-left: 5px;
+            width: 100%;
+
+            .name {
+              color: #333;
+              padding-left: 5px;
+            }
+
+            .message {
+              color: #dcdfe6;
+              padding-left: 5px;
+            }
+
+            &:hover {
+              background-color: #409eff;
+              .name {
+                color: #fff;
+              }
+              .delete-icon {
+                display: block;
+              }
+            }
+
+            .delete-icon {
+              // 新增删除图标样式
+              display: none;
+              position: absolute;
+              right: 10px;
+              top: 50%;
+              transform: translateY(-50%);
+              width: 16px;
+              height: 16px;
+              cursor: pointer;
+            }
+
+            .content-main {
+              // 新增内容容器
+              flex: 1;
+              cursor: pointer;
+            }
+
+            .active {
+              color: #0052d9;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  .el-input__icon {
+    cursor: pointer;
+  }
+
+  .el-input__icon:hover {
+    color: #0052d9;
+  }
+
+  .cameraTreeCheckboxWrapper {
+    display: flex;
+    justify-content: space-between;
+
+    .el-checkbox {
+      margin-right: 0;
+    }
+  }
+
+  :deep(.el-radio-group) {
+    flex-direction: column;
+    align-items: flex-start;
+  }
+</style>

+ 51 - 0
src/views/cameras/preview/components/CameraConfigGroup/hooks/useCameraGroupQuery.ts

@@ -0,0 +1,51 @@
+import { reactive, ref, shallowRef } from 'vue';
+import { cloneDeep } from 'lodash-es';
+import { DEFAULT_PAGE_SIZE } from '@/types/common/constants';
+import { CameraPreviewRequest, CameraGroupTableItem, CameraGroupItem } from '@/types/camera/camera-preview';
+import { queryDetectionGroupList } from '@/api/camera/camera-preview-group';
+
+const defaultLoginLogRequest: CameraPreviewRequest = {
+  pageNumber: 1,
+  pageSize: DEFAULT_PAGE_SIZE,
+};
+
+export default function useLoginLogRequest() {
+  const requestParams = reactive<CameraPreviewRequest>(cloneDeep(defaultLoginLogRequest));
+  const cameraGroupList = shallowRef<CameraGroupTableItem[]>([]);
+  const total = ref(0);
+  const loading = ref(false);
+
+  const queryCameraGroupPage = async () => {
+    try {
+      loading.value = true;
+      const data = await queryDetectionGroupList(requestParams);
+      cameraGroupList.value = transformCameraGroupList(data.records);
+      total.value = data.totalRow;
+    } catch (error) {
+      console.log(error);
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  const transformCameraGroupList = (data: CameraGroupItem[]) => {
+    let groupId = 0;
+    return data.flatMap(group => {
+      groupId++;
+      return group.groupDetailList.map(detail => ({
+        id: groupId,  // 添加自增ID
+        cameraDetectionGroupId: group.cameraDetectionGroupId,
+        status: group.status,
+        ...detail
+      }));
+    });
+  }
+
+  return {
+    requestParams,
+    total,
+    loading,
+    cameraGroupList,
+    queryCameraGroupPage,
+  };
+}

+ 8 - 2
src/views/datamanager/playback/components/NvrCameraView.vue

@@ -37,7 +37,12 @@
     </div>
     <div class="nvr-setting-bar">
       <NvrVioCheckbox :available="confirmDate" :camera-id="cameraId" @check-tags="handleVioTags" />
-      <NvrTimeSelect ref="nvrTimeSelectRef" @set-Time="handleSetTime" @download-nvr="handleDownloadNvr" />
+      <NvrTimeSelect
+        ref="nvrTimeSelectRef"
+        @set-Time="handleSetTime"
+        @download-nvr="handleDownloadNvr"
+        v-permission="{ action: [PERM_DATA.PLAYBACK_DOWNLOAD] }"
+      />
     </div>
     <a ref="downloadRef" style="display: none" href="" download />
   </div>
@@ -65,6 +70,7 @@
   } from '@/api/datamanagement/playback';
   import useCameraAlgoStore from '@/views/cameras/preview/store/useCameraAlgoStore';
   import { useFullscreen } from 'vue-hooks-plus';
+  import { PERM_DATA } from '@/types/permission/constants';
 
   const cameraAlgoStore = useCameraAlgoStore();
   defineProps<{ cameraId: number }>();
@@ -298,7 +304,7 @@
 
   .nvr-setting-bar {
     display: flex;
-    justify-content: center;
+    // justify-content: center;
   }
 
   .fullscreen-icon {

+ 4 - 24
src/views/datamanager/playback/components/NvrTimeSelect.vue

@@ -24,18 +24,8 @@
           disabled
           placeholder="点击选择时间"
         /> -->
-        <el-tooltip
-          class="picker-tooltip"
-          effect="dark"
-          content="请先拖动进度条,再点击按钮选择时间"
-          placement="top"
-        >
-          <img
-            @click="callSetTime(true)"
-            style="cursor: pointer"
-            src="@\assets\icons\icon-picker.png"
-            alt=""
-          />
+        <el-tooltip class="picker-tooltip" effect="dark" content="请先拖动进度条,再点击按钮选择时间" placement="top">
+          <img @click="callSetTime(true)" style="cursor: pointer" src="@\assets\icons\icon-picker.png" alt="" />
         </el-tooltip>
         <el-icon class="icon-refresh" :size="18">
           <Refresh @click="startTime = ''" />
@@ -60,18 +50,8 @@
           disabled
           placeholder="点击选择时间"
         /> -->
-        <el-tooltip
-          class="picker-tooltip"
-          effect="dark"
-          content="请先拖动进度条,再点击按钮选择时间"
-          placement="top"
-        >
-          <img
-            @click="callSetTime(false)"
-            style="cursor: pointer"
-            src="@\assets\icons\icon-picker.png"
-            alt=""
-          />
+        <el-tooltip class="picker-tooltip" effect="dark" content="请先拖动进度条,再点击按钮选择时间" placement="top">
+          <img @click="callSetTime(false)" style="cursor: pointer" src="@\assets\icons\icon-picker.png" alt="" />
         </el-tooltip>
         <el-icon class="icon-refresh" :size="18">
           <Refresh @click="endTime = ''" />

+ 23 - 3
src/views/map-config/mini-map/MiniMapConfig.vue

@@ -12,6 +12,7 @@
           class="avatar-uploader"
           :action="actionUrl"
           :show-file-list="false"
+          :before-upload="handleBeforeUpload"
           :on-success="handleAvatarSuccess"
           :with-credentials="true"
           name="file"
@@ -126,7 +127,7 @@
   const isUploadBg = ref<boolean>(true);
 
   const isMap = ref(false);
-  const { urlPrefix } = useGlobSetting();
+  const { urlPrefix, minifyImgUrl } = useGlobSetting();
 
   const actionUrl = computed(() => {
     return urlJoin(urlPrefix!, `/admin/minimap/uploadPicture`);
@@ -159,12 +160,31 @@
     hasBg.value = false;
   };
 
-  const handleBeforeUpload = () => {
+  const handleBeforeUpload = (rawFile) => {
     if (!selectedShopId.value) {
       ElMessage.error({
         message: '请先选择车间',
       });
-      return false;
+      return Promise.reject();
+    }
+    if (rawFile.type !== 'image/jpg' && rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') {
+      ElMessage.error('请上传jpg、jpeg、png格式的图片!');
+      return Promise.reject();
+    }
+    const mSize = 1024 * 1024;
+    if (rawFile.size > 2 * mSize) {
+      const realSize = (rawFile.size / mSize).toFixed(2);
+      ElMessageBox.alert(`<div>当前图片${realSize}M,建议压缩到2M以内。 <div>`, '提示', {
+        dangerouslyUseHTMLString: true,
+        confirmButtonText: minifyImgUrl ? '点我去压缩' : '确定',
+        type: 'warning',
+      }).then(() => {
+        if (minifyImgUrl) {
+          window.open(minifyImgUrl);
+        }
+      });
+
+      return Promise.reject();
     }
   };
 

+ 1 - 1
src/views/message/question-notifications/QuestionNotifications.vue

@@ -40,7 +40,7 @@
     <el-dialog
       v-model="showDialog"
       title="编辑推送文案"
-      align-center="true"
+      align-center
       width="400"
       @close="closeDialog()"
       :close-on-click-modal="false"

+ 61 - 1
src/views/message/question-notifications/components/contentPanel.vue

@@ -57,6 +57,7 @@
         size="small"
         style="height: 16px; margin-left: 12px"
       />
+      <div v-show="showError === true && currentSet === 'atExpiry'" class="subtitle_error">{{ errorMessage }}</div>
     </div>
     <div
       v-show="props.issueType != QuestionStatus.finishe"
@@ -72,6 +73,7 @@
         size="small"
         style="width: 70px; margin-left: 4px; margin-right: 4px"
         controls-position="right"
+        @change="currentSet = 'atExpiry'"
       />小时未{{ props.title }}继续推送给{{ props.title }}员</div
     >
     <div v-show="props.issueType != QuestionStatus.finishe" class="subtitle"
@@ -82,6 +84,9 @@
         size="small"
         style="height: 16px; margin-left: 12px"
       />
+      <div v-show="showError === true && currentSet === 'atLongTimeExpiry'" class="subtitle_error">
+        {{ errorMessage }}
+      </div>
     </div>
     <div
       v-show="props.issueType != QuestionStatus.finishe"
@@ -97,6 +102,7 @@
         size="small"
         style="width: 70px; margin-left: 4px; margin-right: 4px"
         controls-position="right"
+        @change="currentSet = 'atLongTimeExpiry'"
       />小时未{{ props.title }}再次推送给{{ props.title }}员并抄送给
       <el-select
         :disabled="!editDetails.openEdit || !editDetails.atLongTimeExpiry"
@@ -147,9 +153,10 @@
 <script lang="ts" setup>
   import { panelDetails, QuestionStatus, issueDetilasType, PushTypeStatus } from '../type';
   import { EditPen } from '@element-plus/icons-vue';
-  import { ref, watch } from 'vue';
+  import { ref, watch, computed } from 'vue';
   import { SelectedFilterPersonInfo } from '@/api/message/person-group';
   import PersonFilterSelection from '@/views/message/components/PersonFilterSelection.vue';
+  import { ElMessage } from 'element-plus';
 
   const props = defineProps<{
     savedData: Array<issueDetilasType>;
@@ -174,6 +181,10 @@
     editDetails.value.openEdit = true;
   };
   const saveQuestionEdit = () => {
+    if (showError.value) {
+      ElMessage.error('保存失败');
+      return;
+    }
     props.saveUpdate(editDetails.value, props.issueType);
     editDetails.value.openEdit = false;
   };
@@ -181,6 +192,43 @@
   const dialogVisible = ref<boolean>(false);
   const selectedUser = ref<SelectedFilterPersonInfo[]>([]);
 
+  //是否显示错误提示
+  const showError = ref<boolean>(false);
+  //该值为atExpiry或atLongTimeExpiry
+  const currentSet = ref('atExpiry');
+
+  const errorMessage = computed(() => {
+    if (editDetails.value.atExpiry && editDetails.value.atLongTimeExpiry) {
+      if (
+        editDetails.value.expiryTime! > editDetails.value.longTimeValue! ||
+        editDetails.value.expiryTime! === editDetails.value.longTimeValue!
+      ) {
+        if (currentSet.value === 'atExpiry') {
+          showError.value = true;
+          return '超期时长应小于长期时长';
+        } else {
+          showError.value = true;
+          return '长期时长应大于超期时长';
+        }
+      }
+    }
+    if (editDetails.value.atExpiry && currentSet.value === 'atExpiry' && editDetails.value.expiryTime === 0) {
+      showError.value = true;
+      return '超期时长不可为0';
+    }
+    if (
+      editDetails.value.atLongTimeExpiry &&
+      currentSet.value === 'atLongTimeExpiry' &&
+      editDetails.value.longTimeValue === 0
+    ) {
+      showError.value = true;
+      return '长期时长不可为0';
+    }
+    //上面的条件一个都不满足时
+    showError.value = false;
+    return '';
+  });
+
   const openNameTree = () => {
     //如果非编辑状态或长期的开关处于关闭状态则什么都不干
     if (!editDetails.value.atLongTimeExpiry || !editDetails.value.openEdit) {
@@ -286,6 +334,14 @@
       text-align: left;
       display: flex;
       flex-direction: row;
+      .subtitle_error {
+        height: 100%;
+        color: red;
+        font-weight: 400;
+        font-size: 10px;
+        line-height: 15px;
+        margin-left: 12px;
+      }
     }
     .subtitle_explain_disable {
       height: 21px;
@@ -314,4 +370,8 @@
   :deep(.el-textarea__inner) {
     font-size: 10px;
   }
+  :deep(.el-input-number.is-controls-right .el-input__wrapper) {
+    padding-left: 0;
+    padding-right: 24px;
+  }
 </style>

+ 17 - 2
src/views/page-config/ConfigEdit.vue

@@ -125,7 +125,7 @@
 
   const pageConfig = usePageConfig();
   const { label, layoutId } = pageConfig;
-  const { urlPrefix } = useGlobSetting();
+  const { urlPrefix, minifyImgUrl } = useGlobSetting();
   const drawContainer = ref<HTMLDivElement>();
   const mapContainerRef = ref();
 
@@ -154,7 +154,22 @@
   const handleBeforeUpload = (rawFile) => {
     if (rawFile.type !== 'image/jpg' && rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') {
       ElMessage.error('请上传jpg、jpeg、png格式的图片!');
-      return false;
+      return Promise.reject();
+    }
+    const mSize = 1024 * 1024;
+    if (rawFile.size > 2 * mSize) {
+      const realSize = (rawFile.size / mSize).toFixed(2);
+      ElMessageBox.alert(`<div>当前图片${realSize}M,建议压缩到2M以内。 <div>`, '提示', {
+        dangerouslyUseHTMLString: true,
+        confirmButtonText: minifyImgUrl ? '点我去压缩' : '确定',
+        type: 'warning',
+      }).then(() => {
+        if (minifyImgUrl) {
+          window.open(minifyImgUrl);
+        }
+      });
+
+      return Promise.reject();
     }
   };
 

+ 5 - 15
src/views/system-config/scene-manage/SceneManage.vue

@@ -58,7 +58,8 @@
         @on-ok="subWorkshop"
       />
       <!-- 工位 -->
-      <WorkspaceDrawer_shangfei
+      <!-- 上飞定制环境中工位采用自由输入,不采用下拉框 -->
+      <WorkspaceDrawer
         v-if="showDrawer === DrawerType.workspace"
         :detail="detail"
         @on-close="handleUpdataWorkspaceTab"
@@ -107,18 +108,8 @@
   import { DrawerType } from '@/types/scene/constant.ts';
   import SceneDialog from './components/SceneDialog.vue';
   import { colomns } from './hook/use-table-method';
-  import {
-    delCompany,
-    delWorkshop,
-    delWorkspace,
-    updateComShopSpaceTreeSort,
-  } from '@/api/scene/scene';
-  import {
-    ComTreeType,
-    UseComType,
-    UseWorkshopType,
-    UseWorkspaceType,
-  } from '@/types/scene/type.ts';
+  import { delCompany, delWorkshop, delWorkspace, updateComShopSpaceTreeSort } from '@/api/scene/scene';
+  import { ComTreeType, UseComType, UseWorkshopType, UseWorkspaceType } from '@/types/scene/type.ts';
   import useComTree from './store/use-com-tree';
   import { storeToRefs } from 'pinia';
   import { useGlobSetting } from '@/hooks/setting';
@@ -229,8 +220,7 @@
   };
 
   // 公司,车间,工位的模板数据
-  const editedItem: Ref<(UseComType<any> & UseWorkshopType<any> & UseWorkspaceType) | undefined> =
-    ref();
+  const editedItem: Ref<(UseComType<any> & UseWorkshopType<any> & UseWorkspaceType) | undefined> = ref();
 
   //点击所有添加和编辑时显示的数据内容
   const detail = ref({});

+ 11 - 10
src/views/system/comments/PageCommentsManage.vue

@@ -5,29 +5,30 @@
         <el-form :inline="true" :model="listFilter" class="demo-form-inline">
           <el-form-item label="审核状态:">
             <el-select v-model="listFilter.authStatus" placeholder="请选择审核状态" @change="getList">
-              <el-option label="全部" :value="COMMENTSTATUS.all" />
-              <el-option label="已通过" :value="COMMENTSTATUS.passed" />
-              <el-option label="未通过" :value="COMMENTSTATUS.rejected" />
+              <el-option label="全部" :value="COMMENT_STATUS.ALL" />
+              <el-option label="已通过" :value="COMMENT_STATUS.PASSED" />
+              <el-option label="未通过" :value="COMMENT_STATUS.REJECTED" />
             </el-select>
           </el-form-item>
           <el-form-item label="回复状态:">
             <el-select v-model="listFilter.replyStatus" placeholder="请选择审核状态" @change="getList">
-              <el-option label="全部" :value="REPLYSTATUS.all" />
-              <el-option label="已回复" :value="REPLYSTATUS.replied" />
-              <el-option label="未回复" :value="REPLYSTATUS.unReplied" />
+              <el-option label="全部" :value="REPLY_STATUS.ALL" />
+              <el-option label="已回复" :value="REPLY_STATUS.REPLIED" />
+              <el-option label="未回复" :value="REPLY_STATUS.UN_REPLIED" />
             </el-select>
           </el-form-item>
         </el-form>
       </div>
     </div>
-    <div class="problem-list"
-      ><SingleComment
+    <div class="problem-list">
+      <SingleComment
         v-for="(item, index) in commentsList"
         :key="index"
         :problem-data="item"
         @reFreshList="getList"
         style="margin-top: 22px; margin-bottom: 2px"
-    /></div>
+      />
+    </div>
 
     <el-pagination
       v-model:current-page="pageNumber"
@@ -46,7 +47,7 @@
 <script setup lang="ts">
   import SingleComment from './component/SingleComment.vue';
   import useComments from './use-comments.ts';
-  import { REPLYSTATUS, COMMENTSTATUS } from '@/types/comments/constant.ts';
+  import { REPLY_STATUS, COMMENT_STATUS } from '@/types/comments/constant.ts';
 
   const useCommentsList = useComments();
   const { commentsList, pageNumber, pageSize, totalRow, getList, listFilter } = useCommentsList;

+ 200 - 100
src/views/system/comments/component/SingleComment.vue

@@ -1,99 +1,132 @@
 <template>
-  <div
-    class="single-item"
-    :style="`padding-bottom:${props.problemData.isReplied === REPLYSTATUS.replied ? '16px' : '38px'}`"
-  >
-    <div style="display: flex; font-size: 12px">
-      <div style="color: #00000073">评论人:{{ props.problemData.userName }}-{{ props.problemData.staffNo }} </div>
-      <div style="margin-left: 20px; color: #00000073">日期:{{ props.problemData.createdAt }}</div>
-      <!-- <img src="@/assets/icons/phone.png" alt="" /> -->
-      <div class="single-contact">联系方式:{{ props.problemData.mobile }}</div>
-    </div>
-    <div class="buttonBar">
-      <el-button
-        v-show="props.problemData.status === COMMENTSTATUS.passed"
-        type="primary"
-        style="width: 74px; margin-left: auto; margin-right: 0"
-        @click="hideComment"
-        >隐藏</el-button
-      >
-      <el-button
-        v-show="props.problemData.status === COMMENTSTATUS.rejected"
-        type="primary"
-        style="width: 74px; margin-left: auto; margin-right: 0"
-        @click="displayComment"
-        >显示</el-button
-      >
-      <el-button
-        v-show="
-          props.problemData.isReplied === REPLYSTATUS.unReplied && props.problemData.status === COMMENTSTATUS.passed
-        "
-        style="width: 74px"
-        type="primary"
-        @click="openReply = true"
-        >回复</el-button
-      >
-      <el-button v-show="props.problemData.isReplied === REPLYSTATUS.replied" style="width: 74px" disabled
-        >已回复</el-button
-      >
-    </div>
-
-    <el-divider />
-    <div class="problem-describe">
-      <div>评论内容:</div>
-      <div class="problem-content">{{ props.problemData.comment }}</div>
+  <div>
+    <div
+      class="single-item"
+      :style="`padding-bottom:${props.problemData.isReplied === REPLY_STATUS.REPLIED ? '16px' : '38px'}`"
+    >
       <div v-show="props.problemData.isUserDeleted === 1" class="delete-label"></div>
-    </div>
-    <div class="problem-picture">
-      <div class="picture-content" v-for="(item, index) in problemImageUrls" :key="index">
-        <el-image
-          style="width: 80px; height: 80px"
-          :src="item"
-          :zoom-rate="1.2"
-          :max-scale="7"
-          :min-scale="0.2"
-          :preview-src-list="problemImageUrls"
-          :initial-index="index"
-          fit="cover"
-        />
-      </div>
-    </div>
+      <div :style="{ opacity: props.problemData.isUserDeleted === 1 ? 0.6 : 1 }">
+        <div style="display: flex; font-size: 12px">
+          <div style="color: #00000073">
+            评论人:{{ props.problemData.realname }}({{ props.problemData.userName }} )
+          </div>
+          <div style="margin-left: 20px; color: #00000073">日期:{{ props.problemData.createdAt }}</div>
+          <!-- <img src="@/assets/icons/phone.png" alt="" /> -->
+          <div class="single-contact">联系方式:{{ props.problemData.mobile }}</div>
+        </div>
+        <div class="buttonBar">
+          <el-button
+            v-show="props.problemData.status === COMMENT_STATUS.PASSED"
+            type="primary"
+            class="label-button"
+            style="margin-left: auto; margin-right: 0"
+            @click="showDialogBox('隐藏', '取消展示')"
+            :disabled="props.problemData.isUserDeleted"
+          >
+            隐藏
+          </el-button>
+          <el-button
+            v-show="props.problemData.status !== COMMENT_STATUS.PASSED"
+            type="primary"
+            class="label-button"
+            style="margin-left: auto; margin-right: 0"
+            @click="showDialogBox('显示', '公开展示')"
+            :disabled="props.problemData.isUserDeleted"
+          >
+            显示
+          </el-button>
+          <el-button
+            v-show="
+              props.problemData.isReplied === REPLY_STATUS.UN_REPLIED &&
+              props.problemData.status === COMMENT_STATUS.PASSED
+            "
+            class="label-button"
+            type="primary"
+            @click="openReply = true"
+            :disabled="props.problemData.isUserDeleted"
+          >
+            回复
+          </el-button>
+          <el-button v-show="props.problemData.isReplied === REPLY_STATUS.REPLIED" class="label-button" disabled>
+            已回复
+          </el-button>
+        </div>
 
-    <div v-if="openReply === true" style="position: relative">
-      <el-input placeholder="请输入回复(不超过30个字符)" type="textarea" v-model="replyContent" rows="4"> </el-input>
-      <span
-        style="
-          position: absolute;
-          left: 10px;
-          padding-bottom: 5px;
-          bottom: 2px;
-          color: grey;
-          background-color: #fff;
-          line-height: 0;
-          font-size: 12px;
-        "
-      >
-        <span style="line-height: normal" :style="replyContent.length > 203 ? 'color:red' : ''">{{
-          replyContent.length - 3
-        }}</span>
-        <span style="line-height: normal">/200</span>
-      </span>
-      <div style="position: absolute; height: 32px; right: 10px; padding-bottom: 5px; bottom: 2px">
-        <el-button style="margin-top: 3px" type="primary" size="small" @click="submitReply">发布</el-button>
-        <el-button style="margin-top: 3px" size="small" @click="openReply = false">取消</el-button>
-      </div>
-    </div>
+        <el-divider />
+        <div class="problem-describe">
+          <div>评论内容:</div>
+          <div class="problem-content">{{ props.problemData.comment }}</div>
+        </div>
+        <div class="problem-picture">
+          <div class="picture-content" v-for="(item, index) in problemImageUrls" :key="index">
+            <el-image
+              style="width: 80px; height: 80px"
+              :src="item"
+              :zoom-rate="1.2"
+              :max-scale="7"
+              :min-scale="0.2"
+              :preview-src-list="problemImageUrls"
+              :initial-index="index"
+              fit="cover"
+            />
+          </div>
+        </div>
+
+        <div v-if="openReply === true" style="position: relative">
+          <el-input
+            placeholder="请输入回复(不超过200个字符)"
+            type="textarea"
+            v-model="replyContent"
+            rows="4"
+            maxlength="200"
+            resize="none"
+          >
+          </el-input>
+          <span v-show="replyContent.length > 185" class="word-count">
+            <span style="line-height: normal" :style="replyContent.length === 200 ? 'color:red' : ''">
+              {{ replyContent.length }}
+            </span>
+            <span style="line-height: normal">/200</span>
+          </span>
+          <div class="reply-button-area">
+            <el-button
+              style="margin-top: 3px"
+              type="primary"
+              size="small"
+              @click="submitReply"
+              :disabled="replyContent.length === 0"
+            >
+              发布
+            </el-button>
+            <el-button style="margin-top: 3px" size="small" @click="openReply = false">取消</el-button>
+          </div>
+        </div>
 
-    <div v-if="props.problemData.isReplied === REPLYSTATUS.replied" style="position: relative">
-      <el-input type="textarea" v-model="props.problemData.reply" rows="3" disabled> </el-input>
+        <div v-if="props.problemData.isReplied === REPLY_STATUS.REPLIED" style="position: relative">
+          <el-input type="textarea" v-model="props.problemData.reply" rows="3" disabled> </el-input>
+        </div>
+      </div>
     </div>
+    <el-dialog v-model="showDialog" width="424px" top="20%" class="dialog">
+      <template #header="">
+        <div class="dialogHeader">
+          <img src="@/assets/icons/deleteTip.png" class="deleteTip" />
+          <span class="titleSpan">请确认是否{{ title }}</span>
+        </div>
+      </template>
+      <span style="margin-left: 37px">{{ title }}后,该评论将在前台{{ result }}</span>
+      <div class="dialogBottom">
+        <el-button class="dialogBtn" type="primary" @click="submit">确定</el-button>
+        <el-button class="dialogBtn" @click="showDialog = false">取消</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
   import { ref, computed } from 'vue';
   import { undateCommentStatus, replyComment } from '@/api/comments/comments';
-  import { REPLYSTATUS, COMMENTSTATUS } from '@/types/comments/constant';
+  import { REPLY_STATUS, COMMENT_STATUS } from '@/types/comments/constant';
   import { Records } from '@/types/comments/type';
   const props = defineProps<{
     problemData: Records;
@@ -106,13 +139,33 @@
     return imageUrlString ? imageUrlString.split(',') : [];
   });
 
+  const showDialog = ref(false);
+  const title = ref('显示'); //该值为显示或隐藏
+  const result = ref('公开展示'); //该值为公开展示或取消展示
+
+  const showDialogBox = (setTitle: string, setResult: string) => {
+    title.value = setTitle;
+    result.value = setResult;
+    showDialog.value = true;
+  };
+
+  const submit = () => {
+    if (title.value === '显示') {
+      displayComment();
+    } else {
+      hideComment();
+    }
+  };
+
   const hideComment = () => {
-    undateCommentStatus({ id: props.problemData.id, status: COMMENTSTATUS.rejected }).then(() => {
+    undateCommentStatus({ id: props.problemData.id, status: COMMENT_STATUS.REJECTED }).then(() => {
+      showDialog.value = false;
       emit('reFreshList');
     });
   };
   const displayComment = () => {
-    undateCommentStatus({ id: props.problemData.id, status: COMMENTSTATUS.passed }).then(() => {
+    undateCommentStatus({ id: props.problemData.id, status: COMMENT_STATUS.PASSED }).then(() => {
+      showDialog.value = false;
       emit('reFreshList');
     });
   };
@@ -125,7 +178,7 @@
 
   const openReply = ref(false);
 
-  const replyContent = ref('回复:');
+  const replyContent = ref('');
 </script>
 
 <style scoped>
@@ -135,6 +188,23 @@
     border-radius: 6px;
     padding: 16px 12px 0px 12px;
     position: relative;
+    margin-top: 22px;
+    margin-bottom: 2px;
+  }
+
+  .delete-label {
+    width: 80px;
+    height: 70px;
+    position: absolute;
+    right: 40px;
+    top: 50px;
+    background-image: url('@/assets/icons/deleted.png');
+    background-size: 100% 100%;
+    z-index: 99;
+  }
+
+  .label-button {
+    width: 74px;
   }
 
   .single-contact {
@@ -166,18 +236,6 @@
     word-break: break-word;
     display: flex;
     margin-bottom: 8px;
-    .delete-label {
-      width: 100px;
-      height: 60px;
-      position: absolute;
-      right: 80px;
-      top: 60px;
-      background-image: url('@/assets/icons/config-fail.png');
-      background-size: 100% 100%;
-      cursor: pointer;
-
-      background-size: 100% 100%;
-    }
   }
 
   .type-content,
@@ -193,6 +251,48 @@
     gap: 20px;
   }
 
+  .word-count {
+    position: absolute;
+    left: 10px;
+    padding-bottom: 5px;
+    bottom: 2px;
+    color: grey;
+    background-color: #fff;
+    line-height: 0;
+    font-size: 12px;
+  }
+
+  .reply-button-area {
+    position: absolute;
+    height: 32px;
+    right: 10px;
+    padding-bottom: 5px;
+    bottom: 2px;
+  }
+
+  .dialog {
+    .dialogHeader {
+      display: flex;
+      .deleteTip {
+        height: 24px;
+        width: 24px;
+      }
+      .titleSpan {
+        height: 24px;
+        font-size: 16px;
+        color: rgba(0, 0, 0, 0.88);
+        line-height: 24px;
+        text-align: center;
+        margin-left: 12px;
+      }
+    }
+    .dialogBottom {
+      display: flex;
+      justify-content: flex-end;
+      margin-top: 12px;
+    }
+  }
+
   :deep(.el-collapse-item__header) {
     border-bottom: none;
   }

+ 10 - 6
src/views/system/comments/use-comments.ts

@@ -1,5 +1,5 @@
 import { getCommentsList } from '@/api/comments/comments';
-import { REPLYSTATUS, COMMENTSTATUS } from '@/types/comments/constant';
+import { REPLY_STATUS, COMMENT_STATUS } from '@/types/comments/constant';
 import { Records, CommentsQuery } from '@/types/comments/type';
 import { onMounted, ref } from 'vue';
 
@@ -11,8 +11,8 @@ export function useCommentsList() {
   const totalRow = ref<number>();
 
   const listFilter = ref({
-    authStatus: COMMENTSTATUS.all,
-    replyStatus: REPLYSTATUS.all,
+    authStatus: COMMENT_STATUS.ALL,
+    replyStatus: REPLY_STATUS.ALL,
   });
 
   const getList = () => {
@@ -20,19 +20,23 @@ export function useCommentsList() {
       pageNumber: pageNumber.value,
       pageSize: pageSize.value,
     };
-    if (listFilter.value.authStatus !== COMMENTSTATUS.all) {
+    if (listFilter.value.authStatus !== COMMENT_STATUS.ALL) {
       //若传1,则拉取审核通过的;若传0,则拉取审核未通过的和未审核的
       params.queryParam = {
-        isApproved: listFilter.value.authStatus === COMMENTSTATUS.passed ? 1 : 0,
+        isApproved: listFilter.value.authStatus === COMMENT_STATUS.PASSED ? 1 : 0,
       };
     }
-    if (listFilter.value.replyStatus !== REPLYSTATUS.all) {
+    if (listFilter.value.replyStatus !== REPLY_STATUS.ALL) {
       params.queryParam = {
         isReplied: listFilter.value.replyStatus,
       };
     }
     getCommentsList(params).then((res) => {
       commentsList.value = res.records;
+      // 若有回复,将回复内容前面加上“回复:”
+      commentsList.value.forEach((item) => {
+        item.reply = '回复: ' + item.reply;
+      });
       totalPage.value = res.totalPage;
       totalRow.value = res.totalRow;
     });

+ 2 - 0
types/config.d.ts

@@ -118,4 +118,6 @@ export interface GlobConfig {
   imgUrl: string | undefined;
 
   tenantCode: string;
+
+  minifyImgUrl?: string;
 }

+ 3 - 2
utils/devProxy/local/app.config.js

@@ -14,8 +14,9 @@ window.__PRODUCTION__SKYEYEADMIN__CONF__ = {
   /** 问题闭环处理,简单处理 */
   "VITE_GLOB_QUESTION_LIST_VERSION": 'v2',
   // 消息管理可选择的推送渠道
-  "VITE_GLOB_NOTICE_CHANNEL": ['lanxin', 'platform', 'wecom',]
-
+  "VITE_GLOB_NOTICE_CHANNEL": ['lanxin', 'platform', 'wecom'],
+  // 压缩图片的服务地址
+  "minifyImgUrl": 'http://192.168.13.68:3000',
 };
 
 

+ 11 - 14
utils/devProxy/shangfei/app.config.js

@@ -1,30 +1,27 @@
-
 window.__PRODUCTION__SKYEYEADMIN__CONF__ = {
   // document的title,以及显示在左侧导航栏的title,一般是项目的名称
-  "VITE_GLOB_APP_TITLE": "xxx33",
+  VITE_GLOB_APP_TITLE: 'xxx33',
   // 租户tenantCode,部分项目必填
-  "VITE_GLOB_TENANT_CODE": "shangfei",
+  VITE_GLOB_TENANT_CODE: 'shangfei',
   // 接口前缀
-  "VITE_GLOB_API_URL_PREFIX": "./eye_api_bak/api",
+  VITE_GLOB_API_URL_PREFIX: './eye_api_bak/api',
   // app下载地址
   // "VITE_GLOB_APP_DOWNLOAD_QRCODE": "/apk/skyeye.apk",
   // 登录的前端地址
-  "VITE_GLOB_LOGIN_APP": "/skyeye-login-shangfei/#/",
+  VITE_GLOB_LOGIN_APP: '/skyeye-login-shangfei/#/',
   // 平台跳转地址
-  "VITE_GLOB_APP_PC": "/skyeyev3pc-shangfei/",
-  // simple v2 
+  VITE_GLOB_APP_PC: '/skyeyev3pc-shangfei/',
+  // simple v2
   // simple为简单闭环处理,v2为复杂闭环处理
-  "VITE_GLOB_QUESTION_LIST_VERSION": '',
+  VITE_GLOB_QUESTION_LIST_VERSION: '',
   // 为上飞定制的是否允许修改组织结构编辑,上飞不允许编辑,其他项目允许编辑
-  "VITE_GLOB_DISABLE_DEPARTMENT_EDIT": true,
+  VITE_GLOB_DISABLE_DEPARTMENT_EDIT: true,
   // 消息管理可选择的推送渠道
-  "VITE_GLOB_NOTICE_CHANNEL": ['lanxin', 'platform',]
+  VITE_GLOB_NOTICE_CHANNEL: ['lanxin', 'platform'],
 };
 
-
-
 Object.freeze(window.__PRODUCTION__SKYEYEADMIN__CONF__);
-Object.defineProperty(window, "__PRODUCTION__SKYEYEADMIN__CONF__", {
+Object.defineProperty(window, '__PRODUCTION__SKYEYEADMIN__CONF__', {
   configurable: false,
   writable: false,
-});
+});