sd
2025-08-08 54572b3c9db78d4374281087a8c66e5c3b3b6200
整合文搜万物模块
19个文件已添加
2个文件已修改
3397 ■■■■■ 已修改文件
src/api/AiRetrievalView.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/ChatHistoryView.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/SurveyView.ts 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/img/AI-avatar.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/img/UDP配置.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/img/article-fill@1x copy.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/img/conversation.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/img/fold_up.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/img/historical copy.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/img/historical.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/img/live-fill@1x.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/img/time-fill@1x.png 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/datapush/index/RightEvent.vue 74 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/modelTuning/components/rightCardList.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/searchNew/components/AiRetrievalView.vue 938 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/searchNew/components/ChatHistoryView.vue 248 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/searchNew/components/SurveyView.vue 1570 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/searchNew/components/cameraVideo.vue 232 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/searchNew/index/App.vue 131 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/searchNew/index/main.ts 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/searchNew/index/mixins.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/AiRetrievalView.ts
New file
@@ -0,0 +1,25 @@
import request from "@/scripts/httpRequest"
export default {//添加对话记录
  createChat(data) {
    return request({
      url: '/api-v1/v1/chat/addDetail',
      method: 'post',
      data
    })
  },
  getDetail(params) {//获取对话详情
    return request({
      url: '/api-v1/v1/chat/detail',
      method: 'get',
      params
    })
  },
  getChatDetail(params) {//对话检索
    return request({
      url: '/api-v1/v1/record/chat',
      method: 'post',
      params
    })
  },
}
src/api/ChatHistoryView.ts
New file
@@ -0,0 +1,20 @@
import request from "@/scripts/httpRequest"
export default {
  getChats(data) {
    // return service.get('/chat/all', data)
    return request({
      url: '/api-v1/v1/chat/all',
      method: 'get',
      data
    })
  },
  deleteChat(params) {
    // return service.delete('/chat/delete', { params })
    return request({
      url: '/api-v1/v1/chat/delete',
      method: 'delete',
      params
    })
  },
}
src/api/SurveyView.ts
New file
@@ -0,0 +1,101 @@
import request from '@/scripts/httpRequest'
export default {
  // getSurveys(params) {
  //   return service.get('/record/all', { params })
  // },
  createFile(data) {
    // return service.post('/task/add', data)
    return request({
      url: '/api-v1/v1/task/add',
      method: 'post',
      data
    })
  },
  updateFile(data) {
    // return service.put(`/task/update`, data)
    return request({
      url: '/api-v1/v1/task/update',
      method: 'put',
      data
    })
  },
  deleteFile(params) {
    // return service.delete(`/task/delete`, { params })
    return request({
      url: '/api-v1/v1/task/delete',
      method: 'delete',
      params
    })
  },
  getTasks(params) {
    // return service.get('/task/all', { params })
    return request({
      url: '/api-v1/v1/task/all',
      method: 'get',
      params
    })
  },
  getCheckContents(params) {
    // return service.get('/checkContent/all', { params })
    return request({
      url: '/api-v1/v1/checkContent/all',
      method: 'get',
      params
    })
  },
  getWarnings(params) {
    // return service.get('/warningRule/all', { params })
    return request({
      url: '/api-v1/v1/warningRule/all',
      method: 'get',
      params
    })
  },
  getCameras(params) {
    // return service.get('/task/getVideoOption')
    return request({
      url: '/api-v1/v1/task/getVideoOption',
      method: 'get',
      params
    })
  },
  getTimes(params) {
    // return service.get('/workTime/all')
    return request({
      url: '/api-v1/v1/workTime/all',
      method: 'get',
      params
    })
  },
  getEvents(params) {
    // return service.get('/task/getDictType')
    return request({
      url: '/api-v1/v1/task/getDictType',
      method: 'get',
      params
    })
  },
  getSurveys(data) {
    return request({
      url: '/api-v1/v1/record/all',
      method: 'post',
      data
    })
  },
 insertModelTraining  (params){
    return request({
      url: "/api-v1/v1/train/add ",
      method: "post",
      data: params
    })
  },
  downloadFiles(params){
    return request({
      url: '/api-v1/v1/knowledge/download',
      method: 'get',
      params
    })
  },
}
src/assets/img/AI-avatar.png
src/assets/img/UDPÅäÖÃ.png
src/assets/img/article-fill@1x copy.png
src/assets/img/conversation.png
src/assets/img/fold_up.png
src/assets/img/historical copy.png
src/assets/img/historical.png
src/assets/img/live-fill@1x.png
src/assets/img/time-fill@1x.png
src/pages/datapush/index/RightEvent.vue
@@ -15,9 +15,9 @@
        <div>
          <span style="line-height: 38px;margin-right: 20px;">推送方式</span>
          <el-radio :disabled="urls.length > 0" v-model="pushType" label="1">UDP</el-radio>
          <el-radio :disabled="urls.length > 0" v-model="pushType" label="2">HTTP</el-radio>
          <el-radio disabled v-model="pushType" label="3">MQTT</el-radio>
          <el-radio :disabled="urls.length > 0" v-model="taskEditData.pushType" label="1">UDP</el-radio>
          <el-radio :disabled="urls.length > 0" v-model="taskEditData.pushType" label="2">HTTP</el-radio>
          <el-radio disabled v-model="taskEditData.pushType" label="3">MQTT</el-radio>
        </div>
        <span style="line-height: 38px">推送服务器</span>
        <div class="icon-btn" v-if="urls.length < 1" @click="addUrl()">
@@ -28,13 +28,14 @@
          <div>
            <el-checkbox v-model="item.enable"></el-checkbox>
            <span class="ml20">{{ "URL&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" }}</span>
            <el-input v-if="pushType === '1'" v-model="item.ip" style="width: 180px; margin-left: 0px;margin-right: 30px" size="small"
            <el-input v-if="taskEditData.pushType === '1'" v-model="item.server_ip"
              style="width: 180px; margin-left: 0px;margin-right: 30px" size="small"
              placeholder="192.168.1.100"></el-input>
              ç«¯å£  <el-input v-if="pushType === '1'" v-model="item.sort" style="width: 70px; margin-left: 10px" size="small"
              placeholder="8030"></el-input>
              <el-input v-if="pushType === '2'" v-model="item.url" style="width: 360px; margin-left: 0px" size="small"
              placeholder="http://10.10.10.10:8000/dataApi"></el-input>
              <!-- <el-input v-if="pushType === '3'" v-model="item.url" style="width: 360px; margin-left: 0px" size="small"
            ç«¯å£ <el-input v-if="taskEditData.pushType === '1'" v-model="item.port" style="width: 70px; margin-left: 10px"
              size="small" placeholder="8030"></el-input>
            <el-input v-if="taskEditData.pushType === '2'" v-model="item.url" style="width: 360px; margin-left: 0px"
              size="small" placeholder="http://10.10.10.10:8000/dataApi"></el-input>
            <!-- <el-input v-if="taskEditData.pushType === '3'" v-model="item.url" style="width: 360px; margin-left: 0px" size="small"
              placeholder="MQTT"></el-input> -->
          </div>
          <div class="server-add">
@@ -110,10 +111,15 @@
              <el-input v-model="rule.rule_value" placeholder="请输入内容" size="small"></el-input>
            </div>
            <div v-else>
              <el-select v-model="rule.rule_values" multiple collapse-tags placeholder="请选择" size="small"
                @change="selectValue(rule, $event)">
              <el-select v-if="!isWarningSelect" v-model="rule.rule_values" multiple collapse-tags placeholder="请选择"
                size="small" @change="selectValue(rule, $event)">
                <el-option v-for="item in rule.ruleValueOptions" :key="item.id" :label="item.name"
                  :disabled="item.disabled" :value="item.value"></el-option>
              </el-select>
              <el-select v-else v-model="rule.rule_values" collapse-tags placeholder="请选择" size="small"
                @change="selectValue(rule, $event)">
                <el-option v-for="item in rule.ruleValueOptions" :key="item.id" :label="item.name"
                  :value="item.value"></el-option>
              </el-select>
            </div>
          </el-col>
@@ -135,7 +141,8 @@
      </div>
      <div class="config-item">
        <b>推送字段</b>
        <el-button v-if="pushType === '1'" type="primary" size="mini" @click="openPushImsDialog">查看</el-button>
        <el-button v-if="taskEditData.pushType === '1'" type="primary" size="mini"
          @click="openPushImsDialog">查看</el-button>
        <el-button v-else type="primary" size="mini" @click="openPushSetDialog">设置</el-button>
      </div>
      <div class="save-btn">
@@ -145,7 +152,8 @@
    </div>
    <el-dialog :visible="pushImageDialog" :append-to-body="false" :close-on-click-modal="false"
      class="dialog-push-field" @close="pushImageDialog = false">
      <el-image fit="fill" src="https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg"></el-image>
      <!-- <el-image fit="fill" src="https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg"></el-image> -->
      <img src="@/assets/img/UDP配置.png" style="width: 100%;">
    </el-dialog>
    <el-dialog :visible="pushFieldDialog" :append-to-body="false" :close-on-click-modal="false"
      class="dialog-push-field" @close="pushFieldDialog = false">
@@ -203,7 +211,7 @@
  },
  data() {
    return {
      pushType: '1',
      isWarningSelect: true,
      taskEditData: {},
      dataList: [],
      dictionary: [],
@@ -226,7 +234,7 @@
        ruleValueOptions: [],
      },
      pushFieldDialog: false,
      pushImageDialog:false,
      pushImageDialog: false,
      tempPushSet: [],
      pushFields: [],
      allFieldChecked: false,
@@ -252,7 +260,7 @@
            this.taskEditData.lineWay = newVal.link_type;
            this.taskEditData.eventTxt = newVal.rule_text;
            this.taskEditData.radioValue = newVal.is_satisfy_all ? "1" : "2";
            this.pushType = '2'
            this.taskEditData.pushType = newVal.push_type + ""
            if (!this.taskEditData.urls) {
              this.$set(this.taskEditData, "urls", []);
@@ -426,6 +434,8 @@
        checked: true,
        hash: Math.random().toString(36).substr(2),
        url: "",
        server_ip: "",
        port: ""
      });
    },
    delUrl(index) {
@@ -434,6 +444,7 @@
    },
    // ä¿å­˜
    async eventPushsSave() {
      console.log(this.taskEditData.urls)
      if (this.taskEditData.name.length < 1) {
        this.$notify({
          type: "warning",
@@ -450,7 +461,7 @@
        return;
      }
      for (let i = 0; i < this.taskEditData.urls.length; i++) {
        if (this.taskEditData.urls[i].url.length < 1) {
        if (this.taskEditData.urls[i].url.length < 1 && this.taskEditData.urls[i].server_ip.length < 1) {
          this.$notify({
            type: "warning",
            message: "接口URL地址不允许为空",
@@ -511,11 +522,16 @@
        rules: ruleList,
        time_start: this.taskEditData.time[0],
        time_end: this.taskEditData.time[1],
        urls: this.taskEditData.urls,
        urls: this.taskEditData.urls.map(item => {
          return {
            ...item,
            port: item.port ? Number(item.port) : 0
          }
        }),
        is_satisfy_all: this.taskEditData.radioValue === "1",
        link_type: this.taskEditData.lineWay,
        push_set: this.taskEditData.push_set,
        pushType:this.pushType
        push_type: Number(this.taskEditData.pushType)
      };
      let res = await eventPushsSave(json);
@@ -536,6 +552,12 @@
        this.baseRule.topicTypeOptions = this.dictionary.EVENTRULETOPIC;
        this.baseRule.operatorTypeOpionts = this.dictionary.EVENTTYPECOMPUTE;
        this.dictionary["alarmLevel"] = this.dictionary.ALARMLEVEL.map((el) => {
          return {
            name: el.name,
            value: el.name,
          };
        });
        this.dictionary["warning"] = this.dictionary.WARNING.map((el) => {
          return {
            name: el.name,
            value: el.name,
@@ -578,6 +600,7 @@
    // æ–°å»ºé…ç½®
    createSet() {
      this.dataList.push(JSON.parse(JSON.stringify(this.baseRule)));
      console.log(this.dataList, "dataList")
    },
    cleanSet() {
      this.dataList.splice(0, this.dataList.length);
@@ -602,6 +625,11 @@
          }
        }
      });
      if (rule.topic_type && rule.topic_type === 'warning') {
        this.isWarningSelect = true
      } else {
        this.isWarningSelect = false
      }
    },
    selectArg(rule, resetNext = false) {
      let argInfo = rule.topicArgOptions.filter((arg) => {
@@ -668,8 +696,12 @@
          return;
        }
      }
      if (this.isWarningSelect) {
        rule.rule_value = val
      } else {
        rule.rule_value = val.join(",");
      }
      rule.rule_value = val.join(",");
    },
    setOptionsDisable(rule) {
      console.log(rule);
@@ -729,9 +761,9 @@
        radioValue: "1",
        eventTxt: "",
        push_set: this.pushFields,
        pushType: '1'
      };
      this.dataList = [];
      this.pushType ='1'
    },
    onCancle() {
      this.$emit("onCancle");
src/pages/modelTuning/components/rightCardList.vue
@@ -47,7 +47,7 @@
                <!-- é€‰æ‹©æ—¶æ®µ -->
                <el-form-item label="选择时段">
                    <el-date-picker style="width: 256px;" v-model="filter.timeRange" type="daterange"
                        value-format="yyyy-MM-dd hh:mm:ss" range-separator="至" start-placeholder="开始日期"
                        value-format="yyyy-MM-dd" range-separator="至" start-placeholder="开始日期"
                        end-placeholder="结束日期" />
                </el-form-item>
@@ -584,6 +584,7 @@
}
.image-gallery {
    min-width: 1265px;
    background-color: #ffffff;
    padding: 20px;
    border-radius: 4px;
src/pages/searchNew/components/AiRetrievalView.vue
New file
@@ -0,0 +1,938 @@
<template>
  <div class="chat-container">
    <div class="messages">
      <div v-for="(msg, index) in messages" :key="index"
        :class="['message-bubble', msg.role, { 'with-think': msg.think }]">
        <!-- æ€è€ƒå†…容区域 -->
        <div v-if="msg.think" class="think-section">
          <div class="think-header" @click="toggleThink(index)">
            <span>思考过程</span>
            <i :class="[
              'arrow',
              msg.isThinkExpanded
                ? 'el-icon-arrow-down'
                : 'el-icon-arrow-right',
            ]"></i>
          </div>
          <transition name="slide">
            <div v-show="msg.isThinkExpanded" class="think-content">
              {{ msg.think }}
            </div>
          </transition>
        </div>
        <!-- æ­£å¸¸æ¶ˆæ¯å†…容 -->
        <div class="content">
          <template v-if="msg.role === 'assistant'">
            <span v-if="isGenerating && index === messages.length - 1">
              <span v-if="!msg.think" class="generating-indicator">正在思考...</span>
              <!-- <span class="cursor"></span> -->
            </span>
            {{ msg.content }}
          </template>
          <template v-else>
            {{ msg.content }}
          </template>
        </div>
      </div>
    </div>
    <div class="preview-area" v-if="uploadedImages.length > 0">
      <div v-for="(img, index) in uploadedImages" :key="index" class="image-preview">
        <img :src="'data:image/png;base64,' + img.base64" class="thumbnail" />
        <span class="image-name">{{ img.name }}</span>
        <span class="image-size">{{ formatSize(img.size) }}</span>
        <i class="el-icon-close remove-icon" @click="removeImage(index)"></i>
      </div>
    </div>
    <div class="input-area">
      <div class="input-area2">
        <textarea v-model="inputText" @keydown.enter.prevent="handleEnter" :disabled="isGenerating"
          placeholder="请输入你的问题" class="input-textarea" rows="1" @input="autoResize"></textarea>
      </div>
      <div class="action-buttons">
        <el-upload action="" :auto-upload="false" :show-file-list="false" :on-change="handleImageUpload"
          accept="image/*" class="action-img" :disabled="true">
          <i class="el-icon-picture upload-icon"></i>
        </el-upload>
        <div class="action-img">
          <i class="el-icon-microphone microphone-icon"></i>
        </div>
        <button @click="startStream" :disabled="isGenerating" class="send-button">
          <i class="el-icon-position send-icon"></i>
        </button>
      </div>
    </div>
    <div class="tip-text">内容由AI大模型生成,请仔细甄别</div>
  </div>
</template>
<script>
import AiRetrieval from "@/api/AiRetrievalView";
export default {
  props: ["currentChatId"],
  watch: {
    currentChatId: {
      immediate: true,
      handler(newVal) {
        if (newVal) this.loadChat(); // ç›‘听chatId变化
      },
    },
  },
  name: "AiRetrieval",
  data() {
    return {
      chatUrl: "",
      severUrl:"",
      params: "",
      chatId: null,
      messages: [],
      inputText: "",
      isGenerating: false,
      controller: null,
      uploadedImages: [],
    };
  },
  mounted() {
    this.fetchCameraInfo();
  },
  methods: {
    async fetchCameraInfo() {
      const response = await fetch("/config.json");
      if (!response.ok) throw new Error(`请求失败: ${response.status}`);
      const responseData = await response.json();
      this.chatUrl = responseData.chatUrl;
      this.severUrl = responseData.severUrl;
      // console.info("chatUrl:"+this.chatUrl);
    },
    toggleThink(index) {
      this.$set(
        this.messages[index],
        "isThinkExpanded",
        !this.messages[index].isThinkExpanded
      );
    },
    handleItemSend(params) {
      this.$emit("list-selected", params); // è§¦å‘自定义事件 åˆ·æ–°å³ä¾§å†…容区域
    },
    async loadChat() {
      if (!this.currentChatId) return;
      try {
        // console.info("新对话"+this.currentChatId);
        const res = await AiRetrieval.getDetail({
          chatId: this.currentChatId,
        });
        // console.info(res.data);
        this.messages = [];
        this.chatId = res.data[0].chatId;
        // console.info(+this.chatId )
        for (let i = 0; i < res.data.length; i++) {
          this.messages.push({
            role: "user",
            content: res.data[i].question,
          });
          this.messages.push({
            role: "assistant",
            content: res.data[i].content,
            think: res.data[i].think,
          });
        }
        // this.messages = res.data.data;  // å‡è®¾æŽ¥å£è¿”回消息列表
        this.$nextTick(() => {
          const container = this.$el.querySelector(".messages");
          container.scrollTop = container.scrollHeight;
        });
      } catch (error) {
        console.error("加载聊天失败:", error);
      }
    },
    // async saveToDatabase(chatData) {
    //   if (this.currentChatId) {            // æ›´æ–°é€»è¾‘
    //     await AiRetrieval.updateChat(this.currentChatId, chatData);
    //   } else {                             // æ–°å»ºé€»è¾‘
    //     const res = await AiRetrieval.createChat(chatData);
    //     this.currentChatId = res.data.chatId;
    //   }
    // },
    reset() {
      // console.info("清空对话记录");
      // æ¸…空对话记录
      this.messages = [];
      // é‡ç½®è¾“入框
      this.inputMessage = "";
      // å¦‚果有其他需要重置的状态
      this.searchResults = [];
      this.currentPage = 1;
      // å¯ä»¥æ ¹æ®éœ€è¦æ·»åŠ å…¶ä»–é‡ç½®é€»è¾‘
    },
    // æ–°å¢žè‡ªåŠ¨è°ƒæ•´é«˜åº¦æ–¹æ³•
    autoResize(e) {
      const textarea = e.target;
      textarea.style.height = "auto"; // é‡ç½®é«˜åº¦
      const maxHeight = 6 * 24; // 6行*24px行高 + 8px*2内边距
      textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight)}px`;
    },
    handleImageUpload(file) {
      const reader = new FileReader();
      reader.onload = (e) => {
        const base64Data = e.target.result.split(",")[1];
        this.uploadedImages.push({
          name: file.name,
          size: file.size,
          base64: base64Data,
        });
      };
      reader.readAsDataURL(file.raw);
    },
    removeImage(index) {
      this.uploadedImages.splice(index, 1);
    },
    formatSize(bytes) {
      if (bytes === 0) return "0 B";
      const k = 1024;
      const sizes = ["B", "KB", "MB", "GB"];
      const i = Math.floor(Math.log(bytes) / Math.log(k));
      return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
    },
    handleEnter(e) {
      if (!e.shiftKey && !this.isGenerating) {
        this.startStream();
      }
    },
    async startStream() {
      if (!this.inputText.trim() || this.isGenerating) return;
      this.messages.push({ role: "user", content: this.inputText });
      const userMessage = this.inputText;
      this.inputText = "";
      this.messages.push({
        role: "assistant",
        content: "",
        think: "",
        isThinkExpanded: true,
      });
      // this.$nextTick(() => {
      //   const container = this.$el.querySelector(".messages");
      //   container.scrollTop = container.scrollHeight;
      // });
      this.isGenerating = true;
      try {
        this.controller = new AbortController();
        let ids = [];
        let idsStr = "";
        //筛选最终结果
        // const response = await AiRetrieval.getChatDetail(
        //   {
        //     message:userMessage
        //   }
        // );
        const response = await fetch(this.severUrl +"/v1/record/chat", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            message: userMessage
          }),
        });
        if (!response.ok) throw new Error(`请求失败: ${response.status}`);
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let boo = true;
        // ä¿®æ”¹åŽçš„fetch处理逻辑
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          const chunk = decoder.decode(value, { stream: true });
          const lines = chunk.split("\n").filter((line) => line.trim());
          // ä¿å­˜æœ€æ–°æ¶ˆæ¯çš„引用
          const lastMessage = this.messages[this.messages.length - 1];
          for (const line of lines) {
            try {
              const json = JSON.parse(line.replace("data:", "").trim());
              if (json.data === true) break;
              // ç›´æŽ¥æ›¿æ¢è€Œä¸æ˜¯è¿½åŠ 
              if (json.data) {
                // ä½¿ç”¨æ­£åˆ™è¡¨è¾¾å¼åŒ¹é…æ€è€ƒå†…容
                const thoughtMatch = json.data.match(
                  /<think>(.*?)(?=\n<\/think>|$)/s
                );
                if (thoughtMatch) {
                  // æ›´æ–°æ¶ˆæ¯å†…容 å¸¦think
                  lastMessage.think = thoughtMatch[1].trim();
                  let con = json.data //第一部分:
                    .match(/<\/think>([\s\S]*)/)[1]
                    .trim();
                  // console.info('con='+con)
                  const rescou = con
                    .match(/第一部分:(.*?)(?=\n第二部分:|$)/s)[1]
                    .trim();
                  // console.info('rescou='+rescou)
                  if (rescou) {
                    lastMessage.content = rescou;
                    idsStr = con.match(/第二部分:([\s\S]*)/)[1].trim();
                    // console.info("idsStr=" + idsStr);
                    boo = false;
                  }
                } else {
                  // æ›´æ–°æ¶ˆæ¯å†…容
                  lastMessage.think = json.data
                    .match(/<think>([\s\S]*)/)[1]
                    .trim();
                }
              }
              // console.info("boo:" + boo)
            } catch (e) {
              console.info("")
            }
          }
        }
        // console.info("boo:"+boo)
        if (boo) {
          this.messages[this.messages.length - 1].content = "匹配到的数据为0条";
        } else {
          ids = idsStr.split(",");
        }
        // this.handleItemSend(lists);//发送数据展示到页面
        this.handleItemSend(ids);
        // // ä¿å­˜åˆ°æ•°æ®åº“的逻辑
        // console.info("保存旧数据"+this.chatId)
        if (this.messages.length >= 2) {
          // console.info(this.messages)
          const userMsg = this.messages[this.messages.length - 2];
          const assistantMsg = this.messages[this.messages.length - 1];
          this.saveToDatabase({
            question: userMsg.content,
            content: assistantMsg.content,
            think: assistantMsg.think,
            chatId: this.chatId,
            // images: this.uploadedImages
          });
        }
      } catch (error) {
        if (error.name !== "AbortError") {
          console.error("生成失败:", error);
          this.messages[this.messages.length - 1].content += "(生成失败)";
        }
      } finally {
        this.isGenerating = false;
        this.controller = null;
      }
    },
    // async startStream() {
    //   if (!this.inputText.trim() || this.isGenerating) return;
    //   this.messages.push({ role: "user", content: this.inputText });
    //   const userMessage = this.inputText;
    //   this.inputText = "";
    //   this.messages.push({
    //     role: "assistant",
    //     content: "",
    //     think: "",
    //     isThinkExpanded: true,
    //   });
    //   // this.$nextTick(() => {
    //   //   const container = this.$el.querySelector(".messages");
    //   //   container.scrollTop = container.scrollHeight;
    //   // });
    //   this.isGenerating = true;
    //   try {
    //     this.controller = new AbortController();
    //     //优化用户问题
    //     const resstar =
    //       "milvus数据库中存储了id、摄像机抓拍图片、图片的文本描述,图片文本描述是通过qwen模型把图片中包含的内容形成了文本内容。用户会输入从milvus数据库中查找图片的需求,请帮我把用户的问题改写一下,以便在milvus数据库中查找到更加准确的且符合用户期望的数据。不要输出改写说明 /nothink";
    //     const startRes = await fetch(this.chatUrl + "/chat", {
    //       method: "POST",
    //       headers: {
    //         "Content-Type": "application/json",
    //       },
    //       body: JSON.stringify({
    //         prompt: resstar,
    //         llm_name: "qwen3:8b",//qwen2.5vl
    //         messages: [
    //           ...this.messages.slice(0, -1),
    //           { role: "user", content: userMessage },
    //         ],
    //         stream: false,
    //         gen_conf: {
    //           temperature: 0.1,
    //           max_tokens: 1000
    //         }
    //         // image:this.uploadedImages[0]?.base64 || ''
    //       }),
    //     });
    //     // console.info("chat地址:"+"/api-AI" + "/chat")
    //     if (!startRes.ok) throw new Error(`请求失败: ${startRes.status}`);
    //     const responseData = await startRes.json();
    //      let res1 = responseData.data.split('\n\n')
    //      let rescou = res1.length>1?res1[2]:""
    //     //let rescou = responseData.data.match(/!(.*?)(?=\n!|$)/s)[1].trim();
    //     console.info(rescou);
    //     if (!rescou) {
    //       rescou = userMessage;
    //     }
    //     //检索向量数据库
    //     const res = await fetch(this.chatUrl + "/search", {
    //       method: "POST",
    //       headers: {
    //         "Content-Type": "application/json",
    //       },
    //       body: JSON.stringify({
    //         collection_name: "smartrag",
    //         query_text: rescou,
    //         search_mode: "hybrid",
    //         limit: 20,
    //         weight_dense: 0.7,
    //         weight_sparse: 0.3,
    //         filter_expr: "",
    //         output_fields: [
    //           "id",
    //           "image_path",
    //           "create_timestamp",
    //           "task_name",
    //           "event_level_name",
    //           "rtsp_address",
    //           "video_point_id",
    //           "zh_desc_class",
    //           "video_path",
    //           "detect_time",
    //         ],
    //       }),
    //     });
    //     // console.info(res.json())
    //     const text = await res.text();
    //     const data = JSONbig({ storeAsString: true }).parse(text);
    //     console.info(data);
    //     let lists = [];
    //     let ids = [];
    //     let idsStr = "";
    //     for (let i = 0; i < data.results.length; i++) {
    //       // console.info(data.results[i].entity.id)
    //       this.params +=
    //         "信息id:" +
    //         data.results[i].entity.id +
    //         ",内容:" +
    //         data.results[i].entity.video_path +
    //         "摄像头在" +
    //         data.results[i].entity.detect_time +
    //         "时间拍摄的图片" +
    //         data.results[i].entity.zh_desc_class +
    //         "\n";
    //       lists.push(data.results[i].entity);
    //     }
    //     // const mes =
    //     //   "用户询问【" +
    //     //   rescou +
    //     //   "】。\n请在以下信息中进行匹配,信息为\n" +
    //     //   this.params +
    //     //   "。\n回答:【XXX信息id】";
    //     const mes =
    //       "用户询问【" +
    //       rescou +
    //       "】。\n请在以下信息中进行匹配,信息为\n" +
    //       this.params +
    //       "。\n不进行推理和解译,必须返回两部分结果,且结果中必须带有第一部分或第二部分四个字,第一部分为总结匹配到多少条,第二部分的格式必须是 ç¬¬äºŒéƒ¨åˆ†ï¼šXXX,XXX其中XXX为数据库id";
    //     this.params = "";
    //     if (!res.ok) throw new Error(`请求失败: ${res.status}`);
    //     let contentMes =  mes.replace(/\s+/g, '')
    //     //筛选最终结果
    //     const response = await fetch(this.chatUrl + "/chat", {
    //       method: "POST",
    //       headers: {
    //         "Content-Type": "application/json",
    //       },
    //       body: JSON.stringify({
    //         prompt: "",
    //         llm_name: "qwen3:8b",//qwen2.5vl
    //         messages: [
    //           ...this.messages.slice(0, -1),
    //           { role: "user", content: contentMes },
    //         ],
    //         stream: true,
    //         gen_conf: {
    //           temperature: 0.7,
    //           max_tokens: 4000,
    //         },
    //         // image:this.uploadedImages[0]?.base64 || ''
    //       }),
    //     });
    //     if (!response.ok) throw new Error(`请求失败: ${response.status}`);
    //     const reader = response.body.getReader();
    //     const decoder = new TextDecoder();
    //     let boo = true;
    //     // ä¿®æ”¹åŽçš„fetch处理逻辑
    //     while (true) {
    //       const { done, value } = await reader.read();
    //       if (done) break;
    //       const chunk = decoder.decode(value, { stream: true });
    //       const lines = chunk.split("\n").filter((line) => line.trim());
    //       // ä¿å­˜æœ€æ–°æ¶ˆæ¯çš„引用
    //       const lastMessage = this.messages[this.messages.length - 1];
    //       for (const line of lines) {
    //         try {
    //           const json = JSON.parse(line.replace("data:", "").trim());
    //           if (json.data === true) break;
    //           // ç›´æŽ¥æ›¿æ¢è€Œä¸æ˜¯è¿½åŠ 
    //           if (json.data) {
    //               // ä½¿ç”¨æ­£åˆ™è¡¨è¾¾å¼åŒ¹é…æ€è€ƒå†…容
    //             const thoughtMatch = json.data.match(
    //               /<think>(.*?)(?=\n<\/think>|$)/s
    //             );
    //             if (thoughtMatch) {
    //               // æ›´æ–°æ¶ˆæ¯å†…容 å¸¦think
    //               lastMessage.think = thoughtMatch[1].trim();
    //               let con = json.data //第一部分:
    //                 .match(/<\/think>([\s\S]*)/)[1]
    //                 .trim();
    //               // console.info('con='+con)
    //               const rescou = con
    //                 .match(/第一部分:(.*?)(?=\n第二部分:|$)/s)[1]
    //                 .trim();
    //               // console.info('rescou='+rescou)
    //               if (rescou) {
    //                 lastMessage.content = rescou;
    //                 idsStr = con.match(/第二部分:([\s\S]*)/)[1].trim();
    //                 // console.info("idsStr=" + idsStr);
    //                 boo = false;
    //               }
    //             } else {
    //               // æ›´æ–°æ¶ˆæ¯å†…容
    //               lastMessage.think = json.data
    //                 .match(/<think>([\s\S]*)/)[1]
    //                 .trim();
    //             }
    //           }
    //         } catch (e) {
    //           // console.error("解析错误:", e);
    //         }
    //       }
    //       // this.$nextTick(() => {
    //       //   const container = this.$el.querySelector(".messages");
    //       //   container.scrollTop = container.scrollHeight;
    //       // });
    //     }
    //     if (boo) {
    //       this.messages[this.messages.length - 1].content = "匹配到的数据为0条";
    //       lists = [];
    //     } else {
    //       ids = idsStr.split(",");
    //     }
    //     // this.handleItemSend(lists);//发送数据展示到页面
    //     this.handleItemSend(ids);
    //     // // ä¿å­˜åˆ°æ•°æ®åº“的逻辑
    //     // console.info("保存旧数据"+this.chatId)
    //     if (this.messages.length >= 2) {
    //       // console.info(this.messages)
    //       const userMsg = this.messages[this.messages.length - 2];
    //       const assistantMsg = this.messages[this.messages.length - 1];
    //       this.saveToDatabase({
    //         question: userMsg.content,
    //         content: assistantMsg.content,
    //         think: assistantMsg.think,
    //         chatId: this.chatId,
    //         // images: this.uploadedImages
    //       });
    //     }
    //   } catch (error) {
    //     if (error.name !== "AbortError") {
    //       console.error("生成失败:", error);
    //       this.messages[this.messages.length - 1].content += "(生成失败)";
    //     }
    //   } finally {
    //     this.isGenerating = false;
    //     this.controller = null;
    //   }
    // },
    async saveToDatabase(chatData) {
      // å®žé™…调用API接口
      try {
        const response = await AiRetrieval.createChat(chatData);
        // console.info(response.data);
        this.chatId = response.data || null;
      } catch (error) {
        console.error("API Error:", error);
      }
    },
    cancelGeneration() {
      if (this.controller) this.controller.abort();
    },
  },
};
</script>
<style scoped>
/* æ–°å¢žæ€è€ƒå†…容样式 */
.message-bubble.assistant.with-think {
  border-radius: 12px;
}
.think-section {
  background: #f8f9fa;
  padding: 12px;
  border-bottom: 1px solid #e9ecef;
  text-align: left;
}
.think-header {
  font-size: 12px;
  color: #6c757d;
  margin-bottom: 8px;
  display: flex;
  align-items: center;
  font-weight: 500;
}
.think-header::before {
  content: "💡";
  margin-right: 6px;
}
.think-content {
  font-size: 10px;
  color: #495057;
  line-height: 1.6;
  white-space: pre-wrap;
}
.generating-indicator {
  color: #6c757d;
  font-size: 0.9em;
  margin-right: 8px;
}
/* è°ƒæ•´åŽŸæœ‰æ ·å¼ */
.message-bubble.assistant .content {
  background: #f5f6f8;
  padding: 12px 15px;
  border-radius: 0 0 12px 12px;
  text-align: left;
}
/* æ–°å¢žå›¾ç‰‡ç›¸å…³æ ·å¼ */
.preview-area {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 8px;
  background: #f5f5f5;
  border-radius: 4px;
  margin-bottom: 8px;
}
.image-preview {
  position: relative;
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px;
  background: white;
  border-radius: 4px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.thumbnail {
  width: 30px;
  height: 30px;
  object-fit: cover;
  border-radius: 2px;
}
.image-name {
  font-size: 12px;
  max-width: 100px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.image-size {
  font-size: 10px;
  color: #666;
}
.remove-icon {
  position: absolute;
  top: -8px;
  right: -8px;
  font-size: 12px;
  background: white;
  border-radius: 50%;
  cursor: pointer;
}
.action-img {
  width: 20px;
  height: 20px;
  border-radius: 13px;
  padding: 6px;
  background-color: #f2f8ff;
}
.upload-icon {
  font-size: 20px;
  color: #7fade6;
  cursor: pointer;
}
.chat-container {
  position: relative;
  width: 100%;
  max-width: 500px;
  height: 600px;
  margin: 0 auto;
  /* background: #f1f1f1; */
  border-radius: 12px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  display: flex;
  flex-direction: column;
}
.close-button {
  position: absolute;
  right: 15px;
  top: 15px;
  font-size: 20px;
  color: #999;
  cursor: pointer;
  z-index: 1;
  transition: color 0.2s;
}
.close-button:hover {
  color: #666;
}
.messages {
  flex: 1;
  padding: 20px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 15px;
}
.message-bubble {
  max-width: 80%;
  position: relative;
  font-size: 14px;
  line-height: 1.5;
}
.message-bubble.user {
  align-self: flex-end;
}
.message-bubble.assistant {
  align-self: flex-start;
}
.content {
  padding: 12px 15px;
  border-radius: 5px;
  word-break: break-word;
}
.user .content {
  background: #cadcff;
  color: #2e2f31;
  border-radius: 12px 12px 0 12px;
}
.assistant .content {
  background: white;
  color: #2e2f31;
  border-radius: 12px 12px 12px 0;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.input-area {
  display: flex;
  flex-direction: column;
  /* æ”¹ä¸ºåž‚直布局 */
  background: #fff;
  border: 2px solid #1890ff;
  border-radius: 10px;
  padding-top: 8px;
  gap: 8px;
  /* æ·»åŠ é—´è· */
  min-height: 80px;
  max-height: 200px;
  align-items: center;
  /* position: relative; */
}
.input-area2 {
  width: 100%;
  display: flex;
  background: #fff;
  min-height: 60px;
  max-height: 180px;
  /* border: 2px solid #1890ff; */
}
.input-textarea {
  /* background: #0c0c0c; */
  width: 100%;
  border: none;
  resize: none;
  line-height: 24px;
  /* ç²¾ç¡®è¡Œé«˜ */
  font-size: 14px;
  padding: 8px 15px;
  min-height: 24px !important;
  /* å¼ºåˆ¶å•行高度 */
  max-height: 152px !important;
  /* å¼ºåˆ¶6行高度 */
  overflow-y: auto;
  box-sizing: content-box;
  /* é˜²æ­¢padding影响计算 */
}
.input-textarea:focus {
  outline: none;
  box-shadow: none;
}
.wechat-input {
  flex: 1;
  min-height: 40px;
  max-height: 144px;
  /* 6行 x 24px行高 */
  padding: 8px 0;
  line-height: 1.5;
  border: 0;
  background: transparent;
  font-size: 16px;
  color: #2c3e50;
  resize: none;
  overflow-y: auto;
}
.wechat-input:focus {
  border-color: #07c160;
  background: white;
  box-shadow: 0 4px 12px rgba(7, 193, 96, 0.1);
  /* æ·»åŠ èšç„¦é˜´å½± */
}
.wechat-input::placeholder {
  color: #a8c5e6;
  /* æµ…蓝色占位符 */
}
.microphone-icon {
  font-size: 20px;
  color: #7fade6;
  cursor: pointer;
}
/* æ“ä½œæŒ‰é’®åŒºåŸŸ */
.action-buttons {
  width: 100%;
  display: flex;
  justify-content: flex-end;
  /* å³å¯¹é½æŒ‰é’® */
  gap: 8px;
  padding: 0 0 5px 0;
  background: transparent;
  /* border: 2px solid #1890ff; */
}
/* å‘送按钮改造 */
.send-button {
  width: 45px;
  height: 25px;
  border-radius: 10px;
  background: #1890ff;
  border: none;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.3s ease;
  margin: 2px 6px;
}
.send-icon {
  color: white;
  font-size: 16px;
  /* transform: rotate(45deg); */
}
.send-button:disabled {
  background: #b6d7ff;
  cursor: not-allowed;
}
/* æç¤ºæ–‡æœ¬ */
.tip-text {
  text-align: center;
  font-size: 12px;
  color: #95a5c6;
  margin: 12px 0 10px 0;
  letter-spacing: 0.5px;
}
/* è¾“入框禁用状态 */
.wechat-input:disabled {
  background: #f0f0f0;
  opacity: 0.8;
}
.send-button:not(:disabled):hover {
  background: #2482ff;
}
.cursor {
  display: inline-block;
  width: 8px;
  height: 16px;
  background: #666;
  margin-left: 2px;
  animation: blink 1s infinite;
}
@keyframes blink {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0;
  }
}
</style>
src/pages/searchNew/components/ChatHistoryView.vue
New file
@@ -0,0 +1,248 @@
<template>
  <div class="schedule-container">
    <el-card class="schedule-card" shadow="never">
      <div v-for="(item, index) in chatList" :key="index" class="chat-item" @mouseenter="showDelete = index"
           @mouseleave="showDelete = null">
        <!-- æ–°å¢žåˆ é™¤æŒ‰é’® -->
        <i class="el-icon-delete delete-button"
        v-show="showDelete === index"
           @click.stop="handleDelete(item.chatId, $event)"></i>
        <div class="chat-header"></div>
        <div class="chat-header">
          <!-- <span class="chat-time">{{ formatDate(item.createTime) }}</span>
          <span class="chat-id">#{{ item.chatId }}</span> -->
        </div>
        <div class="chat-content" @click="handleItemClick(item.chatId)">
          {{ extractContent(item.title) }}
        </div>
      </div>
    </el-card>
  </div>
</template>
<script>
import chatHistory from "@/api/ChatHistoryView"
export default {
  name: 'ChatHistory',
  data() {
    return {
      showDelete: null, // å½“前显示删除按钮的索引
      chatData: {
        list: [],
        pagination: {}
      }
    }
  },
  computed: {
    chatList() {
      return this.chatData.list || []
    }
  },
  methods: {
    // æ–°å¢žåˆ é™¤å¤„理方法
    async handleDelete(chatId, index) {
      try {
        await this.$confirm('确定要删除此聊天记录吗?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        })
        // è°ƒç”¨åˆ é™¤æŽ¥å£
        await chatHistory.deleteChat({
          chatId:chatId
        })
        // å‰ç«¯ç«‹å³æ›´æ–°åˆ—表
        this.chatData.list.splice(index, 1)
        this.$message.success('删除成功')
      } catch (error) {
        if (error !== 'cancel') {
          console.error('删除失败:', error)
          this.$message.error('删除失败')
        }
      }
    },
    handleItemClick(chatId) {
      // console.info("点击"+chatId)
      this.$emit('item-selected', chatId); // è§¦å‘自定义事件
    },
    async fetchChatHistory() {
      try {
        const tasks = await chatHistory.getChats({//检测对话历史
          // page: 1,
          // pageSize: 999
        });
        // console.info(tasks.data.list)
        this.chatData.list = tasks.data.list
        this.chatData.pagination = tasks.data.pagination
      } catch (error) {
        console.error('获取筛选选项失败:', error);
        this.$message.error('筛选选项加载失败');
      }
    },
    formatDate(dateString) {
      return dateString.split(' ')[0] // æå–日期部分
    },
    extractContent(content) {
      // ç§»é™¤<think>标签内容,提取实际内容
      const cleanContent = content.replace(/<think>[\s\S]*?<\/think>\n*/g, '')
      return cleanContent.trim()
    }
  },
  mounted() {
    // æ¨¡æ‹ŸæŽ¥æ”¶åŽç«¯æ•°æ®
    this.fetchChatHistory()
    // this.chatData = {
    //   list: [
    //     {
    //       chatId: 19,
    //       title: `<think>思考过程...</think>\n\n你好!有什么我可以帮你的吗?😊`,
    //       createTime: "2025-05-27 09:30:33.1284251+00:00"
    //     }
    //     // å…¶ä»–数据项...
    //   ],
    //   pagination: {
    //     page: 1,
    //     total: 6
    //   }
    // }
  }
}
</script>
<style scoped>
/* ä¼˜åŒ–后的样式 */
.schedule-container {
  height: 600px;
  background: white;
  padding: 0;
  overflow-y: auto;
}
.schedule-card {
  padding: 0;
  border: none;
  min-height: 100%;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
}
.chat-item {
  padding: 10px  0;
  /* border-bottom: 1px solid #f0f0f0; */
  cursor: pointer;
  transition: background-color 0.3s;
  position: relative; /* ä¸ºç»å¯¹å®šä½æŒ‰é’®æä¾›å‚考 */
  padding-right: 40px; /* ä¸ºæŒ‰é’®ç•™å‡ºç©ºé—´ */
}
.delete-button {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  cursor: pointer;
  color: #ff4d4f;
  padding: 5px;
  font-size: 16px;
  transition: all 0.3s;
  z-index: 2;
}
.delete-button:hover {
  background-color: rgba(255,77,79,0.1);
  border-radius: 50%;
}
/* ä¼˜åŒ–悬停效果 */
.chat-item:hover .delete-button {
  display: block;
}
.chat-item:hover {
  background-color: #f8f9fa;
}
.chat-header {
  display: flex;
  justify-content: space-between;
  /* margin-bottom: 8px; */
  font-size: 12px;
  color: #666;
}
.item-container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
}
.chat-content {
  flex: 1;
  cursor: pointer;
  color: #1a1a1a;
  font-size: 16px;
  line-height: 1.6;
  word-break: break-word;
  text-align: left; /* æ–°å¢žå·¦å¯¹é½ */
  /* æ–°å¢žå•行省略样式 */
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* ç¡®ä¿å®½åº¦é™åˆ¶ */
  max-width: 100%;
  width: 100%;
}
/* æ‚¬åœæ—¶æ˜¾ç¤ºå®Œæ•´å†…容 */
/* .chat-item:hover .chat-content {
  white-space: normal;
  overflow: visible;
  text-overflow: initial;
} */
.loading-container {
  padding: 40px;
  text-align: center;
  color: #666;
  font-size: 14px;
}
.empty-state {
  text-align: center;
  padding: 40px;
  color: #999;
  i {
    font-size: 48px;
    margin-bottom: 16px;
  }
}
.pagination {
  margin-top: auto;
  padding: 16px;
  justify-content: center;
}
/* æ»šåŠ¨æ¡ä¼˜åŒ– */
::-webkit-scrollbar {
  width: 6px;
}
::-webkit-scrollbar-track {
  background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
  background: #c1c1c1;
  border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
  background: #a8a8a8;
}
</style>
src/pages/searchNew/components/SurveyView.vue
New file
@@ -0,0 +1,1570 @@
<template>
  <div>
    <!-- æ–°å¢žå®žæ—¶ç›‘控弹窗 -->
    <el-dialog title="实时监控" :visible.sync="realTimeVisible" width="60%" custom-class="resizable-dialog" :modal="true"
      :lock-scroll="false" :close-on-click-modal="false">
      <!-- <div class="dialog-content"> -->
      <camera-video @close="realTimeVisible = false" />
      <!-- <p>尝试拖拽边缘或角落调整大小</p>
                <div v-for="handle in handles" :key="handle.position" class="resize-handle" :class="handle.position"
                    @mousedown.prevent="startResize(handle.position, $event)">
                </div>
            </div> -->
    </el-dialog>
    <!-- è¯¦æƒ…弹窗 -->
    <!-- ä¿®æ”¹åŽçš„详情弹窗 -->
    <el-dialog :visible.sync="detailVisible" title="" width="660px" custom-class="detail-dialog right-aligned">
      <!-- é¡¶éƒ¨åˆ‡æ¢æŒ‰é’® -->
      <div class="media-switch">
        <el-radio-group v-model="currentMediaType">
          <el-radio-button label="image" class="media-tab">大图</el-radio-button>
          <el-radio-button label="video" class="media-tab">视频</el-radio-button>
        </el-radio-group>
      </div>
      <el-divider></el-divider>
      <!-- ä¸Šéƒ¨åª’体区 -->
      <div class="media-container">
        <div v-if="currentMediaType === 'image'" class="main-image">
          <el-image :src="detailItem.image_path" alt="事件截图" class="detail-img previewable-image"
            :preview-src-list="[detailItem.image_path]" />
        </div>
        <div v-else class="video-section">
          <div class="video-placeholder">
            <!-- ä¿®æ”¹video标签部分 -->
            <video :src="detailItem.video_path" controls style="width: 100%; height: 100%; object-fit: contain"></video>
            <!-- <wasm-player  style="width: 100%; height: 100%; object-fit: contain"></wasm-player> -->
          </div>
        </div>
      </div>
      <!-- ä¸‹éƒ¨ä¿¡æ¯åŒº -->
      <div class="info-container">
        <!-- <el-descriptions :column="2" border> -->
        <el-descriptions :column="1">
          <el-descriptions-item label="时间" class="info-item">
            <span style="width: 28px"></span>
            <span class="info-value">{{
              formatStartDateTime(detailItem.detect_time)
            }}</span>
          </el-descriptions-item>
          <el-descriptions-item label="视频点位" class="info-item">
            <span class="info-value">{{ detailItem.video_name }}</span>
          </el-descriptions-item>
          <el-descriptions-item label="任务名称" v-if="backendData">
            <span class="multi-value">
              {{ detailItem.task_names }}
            </span>
          </el-descriptions-item>
          <el-descriptions-item label="事件等级" v-if="backendData">
            <span style="background-color: #f70713; width: 25px; height: 15px">
            </span><span style="width: 5px"></span>{{ detailItem.event_levels }}
          </el-descriptions-item>
          <el-descriptions-item label="隐患描述" v-if="backendData">
            <el-tooltip placement="top" :content="detailItem.risk_description" effect="light"
              popper-class="my-tooltip">
              <span class="multi-value ellipsis">{{ detailItem.risk_description }}</span>
            </el-tooltip>
          </el-descriptions-item>
          <el-descriptions-item label="处理建议" v-if="backendData">
            <el-tooltip placement="top" :content="detailItem.suggestion" effect="light" popper-class="my-tooltip">
              <span class="multi-value ellipsis">{{ detailItem.suggestion }}</span>
            </el-tooltip>
          </el-descriptions-item>
          <el-descriptions-item label="相关文档" v-if="backendData">
            <div v-for="(doc, index) in detailItem.knowledge_documents || []" :key="index" class="multi-value">
              <!-- <a :href="getPreviewUrl(doc)" target="_blank" class="doc-link" style="text-decoration: none;">
                <span>《{{ doc.fileName }}》</span>
              </a> -->
              <span class="doc-link" @click="getPreviewUrl2(doc.id)">《{{ doc.fileName }}》</span>
              <span v-if="index < detailItem.knowledge_documents.length - 1">, </span>
            </div>
          </el-descriptions-item>
          <el-descriptions-item label="图片内容" style="">
            <div class="desc_class">
              <span class="multi-value2">
                {{ detailItem.zh_desc_class }}
              </span>
            </div>
          </el-descriptions-item>
        </el-descriptions>
      </div>
    </el-dialog>
    <div class="statistics-container">
      <!-- å·¦ä¾§ç­›é€‰æ  -->
      <div class="left-sidebar" :class="{ collapsed: isSidebarCollapsed }">
        <div class="stats-header">
          <div class="ai-avatar">
            <img :src="require('@/assets/img/AI-avatar.png')" class="avatar-img" />
            <div class="header-text" v-show="!isSidebarCollapsed">
              <h3 class="content-title header-gradient2">你好,我是小贝</h3>
              <div class="stats-numbers">
                <span>很高兴见到你!</span>
              </div>
            </div>
            <div class="header-actions">
              <img :src="require('@/assets/img/fold_up.png')" class="fold-icon" :class="{ reverse: isSidebarCollapsed }"
                @click="toggleSidebar" />
              <img v-if="isSidebarCollapsed" :src="require('@/assets/img/conversation.png')" class="fold-icon"
                :class="{ reverse: isSidebarCollapsed }" @click="handleNewSession" />
              <img v-if="isSidebarCollapsed" :src="require('@/assets/img/historical.png')" class="fold-icon"
                :class="{ reverse: isSidebarCollapsed }" @click="handleHistoryClick" />
            </div>
          </div>
        </div>
        <div class="session-buttons" v-if="!isSidebarCollapsed">
          <el-button @click="handleHistoryClick" type="primary" round class="history-btn"
            style="flex: 1; margin-right: 10px">
            åŽ†å²ä¼šè¯
          </el-button>
          <el-button @click="handleNewSession" type="primary" round class="new-session-btn"
            style="flex: 1; position: relative">
            å¼€å¯æ–°ä¼šè¯
            <i class="el-icon-arrow-right" style="margin-left: 8px; font-size: 14px"></i>
          </el-button>
        </div>
        <el-divider v-if="!isSidebarCollapsed"></el-divider>
        <!-- AI检索组件 -->
        <ai-retrieval @list-selected="handleAiRetrievalSelected" :current-chat-id="selectedChatId" ref="aiRetrieval"
          class="ai-retrieval-container" v-show="showAIRetrieval && !isSidebarCollapsed" />
        <chat-history @item-selected="handleHistorySelected" class="ai-retrieval-container"
          v-show="!showAIRetrieval && !isSidebarCollapsed" />
      </div>
      <!-- å³ä¾§ä¸»å†…容 -->
      <div class="right-content">
        <div class="right-header">
          <h2 class="content-title" >
            <span class="header-gradient">AI</span><span style="margin-left: 15px;font-size: 30px">文搜万物</span>
          </h2>
          <div class="header-actions-right">
            <el-button type="primary" class="action-btn" @click="handleRealTimeMonitor">
              å®žæ—¶ç›‘控
            </el-button>
            <el-button type="primary" class="action-btn" @click="resetList()">
              åˆ·æ–°æ•°æ®
            </el-button>
          </div>
        </div>
        <!-- æ£€æµ‹ç»“果展示 -->
        <div class="content-wrapper">
          <!-- æ·»åŠ æ•°æ®åŠ è½½çŠ¶æ€æç¤º -->
          <div v-if="isLoading" class="loading-container">
            <el-icon class="is-loading" size="24">
              <Loading />
            </el-icon>
            <span>数据加载中...</span>
          </div>
          <!-- æ— é™æ»šåЍ容噍 -->
          <div class="results-container" @scroll="handleScroll">
            <el-empty v-if="results.length === 0" description="暂无数据" style="margin-top: 50px"></el-empty>
            <div v-else class="gallery-section">
              <div class="image-grid-container">
                <div class="image-grid" ref="imageGrid">
                  <div v-for="(item, index) in results" :key="index" class="image-card-wrapper"
                    :style="{ width: cardWidth }">
                    <el-card class="result-card" :class="{ 'selected-card': selectedItemId === item.id }"
                      @click.native.stop="handleCardClick(item)">
                      <div class="image-wrapper">
                        <el-image slot="error" :src="item.image_path" class="result-image" alt="检测结果" />
                        <!-- <img slot="error" src="@/assets/01.png" class="result-image" alt="检测结果" /> -->
                        <div class="image-overlay" v-if="item.is_warning == 1">
                          <span class="check-item">
                            {{ item.task_names }}
                          </span>
                          <el-tag size="mini" class="level-tag">
                            {{ item.event_levels }}
                          </el-tag>
                        </div>
                      </div>
                      <div class="card-content">
                        <div class="meta-info">
                          <div class="time-info">
                            <img src="@/assets/img/time-fill@1x.png" alt="" class="time-icon">
                            <span class="detect-time">{{ formatStartDateTime(item.detect_time) }}</span>
                            <el-popover placement="bottom" width="300" trigger="click" v-model="popoverVisible[index]"
                              v-if="item.is_desc === 2" class="right-btn2">
                              <i class="el-icon-close" @click="closePopover(index)"></i>
                              <span style="color: black; font-weight: 600">图片内容</span>
                              <div class="task-popover-content">
                                {{ item.zh_desc_class }}
                              </div>
                              <img src="@/assets/img/article-fill@1x.png" alt="" class="time-icon2" slot="reference"
                                @click.stop="togglePopover(index)">
                            </el-popover>
                          </div>
                          <div class="device-info">
                            <div class="camera-info">
                              <img src="@/assets/img/live-fill@1x.png" alt="" class="time-icon">
                              <span>{{ item.video_name }}</span>
                              <el-dropdown size="small" @command="handleCommand" class="right-btn">
                                <img src="@/assets/img/modelTraining.png"
                                  style="width: 16px;height: 16px;margin-left: 10px; vertical-align: middle">
                                <el-dropdown-menu slot="dropdown">
                                  <el-dropdown-item
                                    :command="{ ruleName: item.rule_names ? item.rule_names[0].fileName : '', cameraId: item.video_point_id + '', cameraName: item.video_name, imagePath: item.image_path, status: 1 }">正确</el-dropdown-item>
                                  <el-dropdown-item
                                    :command="{ ruleName: item.rule_names ? item.rule_names[0].fileName : '', cameraId: item.video_point_id + '', cameraName: item.video_name, imagePath: item.image_path, status: 2 }">错误</el-dropdown-item>
                                  <el-dropdown-item
                                    :command="{ ruleName: item.rule_names ? item.rule_names[0].fileName : '', cameraId: item.video_point_id + '', cameraName: item.video_name, imagePath: item.image_path, status: 0 }">不确定</el-dropdown-item>
                                </el-dropdown-menu>
                              </el-dropdown>
                            </div>
                          </div>
                        </div>
                      </div>
                    </el-card>
                  </div>
                </div>
              </div>
            </div>
            <!-- æ»šåŠ¨åŠ è½½æç¤º -->
            <div v-if="isLoadingMore" class="loading-more">
              <el-icon class="is-loading" size="16">
                <Loading />
              </el-icon>
              <span>加载更多...</span>
            </div>
            <div v-if="!hasMore && results.length > 0" class="no-more-data">
              æ²¡æœ‰æ›´å¤šæ•°æ®äº†
            </div>
          </div>
          <!-- åˆ†é¡µ -->
          <!-- <div class="pagination-wrapper" v-if="results.length > 0">
                    <el-pagination :current-page="currentPage" :page-size="16" :total="totalResults"
                        layout="prev, pager, next" @current-change="handlePageChange">
                    </el-pagination>
                </div> -->
        </div>
      </div>
    </div>
    </div>
</template>
<script>
import _ from 'lodash'; // ç”¨äºŽé˜²æŠ–
import surey from "@/api/SurveyView";
import aiRetrieval from './AiRetrievalView';
import chatHistory from './ChatHistoryView';
import cameraVideo from './cameraVideo.vue';
export default {
    components: {
        aiRetrieval,
        chatHistory,
        cameraVideo
    },
  data() {
    return {
      // æ–°å¢žå¡ç‰‡å®½åº¦è®¡ç®—相关数据
      cardWidth: '300px', // é»˜è®¤å¡ç‰‡å®½åº¦
      minCardWidth: 300, // å¡ç‰‡æœ€å°å®½åº¦
      margin: 20, // å¡ç‰‡é—´è·
      idsList: [],
      pageSize: 30,          // æ¯é¡µåŠ è½½æ•°æ®é‡
      hasMore: true,          // æ˜¯å¦æœ‰æ›´å¤šæ•°æ®
      isLoadingMore: false,   // æ˜¯å¦æ­£åœ¨åŠ è½½æ›´å¤š
      scrollTop: 0,           // æ»šåŠ¨ä½ç½®è®°å½•
      docUrl: "",
      isLoading: false, // æ–°å¢žåŠ è½½çŠ¶æ€å˜é‡
      backendData: true,
      popoverVisible: {}, // ç”¨äºŽæŽ§åˆ¶æ¯ä¸ªå¡ç‰‡çš„æ°”泡显示状态
      dialogVisible: true,
      handles: [
        { position: "top" },
        { position: "bottom" },
        { position: "left" },
        { position: "right" },
        { position: "top-left" },
        { position: "top-right" },
        { position: "bottom-left" },
        { position: "bottom-right" },
      ],
      isResizing: false,
      startX: 0,
      startY: 0,
      startWidth: 0,
      startHeight: 0,
      startLeft: 0,
      startTop: 0,
      realTimeVisible: false, // æ–°å¢žå¼¹çª—控制变量
      selectedChatId: null,
      showAIRetrieval: true,
      isSidebarCollapsed: false,
      selectedItemId: null, // æ–°å¢žé€‰ä¸­é¡¹ID
      filterOptions: {
        tasks: [],
        levels: [],
        cameras: [],
        rules: [],
      },
      currentMediaType: "image", // å½“前媒体类型
      detailVisible: false,
      detailItem: {},
      currentPage: 1,
      totalResults: 40, // æ€»æ•°æ®é‡
      filter: {
        keyword: "",
        task: "",
        timeRange: [],
        level: "",
        camera: "",
        rules: "",
      },
      showWarningOnly: true,
      results: [
        {
          // image_path: require("@/assets/01.png"),
          // video_path: require("@/assets/video.mp4"),
          risk_description: "隐患描述",
          detect_time: "2021-12-09 20:23",
          video_name: "摄像机A",
          event_level: "1",
          is_warning: 1,
          event_levels: "一级",
          task_names: "生产任务管控,生产任务管控,生产任务管控,生产任务管控,生产任务管控", // æ”¹ä¸ºæ•°ç»„
          suggestion: "处理建议", // å¤„理建议
          video_point_id: 1,
          rule_names: "",
          knowledge_documents: [
            { ruleId: 1, fileName: "安全准则", file_url: "https://image.baidu.com/search/detail?z=0&word=%E5%9B%BE%E7%89%87&hs=0&pn=1&spn=0&di=7498023338351001601&pi=0&rn=1&tn=baiduimagedetail&is=0%2C0&ie=utf-8&oe=utf-8&lm=&cs=2629589424%2C2655723787&os=2900564191%2C785059641&simid=3974997321%2C325136453&adpicid=0&lpn=0&fr=click-pic&fm=&ic=&hd=&latest=&copyright=&isImgSet=&commodity=&hot=&imgratio=&imgformat=&sme=&width=0&height=0&cg=&bdtype=0&oriquery=&objurl=https%3A%2F%2Fww2.sinaimg.cn%2Fmw690%2F61d7678dgy1hvt194v9kqj20p00uuape.jpg&fromurl=ippr_z2C%24qAzdH3FAzdH3Fojtk5_z%26e3Bv54AzdH3F8m98cam0a8AzdH3FP81pPuN3N&gsm=1e&islist=&querylist=&lid=9dec68e500008991" },
            { ruleId: 1, fileName: "安全准则", file_url: "http://192.168.1.232:7010/home/debian/GroundingDINO/txt/zs/AI-avatar.png" },
          ], // é¢„警规则数组
          zh_desc_class: "图片显示的是一个室内场景,时间戳显示为2025å¹´6月27日星期五16:57:12。画面中可以看到一个走廊过道的环境,右侧有一扇玻璃门,门上覆盖着蓝色的防尘布。左侧墙上有一个黑板,上面写有一些中文和英文的文字,内容包括“莫伟达”、“int4”、“AWQ”、“4G”等。黑板旁边有一个白色的柜子,柜子上放着一些物品。墙角处还有一株小树。整个房间的墙壁是白色的,天花板也是白色的图片显示的是一个室内场景,时间戳显示为2025å¹´6月27日星期五16:57:12。画面中可以看到一个走廊过道的环境,右侧有一扇玻璃门,门上覆盖着蓝色的防尘布。左侧墙上有一个黑板,上面写有一些中文和英文的文字,内容包括“莫伟达”、“int4”、“AWQ”、“4G”等。黑板旁边有一个白色的柜子,柜子上放着一些物品。墙角处还有一株小树。整个房间的墙壁是白色的,天花板也是白色的"
        },
        {
          image_path: "",
          risk_description: "隐患描述",
          detect_time: "2021-12-09 21:15",
          video_name: "摄像机B",
          is_desc: 2,
          zh_desc_class: "图片显示的是一个室内场景,时间戳显示为2025å¹´6月27日星期五16:57:12。画面中可以看到一个走廊过道的环境,右侧有一扇玻璃门,门上覆盖着蓝色的防尘布。左侧墙上有一个黑板,上面写有一些中文和英文的文字,内容包括“莫伟达”、“int4”、“AWQ”、“4G”等。黑板旁边有一个白色的柜子,柜子上放着一些物品。墙角处还有一株小树。整个房间的墙壁是白色的,天花板也是白色的图片显示的是一个室内场景,时间戳显示为2025å¹´6月27日星期五16:57:12。画面中可以看到一个走廊过道的环境,右侧有一扇玻璃门,门上覆盖着蓝色的防尘布。左侧墙上有一个黑板,上面写有一些中文和英文的文字,内容包括“莫伟达”、“int4”、“AWQ”、“4G”等。黑板旁边有一个白色的柜子,柜子上放着一些物品。墙角处还有一株小树。整个房间的墙壁是白色的,天花板也是白色的图片显示的是一个室内场景,时间戳显示为2025å¹´6月27日星期五16:57:12。画面中可以看到一个走廊过道的环境,右侧有一扇玻璃门,门上覆盖着蓝色的防尘布。左侧墙上有一个黑板,上面写有一些中文和英文的文字,内容包括“莫伟达”、“int4”、“AWQ”、“4G”等。黑板旁边有一个白色的柜子,柜子上放着一些物品。墙角处还有一株小树。整个房间的墙壁是白色的,天花板也是白色的图片显示的是一个室内场景,时间戳显示为2025å¹´6月27日星期五16:57:12。画面中可以看到一个走廊过道的环境,右侧有一扇玻璃门,门上覆盖着蓝色的防尘布。左侧墙上有一个黑板,上面写有一些中文和英文的文字,内容包括“莫伟达”、“int4”、“AWQ”、“4G”等。黑板旁边有一个白色的柜子,柜子上放着一些物品。墙角处还有一株小树。整个房间的墙壁是白色的,天花板也是白色的图片显示的是一个室内场景,时间戳显示为2025å¹´6月27日星期五16:57:12。画面中可以看到一个走廊过道的环境,右侧有一扇玻璃门,门上覆盖着蓝色的防尘布。左侧墙上有一个黑板,上面写有一些中文和英文的文字,内容包括“莫伟达”、“int4”、“AWQ”、“4G”等。黑板旁边有一个白色的柜子,柜子上放着一些物品。墙角处还有一株小树。整个房间的墙壁是白色的,天花板也是白色的图片显示的是一个室内场景,时间戳显示为2025å¹´6月27日星期五16:57:12。画面中可以看到一个走廊过道的环境,右侧有一扇玻璃门,门上覆盖着蓝色的防尘布。左侧墙上有一个黑板,上面写有一些中文和英文的文字,内容包括“莫伟达”、“int4”、“AWQ”、“4G”等。黑板旁边有一个白色的柜子,柜子上放着一些物品。墙角处还有一株小树。整个房间的墙壁是白色的,天花板也是白色的"
        },
      ],
    };
  },
  computed: {
    normalizedPath() {
      return (backendPath) => {
        // æ›¿æ¢é€»è¾‘
        return require(backendPath
          .replace(/^[a-zA-Z]:/, "") // åŽ»é™¤ç›˜ç¬¦
          .replace(/\/+/g, "/") // åˆå¹¶å¤šä½™æ–œæ 
          .replace("/opt/smart", "@/assets")); // å…³é”®è·¯å¾„替换
      };
    },
    visibleResults() {
      return this.results.slice(
        (this.currentPage - 1) * 8,
        this.currentPage * 8
      );
    },
    boxHeight() {
      // æ ¹æ®åŽç«¯æ•°æ®å†³å®šé«˜åº¦
      //   if (!this.backendData) return "380px";
      return this.backendData ? "160px" : "320px";
    },
  },
  mounted() {
    // this.fetchFilterOptions();//筛选列表
    this.handleSearch([]);
    this.fetchCameraInfo();
    this.resetList();
    this.calculateCardWidth();
    // æ·»åŠ é˜²æŠ–çš„resize监听
    window.addEventListener('resize', _.debounce(this.calculateCardWidth, 100));
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.calculateCardWidth);
  },
  methods: {
    // æ–°å¢žå¡ç‰‡å®½åº¦è®¡ç®—方法
    calculateCardWidth() {
      if (this.$refs.imageGrid) {
        const containerWidth = this.$refs.imageGrid.clientWidth;
        // è®¡ç®—每行可以放置的卡片数量,最少3列
        const cardsPerRow = Math.max(3, Math.floor(containerWidth / (this.minCardWidth + this.margin)));
        // è®¾ç½®å¡ç‰‡å®½åº¦å…¬å¼
        this.cardWidth = `calc(${100 / cardsPerRow}% - ${this.margin}px)`;
      }
    },
    handleCommand(command) {
      console.log(JSON.stringify(command))
      surey.insertModelTraining(JSON.stringify(command)).then((res) => {
        if (res && res.status === 200) {
          this.$notify({
            type: "success",
            message: "添加成功",
          });
        } else {
          this.$notify({
            type: "error",
            message: "添加失败!",
          });
        }
      });
    },
    // é‡ç½®åˆ—表
    resetList() {
      this.idsList = [];
      this.currentPage = 1;
      this.hasMore = true;
      this.results = [];
      this.handleSearch([]);
    },
    // æ»šåŠ¨äº‹ä»¶å¤„ç†
    handleScroll(event) {
      const container = event.target;
      // è®°å½•滚动位置
      this.scrollTop = container.scrollTop;
      // æ£€æŸ¥æ˜¯å¦æ»šåŠ¨åˆ°åº•éƒ¨
      const isBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 50;
      // å½“滚动到底部且有更多数据,且当前没有加载中,则加载更多
      if (isBottom && this.hasMore && !this.isLoading && !this.isLoadingMore) {
        this.loadMoreData();
      }
    },
    // åŠ è½½æ›´å¤šæ•°æ®
    async loadMoreData() {
      if (this.isLoadingMore || !this.hasMore) return;
      try {
        this.isLoadingMore = true;
        this.currentPage++;
        const params = {
          ids: [],
          page: this.currentPage,
          pageSize: this.pageSize
        };
        const response = await surey.getSurveys(params);
        const lists = response.data.list || [];
        if (lists) {
          for (let i = 0; i < lists.length; i++) {
            console.log("333:" + lists[i].video_point_id)
            this.results.push({
              task_names: lists[i].task_name,
              video_name: lists[i].video_name,
              image_path: "/api-img" + lists[i].image_path,
              video_path: "/api-img" + lists[i].video_path,
              detect_time: lists[i].detect_time,
              event_levels: lists[i].event_level_name,
              zh_desc_class: lists[i].zh_desc_class,
              is_warning: lists[i].is_warning,
              is_desc: lists[i].is_desc,
              video_point_id: lists[i].video_point_id,
              rule_names: lists[i].rule_names,
              knowledge_documents: lists[i].knowledge_documents.map(file => {
                return {
                  ...file,
                  file_url: "/api-img" + file.file_url,
                  fileName: file.title
                }
              })
            });
          }
        }
        // åˆå¹¶ç»“æžœ
        // this.results = [...this.results, ...lists];
        // æ›´æ–°æ˜¯å¦æœ‰æ›´å¤šæ•°æ®çŠ¶æ€
        this.hasMore = lists.length >= this.pageSize;
      } catch (error) {
        console.error("加载更多失败:", error);
      } finally {
        this.isLoadingMore = false;
      }
    },
    async fetchCameraInfo() {
      const response = await fetch("/config.json");
      if (!response.ok) throw new Error(`请求失败: ${response.status}`);
      const responseData = await response.json();
      this.docUrl = responseData.docUrl;
      console.info("docUrl:" + this.docUrl)
    },
    closePopover(index) {
      this.$set(this.popoverVisible, index, false);
    },
    startResize(position, e) {
      this.isResizing = true;
      const dialog = document.querySelector(".resizable-dialog");
      this.startX = e.clientX;
      this.startY = e.clientY;
      this.startWidth = dialog.offsetWidth;
      this.startHeight = dialog.offsetHeight;
      this.startLeft = dialog.offsetLeft;
      this.startTop = dialog.offsetTop;
      document.addEventListener("mousemove", this.handleResize(position));
      document.addEventListener("mouseup", this.stopResize);
    },
    handleResize(position) {
      return (e) => {
        if (!this.isResizing) return;
        const dialog = document.querySelector(".resizable-dialog");
        const deltaX = e.clientX - this.startX;
        const deltaY = e.clientY - this.startY;
        // é™åˆ¶æœ€å°å°ºå¯¸
        const minWidth = 400;
        const minHeight = 300;
        switch (position) {
          case "top":
            dialog.style.height =
              Math.max(minHeight, this.startHeight - deltaY) + "px";
            dialog.style.top = `${this.startTop + deltaY}px`;
            break;
          case "bottom":
            dialog.style.height =
              Math.max(minHeight, this.startHeight + deltaY) + "px";
            break;
          case "left":
            dialog.style.width =
              Math.max(minWidth, this.startWidth - deltaX) + "px";
            dialog.style.left = `${this.startLeft + deltaX}px`;
            break;
          case "right":
            dialog.style.width =
              Math.max(minWidth, this.startWidth + deltaX) + "px";
            break;
          case "top-left":
            dialog.style.width =
              Math.max(minWidth, this.startWidth - deltaX) + "px";
            dialog.style.height =
              Math.max(minHeight, this.startHeight - deltaY) + "px";
            dialog.style.left = `${this.startLeft + deltaX}px`;
            dialog.style.top = `${this.startTop + deltaY}px`;
            break;
          case "top-right":
            dialog.style.width =
              Math.max(minWidth, this.startWidth + deltaX) + "px";
            dialog.style.height =
              Math.max(minHeight, this.startHeight - deltaY) + "px";
            dialog.style.top = `${this.startTop + deltaY}px`;
            break;
          case "bottom-left":
            dialog.style.width =
              Math.max(minWidth, this.startWidth - deltaX) + "px";
            dialog.style.height =
              Math.max(minHeight, this.startHeight + deltaY) + "px";
            dialog.style.left = `${this.startLeft + deltaX}px`;
            break;
          case "bottom-right":
            dialog.style.width =
              Math.max(minWidth, this.startWidth + deltaX) + "px";
            dialog.style.height =
              Math.max(minHeight, this.startHeight + deltaY) + "px";
            break;
        }
      };
    },
    stopResize() {
      this.isResizing = false;
      document.removeEventListener("mousemove", this.handleResize);
      document.removeEventListener("mouseup", this.stopResize);
    },
    // å®žæ—¶ç›‘控方法
    handleRealTimeMonitor() {
      this.realTimeVisible = true;
    },
    handleHistorySelected(chatId) {
      this.showAIRetrieval = true; // åˆ‡æ¢ç»„ä»¶
      this.selectedChatId = chatId; // ä¿å­˜chatId
      //   this.$nextTick(() => {
      //     this.$refs.aiRetrieval.loadChat(); // è§¦å‘子组件加载
      //   });
    },
    handleAiRetrievalSelected(params) {
      console.info("params:" + params)
      //根据id查询数据
      this.idsList = params;
      this.handleSearch()
    },
    handleHistoryClick() {
      this.showAIRetrieval = false;
      this.isSidebarCollapsed = false;
    },
    handleNewSession() {
      // è°ƒç”¨AI检索组件的重置方法
      if (
        this.$refs.aiRetrieval &&
        typeof this.$refs.aiRetrieval.reset === "function"
      ) {
        this.$refs.aiRetrieval.reset();
      }
      // ä¿æŒä¾§è¾¹æ å±•开状态
      this.isSidebarCollapsed = false;
      this.showAIRetrieval = true;
    },
    toggleSidebar() {
      this.isSidebarCollapsed = !this.isSidebarCollapsed;
    },
    // ä¿®æ”¹åŽçš„点击处理方法
    handleCardClick(item) {
      this.selectedItemId = item.id;
      this.showDetail(item);
    },
    // getLevelType(level) {
    //     const typeMap = {
    //         '1': 'danger',
    //         '2': 'warning',
    //         '3': 'primary',
    //         '4': 'info',
    //         '5': 'success'
    //     }
    //     return typeMap[level] || 'info'
    // },
    // èŽ·å–ç­›é€‰æ¡ä»¶é€‰é¡¹
    async fetchFilterOptions() {
      try {
        const tasks = await surey.getTasks({
          //检测内容
          page: 1,
          pageSize: 999,
        });
        this.filterOptions.tasks = tasks.data.list;
        const cameras = await surey.getCameras(); //视频点位
        this.filterOptions.cameras = cameras.data;
        const warnings = await surey.getWarnings({
          //预警规则
          page: 1,
          pageSize: 999,
        });
        this.filterOptions.rules = warnings.data.list;
        const events = await surey.getEvents(); //事件等级
        this.filterOptions.levels = events.data;
        // console.info(this.filterOptions.levels)
      } catch (error) {
        console.error("获取筛选选项失败:", error);
        this.$message.error("筛选选项加载失败");
      }
    },
    // æ ¼å¼åŒ–开始时间(保持原时间)
    formatStartDateTime(date) {
      if (!date) return null;
      const d = new Date(date);
      const pad = (num) => num.toString().padStart(2, "0");
      return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(
        d.getDate()
      )} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
    },
    // æ ¼å¼åŒ–结束时间(固定为 23:59:59)
    formatEndDateTime(date) {
      if (!date) return null;
      const d = new Date(date);
      d.setHours(23, 59, 59); // å¼ºåˆ¶è®¾ç½®ä¸ºå½“天的 23:59:59
      const pad = (num) => num.toString().padStart(2, "0");
      return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(
        d.getDate()
      )} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
    },
    // æŸ¥è¯¢æ–¹æ³•
    async handleSearch() {
      console.info("this.idsList:" + this.idsList)
      this.isLoading = true; // å¼€å§‹åŠ è½½
      // console.info(ids)
      try {
        this.currentPage = 1;
        const params = {
          ids: this.idsList,
          page: 1,
          pageSize: this.pageSize,
        };
        const response = await surey.getSurveys(params);
        const lists = response.data.list || [];
        this.results = [];
        if (lists) {
          for (let i = 0; i < lists.length; i++) {
            this.results.push({
              task_names: lists[i].task_name,
              video_name: lists[i].video_name,
              image_path: "/api-img" + lists[i].image_path,
              video_path: "/api-img" + lists[i].video_path,
              detect_time: lists[i].detect_time,
              event_levels: lists[i].event_level_name,
              zh_desc_class: lists[i].zh_desc_class,
              is_warning: lists[i].is_warning,
              is_desc: lists[i].is_desc,
              video_point_id: lists[i].video_point_id,
              rule_names: lists[i].rule_names,
              knowledge_documents: lists[i].knowledge_documents.map(file => {
                return {
                  ...file,
                  file_url: "/api-img" + file.file_url,
                  fileName: file.title
                }
              }),
              risk_description: lists[i].risk_description,
              suggestion: lists[i].suggestion
            });
          }
        }
        this.totalResults = response.data.pagination.total;
        // æ›´æ–°æ˜¯å¦æœ‰æ›´å¤šæ•°æ®çŠ¶æ€
        this.hasMore = lists.length >= this.pageSize;
        // console.info(response.data)
      } catch (error) {
        console.error("查询失败:", error);
        this.$message.error("查询失败,请稍后重试");
      } finally {
        this.isLoading = false; // ç»“束加载
      }
    },
    // æ–°å¢žè¯¦æƒ…展示方法
    showDetail(item) {
      // console.info(item)
      this.backendData = item.is_warning == 1 ? true : false;
      //   console.info(item.is_warning)
      this.currentMediaType = "image";
      this.detailItem = {
        ...item,
        task: "生产任务管控", // æ ¹æ®å›¾ç‰‡ä¿¡æ¯æ·»åŠ ä»»åŠ¡åç§°
      };
      // this.detailItem.image_path = 'http://192.168.1.232:7010/' + item.image_path
      // this.detailItem.video_path = 'http://192.168.1.232:7010/' + item.video_path
      this.detailItem.image_path = item.image_path;
      this.detailItem.video_path = item.video_path;
      this.detailItem.is_warning = item.is_warning;
      this.detailItem.suggestion = item.suggestion;
      this.detailItem.risk_description = item.risk_description;
      this.detailVisible = true;
      // console.info(item)
    },
    // åˆ†é¡µå¤„理
    handlePageChange(page) {
      this.currentPage = page;
      this.handleSearch([]);
    },
    // é‡ç½®ç­›é€‰æ¡ä»¶
    resetFilter() {
      this.filter = {
        keyword: "",
        task: "",
        timeRange: [],
        level: "",
        camera: "",
      };
      this.currentPage = 1;
      this.handleSearch([]);
    },
    // èŽ·å–æ–‡æ¡£é¢„è§ˆURL
    getPreviewUrl(doc) {
      // èŽ·å–æ–‡ä»¶æ‰©å±•å
      const extension = this.getFileExtension(doc.fileName);
      // PDF使用直接访问
      if (extension === 'pdf') {
        return doc.file_url;
      }
      // Office文档使用微软预览服务
      // if (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(extension)) {
      //   return `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(doc.file_url)}`;
      // }
      // å…¶ä»–类型直接返回
      return doc.file_url;
    },
    getPreviewUrl2(id) {
      window.location.href = "/api/v1/knowledge/download?id=" + id
      // window.location.href="http://192.168.1.176:8088/v1/knowledge/download?id="+id
    },
    // èŽ·å–æ–‡ä»¶æ‰©å±•å
    getFileExtension(filename) {
      const parts = filename.split('.');
      return parts.length > 1 ? parts.pop().toLowerCase() : '';
    },
  },
  watch: {
    detailVisible(newVal) {
      if (!newVal) {
        this.selectedItemId = null; // å…³é—­å¼¹çª—时清除选中状态
      }
    },
  },
};
</script>
<style>
/* å…¨å±€ç”Ÿæ•ˆï¼Œå¯è¦†ç›– tooltip */
.my-tooltip {
  max-width: 540px !important;
  color: #606266 !important;
}
</style>
<style lang="scss" scoped>
/* æ–°å¢žå›¾ç‰‡ç½‘格布局样式 */
.image-grid-container {
  width: 100%;
  overflow: hidden; /* è¶…出部分隐藏 */
}
.image-grid {
  display: flex;
  flex-wrap: wrap;
  margin: -10px; /* è´Ÿè¾¹è·æŠµæ¶ˆåŒ…裹元素的边距 */
  width: 100%;
}
.image-card-wrapper {
  margin: 10px; /* è®¾ç½®å¡ç‰‡é—´è· */
  // box-sizing: border-box;
  // transition: width 0.3s ease; /* æ·»åŠ å¹³æ»‘è¿‡æ¸¡æ•ˆæžœ */
  min-width: 300px; /* å¡ç‰‡æœ€å°å®½åº¦ */
  flex-shrink: 0; /* é˜²æ­¢å¡ç‰‡ç¼©å° */
}
.right-btn {
  position: absolute;
  right: 8%;
  top: 50%;
  transform: translateY(-50%);
  /* åž‚直居中 */
}
.right-btn2 {
  position: absolute;
  right: 10.5%;
  top: 74.5%;
  transform: translateY(-50%);
  /* åž‚直居中 */
}
.time-icon {
  width: 16px;
  height: 16px;
  margin-right: 20px;
  vertical-align: middle;
}
.time-icon2 {
  width: 16px;
  height: 16px;
  margin-right: 0px;
  vertical-align: middle;
}
/* å…³é—­æŒ‰é’®æ ·å¼ */
.el-icon-close {
  position: absolute;
  top: 10px;
  right: 5px;
  padding: 0;
  width: 24px;
  height: 24px;
  font-size: 14px;
  color: #909399;
  background: none;
  border: none;
  cursor: pointer;
  transition: all 0.2s;
}
/* æ–°å¢žæ°”泡内容样式 */
.task-popover-content {
  max-height: 100px;
  overflow-y: auto;
  padding: 20px 15px 15px 15px;
  /* å¢žåŠ é¡¶éƒ¨ç©ºé—´ç»™å…³é—­æŒ‰é’® */
  line-height: 1.5;
  word-break: break-word;
  white-space: pre-line;
  border-radius: 10px;
  /* è‡ªå®šä¹‰æ»šåŠ¨æ¡æ ·å¼ */
  &::-webkit-scrollbar {
    width: 4px;
    /* ç¼©å°æ»šåŠ¨æ¡å®½åº¦ */
    background-color: transparent;
  }
  &::-webkit-scrollbar-track {
    background: transparent;
    /* éšè—è½¨é“ */
  }
  &::-webkit-scrollbar-thumb {
    background-color: #c0c4cc;
    /* è®¾ç½®æ»‘块颜色 */
    border-radius: 2px;
  }
  /* ç§»é™¤æ»šåŠ¨æ¡ç®­å¤´ */
  &::-webkit-scrollbar-button {
    display: none;
    /* éšè—ä¸Šä¸‹ç®­å¤´ */
  }
}
.resizable-dialog {
  position: fixed !important;
  margin: 0 !important;
  padding-top: 0px;
  min-width: 200px;
  min-height: 300px;
  overflow: auto;
  top: 50% !important;
  left: 50% !important;
  transform: translate(-50%, -50%) !important;
}
.resize-handle {
  position: absolute;
  background: #409eff !important;
  z-index: 9999;
  opacity: 0;
  transition: opacity 0.2s;
}
.resize-handle:hover {
  opacity: 1;
}
/* å„方向手柄定位 */
.top {
  top: 0;
  left: 0;
  right: 0;
  height: 4px;
  cursor: ns-resize;
}
.bottom {
  bottom: 0;
  left: 0;
  right: 0;
  height: 4px;
  cursor: ns-resize;
}
.left {
  top: 0;
  left: 0;
  width: 4px;
  bottom: 0;
  cursor: ew-resize;
}
.right {
  top: 0;
  right: 0;
  width: 4px;
  bottom: 0;
  cursor: ew-resize;
}
.top-left {
  top: 0;
  left: 0;
  width: 10px;
  height: 10px;
  cursor: nwse-resize;
}
.top-right {
  top: 0;
  right: 0;
  width: 10px;
  height: 10px;
  cursor: nesw-resize;
}
.bottom-left {
  bottom: 0;
  left: 0;
  width: 10px;
  height: 10px;
  cursor: nesw-resize;
}
.bottom-right {
  bottom: 0;
  right: 0;
  width: 10px;
  height: 10px;
  cursor: nwse-resize;
}
.el-dialog__body {
  height: calc(100% - 54px);
  overflow: auto;
}
.fold-icon {
  width: 24px;
  height: 24px;
  cursor: pointer;
  transition: transform 0.3s ease;
  margin: 8px;
  &:hover {
    opacity: 0.8;
    filter: drop-shadow(0 0 2px rgba(11, 113, 216, 0.5));
  }
  &.reverse {
    transform: rotate(180deg);
  }
}
/* ai-retrieval容器样式 */
.ai-retrieval-container {
  position: absolute;
  // left: 5px;
  width: 360px;
  // height: 1000px;
  z-index: 1000;
  background: rgb(245, 244, 244);
  // box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
}
/* æ–°å¢žå¤šå€¼æ˜¾ç¤ºæ ·å¼ */
.multi-value {
  display: inline-block;
  max-width: 540px;
  //   white-space: nowrap;
  overflow: hidden;
  //   text-overflow: ellipsis;
}
.desc_class {
  //   max-height: 380px;
  overflow-y: auto;
}
.multi-value2 {
  display: inline-block;
  //   max-height: 380px;
}
.stats-header {
  margin-bottom: 20px;
  .ai-avatar {
    display: flex;
    align-items: flex-start; // é¡¶éƒ¨å¯¹é½
    .header-text {
      margin-left: 10px;
      .content-title {
        text-align: left;
        margin: 5px 0 10px;
        color: #0e3eaa;
      }
      .stats-numbers {
        font-size: 12px;
        text-align: left;
      }
    }
    .header-actions {
      flex: 1;
      text-align: right;
      margin-top: 10px;
    }
  }
}
.results-container {
  padding: 0 15px;
  //   min-height: 600px;
  overflow-x: hidden; /* éšè—æ¨ªå‘滚动条 */
  overflow-y: auto; /* ä¿ç•™çºµå‘滚动 */
  /* æ–°å¢žæ»šåŠ¨æ¡ */
  height: 900px;
  .result-card {
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
    border: none;
    position: relative; // ä¸ºç»å¯¹å®šä½æä¾›åŸºå‡†
    overflow: visible; // é˜²æ­¢æŒ‰é’®è¢«è£å‰ª
    .image-wrapper {
      position: relative;
      //   height: 200px;
      overflow: visible;
      border-radius: 6px 6px 0 0;
      .result-image {
        width: 100%;
        // height: 130px;
        object-fit: cover;
        transition: transform 0.3s;
      }
      .image-overlay {
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        background: rgba(0, 0, 0, 0.6);
        /* åŠé€æ˜Žé»‘色背景 */
        padding: 5px 10px;
        display: flex;
        justify-content: space-between;
        align-items: center;
        z-index: 2;
        /* ç¡®ä¿åœ¨å›¾ç‰‡ä¸Šæ–¹ */
      }
      .check-item {
        color: white;
        font-size: 12px;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        max-width: 70%;
      }
      .level-tag {
        z-index: 3;
        /* ç¡®ä¿åœ¨è¦†ç›–层上方 */
        transform: scale(0.9);
        margin: 2px;
        color: #fa0505;
        background-color: #f3e1e1;
      }
    }
    .card-content {
      padding: 15px;
      .meta-info {
        .time-info {
          display: flex;
          text-align: left;
          align-items: center;
          margin-bottom: 10px;
          color: #909399;
          width: 100%;
          margin-bottom: 10px;
          .el-icon-time {
            color: #409eff !important;
            margin-right: 20px;
            font-size: 16px;
          }
          span {
            font-size: 13px;
            margin-right: 20px;
          }
          .el-icon-document {
            color: #034914;
          }
          .detect-time {
            flex-shrink: 0;
            color: #909399;
            font-size: 13px;
          }
        }
        .device-info {
          //   display: flex;
          justify-content: space-between;
          align-items: center;
          .camera-info {
            // display: flex;
            position: relative;
            text-align: left;
            align-items: center;
            i {
              color: #409eff !important;
              margin-right: 20px;
              font-size: 16px;
            }
            span {
              font-size: 14px;
              color: #303133;
            }
          }
        }
      }
    }
  }
}
.pagination-wrapper {
  //   position: fixed;
  right: 30px;
  bottom: 30px;
  background: #fff;
  padding: 10px 20px;
  border-radius: 4px;
  //   box-shadow: 0 2px 12px rgba(0,0,0,0.1);
}
// è°ƒæ•´å¸ƒå±€ç»“æž„
.statistics-container {
  display: flex;
  height: 98vh; // æ–°å¢ž
  .left-sidebar {
    width: 320px;
    transition: all 0.3s ease;
    left: 0;
    // height: 100vh;
    z-index: 1000;
    background: #fff;
    box-shadow: 1px 0 10px rgba(0, 0, 0, 0.1);
    padding: 10px;
    border-right: 1px solid #ebeef5;
    position: relative;
    padding-bottom: 40px;
    /* ä¸ºåº•部按钮留出空间 */
    /* ä¸ºç»å¯¹å®šä½åˆ›å»ºå‚ç…§ */
    .session-buttons {
      // position: absolute;
      bottom: 20px;
      left: 20px;
      right: 20px;
      display: flex;
      gap: 0;
      .el-button {
        height: 50px;
        border-radius: 5px;
        transition: all 0.3s ease;
        background-color: #e1ebff !important;
        color: #2482ff;
        border: 1px solid #e1ebff !important;
        font-size: 16px !important;
        padding: 0 12px;
      }
      ::v-deep .el-button {
        background-color: #e1ebff !important;
      }
    }
    &.collapsed {
      width: 40px;
      .ai-avatar {
        flex-direction: column;
        align-items: center;
        padding: 10px 0;
        .avatar-img {
          margin: 0;
          width: 51px;
          height: 35px;
        }
      }
      .header-actions i {
        transform: rotate(180deg);
        margin-top: 10px;
      }
    }
    .header-actions {
      i {
        transition: all 0.3s ease;
        cursor: pointer;
        font-size: 20px;
        color: #666;
        &:hover {
          color: #0b71d8;
        }
        &.reverse {
          transform: rotate(180deg);
        }
      }
    }
    .right-content {
      transition: margin 0.3s ease;
      padding: 20px;
      min-height: 100vh;
      background: #f5f6fa;
    }
    .ai-retrieval-container {
      width: 100%;
      height: calc(100% - 150px);
      /* æ ¹æ®å®žé™…布局调整高度 */
      position: relative;
      box-shadow: none;
      background: #fff;
    }
    .el-checkbox {
      margin-left: 15px;
      display: block;
      text-align: left;
    }
    .el-form-item__content {
      margin-left: 0 !important;
    }
  }
  .right-content {
    flex: 1;
    margin-left: 20px;
    position: relative;
    box-shadow: 1px 0 10px rgba(0, 0, 0, 0.1);
    padding: 20px;
    padding-top: 0px;
    border-left: 1px solid #ebeef5;
    .right-header {
      display: flex;
      margin-bottom: 20px;
      border-bottom: 1px solid #ebeef5;
      padding-bottom: 10px;
      justify-content: space-between;
      height: 70px;
      /* æ–°å¢žï¼šå·¦å³ä¸¤ç«¯å¯¹é½ */
      .content-title{
        text-align: left;
        margin: 15px 0 0 0;
      }
      .header-actions-right {
        margin-top: 20px;
        .action-btn {
          height: 40px;
          margin-bottom: 0px;
          padding: 10px 20px;
          border-radius: 6px;
          letter-spacing: 0.5px;
        }
      }
    }
  }
}
.form-actions {
  margin-top: 50px;
  display: flex;
  gap: 10px; // æŒ‰é’®é—´è·
  .full-width-btn {
    flex: 1; // ç­‰åˆ†å‰©ä½™ç©ºé—´
    margin-left: 0 !important;
  }
  // æ¸…除Element默认边距
  ::v-deep .el-button {
    margin-left: 0;
    margin-right: 0;
  }
}
// è¯¦æƒ…弹窗样式
.detail-dialog {
  .el-dialog__header {
    border-bottom: 1px solid #ebeef5;
  }
  .media-switch {
    margin: -50px 0 15px;
    text-align: left;
    .media-tab {
      &.is-active {
        background: #409eff !important;
        border-color: #409eff !important;
        color: white;
      }
    }
  }
  .media-container {
    height: 350px;
    border: 1px solid #ebeef5;
    border-radius: 4px;
    margin-bottom: 20px;
    position: relative;
    overflow: hidden; // æ·»åŠ æº¢å‡ºéšè—
    .main-image {
      height: 100%;
      padding: 0px;
      .detail-img {
        width: 100%;
        height: 100%;
        object-fit: contain;
      }
    }
    .video-section {
      height: 100%;
      width: 100%;
      .video-placeholder {
        width: 100%;
        height: 100%;
        position: relative;
        video {
          max-width: 100%;
          max-height: 100%;
          display: block;
          margin: 0 auto;
        }
      }
    }
  }
  .info-container {
    height: 400px;
    // overflow-y: auto;
    ::v-deep .el-descriptions__body {
      background: white;
    }
    .info-item {
      padding: 12px 10px;
      &>.el-descriptions-item__label {
        color: #606266;
        width: 90px;
      }
    }
    .red-text {
      color: #f56c6c !important;
    }
    .blue-text {
      color: #409eff !important;
    }
    .doc-link {
      padding-left: 10px;
      text-decoration: none;
      color: #1b50e4;
      // display: inline-block;
      // padding: 5px 8px;
      // border-radius: 4px;
      // transition: all 0.3s;
      // color: #165DFF;
      &:hover {
        cursor: pointer;
      }
    }
    ::v-deep .el-descriptions-item__label {
      color: black !important;
      font-weight: 600 !important;
    }
  }
}
/* å³ä¾§å¯¹é½æ ·å¼ */
.detail-dialog {
  position: fixed !important;
  top: 50% !important;
  left: auto !important;
  right: 20px !important;
  transform: translateY(-50%) !important;
  width: 660px !important;
  margin: 0 !important;
  z-index: 2000 !important;
}
::v-deep .el-dialog {
  // margin-right: 0px;
  .el-divider--horizontal {
    margin: 12px 0;
  }
}
/* è¡¨æ ¼è¡¨å¤´æ ·å¼ */
.header-gradient {
  background: radial-gradient(circle at 20% 30%, #0e5397, #2482FF);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  font-weight: bold;
  font-size: 35px;
}
.header-gradient2 {
  font-size: 18px;
  background: radial-gradient(circle at 20% 30%, #165DFF, #01040a);
  -webkit-background-clip: text;
  background-clip: text;
}
/* æ–°å¢žåŠ è½½çŠ¶æ€æ ·å¼ */
.loading-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 300px;
  color: #606266;
  font-size: 16px;
  .el-icon {
    margin-bottom: 15px;
    animation: rotating 1.5s linear infinite;
    @keyframes rotating {
      0% {
        transform: rotate(0deg);
      }
      100% {
        transform: rotate(360deg);
      }
    }
  }
}
.ellipsis {
  display: inline-block;
  /* æˆ– block */
  // max-width: 100%;       /* æ ¹æ®å®žé™…容器宽度调整 */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* å…³é”®ï¼šç¦æ­¢æ¢è¡Œ */
}
</style>
src/pages/searchNew/components/cameraVideo.vue
New file
@@ -0,0 +1,232 @@
<template>
  <div class="container">
    <!-- å¸ƒå±€æŽ§åˆ¶åŠæ’­æ”¾åœ°å€é€‰æ‹© -->
    <div class="layout-controls">
      <el-select
        v-model="selectedUrl"
        placeholder="选择播放地址"
        class="left-control"
        style="width: 200px; margin-right: 20px"
        @change="handleUrlChange"
        :disabled="activeIndex === -1"
      >
        <el-option
          v-for="item in playbackOptions"
          :key="item.videoId"
          :label="item.deviceName"
          :value="item.videoId"
        />
      </el-select>
      <el-button-group class="right-control">
        <el-button
          :type="gridLayout === 1 ? 'primary' : ''"
          @click="changeLayout(1)"
        >
          å•æ ¼
        </el-button>
        <el-button
          :type="gridLayout === 4 ? 'primary' : ''"
          @click="changeLayout(4)"
        >
          å››æ ¼
        </el-button>
        <el-button
          :type="gridLayout === 9 ? 'primary' : ''"
          @click="changeLayout(9)"
        >
          ä¹æ ¼
        </el-button>
      </el-button-group>
    </div>
    <!-- è§†é¢‘网格布局 -->
    <el-row :gutter="5">
      <el-col
        v-for="(item, index) in showVideos"
        :key="index"
        :span="colSpan"
        class="video-col"
        :class="{ active: activeIndex === index }"
      >
        <div class="video-container"
        @click="handleGridClick(index)">
          <!-- çº¯é»‘背景封面 -->
          <div
            class="video-cover"
            v-show="!item.playing"
            style="pointer-events: none"
          />
          <!-- è§†é¢‘播放器 -->
          <iframe
            v-show="item.playing"
            class="video-iframe"
            :src="item.url"
            frameborder="0"
            allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
            allowfullscreen
            style="pointer-events: none"
          ></iframe>
        </div>
      </el-col>
    </el-row>
  </div>
</template>
<script>
import camera from "@/api/SurveyView"
export default {
  name: 'cameraVideo',
  data() {
    return {
      iframeUrl:'',
      gridLayout: 1,
      activeIndex: -1,
      selectedUrl: '',
      playbackOptions: [
        {
          label: '摄像头1',
          value: 'rtsp://Admin:1234@192.168.1.209/h264'
        },
        {
          label: '摄像头2',
          value: 'http://example.com/another-video-source'
        }
      ],
      allVideos: Array(9).fill(null).map((_, i) => ({
        title: `视频 ${i + 1}`,
        url: '',
        playing: false
      }))
    }
  },
  computed: {
    showVideos() {
      return this.allVideos.slice(0, this.gridLayout)
    },
    colSpan() {
      return 24 / Math.sqrt(this.gridLayout)
    }
  },
  mounted() {
        this.fetchSelect();
    this.fetchCameraInfo()
},
  methods: {
    async fetchCameraInfo() {
    const response = await fetch('/config.json');
    if (!response.ok) throw new Error(`请求失败: ${response.status}`);
        const responseData = await response.json();
    this.iframeUrl = responseData.iframeUrl;
    console.info(this.iframeUrl)
  },
    async fetchSelect(){
        const cameras = await camera.getCameras();//视频点位
        this.playbackOptions = cameras.data
    },
    changeLayout(num) {
      this.gridLayout = num
      this.activeIndex = -1
      this.resetAllVideos()
    },
    handleGridClick(index) {
      this.activeIndex = index
      console.info('点击事件生效')
    },
    handleUrlChange(val) {
        console.info(process.env.NODE_ENV)
      if (this.activeIndex === -1) return
    //   console.info('点击选中的数据:'+this.playbackOptions.find(item => item.videoId === val).rtspAddress)
      const currentVideo = this.allVideos[this.activeIndex]
      currentVideo.url = this.iframeUrl+`/view/cameraPlayer/index.html?rtspUrl=${encodeURIComponent(this.playbackOptions.find(item => item.videoId === val).rtspAddress)}`
      currentVideo.playing = true
        console.info("地址:"+currentVideo.url)
    },
    resetAllVideos() {
      this.allVideos.forEach((video, index) => {
        if (index >= this.gridLayout) {
          video.playing = false
          video.url = ''
        }
      })
    }
  }
}
</script>
<style scoped>
/* ç®€åŒ–后的样式 */
.container {
  padding-top: 80px;
  position: relative;
}
.layout-controls {
  position: absolute;
  left: 60px;
  right: 60px; /* æ–°å¢žå³ä¾§å¯¹é½ */
  top: 20px;
  z-index: 1000;
  display: flex;
  justify-content: space-between; /* å…³é”®å±žæ€§ */
  align-items: center;
}
/* ç§»é™¤åŽŸæœ‰çš„ margin-right */
.el-select {
  width: 200px;
}
/* å¯é€‰ï¼šé˜²æ­¢æŒ‰é’®ç»„换行 */
.el-button-group {
  flex-shrink: 0;
}
.video-col {
  position: relative;
  margin-bottom: 5px;
  transition: all 0.3s;
  min-height: 100px;
}
.video-col.active::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  border: 3px solid #409EFF;
  pointer-events: none;
  z-index: 2;
}
.video-container {
  position: relative;
  width: 100%;
  height: 0;
  padding-top: 56.25%;
  cursor: pointer;
  background-color: #000;
  overflow: hidden;
}
.video-cover {
    pointer-events: none; /* å…è®¸äº‹ä»¶ç©¿é€ */
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: #000; /* çº¯é»‘背景 */
}
.video-iframe {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: #000;
  pointer-events: none;
}
</style>
src/pages/searchNew/index/App.vue
New file
@@ -0,0 +1,131 @@
<template>
  <div class="column">
    <div class="column-right">
       <survey-view ref="cardlist"/>
    </div>
  </div>
</template>
<script>
import { getUrlKey } from "@/api/utils";
import surveyView from "../components/SurveyView";
export default {
  name: "VideoManage",
  components: {
    surveyView
  },
  computed: {
    app() {
      return getUrlKey("dataStack") !== null ? "DataStack" : "Camera";
    },
  },
  data() {
    return {
      leftWith: 0,
      screenHeight: 0,
    };
  },
  mounted() {
    this.screenHeight = document.documentElement.clientHeight;
    window.onresize = () => {
      return (() => {
        this.screenHeight = document.documentElement.clientHeight;
      })();
    };
    this.leftWith = this.$refs["left"].offsetWidth;
    this.TreeDataPool.readonly = false;
    this.TreeDataPool.gbReadonly = false;
    this.DataStackPool.readonly = false;
  },
  methods: {
    changeTrainId(trainId){
      if (this.$refs.cardlist) {
        this.$refs.cardlist.changeTrainId(trainId);
      }
    }
  },
};
</script>
<style lang="scss" scoped>
.column {
  overflow: hidden;
  //min-width: 1399px;
  //min-width: 1920px;
  height: 100%;
}
.column-right {
  padding: 5px;
  height: 100vh;
  // background-color: #eee;
  box-sizing: border-box;
  overflow: scroll;
}
.heigher-index {
  position: absolute;
  top: 0;
  z-index: 10;
  width: 100%;
  height: 100%;
}
.resize-save {
  position: absolute;
  top: 0;
  right: 5px;
  bottom: 0;
  left: 0;
  padding: 16px;
  padding-top: 8px;
  overflow-x: hidden;
  overflow-y: auto;
}
.resize-bar {
  width: 338px;
  height: inherit;
  resize: horizontal;
  cursor: ew-resize;
  opacity: 0;
  overflow: scroll;
  max-width: 500px; //设定最大拉伸长度
  min-width: 33px; //设定最小宽度
}
/* æ‹–拽线 */
.resize-line {
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  border-right: 2px solid #efefef;
  border-left: 1px solid #e0e0e0;
  pointer-events: none;
}
.resize-bar:hover ~ .resize-line,
.resize-bar:active ~ .resize-line {
  border-left: 1px dashed skyblue;
}
.resize-bar::-webkit-scrollbar {
  width: 200px;
  height: inherit;
}
/* Firefox只有下面一小块区域可以拉伸 */
@supports (-moz-user-select: none) {
  .resize-bar:hover ~ .resize-line,
  .resize-bar:active ~ .resize-line {
    border-left: 1px solid #bbb;
  }
  .resize-bar:hover ~ .resize-line::after,
  .resize-bar:active ~ .resize-line::after {
    content: "";
    position: absolute;
    width: 16px;
    height: 16px;
    bottom: 0;
    right: -8px;
    // background: url(./resize.svg);
    background-size: 100% 100%;
  }
}
</style>
src/pages/searchNew/index/main.ts
New file
@@ -0,0 +1,30 @@
import Vue from "vue";
import App from './App.vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
// import "@/assets/css/element-variables.scss";
import ToggleButton from 'vue-js-toggle-button';
import VueAwesomeSwiper from "vue-awesome-swiper";
import "swiper/dist/css/swiper.css";
import * as VueWindow from "@hscmap/vue-window";
import moment from 'moment';
import Mixin from "./mixins";
Vue.prototype.$moment = moment;
Vue.use(ElementUI);
Vue.use(ToggleButton);
Vue.use(VueAwesomeSwiper as any);
Vue.use(VueWindow);
Vue.filter('moment', function (value, formatString) {
  formatString = formatString || 'YYYY-MM-DD HH:mm:ss';
  return moment(value).format(formatString);
});
Vue.mixin(Mixin);
new Vue({
  el: '#app',
  render: h => h(App)
})
src/pages/searchNew/index/mixins.ts
New file
@@ -0,0 +1,25 @@
import TreeDataPool from "@/Pool/TreeData";
import DataStackPool from "@/Pool/dataStack"
import DataPool from "@/Pool/PollData"
import VideoManageData from "@/Pool/VideoManageData";
import TaskMange from '@/Pool/TaskMange'
/* eslint-disable */
const onlyTreeDataPool = new TreeDataPool
const onlyDataStack = new DataStackPool
const onlyDataPool = new DataPool
const onlyVideoManageData = new VideoManageData
const onlyTaskMange = new TaskMange
const mixin = {
  data() {
    return {
      TreeDataPool: onlyTreeDataPool,
      DataStackPool: onlyDataStack,
      VideoManageData: onlyVideoManageData,
      TaskMange: onlyTaskMange,
      PollData: onlyDataPool
    };
  },
};
export default mixin;